Skip to content

Commit 1b50ae2

Browse files
committed
refactor: Refactoring a project using sequence ✨
1 parent 0bb3c4c commit 1b50ae2

10 files changed

Lines changed: 557 additions & 344 deletions

File tree

index.js

Lines changed: 7 additions & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1+
// app.js
12
const Koa = require('koa');
2-
const Router = require('koa-router');
33
const { koaBody } = require('koa-body');
4-
const tinify = require('tinify');
54
const path = require('path');
65
const fs = require('fs');
7-
const sharp = require('sharp');
8-
const { checkAndCreateTable } = require('./utils/checkAndCreateTable');
9-
const pool = require('./utils/db');
10-
const { appendSuffixToFilename } = require('./utils/appendSuffixToFilename');
11-
const { v4: uuidv4 } = require('uuid');
12-
const { detectFileType } = require('./utils/detectFileType');
13-
const { imageMimeTypes, tinifySupportedMimeTypes} = require('./constants/file')
6+
const sequelize = require('./utils/dbInstance'); // 确保路径正确
7+
const filesRouter = require('./routers/files'); // 确保路径正确
8+
149
require('dotenv').config({ path: '.env.local' });
1510

1611
const app = new Koa();
17-
const router = new Router();
18-
19-
tinify.key = process.env.TINIFY_KEY;
2012

2113
app.use(require('koa-static')(path.join(__dirname, 'public')));
2214

@@ -44,256 +36,11 @@ app.use(
4436
})
4537
);
4638

