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"