koneksi-ts-sdk
Version:
A JS/TS SDK for interacting with Koneksi API services
519 lines (518 loc) • 21.7 kB
JavaScript
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;
}
}