mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-23 19:47:03 -04:00
fix(repository): Move backend code into subdirectory
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
86584e705f
commit
bf30cbcf48
272 changed files with 87 additions and 67 deletions
83
backend/src/media/backends/azure-backend.ts
Normal file
83
backend/src/media/backends/azure-backend.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import {
|
||||
BlobServiceClient,
|
||||
BlockBlobClient,
|
||||
ContainerClient,
|
||||
} from '@azure/storage-blob';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
||||
import { MediaBackendError } from '../../errors/errors';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { MediaBackend } from '../media-backend.interface';
|
||||
import { BackendData } from '../media-upload.entity';
|
||||
import { BackendType } from './backend-type.enum';
|
||||
|
||||
@Injectable()
|
||||
export class AzureBackend implements MediaBackend {
|
||||
private config: MediaConfig['backend']['azure'];
|
||||
private client: ContainerClient;
|
||||
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@Inject(mediaConfiguration.KEY)
|
||||
private mediaConfig: MediaConfig,
|
||||
) {
|
||||
this.logger.setContext(AzureBackend.name);
|
||||
this.config = mediaConfig.backend.azure;
|
||||
if (mediaConfig.backend.use === BackendType.AZURE) {
|
||||
// only create the client if the backend is configured to azure
|
||||
const blobServiceClient = BlobServiceClient.fromConnectionString(
|
||||
this.config.connectionString,
|
||||
);
|
||||
this.client = blobServiceClient.getContainerClient(this.config.container);
|
||||
}
|
||||
}
|
||||
|
||||
async saveFile(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
): Promise<[string, BackendData]> {
|
||||
const blockBlobClient: BlockBlobClient =
|
||||
this.client.getBlockBlobClient(fileName);
|
||||
try {
|
||||
await blockBlobClient.upload(buffer, buffer.length);
|
||||
const url = this.getUrl(fileName);
|
||||
this.logger.log(`Uploaded ${url}`, 'saveFile');
|
||||
return [url, null];
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`error: ${(e as Error).message}`,
|
||||
(e as Error).stack,
|
||||
'saveFile',
|
||||
);
|
||||
throw new MediaBackendError(`Could not save '${fileName}' on Azure`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(fileName: string, _: BackendData): Promise<void> {
|
||||
const blockBlobClient: BlockBlobClient =
|
||||
this.client.getBlockBlobClient(fileName);
|
||||
try {
|
||||
await blockBlobClient.delete();
|
||||
const url = this.getUrl(fileName);
|
||||
this.logger.log(`Deleted ${url}`, 'deleteFile');
|
||||
return;
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`error: ${(e as Error).message}`,
|
||||
(e as Error).stack,
|
||||
'deleteFile',
|
||||
);
|
||||
throw new MediaBackendError(`Could not delete '${fileName}' on Azure`);
|
||||
}
|
||||
}
|
||||
|
||||
private getUrl(fileName: string): string {
|
||||
return `${this.client.url}/${fileName}`;
|
||||
}
|
||||
}
|
13
backend/src/media/backends/backend-type.enum.ts
Normal file
13
backend/src/media/backends/backend-type.enum.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export enum BackendType {
|
||||
FILESYSTEM = 'filesystem',
|
||||
S3 = 's3',
|
||||
IMGUR = 'imgur',
|
||||
AZURE = 'azure',
|
||||
WEBDAV = 'webdav',
|
||||
}
|
85
backend/src/media/backends/filesystem-backend.ts
Normal file
85
backend/src/media/backends/filesystem-backend.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
||||
import { MediaBackendError } from '../../errors/errors';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { MediaBackend } from '../media-backend.interface';
|
||||
import { BackendData } from '../media-upload.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FilesystemBackend implements MediaBackend {
|
||||
uploadDirectory = './uploads';
|
||||
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@Inject(mediaConfiguration.KEY)
|
||||
private mediaConfig: MediaConfig,
|
||||
) {
|
||||
this.logger.setContext(FilesystemBackend.name);
|
||||
this.uploadDirectory = mediaConfig.backend.filesystem.uploadPath;
|
||||
}
|
||||
|
||||
async saveFile(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
): Promise<[string, BackendData]> {
|
||||
const filePath = this.getFilePath(fileName);
|
||||
this.logger.debug(`Writing file to: ${filePath}`, 'saveFile');
|
||||
await this.ensureDirectory();
|
||||
try {
|
||||
await fs.writeFile(filePath, buffer, null);
|
||||
return ['/uploads/' + fileName, null];
|
||||
} catch (e) {
|
||||
this.logger.error((e as Error).message, (e as Error).stack, 'saveFile');
|
||||
throw new MediaBackendError(`Could not save '${filePath}'`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(fileName: string, _: BackendData): Promise<void> {
|
||||
const filePath = this.getFilePath(fileName);
|
||||
try {
|
||||
return await fs.unlink(filePath);
|
||||
} catch (e) {
|
||||
this.logger.error((e as Error).message, (e as Error).stack, 'deleteFile');
|
||||
throw new MediaBackendError(`Could not delete '${filePath}'`);
|
||||
}
|
||||
}
|
||||
|
||||
private getFilePath(fileName: string): string {
|
||||
return join(this.uploadDirectory, fileName);
|
||||
}
|
||||
|
||||
private async ensureDirectory(): Promise<void> {
|
||||
this.logger.debug(
|
||||
`Ensuring presence of directory at ${this.uploadDirectory}`,
|
||||
'ensureDirectory',
|
||||
);
|
||||
try {
|
||||
await fs.access(this.uploadDirectory);
|
||||
} catch (e) {
|
||||
try {
|
||||
this.logger.debug(
|
||||
`The directory '${this.uploadDirectory}' can't be accessed. Trying to create the directory`,
|
||||
'ensureDirectory',
|
||||
);
|
||||
await fs.mkdir(this.uploadDirectory);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
(e as Error).message,
|
||||
(e as Error).stack,
|
||||
'ensureDirectory',
|
||||
);
|
||||
throw new MediaBackendError(
|
||||
`Could not create '${this.uploadDirectory}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
101
backend/src/media/backends/imgur-backend.ts
Normal file
101
backend/src/media/backends/imgur-backend.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
||||
import { MediaBackendError } from '../../errors/errors';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { MediaBackend } from '../media-backend.interface';
|
||||
import { BackendData } from '../media-upload.entity';
|
||||
|
||||
type UploadResult = {
|
||||
data: {
|
||||
link: string;
|
||||
deletehash: string;
|
||||
};
|
||||
};
|
||||
|
||||
@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,
|
||||
// eslint-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 ${fileName}`, 'saveFile');
|
||||
return [result.data.link, result.data.deletehash];
|
||||
} catch (e) {
|
||||
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<void> {
|
||||
if (backendData === null) {
|
||||
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((res) => ImgurBackend.checkStatus(res));
|
||||
this.logger.debug(`Response: ${result.toString()}`, 'deleteFile');
|
||||
this.logger.log(`Deleted ${fileName}`, 'deleteFile');
|
||||
return;
|
||||
} catch (e) {
|
||||
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: Response): Response {
|
||||
if (res.ok) {
|
||||
// res.status >= 200 && res.status < 300
|
||||
return res;
|
||||
} else {
|
||||
throw new MediaBackendError(res.statusText);
|
||||
}
|
||||
}
|
||||
}
|
79
backend/src/media/backends/s3-backend.ts
Normal file
79
backend/src/media/backends/s3-backend.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Client } from 'minio';
|
||||
import { URL } from 'url';
|
||||
|
||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
||||
import { MediaBackendError } from '../../errors/errors';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { MediaBackend } from '../media-backend.interface';
|
||||
import { BackendData } from '../media-upload.entity';
|
||||
import { BackendType } from './backend-type.enum';
|
||||
|
||||
@Injectable()
|
||||
export class S3Backend implements MediaBackend {
|
||||
private config: MediaConfig['backend']['s3'];
|
||||
private client: Client;
|
||||
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@Inject(mediaConfiguration.KEY)
|
||||
private mediaConfig: MediaConfig,
|
||||
) {
|
||||
this.logger.setContext(S3Backend.name);
|
||||
if (mediaConfig.backend.use === BackendType.S3) {
|
||||
this.config = mediaConfig.backend.s3;
|
||||
const url = new URL(this.config.endPoint);
|
||||
const secure = url.protocol === 'https:'; // url.protocol contains a trailing ':'
|
||||
const endpoint = `${url.hostname}${url.pathname}`;
|
||||
let port = parseInt(url.port);
|
||||
if (isNaN(port)) {
|
||||
port = secure ? 443 : 80;
|
||||
}
|
||||
this.client = new Client({
|
||||
endPoint: endpoint.substr(0, endpoint.length - 1), // remove trailing '/'
|
||||
port: port,
|
||||
useSSL: secure,
|
||||
accessKey: this.config.accessKeyId,
|
||||
secretKey: this.config.secretAccessKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async saveFile(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
): Promise<[string, BackendData]> {
|
||||
try {
|
||||
await this.client.putObject(this.config.bucket, fileName, buffer);
|
||||
this.logger.log(`Uploaded file ${fileName}`, 'saveFile');
|
||||
return [this.getUrl(fileName), null];
|
||||
} catch (e) {
|
||||
this.logger.error((e as Error).message, (e as Error).stack, 'saveFile');
|
||||
throw new MediaBackendError(`Could not save '${fileName}' on S3`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(fileName: string, _: BackendData): Promise<void> {
|
||||
try {
|
||||
await this.client.removeObject(this.config.bucket, fileName);
|
||||
const url = this.getUrl(fileName);
|
||||
this.logger.log(`Deleted ${url}`, 'deleteFile');
|
||||
return;
|
||||
} catch (e) {
|
||||
this.logger.error((e as Error).message, (e as Error).stack, 'saveFile');
|
||||
throw new MediaBackendError(`Could not delete '${fileName}' on S3`);
|
||||
}
|
||||
}
|
||||
|
||||
private getUrl(fileName: string): string {
|
||||
const url = new URL(this.config.endPoint);
|
||||
const port = url.port !== '' ? `:${url.port}` : '';
|
||||
const bucket = this.config.bucket;
|
||||
return `${url.protocol}//${url.hostname}${port}${url.pathname}${bucket}/${fileName}`;
|
||||
}
|
||||
}
|
139
backend/src/media/backends/webdav-backend.ts
Normal file
139
backend/src/media/backends/webdav-backend.ts
Normal file
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { URL } from 'url';
|
||||
|
||||
import mediaConfiguration, { MediaConfig } from '../../config/media.config';
|
||||
import { MediaBackendError } from '../../errors/errors';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { MediaBackend } from '../media-backend.interface';
|
||||
import { BackendData } from '../media-upload.entity';
|
||||
import { BackendType } from './backend-type.enum';
|
||||
|
||||
@Injectable()
|
||||
export class WebdavBackend implements MediaBackend {
|
||||
private config: MediaConfig['backend']['webdav'];
|
||||
private authHeader: string;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@Inject(mediaConfiguration.KEY)
|
||||
private mediaConfig: MediaConfig,
|
||||
) {
|
||||
this.logger.setContext(WebdavBackend.name);
|
||||
if (mediaConfig.backend.use === BackendType.WEBDAV) {
|
||||
this.config = mediaConfig.backend.webdav;
|
||||
const url = new URL(this.config.connectionString);
|
||||
const port = url.port !== '' ? `:${url.port}` : '';
|
||||
this.baseUrl = `${url.protocol}//${url.hostname}${port}${url.pathname}`;
|
||||
if (this.config.uploadDir && this.config.uploadDir !== '') {
|
||||
this.baseUrl = WebdavBackend.joinURL(
|
||||
this.baseUrl,
|
||||
this.config.uploadDir,
|
||||
);
|
||||
}
|
||||
this.authHeader = WebdavBackend.generateBasicAuthHeader(
|
||||
url.username,
|
||||
url.password,
|
||||
);
|
||||
fetch(this.baseUrl, {
|
||||
method: 'PROPFIND',
|
||||
headers: {
|
||||
Accept: 'text/plain', // eslint-disable-line @typescript-eslint/naming-convention
|
||||
Authorization: this.authHeader, // eslint-disable-line @typescript-eslint/naming-convention
|
||||
Depth: '0', // eslint-disable-line @typescript-eslint/naming-convention
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Can't access ${this.baseUrl}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error(`Can't access ${this.baseUrl}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async saveFile(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
): Promise<[string, BackendData]> {
|
||||
try {
|
||||
const contentLength = buffer.length;
|
||||
await fetch(WebdavBackend.joinURL(this.baseUrl, '/', fileName), {
|
||||
method: 'PUT',
|
||||
body: buffer,
|
||||
headers: {
|
||||
Authorization: this.authHeader, // eslint-disable-line @typescript-eslint/naming-convention
|
||||
'Content-Type': 'application/octet-stream', // eslint-disable-line @typescript-eslint/naming-convention
|
||||
'Content-Length': `${contentLength}`, // eslint-disable-line @typescript-eslint/naming-convention
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'If-None-Match': '*', // Don't overwrite already existing files
|
||||
},
|
||||
}).then((res) => WebdavBackend.checkStatus(res));
|
||||
this.logger.log(`Uploaded file ${fileName}`, 'saveFile');
|
||||
return [this.getUrl(fileName), null];
|
||||
} catch (e) {
|
||||
this.logger.error((e as Error).message, (e as Error).stack, 'saveFile');
|
||||
throw new MediaBackendError(`Could not save '${fileName}' on WebDav`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(fileName: string, _: BackendData): Promise<void> {
|
||||
try {
|
||||
await fetch(WebdavBackend.joinURL(this.baseUrl, '/', fileName), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
Authorization: this.authHeader,
|
||||
},
|
||||
}).then((res) => WebdavBackend.checkStatus(res));
|
||||
const url = this.getUrl(fileName);
|
||||
this.logger.log(`Deleted ${url}`, 'deleteFile');
|
||||
return;
|
||||
} catch (e) {
|
||||
this.logger.error((e as Error).message, (e as Error).stack, 'saveFile');
|
||||
throw new MediaBackendError(`Could not delete '${fileName}' on WebDav`);
|
||||
}
|
||||
}
|
||||
|
||||
private getUrl(fileName: string): string {
|
||||
return WebdavBackend.joinURL(this.config.publicUrl, '/', fileName);
|
||||
}
|
||||
|
||||
private static generateBasicAuthHeader(
|
||||
username: string,
|
||||
password: string,
|
||||
): string {
|
||||
const encoded = Buffer.from(`${username}:${password}`).toString('base64');
|
||||
return `Basic ${encoded}`;
|
||||
}
|
||||
|
||||
private static joinURL(...urlParts: Array<string>): string {
|
||||
return urlParts.reduce((output, next, index) => {
|
||||
if (
|
||||
index === 0 ||
|
||||
next !== '/' ||
|
||||
(next === '/' && output[output.length - 1] !== '/')
|
||||
) {
|
||||
output += next;
|
||||
}
|
||||
return output;
|
||||
}, '');
|
||||
}
|
||||
|
||||
private static checkStatus(res: Response): Response {
|
||||
if (res.ok) {
|
||||
// res.status >= 200 && res.status < 300
|
||||
return res;
|
||||
} else {
|
||||
throw new MediaBackendError(res.statusText);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue