add: bunnycdn storage support

This commit is contained in:
Marie 2025-03-28 00:21:44 +01:00
parent 92382b2ed4
commit 0dead4637e
No known key found for this signature in database
GPG key ID: 7ADF6C9CD9A28555
3 changed files with 99 additions and 16 deletions

View file

@ -0,0 +1,68 @@
/*
* SPDX-FileCopyrightText: marie and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as https from 'node:https';
import * as fs from 'node:fs';
import { Readable } from 'node:stream';
import { finished } from 'node:stream/promises';
import { Injectable } from '@nestjs/common';
import type { MiMeta } from '@/models/Meta.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class BunnyService {
constructor(
private httpRequestService: HttpRequestService,
) {
}
@bindThis
public getBunnyInfo(meta: MiMeta) {
return {
endpoint: meta.objectStorageEndpoint ?? undefined,
accessKey: meta.objectStorageSecretKey ?? '',
zone: meta.objectStorageBucket ?? undefined,
prefix: meta.objectStoragePrefix ?? '',
};
}
@bindThis
public async upload(meta: MiMeta, path: string, input: fs.ReadStream | Buffer) {
const client = this.getBunnyInfo(meta);
// Required to convert the buffer from webpulic and thumbnail to a ReadableStream for PUT
const data = Buffer.isBuffer(input) ? Readable.from(input) : input;
const options = {
method: 'PUT',
host: client.endpoint,
path: `/${client.zone}/${path}`,
headers: {
AccessKey: client.accessKey,
'Content-Type': 'application/octet-stream',
},
};
const req = https.request(options);
req.on('error', (error) => {
console.error(error);
});
data.pipe(req).on('finish', () => {
data.destroy();
});
// wait till stream gets destroyed upon finish of piping to prevent the UI from showing the upload as success wait too early
await finished(data);
}
@bindThis
public delete(meta: MiMeta, file: string) {
const client = this.getBunnyInfo(meta);
return this.httpRequestService.send(`https://${client.endpoint}/${client.zone}/${file}`, { method: 'DELETE', headers: { AccessKey: client.accessKey } });
}
}

View file

@ -61,6 +61,7 @@ import { ReactionsBufferingService } from './ReactionsBufferingService.js';
import { RelayService } from './RelayService.js';
import { RoleService } from './RoleService.js';
import { S3Service } from './S3Service.js';
import { BunnyService } from './BunnyService.js';
import { SignupService } from './SignupService.js';
import { WebAuthnService } from './WebAuthnService.js';
import { UserBlockingService } from './UserBlockingService.js';
@ -208,6 +209,7 @@ const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingServi
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
const $BunnyService: Provider = { provide: 'BunnyService', useExisting: BunnyService };
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
@ -367,6 +369,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
RelayService,
RoleService,
S3Service,
BunnyService,
SignupService,
WebAuthnService,
UserBlockingService,
@ -522,6 +525,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$RelayService,
$RoleService,
$S3Service,
$BunnyService,
$SignupService,
$WebAuthnService,
$UserBlockingService,
@ -678,6 +682,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
RelayService,
RoleService,
S3Service,
BunnyService,
SignupService,
WebAuthnService,
UserBlockingService,
@ -832,6 +837,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$RelayService,
$RoleService,
$S3Service,
$BunnyService,
$SignupService,
$WebAuthnService,
$UserBlockingService,

View file

@ -44,6 +44,7 @@ import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { BunnyService } from '@/core/BunnyService.js';
type AddFileArgs = {
/** User who wish to add file */
@ -121,6 +122,7 @@ export class DriveService {
private downloadService: DownloadService,
private internalStorageService: InternalStorageService,
private s3Service: S3Service,
private bunnyService: BunnyService,
private imageProcessingService: ImageProcessingService,
private videoProcessingService: VideoProcessingService,
private globalEventService: GlobalEventService,
@ -405,6 +407,7 @@ export class DriveService {
);
if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read';
if (this.meta.objectStorageAccessKey) {
await this.s3Service.upload(this.meta, params)
.then(
result => {
@ -419,6 +422,9 @@ export class DriveService {
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
},
);
} else {
await this.bunnyService.upload(this.meta, key, stream);
}
}
// Expire oldest file (without avatar or banner) of remote user
@ -814,8 +820,11 @@ export class DriveService {
Bucket: this.meta.objectStorageBucket,
Key: key,
} as DeleteObjectCommandInput;
if (this.meta.objectStorageAccessKey) {
await this.s3Service.delete(this.meta, param);
} else {
await this.bunnyService.delete(this.meta, key);
}
} catch (err: any) {
if (err.name === 'NoSuchKey') {
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);