Skip to content

Commit a3d5445

Browse files
authored
Merge pull request #1 from CrazyMrYan/feat/full/1.0.0
feat: 支持各种格式上传&文件默认缩略图
2 parents c5435fc + 0bb3c4c commit a3d5445

File tree

17 files changed

+375
-209
lines changed

17 files changed

+375
-209
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,31 @@ nodejs 实现文件上传功能
3030

3131
### 创建表的字段说明
3232

33+
> 启动项目则会,自动创建,默认创建 `files` 表结构
34+
35+
```sql
36+
CREATE TABLE files (
37+
id VARCHAR(50) DEFAULT NULL,
38+
filename VARCHAR(255) NOT NULL,
39+
filesize BIGINT(20) NOT NULL,
40+
filelocation VARCHAR(255) NOT NULL,
41+
created_by VARCHAR(255) NOT NULL,
42+
created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
43+
updated_by VARCHAR(255) DEFAULT NULL,
44+
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
45+
is_public TINYINT(1) DEFAULT '0',
46+
public_expiration TIMESTAMP NULL DEFAULT NULL,
47+
public_by VARCHAR(255) DEFAULT NULL,
48+
is_thumb TINYINT(1) DEFAULT NULL,
49+
thumb_location VARCHAR(255) DEFAULT NULL,
50+
is_delete TINYINT(1) NOT NULL DEFAULT '0',
51+
real_file_location VARCHAR(255) DEFAULT NULL,
52+
real_file_thumb_location VARCHAR(255) DEFAULT NULL,
53+
mime VARCHAR(255) DEFAULT NULL,
54+
ext VARCHAR(50) DEFAULT NULL,
55+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
56+
```
57+
3358
| 列名 | 数据类型 | 是否为空 | 默认值 | 注释 |
3459
|------------------|----------------|----------|----------------------|----------------------------------------------|
3560
| `id` | int(11) | NOT NULL | | 文件的唯一标识 |

constants/file.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const tinifySupportedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
2+
3+
const imageMimeTypes = [
4+
'image/jpeg',
5+
'image/png',
6+
'image/webp',
7+
'image/gif',
8+
'image/bmp',
9+
'image/tiff',
10+
'image/x-icon',
11+
'image/svg+xml'
12+
];
13+
14+
module.exports = {
15+
imageMimeTypes,
16+
tinifySupportedMimeTypes
17+
}

index.js

Lines changed: 98 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,20 @@ const { koaBody } = require('koa-body');
44
const tinify = require('tinify');
55
const path = require('path');
66
const fs = require('fs');
7-
const mime = require('mime-types');
87
const sharp = require('sharp');
98
const { checkAndCreateTable } = require('./utils/checkAndCreateTable');
109
const pool = require('./utils/db');
1110
const { appendSuffixToFilename } = require('./utils/appendSuffixToFilename');
1211
const { v4: uuidv4 } = require('uuid');
12+
const { detectFileType } = require('./utils/detectFileType');
13+
const { imageMimeTypes, tinifySupportedMimeTypes} = require('./constants/file')
1314
require('dotenv').config({ path: '.env.local' });
1415

1516
const app = new Koa();
1617
const router = new Router();
1718

1819
tinify.key = process.env.TINIFY_KEY;
1920

20-
const tinifySupportedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
21-
const imageMimeTypes = [
22-
'image/jpeg',
23-
'image/png',
24-
'image/webp',
25-
'image/gif',
26-
'image/bmp',
27-
'image/tiff',
28-
'image/x-icon',
29-
'image/svg+xml'
30-
];
31-
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-
4921
app.use(require('koa-static')(path.join(__dirname, 'public')));
5022

