diff --git a/.env b/.env index fb875b2..a072693 100644 --- a/.env +++ b/.env @@ -13,3 +13,5 @@ DB_HOST=localhost DB_PORT=5432 DB_NAME=est-db DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} + +FILE_STORAGE_PATH=storage diff --git a/.env.example b/.env.example index 8abc455..ffcd84a 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,5 @@ DB_HOST=localhost DB_PORT=5432 DB_NAME=est-db DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} + +FILE_STORAGE_PATH=storage diff --git a/.gitignore b/.gitignore index a07df91..49668a2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ pnpm-debug.log* # Misc .DS_Store -dist coverage *.local @@ -31,4 +30,5 @@ coverage *.sw? *.zip -temp +/storage +/temp diff --git a/package.json b/package.json index 9e39225..ba97985 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "http-errors": "~2.0.0", "jsonwebtoken": "^9.0.0", "morgan": "~1.10.0", + "multer": "1.4.5-lts.1", "pg": "^8.11.0", "prisma": "^4.14.1", "tsconfig-paths": "^4.2.0", @@ -59,6 +60,7 @@ "@types/http-errors": "^2.0.1", "@types/jsonwebtoken": "^9.0.2", "@types/morgan": "^1.9.4", + "@types/multer": "^1.4.7", "@types/node": "20.2.3", "@types/pg": "^8.10.1", "@types/uuid": "^9.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69d2147..0350308 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ dependencies: morgan: specifier: ~1.10.0 version: 1.10.0 + multer: + specifier: 1.4.5-lts.1 + version: 1.4.5-lts.1 pg: specifier: ^8.11.0 version: 8.11.0 @@ -75,6 +78,9 @@ devDependencies: '@types/morgan': specifier: ^1.9.4 version: 1.9.4 + '@types/multer': + specifier: ^1.4.7 + version: 1.4.7 '@types/node': specifier: 20.2.3 version: 20.2.3 @@ -812,6 +818,12 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true + /@types/multer@1.4.7: + resolution: {integrity: sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==} + dependencies: + '@types/express': 4.17.17 + dev: true + /@types/node@20.2.3: resolution: {integrity: sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==} dev: true @@ -1075,6 +1087,10 @@ packages: picomatch: 2.3.1 dev: true + /append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + dev: false + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true @@ -1230,6 +1246,10 @@ packages: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} dev: false + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + /buffer-writer@2.0.0: resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} engines: {node: '>=4'} @@ -1242,6 +1262,13 @@ packages: run-applescript: 5.0.0 dev: true + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + /bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1373,6 +1400,16 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + dev: false + /configstore@5.0.1: resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} engines: {node: '>=8'} @@ -1412,7 +1449,6 @@ packages: /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: true /cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} @@ -2800,6 +2836,10 @@ packages: is-docker: 2.2.1 dev: true + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3052,6 +3092,13 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: false + /morgan@1.10.0: resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} engines: {node: '>= 0.8.0'} @@ -3075,6 +3122,19 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /multer@1.4.5-lts.1: + resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} + engines: {node: '>= 6.0.0'} + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + dev: false + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -3575,6 +3635,10 @@ packages: '@prisma/engines': 4.14.1 dev: false + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3637,6 +3701,18 @@ packages: path-type: 3.0.0 dev: true + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3927,6 +4003,11 @@ packages: engines: {node: '>= 0.8'} dev: false + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + /string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3984,6 +4065,12 @@ packages: es-abstract: 1.21.2 dev: true + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4190,6 +4277,10 @@ packages: is-typedarray: 1.0.0 dev: true + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: false + /typescript@5.0.4: resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} @@ -4232,6 +4323,10 @@ packages: punycode: 2.3.0 dev: true + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} diff --git a/src/app.ts b/src/app.ts index 5350925..1494baa 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,8 @@ import bodyParser from 'body-parser' +import chalk from 'chalk' import type { Express, NextFunction, Request, Response } from 'express' import express from 'express' +import fs from 'fs' import createError from 'http-errors' import logger from 'morgan' import morgan from 'morgan' @@ -8,6 +10,8 @@ import path from 'path' import routes from '@/routes' +import { GlobalFileStorageConfig } from './shared' + const App: Express = express() App.use(logger('dev')) @@ -18,9 +22,17 @@ App.use(bodyParser.json()) App.use(bodyParser.urlencoded({ extended: true })) // Static files setup -App.use('/public', express.static(path.join(__dirname, '@/public'))) App.use('/static', express.static(path.join(__dirname, '@/static'))) +// Create file storage folder if not exists +const storageFolder = GlobalFileStorageConfig.FILE_STORAGE_PATH +try { + fs.accessSync(storageFolder) +} catch (e) { + fs.mkdirSync(storageFolder, { recursive: true }) + console.log(chalk.green('[File Storage] File storage folder created.')) +} + // Init routes routes.forEach((route) => { App.use(route.path, route.router) diff --git a/src/prisma/seed.ts b/src/prisma/seed.ts index 3d6d698..ad8653f 100644 --- a/src/prisma/seed.ts +++ b/src/prisma/seed.ts @@ -12,18 +12,14 @@ const SEED_USER = 'Admin' const defaultUser: Prisma.UserCreateInput = { uuid: generateUUID(), - username: 'BruceSong', - email: 'recall4056@gmail.com', + username: 'Admin', password: '$2b$10$kZEDiHCgDhFmX7/sKwYm1ORMK99FNk/QQgebcwBflKrAWKGA.D46W', - name: 'Bruce Song', - firstName: 'Bruce', - lastName: 'Song', + name: 'Admin', gender: Gender.UNDEFINED, phoneNumber: randPhoneNumber(), birthDate: randPastDate(), address: randFullAddress(), avatarUrl: randAvatar(), - biography: 'I am a Web developer.', verified: true, enabled: true, roles: Array.of(Role.ADMIN), diff --git a/src/routes/index.ts b/src/routes/index.ts index 7aca361..22cc2ea 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,6 +1,7 @@ import homeRouter from './home.controller' import loginRouter from './login.controller' import settingsRouter from './settings.controller' +import uploadRouter from './upload.controller' import userRouter from './users.controller' const routes = [ @@ -19,6 +20,10 @@ const routes = [ { path: '/settings', router: settingsRouter + }, + { + path: '/upload', + router: uploadRouter } ] diff --git a/src/routes/upload.controller.ts b/src/routes/upload.controller.ts new file mode 100644 index 0000000..ab28619 --- /dev/null +++ b/src/routes/upload.controller.ts @@ -0,0 +1,43 @@ +import type { Request, Response, Router } from 'express' +import express from 'express' +import { readFileSync } from 'fs' + +import { UploadService } from '@/services/upload' +import { upload } from '@/shared' +import type { BaseResponse } from '@/types' + +const router: Router = express.Router() + +router.get('/', (request: Request, response: Response) => { + const file = readFileSync('storage/images/a278294ea9942307a598d89ba88f572a', { encoding: 'utf8' }) + console.log(file) + response.set('content-type', 'image/png') + response.status(200).send(file) +}) + +router.post('/', upload.single('file'), async (request: Request, response: BaseResponse) => { + const { file } = request + if (!file) { + response.status(400).json({ + message: 'File is required.' + }) + return + } + + UploadService.logFileInfo(file) + + response.json({ + data: file + }) +}) + +router.post('/batch', upload.array('files'), async (request: Request, response: BaseResponse) => { + const { files } = request + + console.log(files) + response.json({ + data: '' + }) +}) + +export default router diff --git a/src/services/upload/index.ts b/src/services/upload/index.ts new file mode 100644 index 0000000..4d7e0dc --- /dev/null +++ b/src/services/upload/index.ts @@ -0,0 +1,2 @@ +export * from './upload.models' +export * as UploadService from './upload.services' diff --git a/src/services/upload/upload.models.ts b/src/services/upload/upload.models.ts new file mode 100644 index 0000000..54a4686 --- /dev/null +++ b/src/services/upload/upload.models.ts @@ -0,0 +1,6 @@ +export type UploadFileInfo = { + mimetype: string + originalname: string + size: string + path: string +} diff --git a/src/services/upload/upload.services.ts b/src/services/upload/upload.services.ts new file mode 100644 index 0000000..c3a7b7d --- /dev/null +++ b/src/services/upload/upload.services.ts @@ -0,0 +1,9 @@ +// export const processUploadFile = async (file: File): Promise => {} + +export const logFileInfo = (file: Express.Multer.File) => { + const { mimetype, originalname, size, path } = file + console.log('Mime Type: %s', mimetype) + console.log('Original Name: %s', originalname) + console.log('File Size: %s', size) + console.log('File Path: %s', path) +} diff --git a/src/shared/config/global-config.ts b/src/shared/config/global-config.ts index bc4663b..2a128dd 100644 --- a/src/shared/config/global-config.ts +++ b/src/shared/config/global-config.ts @@ -31,3 +31,7 @@ export const GlobalDBConfig = Object.freeze({ DB_NAME: getEnvStr('DB_NAME', 'est-db'), DB_URL: getEnvStr('DB_URL', 'postgresql://mars-user:mars-password@localhost:5432/est-db') }) + +export const GlobalFileStorageConfig = Object.freeze({ + FILE_STORAGE_PATH: getEnvStr('FILE_STORAGE_PATH', 'storage') +}) diff --git a/src/shared/file-storage.ts b/src/shared/file-storage.ts new file mode 100644 index 0000000..ef50a46 --- /dev/null +++ b/src/shared/file-storage.ts @@ -0,0 +1,16 @@ +import multer from 'multer' + +import { GlobalFileStorageConfig } from './config' + +const storage = multer.diskStorage({ + destination(req, file, cb) { + cb(null, GlobalFileStorageConfig.FILE_STORAGE_PATH) + }, + filename(req, file, cb) { + cb(null, `${file.filename}-${Date.now()}`) + } +}) + +const upload = multer({ storage }) + +export { upload } diff --git a/src/shared/index.ts b/src/shared/index.ts index 9a588ab..97427df 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1,3 +1,4 @@ export * from './config' +export * from './file-storage' export * from './prisma' export * from './uuid'