47-
router.post('/upload', async (ctx) => {
48-
const connection = await pool.getConnection();
49-
try {
50-
const files = ctx.request.files.file;
51-
const fileList = Array.isArray(files) ? files : [files];
52-
const responses = [];
53-
54-
const compress = ctx.query.compress !== 'false'; // 默认压缩
55-
const keepTemp = ctx.query.keepTemp === 'true'; // 默认不保留临时文件
56-
const isThumb = Number(ctx.query.isThumb === 'true');
57-
const isPublic = Number(ctx.query.isPublic === 'true');
58-
const responseType = ctx.query.type;
59-
60-
for (const file of fileList) {
61-
const fileId = uuidv4(); // 生成文件唯一ID
62-
63-
const outputFilePath = path.join(
64-
__dirname,
65-
'public',
66-
'files',
67-
fileId + path.extname(file.filepath) // 使用UUID作为文件名称
68-
);
69-
70-
const { mime, ext } = await detectFileType(file.filepath, file);
71-
72-
let outputFileThumbPath = null;
73-
if (isThumb && imageMimeTypes.includes(mime)) {
74-
const fileThumbName = `${fileId}_thumb${path.extname(file.filepath)}`; // 缩略图文件名称
75-
76-
outputFileThumbPath = path.join(
77-
__dirname,
78-
'public',
79-
'files',
80-
fileThumbName
81-
);
82-
83-
await sharp(file.filepath)
84-
.resize(200, 200) // 调整图像大小为200x200像素
85-
.toFile(outputFileThumbPath);
86-
} else if(isThumb) {
87-
const back_thumbs = {
88-
video: path.join(__dirname, 'public', 'icons', 'video.png'),
89-
sheet: path.join(__dirname, 'public', 'icons', 'xlsx.png'),
90-
pdf: path.join(__dirname, 'public', 'icons', 'pdf.png'),
91-
document: path.join(__dirname, 'public', 'icons', 'doc.png'),
92-
}
93-
94-
const unknown = path.join(__dirname, 'public', 'icons', 'unknown_file_types.png');
95-
96-
const thumb = Object.keys(back_thumbs).find(key => mime.includes(key));
97-
98-
outputFileThumbPath = back_thumbs[thumb] ?? unknown;
99-
}
100-
101-
if (compress && tinifySupportedMimeTypes.includes(mime)) {
102-
await tinify.fromFile(file.filepath).toFile(outputFilePath);
103-
} else {
104-
// 如果不支持压缩或者不要求压缩,保留临时文件则复制文件,否则移动文件
105-
if (keepTemp) {
106-
fs.copyFileSync(file.filepath, outputFilePath);
107-
} else {
108-
fs.renameSync(file.filepath, outputFilePath);
109-
}
110-
}
111-
112-
const fileUrl = `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${fileId}`;
113-
const thumb_location = outputFileThumbPath ? `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${fileId}?type=thumb` : null;
114-
115-
await connection.execute(
116-
`INSERT INTO files (
117-
id,
118-
filename,
119-
filesize,
120-
filelocation,
121-
real_file_location,
122-
created_by,
123-
is_public,
124-
thumb_location,
125-
is_thumb,
126-
is_delete,
127-
real_file_thumb_location,
128-
mime,
129-
ext
130-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
131-
[
132-
fileId, // 使用UUID作为ID
133-
path.basename(outputFilePath),
134-
fs.statSync(outputFilePath).size,
135-
fileUrl,
136-
outputFilePath, // 存储实际文件路径
137-
ctx.query.createdBy || 'anonymous',
138-
isPublic,
139-
thumb_location,
140-
isThumb,
141-
0,
142-
outputFileThumbPath,
143-
mime,
144-
ext
145-
]
146-
);
147-
148-
if (responseType === 'md' && imageMimeTypes.includes(mime)) {
149-
responses.push({
150-
filepath: `![${path.basename(outputFilePath)}](${fileUrl})`
151-
});
152-
} else {
153-
responses.push({ filepath: fileUrl });
154-
}
155-
156-
if (!keepTemp && fs.existsSync(file.filepath)) {
157-
fs.unlinkSync(file.filepath);
158-
}
159-
}
160-
161-
ctx.body = fileList.length > 1 ? responses : responses[0];
162-
} catch (error) {
163-
ctx.status = 500;
164-
ctx.body = 'Error processing your request: ' + error.message;
165-
} finally {
166-
connection.release();
167-
}
168-
});
169-
170-
router.get('/files', async (ctx) => {
171-
const connection = await pool.getConnection();
172-
try {
173-
const limit = parseInt(ctx.query.limit, 10) || 10; // 每页数量,默认为 10
174-
const offset = parseInt(ctx.query.offset, 10) || 0; // 偏移量,默认为 0
175-
const type = ctx.query.type ?? ''; // 获取查询参数中的类型
176-
177-
const types = {
178-
image: 'image',
179-
video: 'video',
180-
all: '',
181-
}
182-
183-
const excludedTypes = ['image', 'video']; // 要排除的类型
184-
185-
let mimeCondition = ''; // 初始化mime条件
186-
187-
// 构建 mime 条件
188-
if (type === 'file') {
189-
mimeCondition = excludedTypes.map(t => `mime NOT LIKE '%${t}%'`).join(' AND ');
190-
} else if (types[type]) {
191-
mimeCondition = `mime LIKE '%${types[type]}%'`;
192-
}
193-
194-
// 构建完整的 SQL 语句
195-
const sql = `
196-
SELECT
197-
created_by,
198-
created_at,
199-
public_by,
200-
public_expiration,
201-
updated_at,
202-
updated_by,
203-
filesize,
204-
filename,
205-
filelocation,
206-
thumb_location,
207-
is_public
208-
FROM
209-
files
210-
WHERE
211-
is_delete = 0
212-
AND is_public = 1
213-
${mimeCondition ? `AND ${mimeCondition}` : ''}
214-
LIMIT ? OFFSET ?`;
215-
216-
// 执行查询
217-
const [rows] = await connection.execute(
218-
sql,
219-
[String(limit), String(offset)]
220-
);
221-
222-
223-
ctx.body = rows;
224-
} catch (error) {
225-
ctx.status = 500;
226-
ctx.body = 'Error retrieving files: ' + error.message;
227-
} finally {
228-
connection.release();
229-
}
230-
});
231-
232-
router.get('/files/:id', async (ctx) => {
233-
const { id } = ctx.params;
234-
const { type } = ctx.query; // 获取查询参数 'type',可以是 'thumb' 或 'original'
235-
const connection = await pool.getConnection();
236-
237-
try {
238-
// 查询文件数据,只获取必要字段
239-
const [rows] = await connection.execute(
240-
`
241-
SELECT
242-
filename,
243-
is_delete,
244-
is_public,
245-
public_expiration,
246-
real_file_location,
247-
real_file_thumb_location,
248-
is_thumb,
249-
mime,
250-
ext
251-
FROM files
252-
WHERE id = ?
253-
AND is_delete = 0
254-
AND (is_public = 1 AND (public_expiration IS NULL OR public_expiration > NOW()))`,
255-
[id]
256-
);
257-
258-
if (rows.length === 0) {
259-
ctx.status = 404;
260-
ctx.body = { message: 'File not found or not accessible' };
261-
return;
262-
}
263-
264-
const file = rows[0];
265-
266-
let fileLocation = file.real_file_location;
267-
// 根据查询参数 'type' 决定返回原图或缩略图
268-
if(file.is_thumb && type === 'thumb') {
269-
fileLocation = file.real_file_thumb_location;
270-
}
271-
272-
// 检查文件是否存在
273-
if (!fs.existsSync(fileLocation)) {
274-
ctx.status = 404;
275-
ctx.body = { message: 'File not found' };
276-
return;
277-
}
278-
const { mime } = await detectFileType(fileLocation);
279-
// 设置响应头
280-
ctx.set('Content-Type', mime);
281-
ctx.set('Content-Disposition', `inline; filename="${file.filename}"`);
282-
283-
// 返回文件流
284-
ctx.body = fs.createReadStream(fileLocation);
285-
} catch (error) {
286-
ctx.status = 500;
287-
ctx.body = { message: 'Internal server error', error: error.message };
288-
} finally {
289-
connection.release(); // 释放连接
290-
}
291-
});
292-
293-
app.use(router.routes()).use(router.allowedMethods());
39+
// 挂载文件路由
40+
app.use(filesRouter.routes()).use(filesRouter.allowedMethods());
29441

29542
app.listen(process.env.SERVER_PORT, async () => {
43+
await sequelize.sync();
29644
console.log(`Server is running on ${process.env.INTERNAL_NETWORK_DOMAIN}`);
29745
console.log(`Server is running on ${process.env.PUBLIC_NETWORK_DOMAIN}`);
298-
await checkAndCreateTable();
29946
});

models/files.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const { DataTypes } = require('sequelize');
2+
const sequelize = require('../utils/dbInstance'); // 修改为实际的sequelize实例路径
3+
const File = sequelize.define('File', {
4+
id: {
5+
type: DataTypes.STRING(50),
6+
allowNull: false, // 必须为 NOT NULL
7+
primaryKey: true,
8+
},
9+
filename: {
10+
type: DataTypes.STRING(255),
11+
allowNull: false,
12+
},
13+
filesize: {
14+
type: DataTypes.BIGINT,
15+
allowNull: false,
16+
},
17+
filelocation: {
18+
type: DataTypes.STRING(255),
19+
allowNull: false,
20+
},
21+
created_by: {
22+
type: DataTypes.STRING(255),
23+
allowNull: false,
24+
},
25+
created_at: {
26+
type: DataTypes.DATE,
27+
allowNull: true,
28+
defaultValue: DataTypes.NOW,
29+
},
30+
updated_by: {
31+
type: DataTypes.STRING(255),
32+
allowNull: true,
33+
defaultValue: null,
34+
},
35+
updated_at: {
36+
type: DataTypes.DATE,
37+
allowNull: true,
38+
defaultValue: DataTypes.NOW,
39+
onUpdate: DataTypes.NOW,
40+
},
41+
is_public: {
42+
type: DataTypes.BOOLEAN,
43+
allowNull: true,
44+
defaultValue: false,
45+
},
46+
public_expiration: {
47+
type: DataTypes.DATE,
48+
allowNull: true,
49+
defaultValue: null,
50+
},
51+
public_by: {
52+
type: DataTypes.STRING(255),
53+
allowNull: true,
54+
defaultValue: null,
55+
},
56+
is_thumb: {
57+
type: DataTypes.BOOLEAN,
58+
allowNull: true,
59+
defaultValue: null,
60+
},
61+
thumb_location: {
62+
type: DataTypes.STRING(255),
63+
allowNull: true,
64+
defaultValue: null,
65+
},
66+
is_delete: {
67+
type: DataTypes.BOOLEAN,
68+
allowNull: false,
69+
defaultValue: false,
70+
},
71+
real_file_location: {
72+
type: DataTypes.STRING(255),
73+
allowNull: true,
74+
defaultValue: null,
75+
},
76+
real_file_thumb_location: {
77+
type: DataTypes.STRING(255),
78+
allowNull: true,
79+
defaultValue: null,
80+
},
81+
mime: {
82+
type: DataTypes.STRING(255),
83+
allowNull: true,
84+
defaultValue: null,
85+
},
86+
ext: {
87+
type: DataTypes.STRING(50),
88+
allowNull: true,
89+
defaultValue: null,
90+
},
91+
}, {
92+
tableName: 'files',
93+
timestamps: false,
94+
underscored: true,
95+
charset: 'utf8mb4',
96+
collate: 'utf8mb4_general_ci',
97+
});
98+
99+
module.exports = File;

0 commit comments

Comments
 (0)