Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #1 Feature - Add parameter validation rule definition #2

Merged
merged 1 commit into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"axios": "^1.7.2",
"dotenv": "^16.4.5",
"file-type": "^19.0.0",
"joi": "^17.13.3",
"jszip": "^3.10.1",
"koa": "^2.15.3",
"koa-body": "^6.0.1",
Expand Down
70 changes: 40 additions & 30 deletions routers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const {
imageMimeTypes,
tinifySupportedMimeTypes,
} = require("../constants/file");
const { FILES_UPLOAD_POST_QUERY, FILES_LIST_GET_QUERY, FILES_REST_ID, FILES_BODY_BATCH_IDS } = require("../types/schema/files");
const { validateQuery, validateBody, validateFormData, validateParams } = require("../types");

tinify.key = process.env.TINIFY_KEY;

Expand Down Expand Up @@ -44,17 +46,17 @@ const getDefaultThumbPath = (mime) => {
};

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

const shouldCompress = ctx.query.compress !== "false";
const shouldKeepTemp = ctx.query.keepTemp === "true";
const shouldGenerateThumb = ctx.query.isThumb === "true";
const isFilePublic = ctx.query.isPublic === "true";
const responseType = ctx.query.type;
const shouldCompress = compress === 'true';
const shouldKeepTemp = keepTemp === 'true';
const shouldGenerateThumb = isThumb === 'true';
const isFilePublic = isPublic === 'true';

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

if (shouldGenerateThumb && imageMimeTypes.includes(mime)) {
console.time('thumb')
realThumbPath = getRealThumbPath(fileId);
await sharp(file.filepath)
.resize(200, 200)
.toFile(realThumbPath);
console.timeEnd('thumb');
} else if (shouldGenerateThumb) {
realThumbPath = getDefaultThumbPath(mime);
}

if (shouldCompress && tinifySupportedMimeTypes.includes(mime)) {
console.time('compress')
await tinify.fromFile(file.filepath).toFile(realFilePath);
console.timeEnd('compress')
} else {
if (shouldKeepTemp) {
await fsp.copyFile(file.filepath, realFilePath);
Expand Down Expand Up @@ -131,7 +137,8 @@ router.post("/files", async (ctx) => {
});

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

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

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

// 编辑文件信息接口
router.put('/files/:id', async (ctx) => {
router.put("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
const { id } = ctx.params;
const {
filename,
Expand All @@ -264,12 +271,12 @@ router.put('/files/:id', async (ctx) => {
where: {
id,
is_delete: false,
}
},
});

if (!file) {
ctx.status = 404;
ctx.body = { message: 'File not found' };
ctx.body = { message: "File not found" };
return;
}

Expand Down Expand Up @@ -304,13 +311,16 @@ router.put('/files/:id', async (ctx) => {
ctx.body = updatedFile;
} catch (error) {
ctx.status = 500;
ctx.body = { message: 'Error updating file information', error: error.message };
console.error('Update file error:', error);
ctx.body = {
message: "Error updating file information",
error: error.message,
};
console.error("Update file error:", error);
}
});

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

try {
Expand All @@ -319,48 +329,48 @@ router.delete('/files/:id', async (ctx) => {
where: {
id,
is_delete: false,
}
},
});

if (!file) {
ctx.status = 404;
ctx.body = { message: 'File not found' };
ctx.body = { message: "File not found" };
return;
}

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

// 返回删除成功的信息
ctx.status = 204;
} catch (error) {
ctx.status = 500;
ctx.body = { message: 'Error deleting file', error: error.message };
console.error('Delete file error:', error);
ctx.body = { message: "Error deleting file", error: error.message };
console.error("Delete file error:", error);
}
});

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

if (!ids || !Array.isArray(ids) || ids.length === 0) {
ctx.status = 400;
ctx.body = { message: 'No file ids provided for deletion' };
ctx.body = { message: "No file ids provided for deletion" };
return;
}

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

if (numberOfAffectedRows === 0) {
ctx.status = 404;
ctx.body = { message: 'No files found to delete' };
ctx.body = { message: "No files found to delete" };
return;
}

// 返回删除成功的信息
ctx.status = 204;
} catch (error) {
ctx.status = 500;
ctx.body = { message: 'Error deleting files', error: error.message };
console.error('Delete files error:', error);
ctx.body = { message: "Error deleting files", error: error.message };
console.error("Delete files error:", error);
}
});

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

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

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

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

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

if (ids.length === 0) {
ctx.status = 400;
Expand Down
88 changes: 88 additions & 0 deletions types/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 生成用于校验 query 参数的中间件
function validateQuery(schema) {
return async function(ctx, next) {
try {
const validated = await schema.validateAsync(ctx.query, {
allowUnknown: true,
convert: true,
stripUnknown: true
});
ctx.query = validated;
await next();
} catch (err) {
console.log(err);

ctx.status = 400;
ctx.body = {
message: "Query Validation Error",
error: err.details[0].message,
};
}
};
}

// 生成用于校验 body 数据的中间件
function validateBody(schema) {
return async function(ctx, next) {
try {
const validated = await schema.validateAsync(ctx.request.body, {
allowUnknown: true,
convert: true,
});
ctx.request.body = validated;
await next();
} catch (err) {
ctx.status = 400;
ctx.body = {
message: "Body Validation Error",
error: err.details[0].message,
};
}
};
}

async function validateFormData(ctx, next) {
try {
const files = ctx.request.files ? ctx.request.files.file : null;

// 检查是否上传了文件
if (!files) {
ctx.status = 400;
ctx.body = { message: "File upload is required." };
return;
}

await next();
} catch (err) {
ctx.status = 400;
ctx.body = { message: 'Validation Error', error: err.message };
return;
}
}

function validateParams(schema) {
return async function(ctx, next) {
try {
const validated = await schema.validateAsync(ctx.params, {
allowUnknown: true,
convert: true,
stripUnknown: true
});
ctx.params = validated;
await next();
} catch (err) {
ctx.status = 400;
ctx.body = {
message: "Params Validation Error",
error: err.details[0].message,
};
}
};
}

module.exports = {
validateBody,
validateQuery,
validateFormData,
validateParams
};
46 changes: 46 additions & 0 deletions types/schema/files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const Joi = require("joi");

/**
业务相关定义 = model business method format
*/
const FILES_UPLOAD_POST_QUERY = Joi.object({
compress: Joi.string()
.valid("true", "false").default("false"),
keepTemp: Joi.string()
.valid("true", "false").default("false"),
isThumb: Joi.string()
.valid("true", "false").default("true"),
isPublic: Joi.string()
.valid("true", "false").default("false"),
type: Joi.string()
.valid("md", "url").required()
});

const FILES_LIST_GET_QUERY = Joi.object({
limit: Joi.number().integer().min(1).max(100).default(10),
offset: Joi.number().integer().min(0).default(0),
type: Joi.string().valid('image', 'video', 'file', 'all').default('all')
});


/**
通用定义 = model format fields
*/

const FILES_REST_ID = Joi.object({
id: Joi.string().required()
});

const FILES_BODY_BATCH_IDS = Joi.object({
ids: Joi.array()
.items(Joi.string().required())
.required()
.min(1)
});

module.exports = {
FILES_UPLOAD_POST_QUERY,
FILES_LIST_GET_QUERY,
FILES_REST_ID,
FILES_BODY_BATCH_IDS
};
Loading