@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
text/typescript
/**
* 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 };
}
}
}