/* * 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 { 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 { 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); } } }