Skip to content

Commit bc2c23a

Browse files
committed
feat: add image preview method
1 parent 44f35e4 commit bc2c23a

5 files changed

Lines changed: 1967 additions & 1808 deletions

File tree

index.js

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const sharp = require('sharp');
99
const { checkAndCreateTable } = require('./utils/checkAndCreateTable');
1010
const pool = require('./utils/db');
1111
const { appendSuffixToFilename } = require('./utils/appendSuffixToFilename');
12+
const { v4: uuidv4 } = require('uuid');
1213
require('dotenv').config({ path: '.env.local' });
1314

1415
const app = new Koa();
@@ -28,6 +29,23 @@ const imageMimeTypes = [
2829
'image/svg+xml'
2930
];
3031

32+
function getMimeType(filePath) {
33+
const ext = path.extname(filePath).toLowerCase();
34+
switch (ext) {
35+
case '.jpg':
36+
case '.jpeg':
37+
return 'image/jpeg';
38+
case '.png':
39+
return 'image/png';
40+
case '.gif':
41+
return 'image/gif';
42+
case '.webp':
43+
return 'image/webp';
44+
default:
45+
return 'application/octet-stream';
46+
}
47+
}
48+
3149
app.use(require('koa-static')(path.join(__dirname, 'public')));
3250

