@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
321 lines (276 loc) • 8.75 kB
text/typescript
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}`);
}
}
}