From 0fc18c8176f2f06b9874f558a090440e89b3712d Mon Sep 17 00:00:00 2001 From: rosendo <42633099+rosendolu@users.noreply.github.com> Date: Thu, 2 May 2024 23:34:32 +0800 Subject: [PATCH] feat: file api --- .gitignore | 1 + app/common/constant.js | 3 + app/common/logger.js | 6 +- app/index.js | 3 + app/middleware/file.js | 29 +++++ app/middleware/index.js | 7 +- app/middleware/user.js | 4 +- app/router/content-type.route.js | 5 +- app/router/file.route.js | 7 ++ app/router/index.js | 4 +- app/router/root.route.js | 1 + doc/file.md | 23 ++++ package-lock.json | 137 ++++++++++++++++++++- package.json | 3 + static/{index.json.text => index.json.txt} | 0 test/file.sh | 3 + 16 files changed, 224 insertions(+), 12 deletions(-) create mode 100644 app/middleware/file.js create mode 100644 app/router/file.route.js rename static/{index.json.text => index.json.txt} (100%) create mode 100644 test/file.sh diff --git a/.gitignore b/.gitignore index 05aac98..a8ab8bd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ logs/* *.log *.local +temp/ diff --git a/app/common/constant.js b/app/common/constant.js index d814f61..8d7ad17 100644 --- a/app/common/constant.js +++ b/app/common/constant.js @@ -1,5 +1,8 @@ +const path = require('path'); + module.exports = { isProdEnv: !/dev/i.test(process.env.NODE_ENV || ''), + rootDir: path.resolve(__dirname, '../../'), koaSessionConfig: { maxAge: 864e5 * 3e4, // 3e4 days httpOnly: true /** (boolean) httpOnly or not (default true) */, diff --git a/app/common/logger.js b/app/common/logger.js index d8327bd..a187d19 100644 --- a/app/common/logger.js +++ b/app/common/logger.js @@ -1,9 +1,11 @@ const winston = require('winston'); const utils = require('./utils'); const path = require('path'); -const { isProdEnv } = require('./constant'); +const { isProdEnv, rootDir } = require('./constant'); +const { ensureDirSync } = require('fs-extra'); -const appLogPath = path.resolve('logs', utils.timestamp().split(' ')[0]); +const appLogPath = path.resolve(rootDir, 'temp/logs', utils.timestamp().split(' ')[0]); +ensureDirSync(appLogPath); const loggerFileNameList = ['app']; const fileFormat = winston.format.combine( diff --git a/app/index.js b/app/index.js index 5bf4a49..6fd0786 100644 --- a/app/index.js +++ b/app/index.js @@ -1,4 +1,5 @@ const Koa = require('koa'); +const serve = require('koa-static'); const logger = require('./common/logger'); const cors = require('@koa/cors'); const router = require('./router/index'); @@ -11,12 +12,14 @@ require('dotenv').config({ path: ['.env', '.env.local'], override: true }); const koaSession = require('koa-session'); const constant = require('./common/constant'); const user = require('./middleware/user'); +const path = require('node:path'); const app = new Koa(); app.keys = [process.env.SESSION_KEYS]; app.use(commonHandle()); app.use(koaSession(constant.koaSessionConfig, app)); app.use(user.userHandle()); app.use(useKoaBody()); +app.use(serve(path.join(constant.rootDir, 'temp/upload'))); app.use( cors({ credentials: true, diff --git a/app/middleware/file.js b/app/middleware/file.js new file mode 100644 index 0000000..91032d1 --- /dev/null +++ b/app/middleware/file.js @@ -0,0 +1,29 @@ +const assert = require('assert'); +const { ensureDir, move } = require('fs-extra'); +const path = require('node:path'); +const util = require('node:util'); +const { rootDir } = require('../common/constant'); +const { glob } = require('glob'); +module.exports = { + async uploadFile(ctx, next) { + const { uid, nickname } = ctx.session; + let fileArr = []; + // multiple files + const files = ctx.request.files.file; + assert(files, 'Empty file'); + for (let i = 0; i < (files.length || 1); i++) { + const { filepath, mimetype, newFilename, originalFilename, size } = files[i] || files; + const userFilePath = path.join(rootDir, 'temp/upload/', uid, newFilename); + await ensureDir(path.dirname(userFilePath)); + await move(filepath, userFilePath, { overwrite: true }); + ctx.body = newFilename; + } + await next(); + }, + async listFile(ctx, next) { + let { uid, nickname } = ctx.session; + const userFilePath = path.join(rootDir, 'temp/upload/', uid); + const list = await glob('./**', { cwd: userFilePath, dot: false, maxDepth: 1 }); + ctx.body = list; + }, +}; diff --git a/app/middleware/index.js b/app/middleware/index.js index 28b6974..f1d1e56 100644 --- a/app/middleware/index.js +++ b/app/middleware/index.js @@ -1,7 +1,8 @@ const { koaBody } = require('koa-body'); const path = require('path'); const logger = require('../common/logger'); -const { isProdEnv } = require('../common/constant'); +const { isProdEnv, rootDir } = require('../common/constant'); +const { ensureDirSync } = require('fs-extra'); module.exports = { commonHandle: function commonHandle() { @@ -49,6 +50,8 @@ module.exports = { }; }, useKoaBody: function useKoaBody() { + const uploadDir = path.resolve(rootDir, 'temp/upload'); + ensureDirSync(uploadDir); return koaBody({ multipart: true, jsonLimit: '100mb', @@ -56,7 +59,7 @@ module.exports = { textLimit: '100mb', formidable: { maxFieldsSize: 100 * 1024 * 1024, // 100mb - uploadDir: path.resolve('./temp'), + uploadDir: uploadDir, keepExtensions: true, }, }); diff --git a/app/middleware/user.js b/app/middleware/user.js index 12b6aaf..cf99a4c 100644 --- a/app/middleware/user.js +++ b/app/middleware/user.js @@ -5,14 +5,14 @@ const { isProdEnv } = require('../common/constant'); const utils = require('../common/utils'); module.exports = { - userHandle: function userHandle() { + userHandle() { return async function commonHandleMiddleware(ctx, next) { if (ctx.session.isNew) { ctx.session.visitCount = 0; ctx.session.uid = utils.uid(); ctx.session.lastVisit = utils.timestamp(); + ctx.session.nickname = utils.faker.name(); } - !ctx.session.nickname && (ctx.session.nickname = utils.faker.name()); await next(); ctx.session.visitCount += 1; ctx.session.lastVisit = utils.timestamp(); diff --git a/app/router/content-type.route.js b/app/router/content-type.route.js index 94dcb9b..810e833 100644 --- a/app/router/content-type.route.js +++ b/app/router/content-type.route.js @@ -4,6 +4,7 @@ const { getStaticFile } = require('../common/utils'); const logger = require('../common/logger'); const utils = require('../common/utils'); const JSONStream = require('../common/jsonStream'); +const { rootDir } = require('../common/constant'); const prefix = '/res'; module.exports = router => { @@ -53,9 +54,7 @@ module.exports = router => { ctx.type = 'application/json'; // WARNNING json must be write one-time // ctx.body = getStaticFile('index.json'); - ctx.body = JSON.parse( - fs.readFileSync(path.resolve(__dirname, '../../static/index.json')).toString() - ); + ctx.body = JSON.parse(fs.readFileSync(path.resolve(rootDir, 'static/index.json')).toString()); break; } await next(); diff --git a/app/router/file.route.js b/app/router/file.route.js new file mode 100644 index 0000000..16ce886 --- /dev/null +++ b/app/router/file.route.js @@ -0,0 +1,7 @@ +const fileMiddleware = require('../middleware/file'); + +const prefix = '/file'; +module.exports = router => { + router.post(`${prefix}/upload`, fileMiddleware.uploadFile); + router.get(`${prefix}/list`, fileMiddleware.listFile); +}; diff --git a/app/router/index.js b/app/router/index.js index a94abb3..7796341 100644 --- a/app/router/index.js +++ b/app/router/index.js @@ -2,9 +2,9 @@ const Router = require('@koa/router'); const { glob } = require('glob'); const logger = require('../common/logger'); const path = require('path'); -const { isProdEnv } = require('../common/constant'); +const { isProdEnv, rootDir } = require('../common/constant'); const router = new Router(); -const routerFiles = glob.sync('./**/*.route.js', { cwd: path.resolve('./app/router') }); +const routerFiles = glob.sync('./**/*.route.js', { cwd: path.resolve(rootDir, 'app/router') }); if (!isProdEnv) { logger.debug('Router/index.js %O', routerFiles); diff --git a/app/router/root.route.js b/app/router/root.route.js index 5b70527..41c16a0 100644 --- a/app/router/root.route.js +++ b/app/router/root.route.js @@ -1,5 +1,6 @@ const dayjs = require('dayjs'); const utils = require('../common/utils'); + module.exports = router => { router.all('/', ctx => { const { nickname, visitCount, uid, lastVisit } = ctx.session; diff --git a/doc/file.md b/doc/file.md index 7ac8882..3ccef83 100644 --- a/doc/file.md +++ b/doc/file.md @@ -1 +1,24 @@ # File + +> prefix: `/file` + +## post `/upload` + +Content-Type: multipart/form-data + +req: + +```sh +curl 'http://localhost:3000/file/upload' \ + -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryVuSkafv2P53NsDZB' \ + --data-raw $'------WebKitFormBoundaryVuSkafv2P53NsDZB\r\nContent-Disposition: form-data; name="file"; filename="qr-code.png"\r\nContent-Type: image/png\r\n\r\n\r\n------WebKitFormBoundaryVuSkafv2P53NsDZB--\r\n' + +``` + +## get `/list` + +res + +```json +{ "data": [".", "7262f7574b348cb21528d7e01.png"], "error": null, "message": "OK", "status": 200, "duration": 9 } +``` diff --git a/package-lock.json b/package-lock.json index 592d2d1..5d0d054 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,16 +16,19 @@ "dayjs": "^1.11.10", "dotenv": "^16.4.5", "env-cmd": "^10.1.0", + "fs-extra": "^11.2.0", "glob": "^10.3.12", "koa": "^2.15.2", "koa-body": "^6.0.1", "koa-session": "^6.4.0", + "koa-static": "^5.0.0", "uuid": "^9.0.1", "winston": "^3.13.0" }, "devDependencies": { "@tsconfig/node20": "^20.1.4", "@tsconfig/recommended": "^1.0.6", + "@types/fs-extra": "^11.0.4", "@types/koa": "^2.15.0", "@typescript-eslint/eslint-plugin": "^7.6.0", "@typescript-eslint/parser": "^7.6.0", @@ -374,6 +377,16 @@ "@types/node": "*" } }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/http-assert": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", @@ -390,6 +403,15 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/keygrip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", @@ -1568,6 +1590,19 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1701,6 +1736,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2053,6 +2093,17 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -2136,6 +2187,19 @@ "node": ">= 10" } }, + "node_modules/koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dependencies": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/koa-session": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/koa-session/-/koa-session-6.4.0.tgz", @@ -2158,6 +2222,26 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dependencies": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa-static/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -2441,7 +2525,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2614,6 +2697,50 @@ "node": ">=4" } }, + "node_modules/resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", + "dependencies": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-path/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/resolve-path/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3059,6 +3186,14 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index a2718f6..f7db29e 100644 --- a/package.json +++ b/package.json @@ -29,14 +29,17 @@ "dayjs": "^1.11.10", "dotenv": "^16.4.5", "env-cmd": "^10.1.0", + "fs-extra": "^11.2.0", "glob": "^10.3.12", "koa": "^2.15.2", "koa-body": "^6.0.1", "koa-session": "^6.4.0", + "koa-static": "^5.0.0", "uuid": "^9.0.1", "winston": "^3.13.0" }, "devDependencies": { + "@types/fs-extra": "^11.0.4", "@tsconfig/node20": "^20.1.4", "@tsconfig/recommended": "^1.0.6", "@types/koa": "^2.15.0", diff --git a/static/index.json.text b/static/index.json.txt similarity index 100% rename from static/index.json.text rename to static/index.json.txt diff --git a/test/file.sh b/test/file.sh new file mode 100644 index 0000000..d10a09c --- /dev/null +++ b/test/file.sh @@ -0,0 +1,3 @@ +curl 'http://localhost:3000/file/upload' \ + -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryVuSkafv2P53NsDZB' \ + --data-raw $'------WebKitFormBoundaryVuSkafv2P53NsDZB\r\nContent-Disposition: form-data; name="file"; filename="qr-code.png"\r\nContent-Type: image/png\r\n\r\n\r\n------WebKitFormBoundaryVuSkafv2P53NsDZB--\r\n'