Skip to content

Commit 44f35e4

Browse files
committed
feat: add image thumb
1 parent 139c202 commit 44f35e4

5 files changed

Lines changed: 1804 additions & 1603 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pids
1515
*.pid
1616
*.seed
1717
*.pid.lock
18+
package-lock.json
19+
yarn.lock
1820

1921
# Directory for instrumented libs generated by jscoverage/JSCover
2022
lib-cov
@@ -45,6 +47,7 @@ jspm_packages/
4547
#
4648
public/files/*
4749
provisional/*
50+
privacy/*
4851

4952
# Snowpack dependency directory (https://snowpack.dev/)
5053
web_modules/

index.js

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,18 @@ const tinify = require('tinify');
55
const path = require('path');
66
const fs = require('fs');
77
const mime = require('mime-types');
8+
const sharp = require('sharp');
89
const { checkAndCreateTable } = require('./utils/checkAndCreateTable');
910
const pool = require('./utils/db');
11+
const { appendSuffixToFilename } = require('./utils/appendSuffixToFilename');
1012
require('dotenv').config({ path: '.env.local' });
1113

1214
const app = new Koa();
1315
const router = new Router();
1416

15-
// TinyPNG API 密钥
1617
tinify.key = process.env.TINIFY_KEY;
1718

18-
// Tinify 支持的文件格式
1919
const tinifySupportedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
20-
21-
// 所有图片格式
2220
const imageMimeTypes = [
2321
'image/jpeg',
2422
'image/png',
@@ -30,10 +28,8 @@ const imageMimeTypes = [
3028
'image/svg+xml'
3129
];
3230

33-
// 设置静态文件服务目录
3431
app.use(require('koa-static')(path.join(__dirname, 'public')));
3532

36-
// 创建必要的目录
3733
const createDirectories = () => {
3834
const dirs = [
3935
path.join(__dirname, 'provisional'),
@@ -48,7 +44,6 @@ const createDirectories = () => {
4844

4945
createDirectories();
5046

51-
// 配置 koaBody 中间件处理文件上传
5247
app.use(
5348
koaBody({
5449
multipart: true,
@@ -59,36 +54,44 @@ app.use(
5954
})
6055
);
6156

62-
// 文件上传和压缩的路由
6357
router.post('/upload', async (ctx) => {
6458
const connection = await pool.getConnection();
6559
try {
66-
// 获取上传的文件
6760
const files = ctx.request.files.file;
6861
const fileList = Array.isArray(files) ? files : [files];
6962
const responses = [];
7063

71-
// 检查是否需要压缩
7264
const compress = ctx.query.compress !== 'false'; // 默认压缩
73-
// 检查是否保留临时文件
7465
const keepTemp = ctx.query.keepTemp === 'true'; // 默认不保留临时文件
75-
// 获取请求的返回类型
66+
const isThumb = ctx.query.isThumb === 'true';
7667
const responseType = ctx.query.type;
7768

7869
for (const file of fileList) {
79-
// 获取文件的 MIME 类型
8070
const mimeType = mime.lookup(file.filepath);
8171

82-
// 生成输出文件路径
8372
const outputFilePath = path.join(
8473
__dirname,
8574
'public',
8675
'files',
8776
path.basename(file.filepath)
8877
);
78+
let outputFileThumbPath = null;
79+
if (isThumb && imageMimeTypes.includes(mimeType)) {
80+
const fileThumbName = appendSuffixToFilename(file.newFilename, 'thumb');
81+
82+
outputFileThumbPath = path.join(
83+
__dirname,
84+
'public',
85+
'files',
86+
fileThumbName
87+
);
88+
89+
await sharp(file.filepath)
90+
.resize(200, 200) // 调整图像大小为200x200像素
91+
.toFile(outputFileThumbPath);
92+
}
8993

9094
if (compress && tinifySupportedMimeTypes.includes(mimeType)) {
91-
// 如果文件类型受 Tinify 支持且要求压缩,使用 TinyPNG API 压缩文件
9295
await tinify.fromFile(file.filepath).toFile(outputFilePath);
9396
} else {
9497
// 如果不支持压缩或者不要求压缩,保留临时文件则复制文件,否则移动文件
@@ -99,42 +102,39 @@ router.post('/upload', async (ctx) => {
99102
}
100103
}
101104

102-
// 构建文件 URL
103105
const fileUrl = `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${path.basename(
104106
outputFilePath
105107
)}`;
108+
const thumb_location = outputFileThumbPath ? `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${path.basename(outputFileThumbPath)}` : null;
106109

107-
// 插入文件信息到数据库
108110
await connection.execute(
109111
`INSERT INTO files (
110-
filename, filesize, filelocation, created_by, is_public
111-
) VALUES (?, ?, ?, ?, ?)`,
112+
filename, filesize, filelocation, created_by, is_public, thumb_location, is_delete
113+
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
112114
[
113115
path.basename(outputFilePath),
114116
fs.statSync(outputFilePath).size,
115117
fileUrl,
116118
ctx.query.createdBy || 'anonymous',
117-
ctx.query.isPublic === 'true'
119+
ctx.query.isPublic === 'true',
120+
thumb_location,
121+
0 // 假设默认的 `is_delete` 状态为 0(未删除)
118122
]
119123
);
120124

121125
if (responseType === 'md' && imageMimeTypes.includes(mimeType)) {
122-
// 如果文件是图片格式且请求类型是 'md',返回 Markdown 格式
123126
responses.push({
124127
filepath: `![${path.basename(outputFilePath)}](${fileUrl})`
125128
});
126129
} else {
127-
// 否则返回普通 URL
128130
responses.push({ filepath: fileUrl });
129131
}
130132

131133
if (!keepTemp && fs.existsSync(file.filepath)) {
132-
// 删除临时上传文件
133134
fs.unlinkSync(file.filepath);
134135
}
135136
}
136137

137-
// 返回响应
138138
ctx.body = fileList.length > 1 ? responses : responses[0];
139139
} catch (error) {
140140
ctx.status = 500;
@@ -144,15 +144,12 @@ router.post('/upload', async (ctx) => {
144144
}
145145
});
146146

147-
// 获取文件信息的路由
148147
router.get('/files', async (ctx) => {
149148
const connection = await pool.getConnection();
150149
try {
151-
// 获取分页参数
152150
const limit = parseInt(ctx.query.limit, 10) || 10; // 每页数量,默认为 10
153151
const offset = parseInt(ctx.query.offset, 10) || 0; // 偏移量,默认为 0
154152

155-
// 执行查询
156153
const [rows] = await connection.execute(
157154
`SELECT * FROM files LIMIT ? OFFSET ?`,
158155
[limit, offset]
@@ -167,7 +164,6 @@ router.get('/files', async (ctx) => {
167164
}
168165
});
169166

170-
// 启动服务器
171167
app.use(router.routes()).use(router.allowedMethods());
172168

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

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"mysql2": "^3.10.1",
2323
"pm2": "^5.4.0",
2424
"tinify": "^1.7.1",
25-
"uuid": "^10.0.0"
25+
"uuid": "^10.0.0",
26+
"sharp": "0.31.0"
2627
}
2728
}

public/index.html

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta charset="UTF-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
77
<title>批量上传文件和图床</title>
8-
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
8+
<link href="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/element-plus/2.0.4/index.css" type="text/css" rel="stylesheet" />
99
<style>
1010
#app {
1111
padding: 20px;
@@ -47,8 +47,8 @@
4747

4848
<body>
4949
<div id="app"></div>
50-
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
51-
<script src="https://unpkg.com/element-plus@2.7.5/dist/index.full.js"></script>
50+
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/3.2.31/vue.global.prod.min.js" type="application/javascript"></script>
51+
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/element-plus/2.0.4/index.full.min.js" type="application/javascript"></script>
5252
<script>
5353
const { createApp, ref, onMounted } = Vue;
5454
const { ElTabs, ElTabPane, ElUpload, ElButton, ElMessage, ElSelect, ElOption, ElImage, ElLoading } = ElementPlus;
@@ -72,7 +72,7 @@
7272
const data = await response.json();
7373
images.value = data.map(file => ({
7474
...file,
75-
preview: `${file.url}`
75+
preview: `${file.thumb_location ?? file.filelocation}`
7676
}));
7777
} catch (error) {
7878
ElMessage.error('获取图片列表失败');
@@ -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', {
89+
const response = await fetch('https://source.giao.club/upload?type=md&compress=false&isThumb=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', {
123+
const response = await fetch('https://source.giao.club/upload?type=md&compress=false&keepTemp=true&isThumb=true', {
124124
method: 'POST',
125125
body: formData,
126126
});
@@ -183,14 +183,11 @@
183183
<el-option label="文件" value="files"></el-option>
184184
</el-select>
185185
</div>
186-
{{images}}
187-
<el-loading :loading="loading">
188186
<div class="waterfall">
189187
<div class="waterfall-item" v-for="(image, index) in images" :key="index">
190188
<el-image :src="image.preview" :alt="image.filename" fit="cover" />
191189
</div>
192190
</div>
193-
</el-loading>
194191
</el-tab-pane>
195192
</el-tabs>
196193
`

0 commit comments

Comments
 (0)