5123
const createDirectories = () => {
@@ -86,7 +58,6 @@ router.post('/upload', async (ctx) => {
8658
const responseType = ctx.query.type;
8759

8860
for (const file of fileList) {
89-
const mimeType = mime.lookup(file.filepath);
9061
const fileId = uuidv4(); // 生成文件唯一ID
9162

9263
const outputFilePath = path.join(
@@ -96,8 +67,10 @@ router.post('/upload', async (ctx) => {
9667
fileId + path.extname(file.filepath) // 使用UUID作为文件名称
9768
);
9869

70+
const { mime, ext } = await detectFileType(file.filepath, file);
71+
9972
let outputFileThumbPath = null;
100-
if (isThumb && imageMimeTypes.includes(mimeType)) {
73+
if (isThumb && imageMimeTypes.includes(mime)) {
10174
const fileThumbName = `${fileId}_thumb${path.extname(file.filepath)}`; // 缩略图文件名称
10275

10376
outputFileThumbPath = path.join(
@@ -110,9 +83,22 @@ router.post('/upload', async (ctx) => {
11083
await sharp(file.filepath)
11184
.resize(200, 200) // 调整图像大小为200x200像素
11285
.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;
11399
}
114100

115-
if (compress && tinifySupportedMimeTypes.includes(mimeType)) {
101+
if (compress && tinifySupportedMimeTypes.includes(mime)) {
116102
await tinify.fromFile(file.filepath).toFile(outputFilePath);
117103
} else {
118104
// 如果不支持压缩或者不要求压缩,保留临时文件则复制文件,否则移动文件
@@ -128,8 +114,20 @@ router.post('/upload', async (ctx) => {
128114

129115
await connection.execute(
130116
`INSERT INTO files (
131-
id, filename, filesize, filelocation, real_file_location, created_by, is_public, thumb_location, is_thumb, is_delete, real_file_thumb_location
132-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
133131
[
134132
fileId, // 使用UUID作为ID
135133
path.basename(outputFilePath),
@@ -141,11 +139,13 @@ router.post('/upload', async (ctx) => {
141139
thumb_location,
142140
isThumb,
143141
0,
144-
outputFileThumbPath
142+
outputFileThumbPath,
143+
mime,
144+
ext
145145
]
146146
);
147147

148-
if (responseType === 'md' && imageMimeTypes.includes(mimeType)) {
148+
if (responseType === 'md' && imageMimeTypes.includes(mime)) {
149149
responses.push({
150150
filepath: `![${path.basename(outputFilePath)}](${fileUrl})`
151151
});
@@ -167,17 +167,58 @@ router.post('/upload', async (ctx) => {
167167
}
168168
});
169169

170-
171170
router.get('/files', async (ctx) => {
172171
const connection = await pool.getConnection();
173172
try {
174173
const limit = parseInt(ctx.query.limit, 10) || 10; // 每页数量,默认为 10
175174
const offset = parseInt(ctx.query.offset, 10) || 0; // 偏移量,默认为 0
175+
const type = ctx.query.type ?? ''; // 获取查询参数中的类型
176176

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+
// 执行查询
177217
const [rows] = await connection.execute(
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 ?`,
218+
sql,
179219
[String(limit), String(offset)]
180220
);
221+
181222

182223
ctx.body = rows;
183224
} catch (error) {
@@ -196,12 +237,22 @@ router.get('/files/:id', async (ctx) => {
196237
try {
197238
// 查询文件数据,只获取必要字段
198239
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]
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]
205256
);
206257

207258
if (rows.length === 0) {
@@ -224,9 +275,9 @@ router.get('/files/:id', async (ctx) => {
224275
ctx.body = { message: 'File not found' };
225276
return;
226277
}
227-
278+
const { mime } = await detectFileType(fileLocation);
228279
// 设置响应头
229-
ctx.set('Content-Type', getMimeType(fileLocation));
280+
ctx.set('Content-Type', mime);
230281
ctx.set('Content-Disposition', `inline; filename="${file.filename}"`);
231282

232283
// 返回文件流

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
"@koa/cors": "^5.0.0",
1515
"axios": "^1.7.2",
1616
"dotenv": "^16.4.5",
17+
"file-type": "^19.0.0",
1718
"koa": "^2.15.3",
1819
"koa-body": "^6.0.1",
1920
"koa-router": "^12.0.1",
2021
"koa-static": "^5.0.0",
2122
"koa2-cors": "^2.0.6",
22-
"mime-types": "^2.1.35",
2323
"mysql2": "^3.10.1",
2424
"pm2": "^5.4.0",
2525
"sharp": "0.31.0",

public/icons/doc.png

3.57 KB
Loading

public/icons/document.png

2.42 KB
Loading

public/icons/folders.png

3.19 KB
Loading

public/icons/pdf.png

3.3 KB
Loading

public/icons/psd.png

3.54 KB
Loading

public/icons/shared_folders.png

3.13 KB
Loading

0 commit comments

Comments
 (0)