// [DEF:storageService:Module] /** * @TIER: STANDARD * @purpose Frontend API client for file storage management. * @layer Service * @relation DEPENDS_ON -> backend.api.storage * @SEMANTICS: storage, api, client */ const API_BASE = '/api/storage'; // [DEF:getStorageAuthHeaders:Function] /** * @purpose Returns headers with Authorization for storage API calls. * @returns {Object} Headers object with Authorization if token exists. * @NOTE Unlike api.js getAuthHeaders, this doesn't set Content-Type * to allow FormData to set its own multipart boundary. */ function getStorageAuthHeaders() { const headers = {}; if (typeof window !== 'undefined') { const token = localStorage.getItem('auth_token'); if (token) { headers['Authorization'] = `Bearer ${token}`; } } return headers; } // [/DEF:getStorageAuthHeaders:Function] // [DEF:encodeStoragePath:Function] /** * @purpose Encodes a storage-relative path preserving slash separators. * @param {string} path - Relative storage path. * @returns {string} Encoded path safe for URL segments. */ function encodeStoragePath(path) { return String(path || '') .split('/') .filter((part) => part.length > 0) .map((part) => encodeURIComponent(part)) .join('/'); } // [/DEF:encodeStoragePath:Function] // [DEF:listFiles:Function] /** * @purpose Fetches the list of files for a given category and subpath. * @param {string} [category] - Optional category filter. * @param {string} [path] - Optional subpath filter. * @returns {Promise} * @PRE category and path should be valid strings if provided. * @POST Returns a promise resolving to an array of StoredFile objects. */ export async function listFiles(category, path) { const params = new URLSearchParams(); if (category) { params.append('category', category); } if (path) { params.append('path', path); } const response = await fetch(`${API_BASE}/files?${params.toString()}`, { headers: getStorageAuthHeaders() }); if (!response.ok) { throw new Error(`Failed to fetch files: ${response.statusText}`); } return await response.json(); } // [/DEF:listFiles:Function] // [DEF:uploadFile:Function] /** * @purpose Uploads a file to the storage system. * @param {File} file - The file to upload. * @param {string} category - Target category. * @param {string} [path] - Target subpath. * @returns {Promise} * @PRE file must be a valid File object; category must be specified. * @POST Returns a promise resolving to the metadata of the uploaded file. */ export async function uploadFile(file, category, path) { const formData = new FormData(); formData.append('file', file); formData.append('category', category); if (path) { formData.append('path', path); } const response = await fetch(`${API_BASE}/upload`, { method: 'POST', headers: getStorageAuthHeaders(), body: formData }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || `Failed to upload file: ${response.statusText}`); } return await response.json(); } // [/DEF:uploadFile:Function] // [DEF:deleteFile:Function] /** * @purpose Deletes a file or directory from storage. * @param {string} category - File category. * @param {string} path - Relative path of the item. * @returns {Promise} * @PRE category and path must identify an existing file or directory. * @POST The specified file or directory is removed from storage. */ export async function deleteFile(category, path) { const response = await fetch(`${API_BASE}/files/${category}/${path}`, { method: 'DELETE', headers: getStorageAuthHeaders() }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || `Failed to delete: ${response.statusText}`); } } // [/DEF:deleteFile:Function] // [DEF:downloadFileUrl:Function] /** * @purpose Returns the URL for downloading a file. * @param {string} category - File category. * @param {string} path - Relative path of the file. * @returns {string} * @PRE category and path must identify an existing file. * @POST Returns a valid API URL for file download. * @NOTE Downloads use browser navigation, so auth is handled via cookies * or the backend must allow unauthenticated downloads for valid paths. */ export function downloadFileUrl(category, path) { const safeCategory = encodeURIComponent(String(category || '')); const safePath = encodeStoragePath(path); return `${API_BASE}/download/${safeCategory}/${safePath}`; } // [/DEF:downloadFileUrl:Function] // [DEF:downloadFile:Function] /** * @purpose Downloads a file using authenticated fetch and saves it in browser. * @param {string} category - File category. * @param {string} path - Relative path of the file. * @param {string} [filename] - Optional preferred filename. * @returns {Promise} * @PRE category/path identify an existing file and user has READ permission. * @POST Browser download is triggered or an Error is thrown. */ export async function downloadFile(category, path, filename) { const response = await fetch(downloadFileUrl(category, path), { headers: getStorageAuthHeaders(), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || `Failed to download file: ${response.statusText}`); } const blob = await response.blob(); const objectUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = objectUrl; link.download = filename || String(path || '').split('/').pop() || 'download'; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(objectUrl); } // [/DEF:downloadFile:Function] export default { listFiles, uploadFile, deleteFile, downloadFileUrl, downloadFile }; // [/DEF:storageService:Module]