Skip to content

Commit 95120d9

Browse files
authored
Merge pull request #2 from diskcloud/feat/full/1.2.1
feat: #1 Feature - Add parameter validation rule definition
2 parents fe74c08 + 1e4e86e commit 95120d9

File tree

5 files changed

+215
-30
lines changed

5 files changed

+215
-30
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"axios": "^1.7.2",
1818
"dotenv": "^16.4.5",
1919
"file-type": "^19.0.0",
20+
"joi": "^17.13.3",
2021
"jszip": "^3.10.1",
2122
"koa": "^2.15.3",
2223
"koa-body": "^6.0.1",

routers/files.js

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const {
1313
imageMimeTypes,
1414
tinifySupportedMimeTypes,
1515
} = require("../constants/file");
16+
const { FILES_UPLOAD_POST_QUERY, FILES_LIST_GET_QUERY, FILES_REST_ID, FILES_BODY_BATCH_IDS } = require("../types/schema/files");
17+
const { validateQuery, validateBody, validateFormData, validateParams } = require("../types");
1618

1719
tinify.key = process.env.TINIFY_KEY;
1820

@@ -44,17 +46,17 @@ const getDefaultThumbPath = (mime) => {
4446
};
4547

4648
// 处理文件上传
47-
router.post("/files", async (ctx) => {
49+
router.post("/files", validateFormData, validateQuery(FILES_UPLOAD_POST_QUERY), async (ctx) => {
4850
try {
4951
const files = ctx.request.files.file;
5052
const fileList = Array.isArray(files) ? files : [files];
5153
const responses = [];
54+
const { compress, keepTemp, isThumb, isPublic, type: responseType } = ctx.query;
5255

53-
const shouldCompress = ctx.query.compress !== "false";
54-
const shouldKeepTemp = ctx.query.keepTemp === "true";
55-
const shouldGenerateThumb = ctx.query.isThumb === "true";
56-
const isFilePublic = ctx.query.isPublic === "true";
57-
const responseType = ctx.query.type;
56+
const shouldCompress = compress === 'true';
57+
const shouldKeepTemp = keepTemp === 'true';
58+
const shouldGenerateThumb = isThumb === 'true';
59+
const isFilePublic = isPublic === 'true';
5860

5961
for (const file of fileList) {
6062
const fileId = uuidv4();
@@ -65,16 +67,20 @@ router.post("/files", async (ctx) => {
6567
let realThumbPath = null;
6668

6769
if (shouldGenerateThumb && imageMimeTypes.includes(mime)) {
70+
console.time('thumb')
6871
realThumbPath = getRealThumbPath(fileId);
6972
await sharp(file.filepath)
7073
.resize(200, 200)
7174
.toFile(realThumbPath);
75+
console.timeEnd('thumb');
7276
} else if (shouldGenerateThumb) {
7377
realThumbPath = getDefaultThumbPath(mime);
7478
}
7579

7680
if (shouldCompress && tinifySupportedMimeTypes.includes(mime)) {
81+
console.time('compress')
7782
await tinify.fromFile(file.filepath).toFile(realFilePath);
83+
console.timeEnd('compress')
7884
} else {
7985
if (shouldKeepTemp) {
8086
await fsp.copyFile(file.filepath, realFilePath);
@@ -131,7 +137,8 @@ router.post("/files", async (ctx) => {
131137
});
132138

133139
// 获取文件列表
134-
router.get("/files", async (ctx) => {
140+
router.get("/files", validateQuery(FILES_LIST_GET_QUERY), async (ctx) => {
141+
console.log(ctx.query);
135142
try {
136143
const limit = parseInt(ctx.query.limit, 10) || 10; // 每页数量,默认为 10
137144
const offset = parseInt(ctx.query.offset, 10) || 0; // 偏移量,默认为 0
@@ -199,7 +206,7 @@ router.get("/files", async (ctx) => {
199206
});
200207

201208
// 获取单个文件信息
202-
router.get("/files/:id", async (ctx) => {
209+
router.get("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
203210
const { id } = ctx.params;
204211

205212
try {
@@ -248,7 +255,7 @@ router.get("/files/:id", async (ctx) => {
248255
});
249256

250257
// 编辑文件信息接口
251-
router.put('/files/:id', async (ctx) => {
258+
router.put("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
252259
const { id } = ctx.params;
253260
const {
254261
filename,
@@ -264,12 +271,12 @@ router.put('/files/:id', async (ctx) => {
264271
where: {
265272
id,
266273
is_delete: false,
267-
}
274+
},
268275
});
269276

270277
if (!file) {
271278
ctx.status = 404;
272-
ctx.body = { message: 'File not found' };
279+
ctx.body = { message: "File not found" };
273280
return;
274281
}
275282

@@ -304,13 +311,16 @@ router.put('/files/:id', async (ctx) => {
304311
ctx.body = updatedFile;
305312
} catch (error) {
306313
ctx.status = 500;
307-
ctx.body = { message: 'Error updating file information', error: error.message };
308-
console.error('Update file error:', error);
314+
ctx.body = {
315+
message: "Error updating file information",
316+
error: error.message,
317+
};
318+
console.error("Update file error:", error);
309319
}
310320
});
311321

312322
// 文件删除接口
313-
router.delete('/files/:id', async (ctx) => {
323+
router.delete("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
314324
const { id } = ctx.params;
315325

316326
try {
@@ -319,48 +329,48 @@ router.delete('/files/:id', async (ctx) => {
319329
where: {
320330
id,
321331
is_delete: false,
322-
}
332+
},
323333
});
324334

325335
if (!file) {
326336
ctx.status = 404;
327-
ctx.body = { message: 'File not found' };
337+
ctx.body = { message: "File not found" };
328338
return;
329339
}
330340

331341
// 执行软删除,将 is_delete 字段设置为 true
332342
await file.update({
333343
is_delete: true,
334344
updated_at: new Date(), // 更新更新时间
335-
updated_by: ctx.query.updated_by || 'anonymous' // 可以通过查询参数传递更新者
345+
updated_by: ctx.query.updated_by || "anonymous", // 可以通过查询参数传递更新者
336346
});
337347

338348
// 返回删除成功的信息
339349
ctx.status = 204;
340350
} catch (error) {
341351
ctx.status = 500;
342-
ctx.body = { message: 'Error deleting file', error: error.message };
343-
console.error('Delete file error:', error);
352+
ctx.body = { message: "Error deleting file", error: error.message };
353+
console.error("Delete file error:", error);
344354
}
345355
});
346356

347357
// 文件批量删除接口
348-
router.delete('/files', async (ctx) => {
358+
router.delete("/files", validateBody(FILES_BODY_BATCH_IDS), async (ctx) => {
349359
const { ids } = ctx.request.body; // 获取要删除的文件 ID 列表
350-
const updated_by = ctx.query.updated_by || 'anonymous'; // 获取更新者,默认为匿名
360+
const updated_by = ctx.query.updated_by || "anonymous"; // 获取更新者,默认为匿名
351361
console.log(ctx.request.body);
352362
console.log(JSON.stringify(ctx.request.body));
353363

354364
if (!ids || !Array.isArray(ids) || ids.length === 0) {
355365
ctx.status = 400;
356-
ctx.body = { message: 'No file ids provided for deletion' };
366+
ctx.body = { message: "No file ids provided for deletion" };
357367
return;
358368
}
359369

360370
try {
361371
// 查找并更新指定的文件
362372
const [numberOfAffectedRows] = await Files.update(
363-
{
373+
{
364374
is_delete: true,
365375
updated_by: updated_by,
366376
updated_at: new Date(),
@@ -377,21 +387,21 @@ router.delete('/files', async (ctx) => {
377387

378388
if (numberOfAffectedRows === 0) {
379389
ctx.status = 404;
380-
ctx.body = { message: 'No files found to delete' };
390+
ctx.body = { message: "No files found to delete" };
381391
return;
382392
}
383393

384394
// 返回删除成功的信息
385395
ctx.status = 204;
386396
} catch (error) {
387397
ctx.status = 500;
388-
ctx.body = { message: 'Error deleting files', error: error.message };
389-
console.error('Delete files error:', error);
398+
ctx.body = { message: "Error deleting files", error: error.message };
399+
console.error("Delete files error:", error);
390400
}
391401
});
392402

393403
// 文件预览
394-
router.get("/files/:id/preview", async (ctx) => {
404+
router.get("/files/:id/preview", validateParams(FILES_REST_ID), async (ctx) => {
395405
const { id } = ctx.params;
396406
const { type } = ctx.query; // 获取查询参数 'type',可以是 'thumb' 或 'original'
397407

@@ -454,7 +464,7 @@ router.get("/files/:id/preview", async (ctx) => {
454464
});
455465

456466
// 单文件下载
457-
router.get("/files/:id/export", async (ctx) => {
467+
router.get("/files/:id/download", validateParams(FILES_REST_ID), async (ctx) => {
458468
const { id } = ctx.params;
459469

460470
try {
@@ -506,8 +516,8 @@ router.get("/files/:id/export", async (ctx) => {
506516
});
507517

508518
// 批量下载
509-
router.get("/files/export/batch", async (ctx) => {
510-
const ids = ctx.query.ids ? ctx.query.ids.split(",") : [];
519+
router.post("/files/download", validateBody(FILES_BODY_BATCH_IDS), async (ctx) => {
520+
const ids = ctx.request.body.ids;
511521

512522
if (ids.length === 0) {
513523
ctx.status = 400;

types/index.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// 生成用于校验 query 参数的中间件
2+
function validateQuery(schema) {
3+
return async function(ctx, next) {
4+
try {
5+
const validated = await schema.validateAsync(ctx.query, {
6+
allowUnknown: true,
7+
convert: true,
8+
stripUnknown: true
9+
});
10+
ctx.query = validated;
11+
await next();
12+
} catch (err) {
13+
console.log(err);
14+
15+
ctx.status = 400;
16+
ctx.body = {
17+
message: "Query Validation Error",
18+
error: err.details[0].message,
19+
};
20+
}
21+
};
22+
}
23+
24+
// 生成用于校验 body 数据的中间件
25+
function validateBody(schema) {
26+
return async function(ctx, next) {
27+
try {
28+
const validated = await schema.validateAsync(ctx.request.body, {
29+
allowUnknown: true,
30+
convert: true,
31+
});
32+
ctx.request.body = validated;
33+
await next();
34+
} catch (err) {
35+
ctx.status = 400;
36+
ctx.body = {
37+
message: "Body Validation Error",
38+
error: err.details[0].message,
39+
};
40+
}
41+
};
42+
}
43+
44+
async function validateFormData(ctx, next) {
45+
try {
46+
const files = ctx.request.files ? ctx.request.files.file : null;
47+
48+
// 检查是否上传了文件
49+
if (!files) {
50+
ctx.status = 400;
51+
ctx.body = { message: "File upload is required." };
52+
return;
53+
}
54+
55+
await next();
56+
} catch (err) {
57+
ctx.status = 400;
58+
ctx.body = { message: 'Validation Error', error: err.message };
59+
return;
60+
}
61+
}
62+
63+
function validateParams(schema) {
64+
return async function(ctx, next) {
65+
try {
66+
const validated = await schema.validateAsync(ctx.params, {
67+
allowUnknown: true,
68+
convert: true,
69+
stripUnknown: true
70+
});
71+
ctx.params = validated;
72+
await next();
73+
} catch (err) {
74+
ctx.status = 400;
75+
ctx.body = {
76+
message: "Params Validation Error",
77+
error: err.details[0].message,
78+
};
79+
}
80+
};
81+
}
82+
83+
module.exports = {
84+
validateBody,
85+
validateQuery,
86+
validateFormData,
87+
validateParams
88+
};

types/schema/files.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const Joi = require("joi");
2+
3+
/**
4+
业务相关定义 = model business method format
5+
*/
6+
const FILES_UPLOAD_POST_QUERY = Joi.object({
7+
compress: Joi.string()
8+
.valid("true", "false").default("false"),
9+
keepTemp: Joi.string()
10+
.valid("true", "false").default("false"),
11+
isThumb: Joi.string()
12+
.valid("true", "false").default("true"),
13+
isPublic: Joi.string()
14+
.valid("true", "false").default("false"),
15+
type: Joi.string()
16+
.valid("md", "url").required()
17+
});
18+
19+
const FILES_LIST_GET_QUERY = Joi.object({
20+
limit: Joi.number().integer().min(1).max(100).default(10),
21+
offset: Joi.number().integer().min(0).default(0),
22+
type: Joi.string().valid('image', 'video', 'file', 'all').default('all')
23+
});
24+
25+
26+
/**
27+
通用定义 = model format fields
28+
*/
29+
30+
const FILES_REST_ID = Joi.object({
31+
id: Joi.string().required()
32+
});
33+
34+
const FILES_BODY_BATCH_IDS = Joi.object({
35+
ids: Joi.array()
36+
.items(Joi.string().required())
37+
.required()
38+
.min(1)
39+
});
40+
41+
module.exports = {
42+
FILES_UPLOAD_POST_QUERY,
43+
FILES_LIST_GET_QUERY,
44+
FILES_REST_ID,
45+
FILES_BODY_BATCH_IDS
46+
};

0 commit comments

Comments
 (0)