UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

321 lines (276 loc) 8.75 kB
import { createClient, SupabaseClient } from "@supabase/supabase-js"; export interface SupabaseStorageConfig { url: string; anonKey: string; bucket: string; } export interface DocumentUploadResult { id: string; path: string; url: string; size: number; mimeType: string; uploadedAt: Date; } export interface DocumentVersion { id: string; documentId: string; version: number; path: string; size: number; uploadedAt: Date; metadata?: Record<string, any>; } export class SupabaseStorageManager { private client: SupabaseClient; private bucket: string; constructor(config: SupabaseStorageConfig) { this.client = createClient(config.url, config.anonKey); this.bucket = config.bucket; } async initializeBucket(): Promise<void> { try { // Check if bucket exists const { data: buckets, error } = await this.client.storage.listBuckets(); if (error) { throw new Error(`Failed to list buckets: ${error.message}`); } const bucketExists = buckets?.some( (bucket) => bucket.name === this.bucket ); if (!bucketExists) { // Create bucket if it doesn't exist const { error: createError } = await this.client.storage.createBucket( this.bucket, { public: false, // Private bucket for security allowedMimeTypes: [ "application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/msword", "text/plain", "text/markdown", ], fileSizeLimit: 50 * 1024 * 1024, // 50MB limit } ); if (createError) { throw new Error(`Failed to create bucket: ${createError.message}`); } } } catch (error) { console.warn("Failed to initialize bucket:", error); // Continue execution - bucket might already exist } } async uploadDocument( file: Buffer | File, fileName: string, metadata?: Record<string, any> ): Promise<DocumentUploadResult> { try { const timestamp = new Date().toISOString(); const sanitizedFileName = this.sanitizeFileName(fileName); const path = `documents/${timestamp}_${sanitizedFileName}`; const uploadData = file instanceof File ? file : file; const { data, error } = await this.client.storage .from(this.bucket) .upload(path, uploadData, { cacheControl: "3600", upsert: false, metadata: { ...metadata, originalName: fileName, uploadedAt: timestamp, }, }); if (error) { throw new Error(`Upload failed: ${error.message}`); } const { data: urlData } = this.client.storage .from(this.bucket) .getPublicUrl(data.path); const fileSize = file instanceof File ? file.size : file.length; const mimeType = this.getMimeType(fileName); return { id: this.generateDocumentId(data.path), path: data.path, url: urlData.publicUrl, size: fileSize, mimeType, uploadedAt: new Date(timestamp), }; } catch (error) { throw new Error(`Failed to upload document: ${error}`); } } async downloadDocument(path: string): Promise<Buffer> { try { const { data, error } = await this.client.storage .from(this.bucket) .download(path); if (error) { throw new Error(`Download failed: ${error.message}`); } if (!data) { throw new Error("No data received from download"); } return Buffer.from(await data.arrayBuffer()); } catch (error) { throw new Error(`Failed to download document: ${error}`); } } async deleteDocument(path: string): Promise<void> { try { const { error } = await this.client.storage .from(this.bucket) .remove([path]); if (error) { throw new Error(`Delete failed: ${error.message}`); } } catch (error) { throw new Error(`Failed to delete document: ${error}`); } } async listDocuments(prefix?: string): Promise< Array<{ name: string; id: string; updated_at: string; size: number; metadata: Record<string, any>; }> > { try { const { data, error } = await this.client.storage .from(this.bucket) .list(prefix || "documents"); if (error) { throw new Error(`List failed: ${error.message}`); } return data.map((file) => ({ name: file.name, id: file.id || this.generateDocumentId(file.name), updated_at: file.updated_at || "", size: file.metadata?.size || 0, metadata: file.metadata || {}, })); } catch (error) { throw new Error(`Failed to list documents: ${error}`); } } async createDocumentVersion( originalPath: string, newFile: Buffer | File, version: number, metadata?: Record<string, any> ): Promise<DocumentVersion> { try { const pathParts = originalPath.split("/"); const originalFileName = pathParts[pathParts.length - 1]; const baseFileName = originalFileName.replace( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z_/, "" ); const timestamp = new Date().toISOString(); const versionPath = `documents/versions/${timestamp}_v${version}_${baseFileName}`; const uploadData = newFile instanceof File ? newFile : newFile; const { data, error } = await this.client.storage .from(this.bucket) .upload(versionPath, uploadData, { cacheControl: "3600", upsert: false, metadata: { ...metadata, originalPath, version, createdAt: timestamp, }, }); if (error) { throw new Error(`Version upload failed: ${error.message}`); } const fileSize = newFile instanceof File ? newFile.size : newFile.length; return { id: this.generateDocumentId(data.path), documentId: this.generateDocumentId(originalPath), version, path: data.path, size: fileSize, uploadedAt: new Date(timestamp), metadata, }; } catch (error) { throw new Error(`Failed to create document version: ${error}`); } } async getDocumentVersions(documentId: string): Promise<DocumentVersion[]> { try { const { data, error } = await this.client.storage .from(this.bucket) .list("documents/versions"); if (error) { throw new Error(`Failed to list versions: ${error.message}`); } // Filter versions for the specific document const versions = data .filter( (file) => file.metadata?.originalPath && this.generateDocumentId(file.metadata.originalPath) === documentId ) .map((file) => ({ id: this.generateDocumentId(file.name), documentId, version: file.metadata?.version || 1, path: `documents/versions/${file.name}`, size: file.metadata?.size || 0, uploadedAt: new Date(file.metadata?.createdAt || file.updated_at), metadata: file.metadata, })) .sort((a, b) => b.version - a.version); return versions; } catch (error) { throw new Error(`Failed to get document versions: ${error}`); } } private sanitizeFileName(fileName: string): string { return fileName .replace(/[^a-zA-Z0-9.-]/g, "_") .replace(/_{2,}/g, "_") .toLowerCase(); } private generateDocumentId(path: string): string { return Buffer.from(path) .toString("base64") .replace(/[^a-zA-Z0-9]/g, ""); } private getMimeType(fileName: string): string { const extension = fileName.split(".").pop()?.toLowerCase(); const mimeTypes: Record<string, string> = { pdf: "application/pdf", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", doc: "application/msword", txt: "text/plain", md: "text/markdown", }; return mimeTypes[extension || ""] || "application/octet-stream"; } async getStorageStats(): Promise<{ totalFiles: number; totalSize: number; bucketName: string; }> { try { const files = await this.listDocuments(); const totalSize = files.reduce((sum, file) => sum + file.size, 0); return { totalFiles: files.length, totalSize, bucketName: this.bucket, }; } catch (error) { throw new Error(`Failed to get storage stats: ${error}`); } } }