UNPKG

@smartsamurai/krapi-sdk

Version:

KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)

421 lines (399 loc) 14 kB
/** * Storage Adapter * * Unifies StorageHttpClient and StorageService behind a common interface. */ import { KrapiError } from "../../core/krapi-error"; import { StorageHttpClient } from "../../http-clients/storage-http-client"; import { StorageService } from "../../storage-service"; import { FileInfo } from "../../types"; import { createAdapterInitError } from "./error-handler"; type Mode = "client" | "server"; export class StorageAdapter { private mode: Mode; private httpClient: StorageHttpClient | undefined; private service: StorageService | undefined; constructor(mode: Mode, httpClient?: StorageHttpClient, service?: StorageService) { this.mode = mode; this.httpClient = httpClient; this.service = service; } async uploadFile( projectId: string, file: File | Blob, options?: { folder?: string; filename?: string; metadata?: Record<string, unknown>; public?: boolean; } ): Promise<FileInfo> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const fileObj = file instanceof File ? file : new File([file], options?.filename || "file"); const uploadOptions: { folder_id?: string; metadata?: Record<string, unknown>; is_public?: boolean; } = {}; if (options?.folder) uploadOptions.folder_id = options.folder; if (options?.metadata) uploadOptions.metadata = options.metadata; if (options?.public !== undefined) uploadOptions.is_public = options.public; const response = await this.httpClient.uploadFile(projectId, fileObj, uploadOptions); return (response.data as unknown as FileInfo) || ({} as FileInfo); } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const fileBuffer = Buffer.from(await (file as Blob).arrayBuffer()); const fileName = options?.filename || (file instanceof File ? file.name : "file"); const uploadRequest: { original_name: string; file_size: number; mime_type: string; folder_id?: string; metadata?: Record<string, unknown>; is_public: boolean; uploaded_by: string; } = { original_name: fileName, file_size: fileBuffer.length, mime_type: file instanceof File ? file.type : "application/octet-stream", is_public: options?.public || false, uploaded_by: "system", }; if (options?.folder) uploadRequest.folder_id = options.folder; if (options?.metadata) uploadRequest.metadata = options.metadata; const result = await this.service.uploadFile(projectId, uploadRequest, fileBuffer); return result as unknown as FileInfo; } } async downloadFile(projectId: string, fileId: string): Promise<Blob> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.downloadFile(projectId, fileId); return (response.data as unknown as Blob) || new Blob([]); } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const file = await this.service.getFileById(projectId, fileId); if (!file) { throw KrapiError.notFound("File not found"); } return new Blob([]); } } async getFile(projectId: string, fileId: string): Promise<FileInfo> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.getFile(projectId, fileId); return (response.data as unknown as FileInfo) || ({} as FileInfo); } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const file = await this.service.getFileById(projectId, fileId); if (!file) { throw KrapiError.notFound("File not found"); } return file as unknown as FileInfo; } } async deleteFile(projectId: string, fileId: string): Promise<{ success: boolean }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.deleteFile(projectId, fileId); return { success: response.success }; } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const success = await this.service.deleteFile(projectId, fileId, "system", true); return { success }; } } async getFiles( projectId: string, options?: { folder?: string; limit?: number; offset?: number; search?: string; type?: string; } ): Promise<FileInfo[]> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.getFiles(projectId, options); return (response.data as unknown as FileInfo[]) || []; } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } // Build options object only with defined values const serviceOptions: { limit?: number; offset?: number; filter?: Record<string, string>; } = {}; if (options?.limit !== undefined) serviceOptions.limit = options.limit; if (options?.offset !== undefined) serviceOptions.offset = options.offset; // Build filter object only with defined values const filter: Record<string, string> = {}; if (options?.folder) filter.folder_id = options.folder; if (options?.type) filter.mime_type = options.type.replace("*", ""); if (Object.keys(filter).length > 0) serviceOptions.filter = filter; // Use the service's getAllFiles method with filtering const files = await this.service.getAllFiles(projectId, serviceOptions); // Apply search filter that the service doesn't support directly let filteredFiles = files; if (options?.search) { const searchLower = options.search.toLowerCase(); filteredFiles = filteredFiles.filter(f => f.original_name?.toLowerCase().includes(searchLower) || f.file_name?.toLowerCase().includes(searchLower) ); } return filteredFiles as unknown as FileInfo[]; } } async createFolder( projectId: string, folderData: { name: string; parent_folder_id?: string; metadata?: Record<string, unknown>; } ): Promise<{ id: string; name: string; parent_folder_id?: string; metadata?: Record<string, unknown>; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const folderRequest: { name: string; parent_id?: string; description?: string; } = { name: folderData.name, }; if (folderData.parent_folder_id) folderRequest.parent_id = folderData.parent_folder_id; const response = await this.httpClient.createFolder(projectId, folderRequest); const responseData = response.data; const result: { id: string; name: string; parent_folder_id?: string; metadata?: Record<string, unknown>; } = { id: (responseData && typeof responseData === "object" && "id" in responseData && typeof responseData.id === "string" ? responseData.id : "") || "", name: (responseData && typeof responseData === "object" && "name" in responseData && typeof responseData.name === "string" ? responseData.name : folderData.name) || folderData.name, }; if (responseData && typeof responseData === "object" && "parent_id" in responseData && typeof responseData.parent_id === "string") { result.parent_folder_id = responseData.parent_id; } if (folderData.metadata) result.metadata = folderData.metadata; return result; } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const folderRequest: { name: string; parent_id?: string; description?: string; } = { name: folderData.name, }; if (folderData.parent_folder_id) folderRequest.parent_id = folderData.parent_folder_id; const result = await this.service.createFolder(projectId, folderRequest, "system"); return result as unknown as { id: string; name: string; parent_folder_id?: string; metadata?: Record<string, unknown>; }; } } async getFolders( projectId: string, parentFolderId?: string ): Promise< { id: string; name: string; parent_folder_id?: string; metadata?: Record<string, unknown>; }[] > { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.getFolders(projectId, parentFolderId); const folders = (response.data || []) as Array<{ id: string; name: string; parent_id?: string; metadata?: Record<string, unknown>; }>; return folders.map((f) => { const folder: { id: string; name: string; parent_folder_id?: string; metadata?: Record<string, unknown>; } = { id: f.id, name: f.name, }; if (f.parent_id) { folder.parent_folder_id = f.parent_id; } if (f.metadata) { folder.metadata = f.metadata; } return folder; }); } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const folders = await this.service.getAllFolders(projectId); const filtered = parentFolderId ? folders.filter((f) => f.parent_id === parentFolderId) : folders.filter((f) => !f.parent_id); return filtered.map((f) => { const folder: { id: string; name: string; parent_folder_id?: string; metadata?: Record<string, unknown>; } = { id: f.id, name: f.name, }; if (f.parent_id) { folder.parent_folder_id = f.parent_id; } // FileFolder doesn't have metadata, so we'll leave it undefined return folder; }); } } async deleteFolder( projectId: string, folderId: string ): Promise<{ success: boolean }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.deleteFolder(projectId, folderId); return { success: response.success }; } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const result = await this.service.deleteFolder(projectId, folderId, true); return { success: result.success }; } } async getStatistics(projectId: string): Promise<{ total_files: number; total_size_bytes: number; files_by_type: Record<string, number>; storage_quota: { used: number; limit: number; percentage: number; }; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const response = await this.httpClient.getStorageInfo(projectId); const data = response; if (!data) { throw createAdapterInitError("Storage info", this.mode, "No storage info data returned"); } // Transform to match interface return { total_files: data.total_files, total_size_bytes: data.total_size, files_by_type: {}, // Not available in getStorageInfo storage_quota: { used: data.total_size, limit: data.quota, percentage: data.storage_used_percentage, }, }; } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const stats = await this.service.getStorageStatistics(projectId); const filesByType: Record<string, number> = {}; Object.entries(stats.files_by_type || {}).forEach(([type, data]) => { filesByType[type] = (data as { count: number }).count; }); return { total_files: stats.total_files, total_size_bytes: stats.total_size, files_by_type: filesByType, storage_quota: { used: stats.total_size, limit: 1073741824, // 1GB default - should come from quota percentage: stats.storage_used_percentage, }, }; } } async getFileUrl( projectId: string, fileId: string, options?: { expires_in?: number; download?: boolean; } ): Promise<{ url: string; expires_at?: string }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode); } const urlOptions: { file_id: string; expires_in?: number; access_type?: "stream" | "download" | "preview"; } = { file_id: fileId, }; if (options?.expires_in) urlOptions.expires_in = options.expires_in; urlOptions.access_type = options?.download ? "download" : "preview"; const response = await this.httpClient.getFileUrl(projectId, fileId, urlOptions); return response.data || { url: "" }; } else { if (!this.service) { throw createAdapterInitError("Storage service", this.mode); } const url = await this.service.getFileUrl(projectId, fileId); return { url }; } } }