diff --git a/package.json b/package.json index dc65f01..f85aba1 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/body-parser": "^1.19.1", "@types/morgan": "^1.9.3", "@types/multer": "^1.4.7", + "aws-sdk": "^2.990.0", "axios": "^0.21.1", "bcryptjs": "^2.4.3", "body-parser": "^1.19.0", diff --git a/src/config/cloudStorage.ts b/src/config/cloudStorage.ts new file mode 100644 index 0000000..abe06d9 --- /dev/null +++ b/src/config/cloudStorage.ts @@ -0,0 +1,299 @@ +import path from 'path'; +import FileType from 'file-type'; + +export interface uploadResponse { + success: Boolean; + uri?: string; + url?: string; + path?: string; +} + +export interface deleteResponse { + success: Boolean; +} + +export interface storage { + upload( + buffer: Buffer, + location: string, + fileName: string, + ): Promise; + download(path: string): Promise; + delete(path: string): Promise; + listFiles(prefixPath: string): Promise>; + deletes(directory: string): Promise; +} + +// +// +// | ==================== | +// | AWS S3 Storage | +// | ==================== | +// +// +import AWS from 'aws-sdk'; + +class S3storage implements storage { + accessKeyId: string; + secretAccessKey: string; + region: string; + bucket: string; + s3: AWS.S3; + + constructor( + accessKeyId: string, + secretAccessKey: string, + region: string, + bucket: string, + ) { + this.accessKeyId = accessKeyId; + this.secretAccessKey = secretAccessKey; + this.region = region; + this.bucket = bucket; + this.s3 = new AWS.S3({ + accessKeyId: accessKeyId, + secretAccessKey: secretAccessKey, + region: region, + }); + } + + async deletes(directory: string): Promise { + if (!directory.endsWith('/')) directory += '/'; + + let params: AWS.S3.DeleteObjectsRequest = { + Bucket: this.bucket, + Delete: { Objects: [] }, + }; + + (await this.listFiles(directory)).forEach((ele) => { + if (ele) params.Delete.Objects.push({ Key: ele }); + }); + + return new Promise((resolve, reject) => { + this.s3.deleteObjects(params, async (err: Error, data: Object) => { + if (err) { + reject({ + success: false, + }); + } else { + resolve({ + success: true, + }); + } + }); + }); + } + + async upload( + buffer: Buffer, + location: string, + fileName: string, + ): Promise { + let { ext, mime } = (await FileType.fromBuffer(buffer)) || { + ext: 'unknown', + mime: 'unknown', + }; + + let fullPath = path.join(location, fileName + '.' + ext); + + let params = { + Bucket: this.bucket, + Key: fullPath, + Body: buffer, + ContentType: mime.toString(), + }; + + return new Promise((resolve, reject) => { + this.s3.putObject(params, async (err: AWS.AWSError) => { + if (err) { + reject({ + success: false, + }); + } else { + resolve({ + success: true, + uri: `s3://${this.bucket}/${fullPath}`, + url: `https://${this.bucket}.s3.${this.region}.amazonaws.com/${fullPath}`, + path: fullPath, + }); + } + }); + }); + } + + async download(path: string): Promise { + let params = { + Bucket: this.bucket, + Key: path, + }; + return new Promise((resolve, reject) => { + this.s3.getObject( + params, + async (err: AWS.AWSError, data: AWS.S3.GetObjectOutput) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }, + ); + }); + } + + async delete(path: string): Promise { + let params = { + Bucket: this.bucket, + Key: path, + }; + return new Promise((resolve, reject) => { + this.s3.deleteObject(params, async (err: Error, data: Object) => { + if (err) { + reject({ + success: false, + }); + } else { + resolve({ + success: true, + }); + } + }); + }); + } + + async listFiles(prefixPath: string): Promise> { + if (!prefixPath.endsWith('/')) prefixPath += '/'; + + let params = { + Bucket: this.bucket, + Prefix: prefixPath, + }; + return new Promise((resolve, reject) => { + this.s3.listObjectsV2(params, async (err, files) => { + if (err) { + reject({}); + } else { + if (files.Contents) { + resolve(files.Contents.map((ele) => ele.Key)); + } + } + }); + }); + } +} + +// +// +// | ==================== | +// | Google Cloud Storage | +// | ==================== | +// +// +import { Bucket, DeleteFilesOptions, Storage } from '@google-cloud/storage'; + +class googleStorage implements storage { + keyFilename: string; + bucket: Bucket; + + constructor(keyFilename: string, bucket: string) { + this.keyFilename = keyFilename; + this.bucket = new Storage({ keyFilename: 'google-cloud-key.json' }).bucket( + bucket, + ); + } + + deletes(directory: string): Promise { + if (!directory.endsWith('/')) directory += '/'; + return new Promise((resolve, reject) => { + let params: DeleteFilesOptions = { prefix: directory }; + this.bucket.deleteFiles(params, async (err) => { + if (err) { + reject({ + success: false, + }); + } else { + resolve({ + success: true, + }); + } + }); + }); + } + + async upload( + buffer: Buffer, + location: string, + fileName: string, + ): Promise { + let { ext, mime } = (await FileType.fromBuffer(buffer)) || { + ext: 'unknown', + mime: 'unknown', + }; + + let fullPath = path.join(location, fileName + '.' + ext); + let file = this.bucket.file(fullPath); + + return new Promise((resolve, reject) => { + file.save(buffer, async (err) => { + if (err) { + reject({ + success: false, + }); + } else { + resolve({ + success: true, + uri: `gs://${this.bucket.name}/${fullPath}`, + url: `https://storage.cloud.google.com/${this.bucket.name}/${fullPath}`, + path: fullPath, + }); + } + }); + }); + } + + async download(path: string): Promise { + let file = this.bucket.file(path); + return new Promise((resolve, reject) => { + file.getMetadata(async (err: Error, metadata: Object) => { + if (err) { + reject(err); + } else { + resolve(metadata); + } + }); + }); + } + + async delete(path: string): Promise { + let file = this.bucket.file(path); + + return new Promise((resolve, reject) => { + file.delete(async (err: Error) => { + if (err) { + reject({ + success: false, + }); + } else { + resolve({ + success: true, + }); + } + }); + }); + } + + async listFiles(prefixPath: string): Promise> { + if (!prefixPath.endsWith('/')) prefixPath += '/'; + + return new Promise((resolve, reject) => { + this.bucket.getFiles({ prefix: prefixPath }, async (err, files: any) => { + if (err) { + reject({}); + } else { + resolve(files.map((ele: any) => ele.name)); + } + }); + }); + } +} + +export { S3storage, googleStorage }; diff --git a/src/config/gcp.ts b/src/config/gcp.ts new file mode 100644 index 0000000..de3efcb --- /dev/null +++ b/src/config/gcp.ts @@ -0,0 +1,6 @@ +import { Storage } from '@google-cloud/storage'; + +const storage = new Storage({ keyFilename: 'google-cloud-key.json' }); +const bucket = storage.bucket('stroke_images_3'); + +export default bucket; diff --git a/src/config/s3.ts b/src/config/s3.ts new file mode 100644 index 0000000..d0a782d --- /dev/null +++ b/src/config/s3.ts @@ -0,0 +1,12 @@ +import AWS from 'aws-sdk'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const s3 = new AWS.S3({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.AWS_DEFAULT_REGION +}); + +export default s3; \ No newline at end of file diff --git a/src/config/storage.ts b/src/config/storage.ts index de3efcb..e50d415 100644 --- a/src/config/storage.ts +++ b/src/config/storage.ts @@ -1,6 +1,12 @@ -import { Storage } from '@google-cloud/storage'; +import { S3storage, googleStorage } from './cloudStorage'; +import dotenv from 'dotenv'; -const storage = new Storage({ keyFilename: 'google-cloud-key.json' }); -const bucket = storage.bucket('stroke_images_3'); +dotenv.config(); -export default bucket; +export default new S3storage( + process.env.AWS_ACCESS_KEY_ID || '', + process.env.AWS_SECRET_ACCESS_KEY || '', + process.env.AWS_DEFAULT_REGION || '', + process.env.AWS_BUCKET_NAME || '', +); +// export default new googleStorage('google-cloud-key.json', 'stroke_images_3'); diff --git a/src/controllers/file.ts b/src/controllers/file.ts index 9d1394d..2ca594e 100644 --- a/src/controllers/file.ts +++ b/src/controllers/file.ts @@ -3,6 +3,7 @@ import httpError from '../errorHandler/httpError/httpError'; import upload from '../middlewares/upload'; import download from '../middlewares/download'; +import storage from '../config/storage'; export default { upload: async (req: Request, res: Response) => { @@ -17,13 +18,25 @@ export default { } }, download: async (req: Request, res: Response) => { + // try { + // const filePath = ''; + + // await download(filePath, req, res); + // } catch (err) { + // res + // .status(500) + // .send(httpError(500, 'Could not download the file. ' + err)); + // } try { - const filePath = req.params.filePath; - await download(`${filePath}`, req, res); - } catch (err) { - res - .status(500) - .send(httpError(500, 'Could not download the file. ' + err)); - } + const dl = await storage.download(req.params.path); + if (dl instanceof Error) { + throw new Error(); + } + if (!(typeof dl.ContentType === 'string')) { + throw new Error(); + } + res.setHeader('Content-Type', dl.ContentType); + res.send(dl.Body); + } catch (error: any) {} }, }; diff --git a/src/middlewares/README.md b/src/middlewares/README.md index 0a9f8c8..df14d7f 100644 --- a/src/middlewares/README.md +++ b/src/middlewares/README.md @@ -1,4 +1,4 @@ -# Upload to Google Cloud Storage +# Upload to AWS S3 ## Import @@ -21,5 +21,28 @@ upload(buffer, path, name) import upload from '../middlewares/upload'; const URI = await upload(buffer, "folder1/folder2", "testfile"); -console.log(URI); // { "url": "https://storage.cloud.google.com/stroke_images_3/folder1/folder2/testfile.ext", "gsutilURI": "gs://stroke_images_3/folder1/folder2/testfile.ext" } +console.log(URI); // { "url": "s3://app4stroke/folder1/folder2/testfile.ext", "gsutilURI": "https://app4stroke.s3.ap-southeast-1.amazonaws.com/folder1/folder2/testfile.ext" } ``` + +# Download from AWS S3 + +## Import + +```typescript +import download from '../middlewares/download'; + +``` + +## Usage +` +download(filePath, req, res) +` + +- filePath : string + +## Example +```typescript +import download from '../middlewares/download'; + +await download("folder1/folder2/testfile.ext", req, res); +``` \ No newline at end of file diff --git a/src/middlewares/delete.ts b/src/middlewares/delete.ts new file mode 100644 index 0000000..d9fab68 --- /dev/null +++ b/src/middlewares/delete.ts @@ -0,0 +1,29 @@ +import s3 from '../config/s3'; + +export interface deleteResponse { + success: Boolean; +} + +const del = async (buffer: Buffer, filePath: string) => { + var params = { + Bucket: 'app4stroke', + Key: filePath, + }; + + return new Promise((resolve, reject) => { + s3.deleteObject(params) + .promise() + .then(() => { + resolve({ + success: true, + }); + }) + .catch(() => { + reject({ + success: false, + }); + }); + }); +}; + +export default del; diff --git a/src/middlewares/download.ts b/src/middlewares/download.ts index 0d05b44..7c1d78f 100644 --- a/src/middlewares/download.ts +++ b/src/middlewares/download.ts @@ -1,22 +1,25 @@ import { Request, Response } from 'express'; import httpError from '../errorHandler/httpError/httpError'; -import bucket from '../config/storage'; + +import s3 from '../config/s3'; const download = async (filePath: string, req: Request, res: Response) => { try { - const file = bucket.file(filePath); - const readStream = file.createReadStream(); + const params = { + Bucket: "app4stroke", + Key: filePath + } - await file - .getMetadata() - .then((metadata) => { - res.setHeader("content-type", metadata[0].contentType); - readStream.pipe(res); - }); + res.setHeader('Content-Disposition', 'attachment'); + s3.getObject(params) + .createReadStream() + .on('error', function(err){ + res.status(500).send(httpError(500, "Error: " + err)); + }).pipe(res); } catch (err) { - res.status(404).send(httpError(404, "No such file found.")); + res.status(404).send(httpError(404, "The file could not be accessed.")); } -} +}; export default download; \ No newline at end of file diff --git a/src/middlewares/fileService.ts b/src/middlewares/fileService.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/middlewares/upload.ts b/src/middlewares/upload.ts index 8aa4656..426884b 100644 --- a/src/middlewares/upload.ts +++ b/src/middlewares/upload.ts @@ -1,6 +1,13 @@ -import bucket from '../config/storage'; import path from 'path'; import FileType from 'file-type'; +import s3 from '../config/s3'; + +export interface uploadResponse { + success: Boolean; + uri?: string; + url?: string; + path?: string; +} const upload = async (buffer: any, filePath: string, fileName: string) => { const { ext, mime } = (await FileType.fromBuffer(buffer)) || { @@ -9,19 +16,28 @@ const upload = async (buffer: any, filePath: string, fileName: string) => { }; const fullPath = path.join(filePath, fileName + '.' + ext); - const file = bucket.file(fullPath); - return new Promise((resolve, reject) => { - file - .save(buffer) + var params = { + Bucket: 'app4stroke', + Key: fullPath, + Body: buffer, + }; + + return new Promise((resolve, reject) => { + s3.putObject(params) + .promise() .then(() => { resolve({ - url: `https://storage.cloud.google.com/stroke_images_3/${fullPath}`, - gsutilURI: `gs://stroke_images_3/${fullPath}`, + success: true, + uri: `s3://app4stroke/${fullPath}`, + url: `https://app4stroke.s3.ap-southeast-1.amazonaws.com/${fullPath}`, + path: fullPath, }); }) .catch(() => { - reject({}); + reject({ + success: false, + }); }); }); }; diff --git a/src/routes/file.ts b/src/routes/file.ts index dff1d66..7c3e29b 100644 --- a/src/routes/file.ts +++ b/src/routes/file.ts @@ -4,6 +4,6 @@ import fileController from '../controllers/file'; const router = Router(); router.post('/upload', fileController.upload); -router.get('/download/:filePath(*)', fileController.download); +router.get('/download/:path(*)', fileController.download); export default router;