Skip to content

Commit

Permalink
Merge pull request #81 from Roger13579/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
y0000ga authored May 13, 2024
2 parents b3810a3 + 7248bcf commit dd1e8f6
Show file tree
Hide file tree
Showing 16 changed files with 410 additions and 13 deletions.
9 changes: 9 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ declare global {
FRONTEND_URL: string;
GMAIL_USER: string;
GMAIL_PASS: string;
FIREBASE_TYPE: string;
FIREBASE_PROJECT_ID: string;
FIREBASE_PRIVATE_KEY: string;
FIREBASE_CLIENT_EMAIL: string;
FIREBASE_CLIENT_ID: string;
FIREBASE_AUTH_URI: string;
FIREBASE_TOKEN_URI: string;
FIREBASE_AUTH_PROVIDER_X509_CERT_URL: string;
FIREBASE_CLIENT_X509_CERT_URL: string;
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/controller/uploadController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextFunction, Response } from 'express';
import { UploadService } from '../service/uploadService';
import { IUploadFileReq } from '../types/upload.type';
import { BaseController } from './baseController';
import { UploadFileDTO } from '../dto/uploadFileDto';
import { CustomResponseType } from '../types/customResponseType';
import { Request } from 'express';

class UploadController extends BaseController {
private readonly uploadService: UploadService = new UploadService();

public uploadFile = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const uploadFileDto = new UploadFileDTO(req as IUploadFileReq);
const url = await this.uploadService.uploadFile(uploadFileDto, res, next);
return this.formatResponse(
CustomResponseType.OK_MESSAGE,
CustomResponseType.OK,
{ url },
);
};
}

export default UploadController;
21 changes: 21 additions & 0 deletions src/dto/uploadFileDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { IUser } from '../models/user';
import { IUploadFileReq } from '../types/upload.type';
import path from 'path';

export class UploadFileDTO {
private readonly _file: Express.Multer.File;
private readonly _name: string;

get file() {
return this._file;
}
get name() {
return this._name;
}

constructor(req: IUploadFileReq) {
const { files, params, user } = req;
this._file = files[0];
this._name = `${params.type}/${params.category}/${(user as IUser)._id}${path.extname(files[0].originalname).toLowerCase()}`;
}
}
56 changes: 56 additions & 0 deletions src/repository/uploadRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextFunction } from 'express';
import firebaseAdmin from '../utils/firebase';
import { AppError } from '../utils/errorHandler';
import { HttpStatus } from '../types/responseType';
import { CustomResponseType } from '../types/customResponseType';

const storage = firebaseAdmin.storage();
const bucket = storage.bucket();

const blobConfig = {
action: 'read' as const,
expires: '12-31-2500',
};

export class UploadRepository {
public uploadFile = async (
file: Express.Multer.File,
name: string,
next: NextFunction,
) => {
const blob = bucket.file(name);
return await new Promise<string>((resolve, reject) => {
const blobStream = blob.createWriteStream();

blobStream
.on('finish', () => {
blob.getSignedUrl(blobConfig, (err, url) => {
if (err || !url) {
reject(
new AppError(
CustomResponseType.INVALID_UPLOAD,
HttpStatus.BAD_REQUEST,
CustomResponseType.INVALID_UPLOAD_MESSAGE +
'檔案 URL 取得失敗',
),
);
} else {
resolve(url);
}
});
})
.on('error', () => {
reject(
next(
new AppError(
CustomResponseType.INVALID_UPLOAD,
HttpStatus.BAD_REQUEST,
CustomResponseType.INVALID_UPLOAD_MESSAGE + '檔案上傳失敗',
),
),
);
})
.end(file.buffer);
});
};
}
2 changes: 2 additions & 0 deletions src/routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { UserRoute } from './userRoute';
import { BaseRoute } from './baseRoute';
import { ProductRoute } from './productRoute';
import { GroupRoute } from './groupRoute';
import { UploadRoute } from './uploadRoute';

export const router: Array<BaseRoute> = [
new IndexRoute(),
new UserRoute(),
new ProductRoute(),
new GroupRoute(),
new UploadRoute(),
];
55 changes: 55 additions & 0 deletions src/routes/uploadRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import UploadController from '../controller/uploadController';
import { UserVerify } from '../middleware/userVerify';
import { uploadFile } from '../utils/upload';
import { UploadFilePipe } from '../validator/upload/uploadFile.pipe';
import { BaseRoute } from './baseRoute';

export class UploadRoute extends BaseRoute {
protected controller!: UploadController;

constructor() {
super();
this.initial();
}

protected initial() {
this.controller = new UploadController();
this.setRouters();
}

protected setRouters() {
this.router.post(
'/v1/uploadFile/:type/:category',
/**
* #swagger.tags= ['Upload']
* #swagger.summary = '上傳檔案'
* #swagger.security=[{"Bearer": []}],
*/

/* #swagger.parameters['type'] = {
in: 'path',
required: true,
description: '上傳種類,e.g. photo',
type: 'string',
}
#swagger.parameters['category'] = {
in: 'path',
required: true,
description: '上傳類型,e.g. user / product',
type: 'string',
}
*/
/*
#swagger.responses[200] = {
description: 'OK',
schema: {
$ref: '#/definitions/UploadFileSuccess' }
}
*/
UserVerify,
this.usePipe(UploadFilePipe),
uploadFile,
this.responseHandler(this.controller.uploadFile),
);
}
}
28 changes: 28 additions & 0 deletions src/service/uploadService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextFunction, Response } from 'express';
import { UploadFileDTO } from '../dto/uploadFileDto';
import { UploadRepository } from '../repository/uploadRepository';
import { AppError } from '../utils/errorHandler';
import { CustomResponseType } from '../types/customResponseType';
import { HttpStatus } from '../types/responseType';

