Files
hedgedoc/backend/src/media/backends/imgur-backend.ts
T
2026-01-14 19:46:27 +01:00

127 lines
3.9 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MediaBackendType } from '@hedgedoc/commons';
import { Inject, Injectable } from '@nestjs/common';
import fetch, { Response } from 'node-fetch';
import { URLSearchParams } from 'url';
import mediaConfiguration, {
ImgurMediaConfig,
MediaConfig,
} from '../../config/media.config';
import { MediaBackendError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { MediaBackend } from '../media-backend.interface';
type UploadResult = {
data: {
link: string;
deletehash: string | null;
};
};
interface ImgurBackendData {
url: string;
deleteHash: string | null;
}
@Injectable()
export class ImgurBackend implements MediaBackend {
private config: ImgurMediaConfig['imgur'];
constructor(
private readonly logger: ConsoleLoggerService,
@Inject(mediaConfiguration.KEY)
private mediaConfig: MediaConfig,
) {
this.logger.setContext(ImgurBackend.name);
// only create the backend if imgur is configured
if (this.mediaConfig.backend.type !== MediaBackendType.IMGUR) {
return;
}
this.config = this.mediaConfig.backend.imgur;
}
async saveFile(uuid: string, buffer: Buffer): Promise<string> {
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,
// oxlint-disable-next-line @typescript-eslint/naming-convention
headers: { Authorization: `Client-ID ${this.config.clientId}` },
})
.then((res) => ImgurBackend.checkStatus(res))
.then((res) => res.json())) as UploadResult;
this.logger.debug(`Response: ${JSON.stringify(result)}`, 'saveFile');
this.logger.log(`Uploaded file ${uuid}`, 'saveFile');
const backendData: ImgurBackendData = {
url: result.data.link,
deleteHash: result.data.deletehash,
};
return JSON.stringify(backendData);
} catch (e) {
this.logger.error(
`error: ${(e as Error).message}`,
(e as Error).stack,
'saveFile',
);
throw new MediaBackendError(`Could not save file ${uuid}`);
}
}
async deleteFile(uuid: string, jsonBackendData: string): Promise<void> {
const backendData = JSON.parse(jsonBackendData) as ImgurBackendData;
if (backendData.deleteHash === null) {
throw new MediaBackendError(
`We don't have any delete tokens for file ${uuid} and therefore can't delete this image`,
);
}
try {
const result = await fetch(
`https://api.imgur.com/3/image/${backendData.deleteHash}`,
{
method: 'DELETE',
// oxlint-disable-next-line @typescript-eslint/naming-convention
headers: { Authorization: `Client-ID ${this.config.clientId}` },
},
);
ImgurBackend.checkStatus(result);
// oxlint-disable-next-line @typescript-eslint/no-base-to-string
this.logger.log(`Deleted file ${uuid}`, 'deleteFile');
return;
} catch (e) {
this.logger.error(
`error: ${(e as Error).message}`,
(e as Error).stack,
'deleteFile',
);
throw new MediaBackendError(`Could not delete file '${uuid}'`);
}
}
getFileUrl(uuid: string, backendData: string | null): Promise<string> {
if (backendData === null) {
throw new MediaBackendError(
`We don't have any data for file ${uuid} and therefore can't get the url of this image`,
);
}
const data = JSON.parse(backendData) as ImgurBackendData;
return Promise.resolve(data.url);
}
private static checkStatus(res: Response): Response {
if (res.ok) {
// res.status >= 200 && res.status < 300
return res;
} else {
throw new MediaBackendError(res.statusText);
}
}
}