/
nasya
/
SafeDrop
Обзор
Документация
Войти
/
nasya
/
SafeDrop
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
packages/shared/src/object-storage.ts
185 строк
6 KB
nasya
Initial commit
24 май 2026, 16:12
24 май 2026, 16:12
16bc032
Код
Авторство
О чём код?
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; export type ObjectStorageMethod = "GET" | "PUT"; export type ObjectStorageDescriptor = { method: ObjectStorageMethod; objectKey: string; url: string; }; export type ObjectStorageConfig = { accessKeyId: string; bucket: string; downloadExpiresInSeconds: number; endpoint: string; forcePathStyle: boolean; publicEndpoint: string; region: string; secretAccessKey: string; uploadExpiresInSeconds: number; }; export type ObjectStorage = { bucket: string; createDownloadDescriptor: (objectKey: string) => Promise<ObjectStorageDescriptor>; createUploadDescriptor: (objectKey: string, contentType?: string) => Promise<ObjectStorageDescriptor>; deleteObject: (objectKey: string) => Promise<void>; }; function env(name: string, fallback: string): string { return process.env[name] ?? fallback; } function envBoolean(name: string, fallback: boolean): boolean { const value = process.env[name]; if (value === undefined) { return fallback; } return value === "true" || value === "1"; } function envNumber(name: string, fallback: number): number { const value = process.env[name]; if (!value) { return fallback; } const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } export function objectStorageConfigFromEnv(): ObjectStorageConfig { const accessKeyId = env("OBJECT_STORAGE_ACCESS_KEY", "safedrop"); const secretAccessKey = env("OBJECT_STORAGE_SECRET_KEY", "safedrop-password"); return { accessKeyId, bucket: env("OBJECT_STORAGE_BUCKET", "safedrop-files"), downloadExpiresInSeconds: envNumber("OBJECT_STORAGE_DOWNLOAD_TTL_SECONDS", 300), endpoint: env("OBJECT_STORAGE_ENDPOINT", "http://minio:9000"), forcePathStyle: envBoolean("OBJECT_STORAGE_FORCE_PATH_STYLE", true), publicEndpoint: env("OBJECT_STORAGE_PUBLIC_ENDPOINT", "http://localhost:9000"), region: env("OBJECT_STORAGE_REGION", "us-east-1"), secretAccessKey, uploadExpiresInSeconds: envNumber("OBJECT_STORAGE_UPLOAD_TTL_SECONDS", 900), }; } export function createDeleteObjectCommand(bucket: string, objectKey: string): DeleteObjectCommand { return new DeleteObjectCommand({ Bucket: bucket, Key: objectKey, }); } export function createObjectStorage(config: ObjectStorageConfig = objectStorageConfigFromEnv()): ObjectStorage { const presignClient = new S3Client({ credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, }, endpoint: config.publicEndpoint, forcePathStyle: config.forcePathStyle, region: config.region, }); const operationClient = new S3Client({ credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, }, endpoint: config.endpoint, forcePathStyle: config.forcePathStyle, region: config.region, }); return { bucket: config.bucket, async createDownloadDescriptor(objectKey) { const url = await getSignedUrl( presignClient, new GetObjectCommand({ Bucket: config.bucket, Key: objectKey, }), { expiresIn: config.downloadExpiresInSeconds }, ); return { method: "GET", objectKey, url, }; }, async createUploadDescriptor(objectKey) { const url = await getSignedUrl( presignClient, new PutObjectCommand({ Bucket: config.bucket, Key: objectKey, }), { expiresIn: config.uploadExpiresInSeconds }, ); return { method: "PUT", objectKey, url, }; }, async deleteObject(objectKey) { await operationClient.send(createDeleteObjectCommand(config.bucket, objectKey)); }, }; } if (import.meta.rstest) { const { describe, expect, it } = import.meta.rstest; const testConfig = { accessKeyId: "test-access-key", bucket: "test-bucket", downloadExpiresInSeconds: 300, endpoint: "http://minio:9000", forcePathStyle: true, publicEndpoint: "http://localhost:9000", region: "us-east-1", secretAccessKey: "test-secret-key", uploadExpiresInSeconds: 900, } satisfies ObjectStorageConfig; describe("object storage descriptors", () => { it("generates presigned PUT URLs for configured bucket and public endpoint", async () => { const storage = createObjectStorage(testConfig); const descriptor = await storage.createUploadDescriptor("files/test.bin", "application/octet-stream"); const url = new URL(descriptor.url); expect(descriptor.method).toBe("PUT"); expect(descriptor.objectKey).toBe("files/test.bin"); expect(url.origin).toBe("http://localhost:9000"); expect(url.pathname).toBe("/test-bucket/files/test.bin"); expect(url.searchParams.has("X-Amz-Signature")).toBe(true); expect(url.searchParams.get("X-Amz-Expires")).toBe("900"); }); it("generates presigned GET URLs without making the bucket public", async () => { const storage = createObjectStorage(testConfig); const descriptor = await storage.createDownloadDescriptor("shares/test.bin"); const url = new URL(descriptor.url); expect(descriptor.method).toBe("GET"); expect(descriptor.objectKey).toBe("shares/test.bin"); expect(url.origin).toBe("http://localhost:9000"); expect(url.pathname).toBe("/test-bucket/shares/test.bin"); expect(url.searchParams.has("X-Amz-Credential")).toBe(true); expect(url.searchParams.get("X-Amz-Expires")).toBe("300"); }); it("creates DeleteObject commands for the configured bucket and key", () => { const command = createDeleteObjectCommand(testConfig.bucket, "shares/missing.bin"); expect(command.input).toEqual({ Bucket: "test-bucket", Key: "shares/missing.bin", }); }); }); }