3351
const createDirectories = () => {
@@ -63,21 +81,24 @@ router.post('/upload', async (ctx) => {
6381

6482
const compress = ctx.query.compress !== 'false'; // 默认压缩
6583
const keepTemp = ctx.query.keepTemp === 'true'; // 默认不保留临时文件
66-
const isThumb = ctx.query.isThumb === 'true';
84+
const isThumb = Number(ctx.query.isThumb === 'true');
85+
const isPublic = Number(ctx.query.isPublic === 'true');
6786
const responseType = ctx.query.type;
6887

6988
for (const file of fileList) {
7089
const mimeType = mime.lookup(file.filepath);
90+
const fileId = uuidv4(); // 生成文件唯一ID
7191

7292
const outputFilePath = path.join(
7393
__dirname,
7494
'public',
7595
'files',
76-
path.basename(file.filepath)
96+
fileId + path.extname(file.filepath) // 使用UUID作为文件名称
7797
);
98+
7899
let outputFileThumbPath = null;
79100
if (isThumb && imageMimeTypes.includes(mimeType)) {
80-
const fileThumbName = appendSuffixToFilename(file.newFilename, 'thumb');
101+
const fileThumbName = `${fileId}_thumb${path.extname(file.filepath)}`; // 缩略图文件名称
81102

82103
outputFileThumbPath = path.join(
83104
__dirname,
@@ -102,23 +123,25 @@ router.post('/upload', async (ctx) => {
102123
}
103124
}
104125

105-
const fileUrl = `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${path.basename(
106-
outputFilePath
107-
)}`;
108-
const thumb_location = outputFileThumbPath ? `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${path.basename(outputFileThumbPath)}` : null;
126+
const fileUrl = `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${fileId}`;
127+
const thumb_location = outputFileThumbPath ? `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${fileId}?type=thumb` : null;
109128

110129
await connection.execute(
111130
`INSERT INTO files (
112-
filename, filesize, filelocation, created_by, is_public, thumb_location, is_delete
113-
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
131+
id, filename, filesize, filelocation, real_file_location, created_by, is_public, thumb_location, is_thumb, is_delete, real_file_thumb_location
132+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
114133
[
134+
fileId, // 使用UUID作为ID
115135
path.basename(outputFilePath),
116136
fs.statSync(outputFilePath).size,
117137
fileUrl,
138+
outputFilePath, // 存储实际文件路径
118139
ctx.query.createdBy || 'anonymous',
119-
ctx.query.isPublic === 'true',
140+
isPublic,
120141
thumb_location,
121-
0 // 假设默认的 `is_delete` 状态为 0(未删除)
142+
isThumb,
143+
0,
144+
outputFileThumbPath
122145
]
123146
);
124147

@@ -144,14 +167,15 @@ router.post('/upload', async (ctx) => {
144167
}
145168
});
146169

170+
147171
router.get('/files', async (ctx) => {
148172
const connection = await pool.getConnection();
149173
try {
150174
const limit = parseInt(ctx.query.limit, 10) || 10; // 每页数量,默认为 10
151175
const offset = parseInt(ctx.query.offset, 10) || 0; // 偏移量,默认为 0
152176

153177
const [rows] = await connection.execute(
154-
`SELECT * FROM files LIMIT ? OFFSET ?`,
178+
`SELECT created_by, created_at, public_by, public_expiration, updated_at, updated_by, filesize, filename, filelocation, thumb_location, is_public FROM files WHERE is_delete = 0 AND is_public = 1 LIMIT ? OFFSET ?`,
155179
[limit, offset]
156180
);
157181

@@ -164,6 +188,57 @@ router.get('/files', async (ctx) => {
164188
}
165189
});
166190

191+
router.get('/files/:id', async (ctx) => {
192+
const { id } = ctx.params;
193+
const { type } = ctx.query; // 获取查询参数 'type',可以是 'thumb' 或 'original'
194+
const connection = await pool.getConnection();
195+
196+
try {
197+
// 查询文件数据,只获取必要字段
198+
const [rows] = await connection.execute(
199+
`SELECT filename, is_delete, is_public, public_expiration, real_file_location, real_file_thumb_location, is_thumb
200+
FROM files
201+
WHERE id = ?
202+
AND is_delete = 0
203+
AND (is_public = 1 AND (public_expiration IS NULL OR public_expiration > NOW()))`,
204+
[id]
205+
);
206+
207+
if (rows.length === 0) {
208+
ctx.status = 404;
209+
ctx.body = { message: 'File not found or not accessible' };
210+
return;
211+
}
212+
213+
const file = rows[0];
214+
215+
let fileLocation = file.real_file_location;
216+
// 根据查询参数 'type' 决定返回原图或缩略图
217+
if(file.is_thumb && type === 'thumb') {
218+
fileLocation = file.real_file_thumb_location;
219+
}
220+
221+
// 检查文件是否存在
222+
if (!fs.existsSync(fileLocation)) {
223+
ctx.status = 404;
224+
ctx.body = { message: 'File not found' };
225+
return;
226+
}
227+
228+
// 设置响应头
229+
ctx.set('Content-Type', getMimeType(fileLocation));
230+
ctx.set('Content-Disposition', `inline; filename="${file.filename}"`);
231+
232+
// 返回文件流
233+
ctx.body = fs.createReadStream(fileLocation);
234+
} catch (error) {
235+
ctx.status = 500;
236+
ctx.body = { message: 'Internal server error', error: error.message };
237+
} finally {
238+
connection.release(); // 释放连接
239+
}
240+
});
241+
167242
app.use(router.routes()).use(router.allowedMethods());
168243

169244
app.listen(process.env.SERVER_PORT, async () => {

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"dependencies": {
1414
"@koa/cors": "^5.0.0",
15+
"axios": "^1.7.2",
1516
"dotenv": "^16.4.5",
1617
"koa": "^2.15.3",
1718
"koa-body": "^6.0.1",
@@ -21,8 +22,8 @@
2122
"mime-types": "^2.1.35",
2223
"mysql2": "^3.10.1",
2324
"pm2": "^5.4.0",
25+
"sharp": "0.31.0",
2426
"tinify": "^1.7.1",
25-
"uuid": "^10.0.0",
26-
"sharp": "0.31.0"
27+
"uuid": "^10.0.0"
2728
}
2829
}

public/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
formData.append('file', file);
8787

8888
try {
89-
const response = await fetch('https://source.giao.club/upload?type=md&compress=false&isThumb=true', {
89+
const response = await fetch('https://source.giao.club/upload?type=md&compress=false&isThumb=true&isPublic=true', {
9090
method: 'POST',
9191
body: formData,
9292
});
@@ -120,7 +120,7 @@
120120
formData.append('file', file);
121121

122122
try {
123-
const response = await fetch('https://source.giao.club/upload?type=md&compress=false&keepTemp=true&isThumb=true', {
123+
const response = await fetch('https://source.giao.club/upload?type=md&compress=false&keepTemp=true&isThumb=true&isPublic=true', {
124124
method: 'POST',
125125
body: formData,
126126
});

utils/checkAndCreateTable.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@ async function checkAndCreateTable() {
1010
if (rows.length === 0) {
1111
await connection.execute(
1212
`CREATE TABLE files (
13-
id INT AUTO_INCREMENT PRIMARY KEY,
13+
id VARCHAR(50) DEFAULT NULL
1414
filename VARCHAR(255) NOT NULL,
1515
filesize BIGINT NOT NULL,
1616
filelocation VARCHAR(255) NOT NULL,
1717
created_by VARCHAR(255) NOT NULL,
1818
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
19-
updated_by VARCHAR(255),
19+
updated_by VARCHAR(255) DEFAULT NULL,
2020
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
2121
is_public BOOLEAN DEFAULT FALSE,
22-
public_expiration TIMESTAMP,
23-
public_by VARCHAR(255)
22+
public_expiration TIMESTAMP DEFAULT NULL,
23+
public_by VARCHAR(255) DEFAULT NULL,
24+
is_thumb BOOLEAN DEFAULT FALSE,
25+
thumb_location VARCHAR(255) DEFAULT NULL,
26+
is_delete BOOLEAN NOT NULL DEFAULT FALSE
2427
);`
2528
);
2629
console.log('Table created successfully.');

0 commit comments

Comments
 (0)