@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
517 lines (514 loc) • 19 kB
JavaScript
import { createClient } from '@supabase/supabase-js';
// Abstract base class for storage operations
class BaseStorage {
constructor(config) {
this.isConnected = false;
this.config = config;
}
getStatus() {
return {
isConnected: this.isConnected,
config: this.config,
};
}
updateConfig(updates) {
this.config = { ...this.config, ...updates };
}
}
// Supabase Storage Implementation
class SupabaseStorage extends BaseStorage {
constructor(config) {
super(config);
this.client = null;
this.progressCallbacks = new Map();
}
async connect() {
try {
if (!this.config.url || !this.config.apiKey) {
throw new Error("Supabase URL and API key are required");
}
this.client = createClient(this.config.url, this.config.apiKey);
// Test connection by listing buckets
const { data, error } = await this.client.storage.listBuckets();
if (error) {
throw new Error(`Failed to connect to Supabase Storage: ${error.message}`);
}
// Check if our bucket exists, create if not
const bucketExists = data === null || data === void 0 ? void 0 : data.some((bucket) => bucket.name === this.config.bucket);
if (!bucketExists) {
const { error: createError } = await this.client.storage.createBucket(this.config.bucket, {
public: false,
allowedMimeTypes: this.config.allowedTypes,
fileSizeLimit: this.config.maxFileSize,
});
if (createError) {
console.warn(`Failed to create bucket ${this.config.bucket}:`, createError);
// Continue anyway, bucket might exist but not be visible to this user
}
}
this.isConnected = true;
}
catch (error) {
this.isConnected = false;
throw new Error(`Failed to connect to Supabase Storage: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
async disconnect() {
this.client = null;
this.isConnected = false;
this.progressCallbacks.clear();
}
async uploadFile(file, options = {}) {
if (!this.isConnected || !this.client) {
throw new Error("Storage is not connected");
}
// Validate file
this.validateFile(file);
const fileId = `doc_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
const fileName = `${fileId}_${file.name}`;
try {
// Update progress
this.updateProgress(fileId, { fileId, progress: 0, status: "uploading" });
// Upload file to Supabase Storage
const { data: uploadData, error: uploadError } = await this.client.storage
.from(this.config.bucket)
.upload(fileName, file, {
cacheControl: "3600",
upsert: options.overwrite || false,
});
if (uploadError) {
throw new Error(`Failed to upload file: ${uploadError.message}`);
}
this.updateProgress(fileId, {
fileId,
progress: 50,
status: "processing",
});
// Extract text content if requested
let extractedText = "";
if (options.extractText !== false) {
// Default to true
try {
extractedText = await this.extractTextFromFile(file);
}
catch (error) {
console.warn("Failed to extract text from file:", error);
// Continue without text extraction
}
}
this.updateProgress(fileId, {
fileId,
progress: 80,
status: "processing",
});
// Create document metadata
const document = {
id: fileId,
title: options.description || file.name,
content: extractedText,
metadata: {
source: fileName,
uploadedAt: new Date(),
size: file.size,
type: file.type,
description: options.description,
tags: options.tags,
},
status: "ready",
};
this.updateProgress(fileId, {
fileId,
progress: 100,
status: "completed",
});
return document;
}
catch (error) {
this.updateProgress(fileId, {
fileId,
progress: 0,
status: "failed",
error: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
async downloadFile(documentId) {
if (!this.isConnected || !this.client) {
throw new Error("Storage is not connected");
}
try {
// Find the file by document ID
const files = await this.listFiles();
const document = files.find((doc) => doc.id === documentId);
if (!document || !document.metadata.source) {
throw new Error(`Document ${documentId} not found`);
}
const { data, error } = await this.client.storage
.from(this.config.bucket)
.download(document.metadata.source);
if (error) {
throw new Error(`Failed to download file: ${error.message}`);
}
return data;
}
catch (error) {
throw new Error(`Failed to download file: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
async deleteFile(documentId) {
if (!this.isConnected || !this.client) {
throw new Error("Storage is not connected");
}
try {
// Find the file by document ID
const files = await this.listFiles();
const document = files.find((doc) => doc.id === documentId);
if (!document || !document.metadata.source) {
throw new Error(`Document ${documentId} not found`);
}
const { error } = await this.client.storage
.from(this.config.bucket)
.remove([document.metadata.source]);
if (error) {
throw new Error(`Failed to delete file: ${error.message}`);
}
}
catch (error) {
throw new Error(`Failed to delete file: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
async listFiles() {
var _a, _b;
if (!this.isConnected || !this.client) {
throw new Error("Storage is not connected");
}
try {
const { data, error } = await this.client.storage
.from(this.config.bucket)
.list();
if (error) {
throw new Error(`Failed to list files: ${error.message}`);
}
// Convert storage objects to documents
const documents = [];
for (const file of data || []) {
try {
// Extract document ID from filename (assuming format: docId_originalName)
const parts = file.name.split("_");
if (parts.length < 2)
continue;
const docId = parts[0];
const originalName = parts.slice(1).join("_");
// Get file URL for content extraction (if needed)
const { data: urlData } = this.client.storage
.from(this.config.bucket)
.getPublicUrl(file.name);
const document = {
id: docId,
title: originalName,
content: "", // Content would be extracted separately if needed
metadata: {
source: file.name,
uploadedAt: new Date(file.created_at || Date.now()),
size: ((_a = file.metadata) === null || _a === void 0 ? void 0 : _a.size) || 0,
type: ((_b = file.metadata) === null || _b === void 0 ? void 0 : _b.mimetype) || "application/octet-stream",
},
status: "ready",
};
documents.push(document);
}
catch (error) {
console.warn(`Failed to process file ${file.name}:`, error);
continue;
}
}
return documents;
}
catch (error) {
throw new Error(`Failed to list files: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
async getFileMetadata(documentId) {
if (!this.isConnected || !this.client) {
throw new Error("Storage is not connected");
}
try {
const files = await this.listFiles();
const document = files.find((doc) => doc.id === documentId);
if (!document) {
throw new Error(`Document ${documentId} not found`);
}
return {
originalName: document.title,
size: document.metadata.size,
type: document.metadata.type,
uploadedAt: document.metadata.uploadedAt,
description: document.metadata.description,
tags: document.metadata.tags,
extractedText: document.content,
processingStatus: "completed",
};
}
catch (error) {
throw new Error(`Failed to get file metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
async healthCheck() {
if (!this.isConnected || !this.client)
return false;
try {
await this.client.storage.listBuckets();
return true;
}
catch (error) {
return false;
}
}
// Progress tracking
setProgressCallback(fileId, callback) {
this.progressCallbacks.set(fileId, callback);
}
removeProgressCallback(fileId) {
this.progressCallbacks.delete(fileId);
}
updateProgress(fileId, progress) {
const callback = this.progressCallbacks.get(fileId);
if (callback) {
callback(progress);
}
}
validateFile(file) {
// Check file size
if (this.config.maxFileSize && file.size > this.config.maxFileSize) {
throw new Error(`File size ${file.size} exceeds maximum allowed size ${this.config.maxFileSize}`);
}
// Check file type
if (this.config.allowedTypes && this.config.allowedTypes.length > 0) {
const isAllowed = this.config.allowedTypes.some((type) => {
if (type.includes("*")) {
const baseType = type.split("/")[0];
return file.type.startsWith(baseType);
}
return file.type === type;
});
if (!isAllowed) {
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedTypes.join(", ")}`);
}
}
}
async extractTextFromFile(file) {
// Simple text extraction for common file types
if (file.type === "text/plain") {
return await file.text();
}
if (file.type === "application/json") {
try {
const text = await file.text();
const json = JSON.parse(text);
return JSON.stringify(json, null, 2);
}
catch (error) {
return await file.text();
}
}
if (file.type.startsWith("text/")) {
return await file.text();
}
// For other file types, we'd need specialized libraries
// For now, return empty string and let the user provide content manually
return "";
}
}
// Local Storage Implementation (for development/testing)
class LocalStorage extends BaseStorage {
constructor() {
super(...arguments);
this.files = new Map();
}
async connect() {
this.isConnected = true;
}
async disconnect() {
this.isConnected = false;
this.files.clear();
}
async uploadFile(file, options = {}) {
if (!this.isConnected) {
throw new Error("Storage is not connected");
}
this.validateFile(file);
const fileId = `doc_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
// Extract text if possible
let extractedText = "";
if (options.extractText !== false) {
try {
if (file.type.startsWith("text/")) {
extractedText = await file.text();
}
}
catch (error) {
console.warn("Failed to extract text:", error);
}
}
const document = {
id: fileId,
title: options.description || file.name,
content: extractedText,
metadata: {
source: file.name,
uploadedAt: new Date(),
size: file.size,
type: file.type,
description: options.description,
tags: options.tags,
},
status: "ready",
};
this.files.set(fileId, { document, blob: file });
return document;
}
async downloadFile(documentId) {
if (!this.isConnected) {
throw new Error("Storage is not connected");
}
const fileData = this.files.get(documentId);
if (!fileData) {
throw new Error(`Document ${documentId} not found`);
}
return fileData.blob;
}
async deleteFile(documentId) {
if (!this.isConnected) {
throw new Error("Storage is not connected");
}
if (!this.files.has(documentId)) {
throw new Error(`Document ${documentId} not found`);
}
this.files.delete(documentId);
}
async listFiles() {
if (!this.isConnected) {
throw new Error("Storage is not connected");
}
return Array.from(this.files.values()).map((fileData) => fileData.document);
}
async getFileMetadata(documentId) {
if (!this.isConnected) {
throw new Error("Storage is not connected");
}
const fileData = this.files.get(documentId);
if (!fileData) {
throw new Error(`Document ${documentId} not found`);
}
const doc = fileData.document;
return {
originalName: doc.title,
size: doc.metadata.size,
type: doc.metadata.type,
uploadedAt: doc.metadata.uploadedAt,
description: doc.metadata.description,
tags: doc.metadata.tags,
extractedText: doc.content,
processingStatus: "completed",
};
}
async healthCheck() {
return this.isConnected;
}
validateFile(file) {
if (this.config.maxFileSize && file.size > this.config.maxFileSize) {
throw new Error(`File size exceeds maximum allowed size`);
}
if (this.config.allowedTypes && this.config.allowedTypes.length > 0) {
const isAllowed = this.config.allowedTypes.some((type) => {
if (type.includes("*")) {
const baseType = type.split("/")[0];
return file.type.startsWith(baseType);
}
return file.type === type;
});
if (!isAllowed) {
throw new Error(`File type ${file.type} is not allowed`);
}
}
}
}
// Factory function to create storage instances
function createStorage(config) {
// For now, we only have Supabase and Local implementations
if (config.url && config.apiKey) {
return new SupabaseStorage(config);
}
else {
return new LocalStorage(config);
}
}
// Storage utilities
class StorageUtils {
static validateConfig(config) {
if (!config.bucket)
return false;
// For cloud storage, we need URL and API key
if (config.url && !config.apiKey)
return false;
if (config.apiKey && !config.url)
return false;
return true;
}
static getDefaultConfig() {
return {
maxFileSize: 50 * 1024 * 1024, // 50MB
allowedTypes: [
"text/*",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/json",
"application/csv",
"text/csv",
],
};
}
static formatFileSize(bytes) {
if (bytes === 0)
return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
static getFileExtension(filename) {
return filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2);
}
static generateUniqueFileName(originalName) {
const ext = this.getFileExtension(originalName);
const baseName = originalName.replace(`.${ext}`, "");
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 5);
return `${baseName}_${timestamp}_${random}.${ext}`;
}
static isTextFile(mimeType) {
return (mimeType.startsWith("text/") ||
mimeType === "application/json" ||
mimeType === "application/xml" ||
mimeType === "application/csv");
}
static isSupportedFileType(mimeType, allowedTypes) {
if (!allowedTypes || allowedTypes.length === 0)
return true;
return allowedTypes.some((type) => {
if (type.includes("*")) {
const baseType = type.split("/")[0];
return mimeType.startsWith(baseType);
}
return mimeType === type;
});
}
}
export { BaseStorage, LocalStorage, StorageUtils, SupabaseStorage, createStorage };
//# sourceMappingURL=storageService.js.map