change storage
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-05-31 01:17:30 +03:30
parent 85cccf3ba6
commit cd35b6349f
+32 -28
View File
@@ -13,28 +13,24 @@ export class S3Service {
private readonly logger = new Logger(S3Service.name);
private readonly s3Client: S3Client;
private readonly bucketName: string;
private readonly region: string; // Region is often 'us-east-1' or similar for Liara, but depends on your config
private readonly endpointUrl: string; // Renamed 'url' to 'endpointUrl' for clarity
private readonly region: string;
private readonly endpointUrl: string;
constructor(private readonly configService: ConfigService) {
// --- Configuration for Liara S3 ---
this.bucketName = this.configService.getOrThrow<string>('BUCKET_NAME');
// Liara S3 usually requires a specific endpoint URL
this.endpointUrl = this.configService.getOrThrow<string>('LIARA_S3_ENDPOINT');
// The region for Liara S3 is often 'us-east-1' or just a placeholder,
// but we'll keep it configurable for consistency.
const rawEndpoint = this.configService.getOrThrow<string>('PARSPACK_S3_ENDPOINT');
this.endpointUrl = rawEndpoint.startsWith('http') ? rawEndpoint : `https://${rawEndpoint}`;
this.region = this.configService.getOrThrow<string>('BUCKET_REGION');
this.s3Client = new S3Client({
region: this.region, // Use the configured region
endpoint: this.endpointUrl, // This is the crucial change for Liara S3
forcePathStyle: true, // Often required when using custom S3 endpoints like Liara
region: this.region,
endpoint: this.endpointUrl,
forcePathStyle: true, // Required for ParsPack S3-compatible API
credentials: {
accessKeyId: this.configService.getOrThrow<string>('BUCKET_ACCESS_KEY'),
secretAccessKey: this.configService.getOrThrow<string>('BUCKET_SECRET_KEY'),
},
});
// ------------------------------------
}
/**
@@ -49,31 +45,26 @@ export class S3Service {
try {
const sanitizedMetadata = metadata
? Object.entries(metadata).reduce(
(acc, [key, value]) => {
// Encode all values to be ASCII-safe
acc[key] = encodeURIComponent(value);
(acc, [metaKey, value]) => {
// S3 user metadata must be US-ASCII; base64 keeps signing stable for Unicode names
acc[metaKey] = Buffer.from(value, 'utf8').toString('base64');
return acc;
},
{} as Record<string, string>,
)
: undefined;
// NOTE: 'ACL: public-read' might not be supported or necessary for Liara S3,
// depending on your bucket configuration on Liara.
// If uploads fail, try removing this line.
const command = new PutObjectCommand({
Bucket: this.bucketName,
Key: key,
Body: buffer,
// ACL: 'public-read',
ContentType: contentType,
Metadata: sanitizedMetadata,
});
await this.s3Client.send(command);
// --- URL for Liara S3 ---
// The public URL for Liara S3 files is typically: ${endpoint}/${bucketName}/${key}
const url = `${this.endpointUrl}/${this.bucketName}/${key}`;
const encodedKey = key.split('/').map(segment => encodeURIComponent(segment)).join('/');
const url = `${this.endpointUrl}/${this.bucketName}/${encodedKey}`;
this.logger.log(`File uploaded to S3: ${key}`);
@@ -88,12 +79,9 @@ export class S3Service {
}
}
// --- Other methods remain the same as they use the S3Client which is now configured for Liara ---
/**
* Get file stream from S3
*/
async getFileStream(key: string): Promise<Readable> {
try {
const command = new GetObjectCommand({
@@ -103,18 +91,17 @@ export class S3Service {
const response = await this.s3Client.send(command);
// ✅ VALIDATION
if (response.Body instanceof Readable) {
return response.Body;
}
// If it's not a Readable stream, throw a specific error
throw new Error(`File body is not a readable stream for key: ${key}`);
} catch (error: unknown) {
this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Generate presigned URL for file download
*/
@@ -153,10 +140,10 @@ export class S3Service {
/**
* Generate S3 key for file
*/
generateFileKey(originalName: string, fileType: FileType): string {
generateFileKey(originalName: string, fileType: FileType, extensionOverride?: string): string {
const timestamp = Date.now();
const randomSuffix = randomInt(1000000, 9999999);
const extension = originalName.split('.').pop();
const extension = extensionOverride ?? this.extractSafeExtension(originalName);
const basePathMap: Record<FileType, string> = {
image: 'images',
@@ -170,4 +157,21 @@ export class S3Service {
const basePath = basePathMap[fileType];
return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
}
/**
* Extract a safe ASCII extension so Persian/spaced names never end up in the S3 object key.
*/
private extractSafeExtension(originalName: string): string {
const basename = originalName.replace(/\\/g, '/').split('/').pop() ?? 'file';
const lastDot = basename.lastIndexOf('.');
if (lastDot > 0 && lastDot < basename.length - 1) {
const ext = basename.slice(lastDot + 1).toLowerCase();
if (/^[a-z0-9]{1,16}$/.test(ext)) {
return ext;
}
}
return 'bin';
}
}