-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from erik1110/feature/api
[feat] add image and shortenURL
- Loading branch information
Showing
14 changed files
with
1,296 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { ApiProperty } from "@nestjs/swagger"; | ||
|
||
export class FileUploadDto { | ||
@ApiProperty({ type: 'string', format: 'binary' }) | ||
file: any; | ||
} | ||
|
||
export class GetImageSuccessDto { | ||
@ApiProperty({ example: true }) | ||
status: boolean; | ||
|
||
@ApiProperty({ example: '取得圖片網址' }) | ||
message: string; | ||
|
||
@ApiProperty({ | ||
example: "http://localhost:3000/api/v1/url/HSIARDTXC" | ||
}) | ||
data: object; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import * as admin from 'firebase-admin'; | ||
|
||
@Injectable() | ||
export class FirebaseService { | ||
private readonly storage: admin.storage.Storage; | ||
|
||
readonly firebase_params = { | ||
type: process.env.FIREBASE_TYPE, | ||
project_id: process.env.FIREBASE_PROJECT_ID, | ||
private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID, | ||
private_key: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), | ||
client_email: process.env.FIREBASE_CLIENT_EMAIL, | ||
client_id: process.env.FIREBASE_CLIENT_ID, | ||
auth_uri: process.env.FIREBASE_AUTH_URI, | ||
token_uri: process.env.FIREBASE_TOKEN_URI, | ||
auth_provider_X509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL, | ||
client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL, | ||
}; | ||
|
||
constructor(){ | ||
admin.initializeApp({ | ||
credential: admin.credential.cert(this.firebase_params as admin.ServiceAccount), | ||
storageBucket: `${process.env.FIREBASE_PROJECT_ID}.appspot.com`, | ||
}); | ||
this.storage = admin.storage(); | ||
} | ||
|
||
getStorageInstance(): admin.storage.Storage{ | ||
return this.storage; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { Controller, Get, HttpStatus, Param, Post, Res, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; | ||
import { FileInterceptor } from '@nestjs/platform-express'; | ||
import { ImageService } from './image.service'; | ||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; | ||
import { FileUploadDto, GetImageSuccessDto } from './dto/fileUpload.dto'; | ||
import { UrlService } from '../url/url.service'; | ||
import { AuthGuard } from '@nestjs/passport'; | ||
import { ApiErrorDecorator } from 'src/common/decorators/error/error.decorator'; | ||
import { getHttpResponse } from 'src/utils/successHandler'; | ||
|
||
@ApiTags('Image - 上傳圖片') | ||
@ApiErrorDecorator( | ||
HttpStatus.INTERNAL_SERVER_ERROR, | ||
'CriticalError', | ||
'系統錯誤,請洽系統管理員', | ||
) | ||
// @UseGuards(AuthGuard('jwt')) | ||
// @ApiBearerAuth() | ||
@Controller('/api/v1/image') | ||
export class ImageController { | ||
constructor( | ||
private readonly imageService: ImageService, | ||
private readonly urlService: UrlService, | ||
) {} | ||
|
||
@Post('upload') | ||
@UseInterceptors(FileInterceptor('file')) | ||
@ApiConsumes('multipart/form-data') | ||
@ApiOperation({ summary: '上傳圖片 Upload an image' }) | ||
@ApiOkResponse({ type: GetImageSuccessDto }) | ||
@ApiBody({ | ||
description: "網址會由該服務進行轉址 The URL will be redirected by the service.", | ||
type: FileUploadDto, | ||
}) | ||
async uploadImage(@UploadedFile() file){ | ||
const imageUrl = await this.imageService.uploadImage(file); | ||
let shortenUrl = await this.urlService.shortenUrl(imageUrl) | ||
if (process.env.NODE_ENV === 'dev') { | ||
shortenUrl = `http://localhost:${process.env.PORT}/api/v1/url/`; | ||
} else { | ||
shortenUrl = `https://${process.env.PRODUCTION_URL}/api/v1/url/`; | ||
} | ||
|
||
const shortenedUrl = await this.urlService.shortenUrl(imageUrl); | ||
const redirectUrl = shortenUrl + shortenedUrl; | ||
return getHttpResponse.successResponse({ | ||
message: '取得圖片網址', | ||
data: redirectUrl, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { FirebaseService } from './firebase.service'; | ||
import { ImageService } from './image.service'; | ||
import { ImageController } from './image.controller'; | ||
import { UrlService } from '../url/url.service'; | ||
import { UrlModule } from '../url/url.module'; | ||
|
||
@Module({ | ||
imports: [UrlModule], | ||
controllers: [ImageController], | ||
providers: [FirebaseService, ImageService] | ||
}) | ||
export class ImageModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { HttpStatus, Injectable } from "@nestjs/common"; | ||
import { FirebaseService } from "./firebase.service"; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import { GetSignedUrlConfig } from '@google-cloud/storage'; | ||
import { AppError } from "src/utils/appError"; | ||
|
||
@Injectable() | ||
export class ImageService { | ||
constructor(private readonly firebaseService: FirebaseService){} | ||
|
||
async uploadImage(file): Promise<string> { | ||
const maxSize = 3 * 1024 * 1024; // 3 MB in bytes | ||
if (file.size > maxSize) { | ||
throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '超過 3 MB'); | ||
} | ||
const storage = this.firebaseService.getStorageInstance(); | ||
const bucket = storage.bucket(); | ||
const encodedOriginalName = encodeURIComponent(file.originalname); | ||
const fileName = `${uuidv4()}_${encodedOriginalName}`; | ||
const fileUpload = bucket.file(fileName); | ||
const stream = fileUpload.createWriteStream({ | ||
metadata: { | ||
contentType: file.mimetype, | ||
} | ||
}); | ||
|
||
return new Promise((resolve, reject) => { | ||
stream.on('error', (error) =>{ | ||
reject(error); | ||
}); | ||
|
||
stream.on('finish', async () => { | ||
const imageUrl = await this.getFirebaseUrl(fileUpload); | ||
resolve(imageUrl); | ||
}); | ||
|
||
stream.end(file.buffer); | ||
}); | ||
} | ||
|
||
private async getFirebaseUrl(fileUpload): Promise<string> { | ||
return new Promise((resolve, reject) => { | ||
const blobStream = fileUpload.createReadStream(); | ||
|
||
const bucket = this.firebaseService.getStorageInstance().bucket(); | ||
const blob = bucket.file(fileUpload.name); | ||
|
||
blobStream.pipe(blob.createWriteStream()) | ||
.on('finish', () => { | ||
const config: GetSignedUrlConfig = { | ||
action: 'read', | ||
expires: '12-31-2024', | ||
}; | ||
|
||
blob.getSignedUrl(config, (err, imgUrl) => { | ||
if (err) { | ||
reject(err); | ||
} else { | ||
resolve(imgUrl); | ||
} | ||
}); | ||
}) | ||
.on('error', (err) => { | ||
reject(err); | ||
}); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { Document } from 'mongoose'; | ||
|
||
export interface IUrl extends Document { | ||
originalUrl: string; | ||
shortUrl: string, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; | ||
import { Document } from 'mongoose'; | ||
import { IUrl } from '../interfaces/url.interface'; | ||
|
||
@Schema({ timestamps: true, versionKey: false }) | ||
export class Url extends Document implements IUrl { | ||
@Prop({ required: true }) | ||
originalUrl: string; | ||
|
||
@Prop({ required: true }) | ||
shortUrl: string; | ||
|
||
} | ||
|
||
export const UrlSchema = SchemaFactory.createForClass(Url); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Controller, Get, HttpStatus, Param, Redirect, UseGuards } from '@nestjs/common'; | ||
import { UrlService } from './url.service'; | ||
import { ApiErrorDecorator } from 'src/common/decorators/error/error.decorator'; | ||
import { ApiOperation, ApiTags } from '@nestjs/swagger'; | ||
import { AppError } from 'src/utils/appError'; | ||
|
||
@ApiTags('ShortenURL - 短網址') | ||
@ApiErrorDecorator( | ||
HttpStatus.INTERNAL_SERVER_ERROR, | ||
'CriticalError', | ||
'系統錯誤,請洽系統管理員', | ||
) | ||
@Controller('/api/v1/url') | ||
export class UrlController { | ||
constructor(private readonly urlService: UrlService) {} | ||
|
||
@Get(':shortUrl') | ||
@ApiOperation({ summary: '轉址短網址 Redirect short URL' }) | ||
@Redirect() | ||
async redirectToOriginalUrl(@Param('shortUrl') shortUrl: string): Promise<{ url: string }> { | ||
const originalUrl = await this.urlService.getOriginalUrl(shortUrl); | ||
|
||
if (originalUrl) { | ||
return { url: originalUrl }; | ||
} else { | ||
throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '無此網址或網址已失效'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { MongooseModule } from '@nestjs/mongoose'; | ||
import { UrlSchema } from './schemas/url.schemas'; | ||
import { UrlService } from './url.service'; | ||
import { UrlController } from './url.controller'; | ||
|
||
@Module({ | ||
imports: [MongooseModule.forFeature([{ name: 'Url', schema: UrlSchema }])], | ||
controllers: [UrlController], | ||
providers: [UrlService], | ||
exports: [UrlService], | ||
}) | ||
export class UrlModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { IUrl } from './interfaces/url.interface'; | ||
import { Model } from 'mongoose'; | ||
import { InjectModel } from '@nestjs/mongoose'; | ||
import * as shortid from 'shortid'; | ||
|
||
@Injectable() | ||
export class UrlService { | ||
constructor(@InjectModel('Url') private readonly urlModel: Model<IUrl>){} | ||
|
||
async shortenUrl(originalUrl: string): Promise<string> { | ||
const existingUrl = await this.urlModel.findOne({ originalUrl }).exec(); | ||
|
||
if (existingUrl) { | ||
return existingUrl.shortUrl; | ||
} | ||
|
||
const shortUrl = shortid.generate(); | ||
const newUrl = new this.urlModel({ originalUrl, shortUrl }); | ||
await newUrl.save(); | ||
|
||
return shortUrl; | ||
} | ||
|
||
async getOriginalUrl(shortUrl: string): Promise<string | null> { | ||
const url = await this.urlModel.findOne({ shortUrl }).exec(); | ||
|
||
return url ? url.originalUrl : null; | ||
} | ||
} |