115 lines
3.4 KiB
TypeScript
115 lines
3.4 KiB
TypeScript
import { StorageProvider } from '@/lib/storage/storage.interface';
|
|
import { TypedResult, wrap } from '@/utils/types/results';
|
|
import {
|
|
DeleteObjectCommand,
|
|
HeadObjectCommand,
|
|
PutObjectCommand,
|
|
S3Client,
|
|
} from '@aws-sdk/client-s3';
|
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
import { z } from 'zod';
|
|
|
|
/**
|
|
* Configuration for S3 storage adapter
|
|
*/
|
|
export const S3StorageConfig = z.object({
|
|
endpoint: z.string(),
|
|
bucket: z.string(),
|
|
region: z.string(),
|
|
accessKey: z.string(),
|
|
secretKey: z.string(),
|
|
});
|
|
export type S3StorageConfig = z.infer<typeof S3StorageConfig>;
|
|
|
|
/**
|
|
* AWS S3 storage adapter
|
|
* Uploads files to S3 and returns public URLs
|
|
*/
|
|
export class S3StorageAdapter implements StorageProvider {
|
|
private readonly s3Client: S3Client;
|
|
private readonly endpoint: string;
|
|
private readonly bucketName: string;
|
|
|
|
readonly get: (
|
|
...args: Parameters<StorageProvider['get']>
|
|
) => Promise<TypedResult<string>>;
|
|
readonly put: (
|
|
...args: Parameters<StorageProvider['put']>
|
|
) => Promise<TypedResult<string>>;
|
|
readonly exists: (
|
|
...args: Parameters<StorageProvider['exists']>
|
|
) => Promise<boolean>;
|
|
readonly delete: (
|
|
...args: Parameters<StorageProvider['delete']>
|
|
) => Promise<TypedResult<void>>;
|
|
|
|
constructor(config: S3StorageConfig, s3Client?: S3Client) {
|
|
this.endpoint = config.endpoint;
|
|
this.bucketName = config.bucket;
|
|
this.s3Client =
|
|
s3Client ||
|
|
new S3Client({
|
|
endpoint: config.endpoint,
|
|
region: config.region,
|
|
forcePathStyle: true,
|
|
credentials: {
|
|
accessKeyId: config.accessKey,
|
|
secretAccessKey: config.secretKey,
|
|
},
|
|
});
|
|
|
|
this.get = wrap(this._get.bind(this));
|
|
this.put = wrap(this._put.bind(this));
|
|
this.delete = wrap(this._delete.bind(this));
|
|
this.exists = this._exists.bind(this);
|
|
}
|
|
|
|
private async _get(key: string): Promise<string> {
|
|
return `${this.endpoint}/${this.bucketName}/${key}`;
|
|
}
|
|
|
|
private async _put(key: string, contentType: string): Promise<string> {
|
|
const command = new PutObjectCommand({
|
|
Bucket: this.bucketName,
|
|
Key: key,
|
|
ContentType: contentType,
|
|
});
|
|
|
|
return await getSignedUrl(this.s3Client, command, { expiresIn: 3600 });
|
|
}
|
|
|
|
private async _exists(key: string): Promise<boolean> {
|
|
try {
|
|
await this.s3Client.send(
|
|
new HeadObjectCommand({
|
|
Bucket: this.bucketName,
|
|
Key: key,
|
|
})
|
|
);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async _delete(key: string): Promise<void> {
|
|
await this.s3Client.send(
|
|
new DeleteObjectCommand({
|
|
Bucket: this.bucketName,
|
|
Key: key,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
export const new_s3_storage_adapter = (): S3StorageAdapter => {
|
|
const config = S3StorageConfig.parse({
|
|
endpoint: process.env.S3_ENDPOINT,
|
|
bucket: process.env.S3_BUCKET_NAME,
|
|
region: process.env.S3_REGION,
|
|
accessKey: process.env.S3_ACCESS_KEY,
|
|
secretKey: process.env.S3_SECRET_KEY,
|
|
});
|
|
return new S3StorageAdapter(config);
|
|
};
|