export class UploadService {
private readonly uploadRepository: UploadRepository = new UploadRepository();

public uploadFile = async (
uploadFileDto: UploadFileDTO,
res: Response,
next: NextFunction,
) => {
const { file, name } = uploadFileDto;
if (!file) {
return next(
new AppError(
CustomResponseType.INVALID_UPLOAD,
HttpStatus.BAD_REQUEST,
CustomResponseType.INVALID_UPLOAD_MESSAGE + '上傳檔案不得為空',
),
);
}
return await this.uploadRepository.uploadFile(file, name, next);
};
}
103 changes: 90 additions & 13 deletions src/swagger-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
{
"name": "Admin",
"description": "後台管理"
},
{
"name": "Upload",
"description": "上傳檔案"
}
],
"schemes": [
Expand Down Expand Up @@ -67,7 +71,7 @@
"type": "string"
},
{
"name": "obj123",
"name": "obj",
"in": "body",
"description": "欲建立的揪團資料",
"schema": {
Expand Down Expand Up @@ -517,6 +521,49 @@
]
}
},
"/v1/uploadFile/{type}/{category}": {
"post": {
"tags": [
"Upload"
],
"summary": "上傳檔案",
"description": "",
"parameters": [
{
"name": "type",
"in": "path",
"required": true,
"type": "string",
"description": "上傳種類,e.g. photo"
},
{
"name": "category",
"in": "path",
"required": true,
"type": "string",
"description": "上傳類型,e.g. user / product"
},
{
"name": "authorization",
"in": "header",
"type": "string"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/UploadFileSuccess"
}
}
},
"security": [
{
"Bearer": []
}
]
}
},
"/v1/user": {
"get": {
"tags": [
Expand Down Expand Up @@ -1013,19 +1060,19 @@
},
"sellEndAt": {
"type": "string",
"example": "2024-05-14T13:43:22.770Z"
"example": "2024-05-14T17:57:16.091Z"
},
"sellStartAt": {
"type": "string",
"example": "2024-05-14T11:43:22.771Z"
"example": "2024-05-14T15:57:16.093Z"
},
"endAt": {
"type": "string",
"example": "2024-05-14T17:43:22.771Z"
"example": "2024-05-14T21:57:16.093Z"
},
"startAt": {
"type": "string",
"example": "2024-05-14T15:43:22.771Z"
"example": "2024-05-14T19:57:16.093Z"
},
"tags": {
"type": "array",
Expand Down Expand Up @@ -1195,6 +1242,36 @@
"data"
]
},
"UploadFileSuccess": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "6000"
},
"message": {
"type": "string",
"example": "成功"
},
"data": {
"type": "object",
"properties": {
"url": {
"type": "string",
"example": "https://storage.googleapis.com/moviego-5071c.appspot.com/photo/user/6640962ff1a9debcf89679de.png"
}
},
"required": [
"url"
]
}
},
"required": [
"status",
"message",
"data"
]
},
"CustomCreateProductsObj": {
"type": "object",
"required": [
Expand Down Expand Up @@ -1352,26 +1429,26 @@
},
"sellEndAt": {
"type": "Date",
"example": "2024-05-14T13:43:22.770Z",
"min": "2024-05-13T12:43:22.771Z",
"example": "2024-05-14T17:57:16.091Z",
"min": "2024-05-13T16:57:16.094Z",
"description": "販賣結束時間,必須晚於販賣開始時間至少一個小時"
},
"sellStartAt": {
"type": "Date",
"example": "2024-05-14T11:43:22.771Z",
"min": "2024-05-13T11:43:22.771Z",
"example": "2024-05-14T15:57:16.093Z",
"min": "2024-05-13T15:57:16.094Z",
"description": "販賣開始時間,必須晚於現在時間至少一天"
},
"endAt": {
"type": "Date",
"example": "2024-05-14T17:43:22.771Z",
"min": "2024-05-13T14:43:22.771Z",
"example": "2024-05-14T21:57:16.093Z",
"min": "2024-05-13T18:57:16.094Z",
"description": "活動結束時間,必須晚於活動開始時間至少一個小時"
},
"startAt": {
"type": "Date",
"example": "2024-05-14T15:43:22.771Z",
"min": "2024-05-13T13:43:22.771Z",
"example": "2024-05-14T19:57:16.093Z",
"min": "2024-05-13T17:57:16.094Z",
"description": "活動開始時間,必須晚於販售結束時間至少一個小時"
},
"tags": {
Expand Down
Loading

0 comments on commit dd1e8f6

Please sign in to comment.