UNPKG

koneksi-ts-sdk

Version:

A JS/TS SDK for interacting with Koneksi API services

519 lines (518 loc) 21.7 kB
import { SERVER_CONFIGS } from "../config.js"; import isNodeEnvironment from "../../utils/isNodeEnvironment.js"; import fs from "fs/promises"; import path from "path"; export class FileOperations { constructor(httpClient) { this.httpClient = httpClient; } /** * Upload a file to a directory * @param params - File upload parameters * @param params.file - File to upload (File object, Buffer, or string file path) * @param params.directory_id - Optional directory ID to upload to (defaults to root) * @param params.passphrase - Optional passphrase for file encryption * @returns Promise<UploadFileResponse> - The upload response * @example * // Upload with File object (browser) * await sdk.file.upload({ * file: fileInput.files[0], * directory_id: "dir_123" * }); * * @example * // Upload with string path (Node.js) * await sdk.file.upload({ * file: "test.txt", * directory_id: "dir_123", * passphrase: "MySecret123!" Alphanumeric passphrase with a special character is required * }); */ async upload(params) { if (!params.file) { throw new Error("File is required"); } if (params.passphrase && typeof params.passphrase !== "string") { throw new Error("Passphrase must be a string"); } if (params.directory_id && typeof params.directory_id !== "string") { throw new Error("directory_id must be a string"); } const formData = new FormData(); if (params.directory_id) { formData.append("directory_id", params.directory_id); } if (params.passphrase) { formData.append("passphrase", params.passphrase); } if (params.file instanceof File) { formData.append("file", params.file); } else if (Buffer.isBuffer(params.file)) { const blob = new Blob([params.file]); formData.append("file", blob); } else if (typeof params.file === "string") { if (isNodeEnvironment()) { try { await fs.access(params.file); const fileBuffer = await fs.readFile(params.file); const fileName = path.basename(params.file); const blob = new Blob([fileBuffer]); formData.append("file", blob, fileName); } catch (error) { if (error.code === "ENOENT") { throw new Error(`File not found: ${params.file}`); } throw new Error(`Failed to read file: ${params.file}. ${error.message}`); } } else { throw new Error("String file paths are not supported in browser environment. " + "Please use File objects (from file input) or Buffer instead. " + "Example: fileInput.files[0] or new File(['content'], 'filename.txt')"); } } else { throw new Error("Invalid file type. Expected File, Buffer, or string file path"); } // Simple progress callback let startTime = Date.now(); const progressHandler = params.onProgress ? (progressEvent) => { const loaded = progressEvent.loaded; const total = progressEvent.total; const percentage = total > 0 ? (loaded / total) * 100 : 0; const elapsedTime = (Date.now() - startTime) / 1000; params.onProgress({ percentage, elapsedTime }); } : undefined; const response = await this.httpClient.uploadFile("/clients/v1/files", formData, progressHandler ? { onProgress: progressHandler } : undefined); if (response.status !== "success" || !response.data) { throw new Error(response.message || "Failed to upload file"); } return response.data; } /** * Read a file by ID * @param params - File read parameters * @param params.fileId - The ID of the file to retrieve * @param params.includeChunks - Optional flag to include IPFS chunks (default: false) * @returns Promise<FileData> - The file details */ async read(params) { if (!params.fileId) { throw new Error("File ID is required"); } if (params.includeChunks !== undefined && typeof params.includeChunks !== "boolean") { throw new Error("includeChunks must be a boolean"); } const queryParams = new URLSearchParams(); if (params.includeChunks) { queryParams.append("include_chunks", "true"); } const response = await this.httpClient.get(`/clients/v1/files/${params.fileId}${queryParams.toString() ? `?${queryParams.toString()}` : ""}`); if (response.status !== "success" || !response.data) { throw new Error(response.message || "Failed to read file"); } return response.data; } /** * Update a file * @param fileId - The ID of the file to update * @param params - File update parameters * @returns Promise<string> - Success message */ async update(fileId, params) { const response = await this.httpClient.put(`/clients/v1/files/${fileId}`, params); if (response.status !== "success") { throw new Error(response.message || "Failed to update file"); } return response.message; } /** * Delete a file * @param fileId - The ID of the file to delete * @returns Promise<string> - Success message */ async delete(fileId) { const response = await this.httpClient.delete(`/clients/v1/files/${fileId}`); if (response.status !== "success") { throw new Error(response.message || "Failed to delete file"); } return response.message; } /** * Download a file by ID (authenticated/private) * @param params - File download parameters * @param params.fileId - The ID of the file to download * @param params.passphrase - Optional passphrase for file decryption (required for encrypted files) * @returns Promise<Buffer> - The file content as buffer * @example * await sdk.file.download({ fileId: "file_1234567890", passphrase: "MySecret123!" }); */ async download(params) { if (!params.fileId) { throw new Error("File ID is required"); } if (params.passphrase && typeof params.passphrase !== "string") { throw new Error("Passphrase must be a string"); } const accessInfo = await this.publicRead({ fileId: params.fileId }); if (!(accessInfo.access === "private" || params.passphrase)) { throw new Error("Authenticated download is only allowed for private or encrypted files. Please use publicDownload instead."); } const queryParams = new URLSearchParams(); const headers = {}; if (params.passphrase) headers["passphrase"] = params.passphrase; if (accessInfo.is_encrypted || params.passphrase) queryParams.append("stream", "false"); const url = `/clients/v1/files/${params.fileId}/download${queryParams.toString() ? `?${queryParams.toString()}` : ""}`; const config = { headers }; const response = await this.httpClient.downloadFile(url, config); return response; } /** * Rename a file by ID * @param params - Rename parameters * @param params.fileId - The ID of the file to rename * @param params.name - The new name for the file * @returns Promise<string> - Success message * @example * await sdk.file.rename({ fileId: "file_1234567890", name: "test2.txt" }); */ async rename(params) { if (!params.fileId) { throw new Error("fileId is required"); } if (!params.name) { throw new Error("new name is required"); } const response = await this.httpClient.put(`/clients/v1/files/${params.fileId}`, { name: params.name }); if (response.status !== "success") { throw new Error(response.message || "Failed to rename file"); } return response.message; } /** * Move a file to a different directory * @param params - Move parameters * @param params.fileId - The ID of the file to move * @param params.directoryId - The ID of the target directory * @returns Promise<string> - Success message * @example * await sdk.file.move({ fileId: "file_1234567890", directoryId: "dir_1234567890" }); */ async move(params) { if (!params.fileId) { throw new Error("fileId is required"); } if (!params.directory_id) { throw new Error("directory_id is required"); } const response = await this.httpClient.put(`/clients/v1/files/${params.fileId}`, { directory_id: params.directory_id }); if (response.status !== "success") { throw new Error(response.message || "Failed to move file"); } return response.message; } /** * Set file access permissions * @param params - File access set parameters * @param params.fileId - The ID of the file to set access for * @param params.access - The access type: "public", "private", "password", "email" * @param params.value - Required for password (string) and email (string[]) * @returns Promise<string> - Success message * @example * // Set to public * await sdk.file.setAccess({ * fileId: "file_1234567890", * access: "public" * }); * * // Set to private * await sdk.file.setAccess({ * fileId: "file_1234567890", * access: "private" * }); * * @example * // Set password protection * await sdk.file.setAccess({ * fileId: "file_1234567890", * access: "password", * value: "1234567890" * }); * * @example * // Set email access * await sdk.file.setAccess({ * fileId: "file_1234567890", * access: "email", * value: ["test@test.com", "test2@test.com"] // make sure to pass existing koneksi email accounts * }); */ async setAccess(params) { if (!params.fileId) { throw new Error("fileId is required"); } if (!params.access) { throw new Error("access is required"); } const validAccessTypes = ["public", "private", "password", "email"]; if (!validAccessTypes.includes(params.access)) { throw new Error(`Invalid access type. Must be one of: ${validAccessTypes.join(", ")}`); } if (params.access === "password") { if (!params.value || typeof params.value !== "string") { throw new Error("value is required and must be a string for password access"); } } if (params.access === "email") { if (!params.value || !Array.isArray(params.value)) { throw new Error("value is required and must be an array of email addresses for email access"); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; for (const email of params.value) { if (typeof email !== "string" || !emailRegex.test(email)) { throw new Error(`Invalid email address: ${email}`); } } } const payload = {}; if (params.access === "private") { payload.is_shared = false; payload.share_type = "private"; } else { payload.is_shared = true; payload.access = params.access; payload.share_type = params.access; if (params.access === "password" || params.access === "email") { payload.value = params.value; } } const queryParams = new URLSearchParams(); queryParams.append("access", params.access); const requestBody = {}; if (params.access === "password" && params.value) { requestBody.password = params.value; } if (params.access === "email" && Array.isArray(params.value)) { const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const validEmails = params.value.filter((email) => emailRegex.test(email)); if (validEmails.length !== params.value.length) { const invalidEmails = params.value.filter((email) => !emailRegex.test(email)); throw new Error(`Invalid email addresses: ${invalidEmails.join(", ")}`); } requestBody.emails = validEmails; } const url = `/clients/v1/files/${params.fileId}/share?${queryParams.toString()}`; const response = await this.httpClient.post(url, requestBody); if (response.status !== "success") { throw new Error(response.message || "Failed to set file access"); } return response.message; } /** * Generate a temporary token for file access * @param params - Generate temporary token parameters * @param params.fileId - The ID of the file to generate a temporary token for * @param params.expiresIn - Optional expiration time in seconds (default: 24 hours) * @returns Promise<GenerateTemporaryTokenResponse> - The temporary token response with file_key and duration * @example * const token = await sdk.file.generateTemporaryToken({ * fileId: "file_1234567890", * expiresIn: 3600 // 1 hour * }); * // Returns: { duration: "1h0m0s", file_key: "file_1234567890_abc123..." } */ async generateTemporaryToken(params) { if (!params.fileId) { throw new Error("fileId is required"); } if (params.expiresIn !== undefined) { if (typeof params.expiresIn !== "number" || params.expiresIn <= 0) { throw new Error("expiresIn must be a positive number"); } } const payload = {}; if (params.expiresIn) { payload.duration = params.expiresIn; } const response = await this.httpClient.post(`/clients/v1/files/${params.fileId}/generate-link`, payload); if (response.status !== "success" || !response.data) { throw new Error(response.message || "Failed to generate temporary token"); } return response.data; } /** * Get a preview link for a file * @param params - Get preview link parameters * @param params.fileId - The ID of the file to get a preview link for * @returns Promise<GetPreviewLinkResponse> - The preview link response * @example * const preview = await sdk.file.getPreviewLink({ * fileId: "file_1234567890" * }); * // Returns: { previewUrl: "https://staging.koneksi.co.kr/preview/file_1234567890" } */ async getPreviewLink(params) { if (!params.fileId) { throw new Error("fileId is required"); } const baseURL = this.httpClient["config"].baseURL; let appURL; if (baseURL.includes("staging")) { appURL = SERVER_CONFIGS.staging.webAppUrl; } else if (baseURL.includes("production")) { appURL = SERVER_CONFIGS.production.webAppUrl; } else if (baseURL.includes("uat")) { appURL = SERVER_CONFIGS.uat.webAppUrl; } else if (baseURL.includes("localhost")) { appURL = SERVER_CONFIGS.local.webAppUrl; } else { appURL = SERVER_CONFIGS.staging.webAppUrl; } const previewUrl = `${appURL}/preview/${params.fileId}`; return { previewUrl: previewUrl, }; } /** * Create a temporary preview link for a file * @param params - Create temporary link parameters * @param params.fileId - The ID of the file to create a temporary link for * @param params.expiresIn - Optional expiration time in seconds (e.g 3600 for 1 hour) * @returns Promise<CreateTemporaryLinkResponse> - The temporary preview link response * @example * const tempLink = await sdk.file.createTemporaryLink({ * fileId: "file_1234567890", * expiresIn: 3600 // 1 hour * }); * // Returns: { * // temporaryPreviewUrl: "https://app-staging.koneksi.co.kr/preview/file_1234567890?token=abc123...", * // duration: "1h0m0s", * // file_key: "file_1234567890_abc123..." * // } */ async createTemporaryLink(params) { if (!params.fileId) { throw new Error("fileId is required"); } if (params.expiresIn !== undefined) { if (typeof params.expiresIn !== "number" || params.expiresIn <= 0) { throw new Error("expiresIn must be a positive number"); } } const tokenResponse = await this.generateTemporaryToken({ fileId: params.fileId, expiresIn: params.expiresIn, }); const baseURL = this.httpClient["config"].baseURL; let appURL; if (baseURL.includes("staging")) { appURL = SERVER_CONFIGS.staging.webAppUrl; } else if (baseURL.includes("production")) { appURL = SERVER_CONFIGS.production.webAppUrl; } else if (baseURL.includes("uat")) { appURL = SERVER_CONFIGS.uat.webAppUrl; } else if (baseURL.includes("localhost")) { appURL = SERVER_CONFIGS.local.webAppUrl; } else { appURL = SERVER_CONFIGS.staging.webAppUrl; } const temporaryPreviewUrl = `${appURL}/preview/${params.fileId}?token=${tokenResponse.file_key}`; return { temporaryPreviewUrl: temporaryPreviewUrl, duration: tokenResponse.duration, file_key: tokenResponse.file_key, }; } /** * Read file access information by ID (using public endpoint) * @param params - File access read parameters * @param params.fileId - The ID of the file to get access information for * @returns Promise<ReadFileAccessResponse> - The file access information * @example * await sdk.file.publicRead({ fileId: "file_1234567890" }); */ async publicRead(params) { if (!params.fileId) { throw new Error("fileId is required"); } const response = await this.httpClient.get(`/public/files/${params.fileId}/read`); if (response.status !== "success" || !response.data) { throw new Error(response.message || "Failed to read file access info"); } return { access: response.data.access, is_encrypted: response.data.is_encrypted, id: response.data.id, size: response.data.size, }; } /** * Download a file by ID (public/token/password access) * @param params - Public file download parameters * @param params.fileId - The ID of the file to download * @param params.temporaryToken - Optional temporary token * @param params.shared - Optional shared access parameters for password-protected files * @param params.passphrase - Optional passphrase for file decryption * @returns Promise<Buffer> - The file content as buffer * @example * await sdk.file.publicDownload({ fileId: "file_1234567890", temporaryToken: "token123" }); * await sdk.file.publicDownload({ fileId: "file_1234567890", shared: { access: "password", value: "Password123!" } }); */ async publicDownload(params) { if (!params.fileId) { throw new Error("File ID is required"); } if (params.temporaryToken && typeof params.temporaryToken !== "string") { throw new Error("temporaryToken must be a string"); } if (params.shared) { if (!params.shared.access || params.shared.access !== "password") { throw new Error("shared.access must be 'password'"); } if (!params.shared.value || typeof params.shared.value !== "string") { throw new Error("shared.value must be a string"); } } if (params.passphrase && typeof params.passphrase !== "string") { throw new Error("Passphrase must be a string"); } const accessInfo = await this.publicRead({ fileId: params.fileId }); if (!(params.temporaryToken || params.shared || accessInfo.access === "public")) { throw new Error("Please pass the correct parameters based on the file access type. Access type: " + accessInfo.access); } const queryParams = new URLSearchParams(); const headers = {}; if (params.temporaryToken) queryParams.append("key", params.temporaryToken); if (params.passphrase) headers["passphrase"] = params.passphrase; if (params.shared) headers["password"] = params.shared.value; if (accessInfo.is_encrypted || params.passphrase) queryParams.append("stream", "false"); const url = `/public/files/${params.fileId}/download${queryParams.toString() ? `?${queryParams.toString()}` : ""}`; const config = { headers }; const response = await this.httpClient.downloadFile(url, config); return response; } }