diff --git a/apps/gauzy/src/app/pages/settings/file-storage/file-storage.component.html b/apps/gauzy/src/app/pages/settings/file-storage/file-storage.component.html
index 57ca75ca392..09aae7d8fc0 100644
--- a/apps/gauzy/src/app/pages/settings/file-storage/file-storage.component.html
+++ b/apps/gauzy/src/app/pages/settings/file-storage/file-storage.component.html
@@ -36,6 +36,11 @@
*ngTemplateOutlet="cloudinaryStorageFormTemplate; context: { group: form }">
+
+
+
+
@@ -244,6 +249,17 @@
/>
+
@@ -358,3 +374,120 @@
+
+
+
+
+
+ {{ 'SETTINGS_FILE_STORAGE.DIGITALOCEAN.HEADER' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/gauzy/src/app/pages/settings/file-storage/file-storage.component.ts b/apps/gauzy/src/app/pages/settings/file-storage/file-storage.component.ts
index b8f83c72925..75611a3a618 100644
--- a/apps/gauzy/src/app/pages/settings/file-storage/file-storage.component.ts
+++ b/apps/gauzy/src/app/pages/settings/file-storage/file-storage.component.ts
@@ -31,12 +31,18 @@ export class FileStorageComponent extends TranslationBaseComponent
loading: boolean = false;
public readonly form: UntypedFormGroup = FileStorageComponent.buildForm(this.fb);
+
+ /**
+ *
+ * @param fb
+ * @returns
+ */
static buildForm(fb: UntypedFormBuilder): UntypedFormGroup {
+ const defaultFileStorageProvider = environment.FILE_PROVIDER.toUpperCase() as FileStorageProviderEnum || FileStorageProviderEnum.LOCAL;
+
+ //
const form = fb.group({
- fileStorageProvider: [
- (environment.FILE_PROVIDER).toUpperCase() as FileStorageProviderEnum || FileStorageProviderEnum.LOCAL,
- Validators.required
- ],
+ fileStorageProvider: [defaultFileStorageProvider, Validators.required],
// Aws Configuration
S3: fb.group({
aws_access_key_id: [],
@@ -50,7 +56,8 @@ export class FileStorageComponent extends TranslationBaseComponent
wasabi_aws_secret_access_key: [],
wasabi_aws_default_region: ['us-east-1'],
wasabi_aws_service_url: ['https://s3.wasabisys.com'],
- wasabi_aws_bucket: ['gauzy']
+ wasabi_aws_bucket: ['gauzy'],
+ wasabi_aws_force_path_style: [false]
}),
// Cloudinary Configuration
CLOUDINARY: fb.group({
@@ -60,6 +67,16 @@ export class FileStorageComponent extends TranslationBaseComponent
cloudinary_api_secure: ['true'],
cloudinary_delivery_url: ['https://res.cloudinary.com']
}),
+ // DigitalOcean Configuration
+ DIGITALOCEAN: fb.group({
+ digitalocean_access_key_id: [],
+ digitalocean_secret_access_key: [],
+ digitalocean_default_region: [{ value: 'us-east-1', disabled: true }],
+ digitalocean_service_url: [],
+ digitalocean_cdn_url: [],
+ digitalocean_s3_bucket: ['gauzy'],
+ digitalocean_s3_force_path_style: [false]
+ }),
});
return form;
}
diff --git a/apps/gauzy/src/app/pages/settings/file-storage/file-storage.module.ts b/apps/gauzy/src/app/pages/settings/file-storage/file-storage.module.ts
index 9f2e0c1d142..d18fe47d0f5 100644
--- a/apps/gauzy/src/app/pages/settings/file-storage/file-storage.module.ts
+++ b/apps/gauzy/src/app/pages/settings/file-storage/file-storage.module.ts
@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { NbButtonModule, NbCardModule, NbInputModule, NbSelectModule, NbSpinnerModule } from '@nebular/theme';
+import { NbButtonModule, NbCardModule, NbInputModule, NbSelectModule, NbSpinnerModule, NbToggleModule } from '@nebular/theme';
import { NgxPermissionsModule } from 'ngx-permissions';
import { FileProviderModule } from '../../../@shared/selectors/file-provider/file-provider.module';
import { SharedModule } from '../../../@shared/shared.module';
@@ -14,20 +14,21 @@ import { FileStorageComponent } from './file-storage.component';
FormsModule,
ReactiveFormsModule,
FileStorageRoutingModule,
- ThemeModule,
- TranslateModule,
- SharedModule,
- NgxPermissionsModule.forChild(),
NbButtonModule,
NbCardModule,
NbInputModule,
NbSelectModule,
+ NbSpinnerModule,
+ NbToggleModule,
+ NgxPermissionsModule.forChild(),
+ ThemeModule,
+ TranslateModule,
+ SharedModule,
FileProviderModule,
- NbSpinnerModule
],
declarations: [
FileStorageComponent
],
providers: []
})
-export class FileStorageModule {}
+export class FileStorageModule { }
diff --git a/apps/gauzy/src/assets/i18n/en.json b/apps/gauzy/src/assets/i18n/en.json
index 39e7a1a6193..d69d7008839 100644
--- a/apps/gauzy/src/assets/i18n/en.json
+++ b/apps/gauzy/src/assets/i18n/en.json
@@ -4445,7 +4445,8 @@
"SECRET_ACCESS_KEY": "Secret access key",
"REGION": "Region",
"BUCKET": "Bucket",
- "SERVICE_URL": "Service URL"
+ "SERVICE_URL": "Service URL",
+ "FORCE_PATH_STYLE": "Force path style URLs"
},
"PLACEHOLDERS": {
"ACCESS_KEY_ID": "Access key id",
@@ -4471,6 +4472,26 @@
"DELIVERY_URL": "Delivery URL",
"SECURE": "Secure"
}
+ },
+ "DIGITALOCEAN": {
+ "HEADER": "DigitalOcean Configuration",
+ "LABELS": {
+ "ACCESS_KEY_ID": "Access key id",
+ "SECRET_ACCESS_KEY": "Secret access key",
+ "REGION": "Region",
+ "BUCKET": "Bucket",
+ "SERVICE_URL": "Service URL",
+ "CDN_URL": "CDN URL",
+ "FORCE_PATH_STYLE": "Force path style URLs"
+ },
+ "PLACEHOLDERS": {
+ "ACCESS_KEY_ID": "Access key id",
+ "SECRET_ACCESS_KEY": "Secret access key",
+ "REGION": "Region",
+ "BUCKET": "Bucket",
+ "SERVICE_URL": "Service URL",
+ "CDN_URL": "CDN URL"
+ }
}
},
"CUSTOM_SMTP_PAGE": {
diff --git a/packages/common/src/interfaces/IAwsConfig.ts b/packages/common/src/interfaces/IAwsConfig.ts
index daebe1a7f06..77f1cdc9ef8 100644
--- a/packages/common/src/interfaces/IAwsConfig.ts
+++ b/packages/common/src/interfaces/IAwsConfig.ts
@@ -4,5 +4,6 @@ export interface IAwsConfig {
region: string;
s3: {
bucket: string;
+ forcePathStyle: boolean;
};
}
diff --git a/packages/common/src/interfaces/IDigitalOceanConfig.ts b/packages/common/src/interfaces/IDigitalOceanConfig.ts
new file mode 100644
index 00000000000..e0a9b88cbeb
--- /dev/null
+++ b/packages/common/src/interfaces/IDigitalOceanConfig.ts
@@ -0,0 +1,21 @@
+/**
+ * DigitalOcean Spaces Configuration
+ */
+export interface IDigitalOceanConfig {
+ /** DigitalOcean Access Key ID */
+ readonly accessKeyId: string;
+ /** DigitalOcean Secret Access Key */
+ readonly secretAccessKey: string;
+ /** DigitalOcean Region */
+ readonly region: string;
+ /** DigitalOcean Service URL */
+ readonly serviceUrl: string;
+ /** The CDN (Content Delivery Network) DigitalOcean configuration. */
+ readonly cdnUrl?: string;
+ /** S3 Bucket Configuration */
+ readonly s3: {
+ /** S3 Bucket Name */
+ readonly bucket: string;
+ readonly forcePathStyle: boolean;
+ };
+}
diff --git a/packages/common/src/interfaces/IWasabiConfig.ts b/packages/common/src/interfaces/IWasabiConfig.ts
index 80babee765e..bb7d7e07e76 100644
--- a/packages/common/src/interfaces/IWasabiConfig.ts
+++ b/packages/common/src/interfaces/IWasabiConfig.ts
@@ -18,5 +18,6 @@ export interface IWasabiConfig {
readonly s3: {
/** S3 Bucket Name */
readonly bucket: string;
+ readonly forcePathStyle: boolean;
};
}
diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts
index a3f526fc17a..219eeb910c4 100644
--- a/packages/common/src/interfaces/index.ts
+++ b/packages/common/src/interfaces/index.ts
@@ -18,6 +18,7 @@ export * from './ITwitterConfig';
export * from './IUnleashConfig';
export * from './IUpworkConfig';
export * from './IWasabiConfig';
+export * from './IDigitalOceanConfig';
export * from './IHubstaffConfig';
export * from './IJitsuConfig';
export * from './IJiraIntegrationConfig';
diff --git a/packages/config/src/environments/environment.prod.ts b/packages/config/src/environments/environment.prod.ts
index 1374042ea45..8aa56f9279d 100644
--- a/packages/config/src/environments/environment.prod.ts
+++ b/packages/config/src/environments/environment.prod.ts
@@ -79,7 +79,8 @@ export const environment: IEnvironment = {
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION || 'us-east-1',
s3: {
- bucket: process.env.AWS_S3_BUCKET || 'gauzy'
+ bucket: process.env.AWS_S3_BUCKET || 'gauzy',
+ forcePathStyle: process.env.AWS_S3_FORCE_PATH_STYLE === 'true' || false
}
},
@@ -92,7 +93,23 @@ export const environment: IEnvironment = {
region: process.env.WASABI_REGION || 'us-east-1',
serviceUrl: process.env.WASABI_SERVICE_URL || 'https://s3.wasabisys.com',
s3: {
- bucket: process.env.WASABI_S3_BUCKET || 'gauzy'
+ bucket: process.env.WASABI_S3_BUCKET || 'gauzy',
+ forcePathStyle: process.env.WASABI_S3_FORCE_PATH_STYLE === 'true' || false
+ }
+ },
+
+ /**
+ * DigitalOcean Spaces Configuration
+ */
+ digitalOcean: {
+ accessKeyId: process.env.DIGITALOCEAN_ACCESS_KEY_ID,
+ secretAccessKey: process.env.DIGITALOCEAN_SECRET_ACCESS_KEY,
+ region: process.env.DIGITALOCEAN_REGION || 'us-east-1',
+ serviceUrl: process.env.DIGITALOCEAN_SERVICE_URL || 'https://gauzy.sfo2.digitaloceanspaces.com', // Find your endpoint in the control panel, under Settings. Prepend "https://".
+ cdnUrl: process.env.DIGITALOCEAN_CDN_URL,
+ s3: {
+ bucket: process.env.DIGITALOCEAN_S3_BUCKET || 'gauzy',
+ forcePathStyle: process.env.DIGITALOCEAN_S3_FORCE_PATH_STYLE === 'true' || false // Configures to use subdomain/virtual calling format.
}
},
diff --git a/packages/config/src/environments/environment.ts b/packages/config/src/environments/environment.ts
index 0d8851c191d..490d52c0777 100644
--- a/packages/config/src/environments/environment.ts
+++ b/packages/config/src/environments/environment.ts
@@ -80,7 +80,8 @@ export const environment: IEnvironment = {
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION || 'us-east-1',
s3: {
- bucket: process.env.AWS_S3_BUCKET || 'gauzy'
+ bucket: process.env.AWS_S3_BUCKET || 'gauzy',
+ forcePathStyle: process.env.AWS_S3_FORCE_PATH_STYLE === 'true' || false
}
},
@@ -93,7 +94,23 @@ export const environment: IEnvironment = {
region: process.env.WASABI_REGION || 'us-east-1',
serviceUrl: process.env.WASABI_SERVICE_URL || 'https://s3.wasabisys.com',
s3: {
- bucket: process.env.WASABI_S3_BUCKET || 'gauzy'
+ bucket: process.env.WASABI_S3_BUCKET || 'gauzy',
+ forcePathStyle: process.env.WASABI_S3_FORCE_PATH_STYLE === 'true' || false
+ }
+ },
+
+ /**
+ * DigitalOcean Spaces Configuration
+ */
+ digitalOcean: {
+ accessKeyId: process.env.DIGITALOCEAN_ACCESS_KEY_ID,
+ secretAccessKey: process.env.DIGITALOCEAN_SECRET_ACCESS_KEY,
+ region: process.env.DIGITALOCEAN_REGION || 'us-east-1',
+ serviceUrl: process.env.DIGITALOCEAN_SERVICE_URL || 'https://gauzy.sfo2.digitaloceanspaces.com', // Find your endpoint in the control panel, under Settings. Prepend "https://".
+ cdnUrl: process.env.DIGITALOCEAN_CDN_URL,
+ s3: {
+ bucket: process.env.DIGITALOCEAN_S3_BUCKET || 'gauzy',
+ forcePathStyle: process.env.DIGITALOCEAN_S3_FORCE_PATH_STYLE === 'true' || false // Configures to use subdomain/virtual calling format.
}
},
diff --git a/packages/config/src/environments/ienvironment.ts b/packages/config/src/environments/ienvironment.ts
index b99424bb61a..8f28f563dd6 100644
--- a/packages/config/src/environments/ienvironment.ts
+++ b/packages/config/src/environments/ienvironment.ts
@@ -17,7 +17,8 @@ import {
IUnleashConfig,
IUpworkConfig,
IWasabiConfig,
- IJiraIntegrationConfig
+ IJiraIntegrationConfig,
+ IDigitalOceanConfig
} from '@gauzy/common';
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
@@ -98,6 +99,7 @@ export interface IEnvironment {
awsConfig?: IAwsConfig;
wasabi?: IWasabiConfig;
cloudinary?: ICloudinaryConfig;
+ digitalOcean?: IDigitalOceanConfig;
github: IGithubIntegrationConfig /** Github Configuration */;
jira: IJiraIntegrationConfig /** Jira Configuration */;
fiverrConfig: IFiverrConfig;
diff --git a/packages/contracts/src/file-provider.ts b/packages/contracts/src/file-provider.ts
index 61f79a77daf..0c2dd5401d2 100644
--- a/packages/contracts/src/file-provider.ts
+++ b/packages/contracts/src/file-provider.ts
@@ -11,10 +11,11 @@ export interface FileSystem {
}
export enum FileStorageProviderEnum {
- LOCAL = "LOCAL",
- S3 = "S3",
- WASABI = "WASABI",
- CLOUDINARY = "CLOUDINARY",
+ LOCAL = 'LOCAL',
+ S3 = 'S3',
+ WASABI = 'WASABI',
+ CLOUDINARY = 'CLOUDINARY',
+ DIGITALOCEAN = 'DIGITALOCEAN'
}
export interface UploadedFile {
@@ -34,6 +35,7 @@ export interface IS3FileStorageProviderConfig {
aws_secret_access_key?: string;
aws_default_region?: string;
aws_bucket?: string;
+ aws_force_path_style?: boolean;
}
export interface IWasabiFileStorageProviderConfig {
@@ -42,6 +44,7 @@ export interface IWasabiFileStorageProviderConfig {
wasabi_aws_default_region?: string;
wasabi_aws_service_url?: string;
wasabi_aws_bucket?: string;
+ wasabi_aws_force_path_style?: boolean;
}
export interface ICloudinaryFileStorageProviderConfig {
@@ -49,3 +52,13 @@ export interface ICloudinaryFileStorageProviderConfig {
cloudinary_api_key?: string;
cloudinary_api_secret?: string;
}
+
+export interface IDigitalOceanFileStorageProviderConfig {
+ digitalocean_access_key_id?: string;
+ digitalocean_secret_access_key?: string;
+ digitalocean_default_region?: string;
+ digitalocean_service_url?: string;
+ digitalocean_cdn_url?: string;
+ digitalocean_s3_bucket?: string;
+ digitalocean_s3_force_path_style?: boolean;
+}
diff --git a/packages/core/src/core/file-storage/providers/digitalocean-s3.provider.ts b/packages/core/src/core/file-storage/providers/digitalocean-s3.provider.ts
new file mode 100644
index 00000000000..4454df016d0
--- /dev/null
+++ b/packages/core/src/core/file-storage/providers/digitalocean-s3.provider.ts
@@ -0,0 +1,439 @@
+import { HttpException, HttpStatus } from '@nestjs/common';
+import * as multerS3 from 'multer-s3';
+import { basename, join } from 'path';
+import * as moment from 'moment';
+import {
+ S3Client,
+ DeleteObjectCommand,
+ DeleteObjectCommandOutput,
+ GetObjectCommand,
+ GetObjectCommandOutput,
+ PutObjectCommand,
+ HeadObjectCommand
+} from '@aws-sdk/client-s3';
+import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
+import { StorageEngine } from 'multer';
+import { environment } from '@gauzy/config';
+import { FileStorageOption, FileStorageProviderEnum, UploadedFile } from '@gauzy/contracts';
+import { addHttpsPrefix, trimAndGetValue } from '@gauzy/common';
+import { Provider } from './provider';
+import { RequestContext } from '../../context';
+
+/**
+ * Digital Ocean Configuration
+ */
+const { digitalOcean } = environment;
+
+/**
+ * Configuration interface for DigitalOcean storage.
+ */
+export interface IDigitalOceanProviderConfig {
+ rootPath: string; // Root path for DigitalOcean storage
+ digitalocean_access_key_id: string; // AWS access key ID for DigitalOcean
+ digitalocean_secret_access_key: string; // AWS secret access key for DigitalOcean
+ digitalocean_default_region: string; // AWS default region for DigitalOcean
+ digitalocean_service_url: string; // AWS service URL for DigitalOcean
+ digitalocean_cdn_url: string; // AWS service CDN for DigitalOcean
+ digitalocean_s3_bucket: string; // AWS bucket name for DigitalOcean
+ digitalocean_s3_force_path_style: boolean; // Whether to force path style URLs for DigitalOcean objects
+}
+
+export class DigitalOceanS3Provider extends Provider {
+
+ public readonly name = FileStorageProviderEnum.DIGITALOCEAN;
+ public instance: DigitalOceanS3Provider;
+ public config: IDigitalOceanProviderConfig;
+ public defaultConfig: IDigitalOceanProviderConfig;
+
+ private readonly _detailedloggingEnabled = false;
+
+ constructor() {
+ super();
+ this.config = this.defaultConfig = {
+ rootPath: '',
+ digitalocean_access_key_id: digitalOcean.accessKeyId,
+ digitalocean_secret_access_key: digitalOcean.secretAccessKey,
+ digitalocean_default_region: digitalOcean.region,
+ digitalocean_service_url: digitalOcean.serviceUrl,
+ digitalocean_cdn_url: digitalOcean.cdnUrl,
+ digitalocean_s3_bucket: digitalOcean.s3.bucket,
+ digitalocean_s3_force_path_style: digitalOcean.s3.forcePathStyle,
+ };
+ }
+
+ /**
+ * Get the singleton instance of DigitalOceanS3Provider
+ *
+ * @returns {DigitalOceanS3Provider} The singleton instance
+ */
+ getProviderInstance(): DigitalOceanS3Provider {
+ if (!this.instance) {
+ this.instance = new DigitalOceanS3Provider();
+ }
+
+ this.setDigitalOceanConfiguration();
+ return this.instance;
+ }
+
+ /**
+ * Set DigitalOcean details based on the current request's tenantSettings.
+ * If such settings does not have any DigitalOcean details, use the default configuration.
+ * If they have DigitalOcean details, use them to override the default configuration.
+ */
+ private setDigitalOceanConfiguration() {
+ // Use the default configuration as a starting point
+ this.config = {
+ ...this.defaultConfig
+ };
+
+ if (this._detailedloggingEnabled) {
+ console.log(`setDigitalOceanConfiguration this config value: ${JSON.stringify(this.config)}`);
+ }
+
+ try {
+ const request = RequestContext.currentRequest();
+ if (request) {
+ const settings = request['tenantSettings'];
+
+ if (settings) {
+ if (this._detailedloggingEnabled) {
+ console.log(`setDigitalOceanConfiguration Tenant Settings Value: ${JSON.stringify(settings)}`);
+ }
+
+ if (trimAndGetValue(settings.digitalocean_access_key_id)) {
+ this.config.digitalocean_access_key_id = trimAndGetValue(settings.digitalocean_access_key_id);
+
+ if (this._detailedloggingEnabled) {
+ console.log(`setDigitalOceanConfiguration this.config.digitalocean_access_key_id value: ${this.config.digitalocean_access_key_id}`);
+ }
+ }
+
+ if (trimAndGetValue(settings.digitalocean_secret_access_key)) {
+ this.config.digitalocean_secret_access_key = trimAndGetValue(settings.digitalocean_secret_access_key);
+
+ if (this._detailedloggingEnabled) {
+ console.log(`setDigitalOceanConfiguration this.config.digitalocean_secret_access_key value: ${this.config.digitalocean_secret_access_key}`);
+ }
+ }
+
+ if (trimAndGetValue(settings.digitalocean_service_url)) {
+ this.config.digitalocean_service_url = addHttpsPrefix(trimAndGetValue(settings.digitalocean_service_url));
+
+ if (this._detailedloggingEnabled) {
+ console.log('setDigitalOceanConfiguration this.config.digitalocean_service_url value: ', this.config.digitalocean_service_url);
+ }
+ }
+
+ if (trimAndGetValue(settings.digitalocean_default_region)) {
+ this.config.digitalocean_default_region = trimAndGetValue(settings.digitalocean_default_region);
+
+ if (this._detailedloggingEnabled) {
+ console.log('setDigitalOceanConfiguration this.config.digitalocean_default_region value: ', this.config.digitalocean_default_region);
+ }
+ }
+
+ if (trimAndGetValue(settings.digitalocean_s3_bucket)) {
+ this.config.digitalocean_s3_bucket = trimAndGetValue(settings.digitalocean_s3_bucket);
+
+ if (this._detailedloggingEnabled) {
+ console.log('setDigitalOceanConfiguration this.config.digitalocean_s3_bucket value: ', this.config.digitalocean_s3_bucket);
+ }
+ }
+
+ // Assuming trimAndGetValue() function trims and retrieves the value from settings
+ const forcePathStyle = trimAndGetValue(settings.digitalocean_s3_force_path_style);
+ this.config.digitalocean_s3_force_path_style = forcePathStyle === 'true' || forcePathStyle === '1';
+
+ if (this._detailedloggingEnabled) {
+ console.log('setDigitalOceanConfiguration this.config.digitalocean_s3_force_path_style value: ', this.config.digitalocean_s3_force_path_style);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error while setting DigitalOcean configuration. Default configuration will be used', error);
+ }
+ }
+
+ /**
+ * Get a pre-signed URL for a given file URL.
+ *
+ * @param fileURL - The file URL for which to generate a pre-signed URL
+ * @returns Pre-signed URL or null if the input is invalid
+ */
+ public async url(fileURL: string): Promise {
+ if (!fileURL || fileURL.startsWith('http')) {
+ return fileURL;
+ }
+
+ try {
+ const s3Client = this.getDigitalOceanInstance();
+
+ if (s3Client) {
+ const signedUrl = await getSignedUrl(
+ s3Client,
+ new GetObjectCommand({
+ Bucket: this.getDigitalOceanBucket(),
+ Key: fileURL
+ }),
+ {
+ expiresIn: 3600
+ }
+ );
+
+ return signedUrl;
+ } else {
+ console.error('Error while retrieving signed URL: s3Client is null');
+ return null;
+ }
+ } catch (error) {
+ console.error('Error while retrieving signed URL:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Get the full path of the file in the storage.
+ *
+ * @param filePath - The file path to join with the root path
+ * @returns Full path or null if filePath is falsy
+ */
+ public path(filePath: string): string | null {
+ return filePath ? join(this.config.rootPath, filePath) : null;
+ }
+
+ /**
+ * Create a Multer storage engine configured for AWS S3 (DigitalOcean).
+ *
+ * @param options - Configuration options for the storage engine
+ * @returns A Multer storage engine
+ */
+ public handler(options: FileStorageOption): StorageEngine {
+ const { dest, filename, prefix = 'file' } = options;
+
+ try {
+ const s3Client = this.getDigitalOceanInstance();
+
+ if (s3Client) {
+ return multerS3({
+ s3: s3Client,
+ bucket: (_req, _file, callback) => {
+ callback(null, this.getDigitalOceanBucket());
+ },
+ metadata: function (_req, _file, callback) {
+ callback(null, { fieldName: _file.fieldname });
+ },
+ key: (_req, file, callback) => {
+ // A string or function that determines the destination path for uploaded
+ const destination = dest instanceof Function ? dest(file) : dest;
+
+ // A file extension, or filename extension, is a suffix at the end of a file.
+ const extension = file.originalname.split('.').pop();
+
+ // A function that determines the name of the uploaded file.
+ let fileName: string;
+
+ if (filename) {
+ fileName = typeof filename === 'string' ? filename : filename(file, extension);
+ } else {
+ fileName = `${prefix}-${moment().unix()}-${parseInt('' + Math.random() * 1000, 10)}.${extension}`;
+ }
+
+ // Replace double backslashes with single forward slashes
+ const fullPath = join(destination, fileName).replace(/\\/g, '/');
+
+ callback(null, fullPath);
+ }
+ });
+ } else {
+ console.error('Error while retrieving Multer for DigitalOcean: s3Client is null');
+ return null;
+ }
+ } catch (error) {
+ console.error('Error while retrieving Multer for DigitalOcean:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Get a file from DigitalOcean storage.
+ *
+ * @param key - The key of the file to retrieve
+ * @returns A Promise resolving to a Buffer containing the file data
+ */
+ async getFile(key: string): Promise {
+ try {
+ const s3Client = this.getDigitalOceanInstance();
+
+ if (s3Client) {
+ // Input parameters when using the GetObjectCommand to retrieve an object from DigitalOcean storage.
+ const command = new GetObjectCommand({
+ Bucket: this.getDigitalOceanBucket(), // The name of the bucket from which to retrieve the object.
+ Key: key // The key (path) of the object to retrieve from the bucket.
+ });
+
+ /**
+ * Send a GetObjectCommand to DigitalOcean to retrieve an object
+ */
+ const data: GetObjectCommandOutput = await s3Client.send(command);
+ return data.Body;
+ } else {
+ console.error('Error while retrieving signed URL: s3Client is null');
+ }
+ } catch (error) {
+ console.error(`Error while fetching file with key '${key}':`, error);
+ }
+ }
+
+ /**
+ * Upload a file to DigitalOcean storage.
+ *
+ * @param fileContent - The content of the file to upload
+ * @param key - The key under which to store the file
+ * @returns A Promise resolving to an UploadedFile, or undefined on error
+ */
+ async putFile(fileContent: string, key: string = ''): Promise {
+ try {
+ // Replace double backslashes with single forward slashes
+ key = key.replace(/\\/g, '/');
+
+ const s3Client = this.getDigitalOceanInstance();
+
+ if (s3Client) {
+ const filename = basename(key);
+
+ // Input parameters for the PutObjectCommand when uploading a file to DigitalOcean storage.
+ const putObjectCommand = new PutObjectCommand({
+ Bucket: this.getDigitalOceanBucket(), // The name of the bucket to which the file should be uploaded.
+ Body: fileContent, // The content of the file to be uploaded.
+ Key: key, // The key (path) under which to store the file in the bucket.
+ ContentDisposition: `inline; ${filename}`, // Additional headers for the object.
+ ContentType: 'image'
+ });
+
+ /**
+ * Send a PutObjectCommand to DigitalOcean to upload the object
+ */
+ await s3Client.send(putObjectCommand);
+
+ // Input parameters for the HeadObjectCommand when retrieving metadata about an object in DigitalOcean storage.
+ const headObjectCommand = new HeadObjectCommand({
+ Key: key, // The key (path) of the object for which to retrieve metadata.
+ Bucket: this.getDigitalOceanBucket() // The name of the bucket where the object is stored.
+ });
+
+ // Send a HeadObjectCommand to DigitalOcean to retrieve ContentLength property metadata
+ const { ContentLength } = await s3Client.send(headObjectCommand);
+
+ const file: Partial = {
+ originalname: filename, // original file name
+ size: ContentLength, // files in bytes
+ filename: filename,
+ path: key, // Full path of the file
+ key: key // Full path of the file
+ };
+
+ return await this.mapUploadedFile(file);
+ } else {
+ console.warn('Error while retrieving signed URL: s3Client is null');
+ }
+ } catch (error) {
+ console.error('Error while put file for DigitalOcean provider', error);
+ }
+ }
+
+ /**
+ * Delete a file from DigitalOcean storage.
+ *
+ * @param key - The key of the file to delete
+ * @returns A Promise that resolves when the file is deleted successfully, or rejects with an error
+ */
+ async deleteFile(key: string): Promise