From 165bb7602ba6dbfe0f5a61097933f5376f731397 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Fri, 26 Feb 2021 16:12:14 +0100 Subject: [PATCH 1/3] MediaUploadEntity: Add fileUrl Save the fileUrl, returned to the user on creation, in the DB. Signed-off-by: Philip Molares --- src/media/media-upload.entity.ts | 3 +++ src/media/media.service.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/media/media-upload.entity.ts b/src/media/media-upload.entity.ts index 7cd2d11b5..c9f253278 100644 --- a/src/media/media-upload.entity.ts +++ b/src/media/media-upload.entity.ts @@ -34,6 +34,9 @@ export class MediaUpload { }) backendType: string; + @Column() + fileUrl: string; + @Column({ nullable: true, }) diff --git a/src/media/media.service.ts b/src/media/media.service.ts index 3869c6da6..d6a8c95c8 100644 --- a/src/media/media.service.ts +++ b/src/media/media.service.ts @@ -100,6 +100,7 @@ export class MediaService { mediaUpload.id, ); mediaUpload.backendData = backendData; + mediaUpload.fileUrl = url; await this.mediaUploadRepository.save(mediaUpload); return url; } From e2b2059bde8fb1dd5f404a0562ddbf474fdd5c2c Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Fri, 26 Feb 2021 16:16:00 +0100 Subject: [PATCH 2/3] ImgurBackend: Add Imgur MediaBackend Add node-fetch dependency. This was chosen as other libs we use already use node-fetch. Signed-off-by: Philip Molares --- package.json | 2 + src/media/backends/imgur-backend.ts | 83 +++++++++++++++++++++++++++++ src/media/media.module.ts | 3 +- src/media/media.service.ts | 5 ++ yarn.lock | 8 +++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/media/backends/imgur-backend.ts diff --git a/package.json b/package.json index e5a9a7421..a7501a690 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@nestjs/typeorm": "7.1.5", "@types/bcrypt": "3.0.0", "@types/cron": "1.7.2", + "@types/node-fetch": "^2.5.8", "@types/passport-http-bearer": "1.0.36", "bcrypt": "5.0.1", "class-transformer": "0.4.0", @@ -43,6 +44,7 @@ "file-type": "16.2.0", "joi": "17.4.0", "nest-router": "1.0.9", + "node-fetch": "^2.6.1", "passport": "0.4.1", "passport-http-bearer": "1.0.1", "raw-body": "2.4.1", diff --git a/src/media/backends/imgur-backend.ts b/src/media/backends/imgur-backend.ts new file mode 100644 index 000000000..46324ec03 --- /dev/null +++ b/src/media/backends/imgur-backend.ts @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import mediaConfiguration from '../../config/media.config'; +import { ConsoleLoggerService } from '../../logger/console-logger.service'; +import { MediaBackend } from '../media-backend.interface'; +import { BackendData } from '../media-upload.entity'; +import { MediaConfig } from '../../config/media.config'; +import fetch from 'node-fetch'; +import { URLSearchParams } from 'url'; +import { MediaBackendError } from '../../errors/errors'; + +@Injectable() +export class ImgurBackend implements MediaBackend { + private config: MediaConfig['backend']['imgur']; + + constructor( + private readonly logger: ConsoleLoggerService, + @Inject(mediaConfiguration.KEY) + private mediaConfig: MediaConfig, + ) { + this.logger.setContext(ImgurBackend.name); + this.config = mediaConfig.backend.imgur; + } + + async saveFile( + buffer: Buffer, + fileName: string, + ): Promise<[string, BackendData]> { + const params = new URLSearchParams(); + params.append('image', buffer.toString('base64')); + params.append('type', 'base64'); + try { + const result = await fetch('https://api.imgur.com/3/image', { + method: 'POST', + body: params, + headers: { Authorization: `Client-ID ${this.config.clientID}` }, + }) + .then(ImgurBackend.checkStatus) + .then((res) => res.json()); + this.logger.debug(`Response: ${JSON.stringify(result)}`, 'saveFile'); + this.logger.log(`Uploaded ${fileName}`, 'saveFile'); + return [result.data.link, result.data.deletehash]; + } catch (e) { + this.logger.error(`error: ${e.message}`, e.stack, 'saveFile'); + throw new MediaBackendError(`Could not save '${fileName}' on imgur`); + } + } + + async deleteFile(fileName: string, backendData: BackendData): Promise { + if (backendData === null) { + throw new Error(); + } + try { + const result = await fetch( + `https://api.imgur.com/3/image/${backendData}`, + { + method: 'POST', + headers: { Authorization: `Client-ID ${this.config.clientID}` }, + }, + ).then(ImgurBackend.checkStatus); + this.logger.debug(`Response: ${result}`, 'saveFile'); + this.logger.log(`Deleted ${fileName}`, 'deleteFile'); + return; + } catch (e) { + this.logger.error(`error: ${e.message}`, e.stack, 'deleteFile'); + throw new MediaBackendError(`Could not delete '${fileName}' on imgur`); + } + } + + private static checkStatus(res) { + if (res.ok) { + // res.status >= 200 && res.status < 300 + return res; + } else { + throw new MediaBackendError(res.statusText); + } + } +} diff --git a/src/media/media.module.ts b/src/media/media.module.ts index d55be0843..26f186814 100644 --- a/src/media/media.module.ts +++ b/src/media/media.module.ts @@ -13,6 +13,7 @@ import { UsersModule } from '../users/users.module'; import { FilesystemBackend } from './backends/filesystem-backend'; import { MediaUpload } from './media-upload.entity'; import { MediaService } from './media.service'; +import { ImgurBackend } from './backends/imgur-backend'; @Module({ imports: [ @@ -22,7 +23,7 @@ import { MediaService } from './media.service'; LoggerModule, ConfigModule, ], - providers: [MediaService, FilesystemBackend], + providers: [MediaService, FilesystemBackend, ImgurBackend], exports: [MediaService], }) export class MediaModule {} diff --git a/src/media/media.service.ts b/src/media/media.service.ts index d6a8c95c8..e24fae0c8 100644 --- a/src/media/media.service.ts +++ b/src/media/media.service.ts @@ -19,6 +19,7 @@ import { FilesystemBackend } from './backends/filesystem-backend'; import { MediaBackend } from './media-backend.interface'; import { MediaUpload } from './media-upload.entity'; import { MediaUploadUrlDto } from './media-upload-url.dto'; +import { ImgurBackend } from './backends/imgur-backend'; @Injectable() export class MediaService { @@ -158,6 +159,8 @@ export class MediaService { switch (this.mediaConfig.backend.use) { case 'filesystem': return BackendType.FILESYSTEM; + case 'imgur': + return BackendType.IMGUR; } } @@ -165,6 +168,8 @@ export class MediaService { switch (type) { case BackendType.FILESYSTEM: return this.moduleRef.get(FilesystemBackend); + case BackendType.IMGUR: + return this.moduleRef.get(ImgurBackend); } } diff --git a/yarn.lock b/yarn.lock index 4374e2eb4..5ace5a1f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1033,6 +1033,14 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/node-fetch@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" + integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*": version "14.14.28" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.28.tgz#cade4b64f8438f588951a6b35843ce536853f25b" From eb7e6b55eb1e49d8f8ad0fead11d5593b83fdaa7 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sat, 27 Feb 2021 11:24:41 +0100 Subject: [PATCH 3/3] DBSchema: Add fileUrl to media_upload table Signed-off-by: Philip Molares --- docs/content/dev/db-schema.plantuml | 1 + src/media/backends/imgur-backend.ts | 39 +++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/content/dev/db-schema.plantuml b/docs/content/dev/db-schema.plantuml index 2ed07aeec..93782290e 100644 --- a/docs/content/dev/db-schema.plantuml +++ b/docs/content/dev/db-schema.plantuml @@ -135,6 +135,7 @@ entity "media_upload" { *noteId : uuid <> *userId : uuid <> *backendType: text + *fileUrl: text backendData: text *createdAt : date } diff --git a/src/media/backends/imgur-backend.ts b/src/media/backends/imgur-backend.ts index 46324ec03..ecced7586 100644 --- a/src/media/backends/imgur-backend.ts +++ b/src/media/backends/imgur-backend.ts @@ -10,10 +10,17 @@ import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { MediaBackend } from '../media-backend.interface'; import { BackendData } from '../media-upload.entity'; import { MediaConfig } from '../../config/media.config'; -import fetch from 'node-fetch'; +import fetch, { Response } from 'node-fetch'; import { URLSearchParams } from 'url'; import { MediaBackendError } from '../../errors/errors'; +type UploadResult = { + data: { + link: string; + deletehash: string; + }; +}; + @Injectable() export class ImgurBackend implements MediaBackend { private config: MediaConfig['backend']['imgur']; @@ -35,44 +42,56 @@ export class ImgurBackend implements MediaBackend { params.append('image', buffer.toString('base64')); params.append('type', 'base64'); try { - const result = await fetch('https://api.imgur.com/3/image', { + const result = (await fetch('https://api.imgur.com/3/image', { method: 'POST', body: params, + // eslint-disable-next-line @typescript-eslint/naming-convention headers: { Authorization: `Client-ID ${this.config.clientID}` }, }) - .then(ImgurBackend.checkStatus) - .then((res) => res.json()); + .then((res) => ImgurBackend.checkStatus(res)) + .then((res) => res.json())) as UploadResult; this.logger.debug(`Response: ${JSON.stringify(result)}`, 'saveFile'); this.logger.log(`Uploaded ${fileName}`, 'saveFile'); return [result.data.link, result.data.deletehash]; } catch (e) { - this.logger.error(`error: ${e.message}`, e.stack, 'saveFile'); + this.logger.error( + `error: ${(e as Error).message}`, + (e as Error).stack, + 'saveFile', + ); throw new MediaBackendError(`Could not save '${fileName}' on imgur`); } } async deleteFile(fileName: string, backendData: BackendData): Promise { if (backendData === null) { - throw new Error(); + throw new MediaBackendError( + `We don't have any delete tokens for '${fileName}' and therefore can't delete this image on imgur`, + ); } try { const result = await fetch( `https://api.imgur.com/3/image/${backendData}`, { method: 'POST', + // eslint-disable-next-line @typescript-eslint/naming-convention headers: { Authorization: `Client-ID ${this.config.clientID}` }, }, - ).then(ImgurBackend.checkStatus); - this.logger.debug(`Response: ${result}`, 'saveFile'); + ).then((res) => ImgurBackend.checkStatus(res)); + this.logger.debug(`Response: ${result.toString()}`, 'saveFile'); this.logger.log(`Deleted ${fileName}`, 'deleteFile'); return; } catch (e) { - this.logger.error(`error: ${e.message}`, e.stack, 'deleteFile'); + this.logger.error( + `error: ${(e as Error).message}`, + (e as Error).stack, + 'deleteFile', + ); throw new MediaBackendError(`Could not delete '${fileName}' on imgur`); } } - private static checkStatus(res) { + private static checkStatus(res: Response): Response { if (res.ok) { // res.status >= 200 && res.status < 300 return res;