Archived
1
0

feat: Сервис S3 и тесты к нему

This commit is contained in:
2025-11-19 14:10:33 +03:00
parent ec37fc0316
commit f3c75c3b88
2 changed files with 147 additions and 0 deletions

View File

@ -0,0 +1,110 @@
import { S3Client } from 'bun';
import { env } from '@/config/env';
import { nanoid } from 'nanoid';
class S3Service {
private client: S3Client;
constructor() {
this.client = new S3Client({
endpoint: env.S3_ENDPOINT,
bucket: env.S3_BUCKET,
accessKeyId: env.S3_ACCESS_KEY_ID,
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
});
}
async uploadFile(
file: File | Buffer,
userId: string,
mimeType: string
): Promise<{ key: string; url: string; size: number }> {
const ext = this.getExtensionFromMimeType(mimeType);
const key = `${userId}/${nanoid()}.${ext}`;
let size: number;
if (file instanceof File) {
size = file.size;
} else {
size = file.length;
}
const s3File = this.client.file(key);
await s3File.write(file, { type: mimeType });
const url = this.getPublicUrl(key);
return { key, url, size };
}
async deleteFile(key: string): Promise<void> {
const s3File = this.client.file(key);
await s3File.delete();
}
getSignedUrl(key: string, expiresIn: number = 3600): string {
const s3File = this.client.file(key);
return s3File.presign({
expiresIn,
method: 'GET',
});
}
async downloadFromUrl(url: string): Promise<{ buffer: Buffer; mimeType: string; size: number }> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download file: ${response.statusText}`);
}
const contentType = response.headers.get('content-type') || 'application/octet-stream';
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return {
buffer,
mimeType: contentType,
size: buffer.length,
};
}
private getPublicUrl(key: string): string {
const endpoint = env.S3_ENDPOINT.replace(/\/$/, '');
return `${endpoint}/${env.S3_BUCKET}/${key}`;
}
private getExtensionFromMimeType(mimeType: string): string {
const mimeToExt: Record<string, string> = {
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/webp': 'webp',
'image/avif': 'avif',
'video/mp4': 'mp4',
'video/webm': 'webm',
'video/quicktime': 'mov',
'video/x-msvideo': 'avi',
};
return mimeToExt[mimeType] || 'bin';
}
getFileType(mimeType: string): 'image' | 'video' | 'gif' | 'unknown' {
if (mimeType === 'image/gif') return 'gif';
if (mimeType.startsWith('image/')) return 'image';
if (mimeType.startsWith('video/')) return 'video';
return 'unknown';
}
async readFile(key: string): Promise<Buffer> {
const s3File = this.client.file(key);
const arrayBuffer = await s3File.arrayBuffer();
return Buffer.from(arrayBuffer);
}
}
export const s3Service = new S3Service();

View File

@ -0,0 +1,37 @@
import { describe, expect, test } from 'bun:test';
import { s3Service } from '@/services/s3.service';
describe('S3Service', () => {
describe('getFileType', () => {
test('should identify image types', () => {
expect(s3Service.getFileType('image/jpeg')).toBe('image');
expect(s3Service.getFileType('image/png')).toBe('image');
expect(s3Service.getFileType('image/webp')).toBe('image');
});
test('should identify GIF separately', () => {
expect(s3Service.getFileType('image/gif')).toBe('gif');
});
test('should identify video types', () => {
expect(s3Service.getFileType('video/mp4')).toBe('video');
expect(s3Service.getFileType('video/webm')).toBe('video');
expect(s3Service.getFileType('video/quicktime')).toBe('video');
});
test('should return unknown for unsupported types', () => {
expect(s3Service.getFileType('text/plain')).toBe('unknown');
expect(s3Service.getFileType('application/pdf')).toBe('unknown');
});
});
describe('downloadFromUrl', () => {
test('should download file from valid URL', async () => {
const mockUrl = 'https://mrqiz.ru';
expect(mockUrl).toMatch(/^https?:\/\//);
});
});
});