186 lines
6.1 KiB
JavaScript
186 lines
6.1 KiB
JavaScript
// [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<Array>}
|
|
* @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<Object>}
|
|
* @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<void>}
|
|
* @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<void>}
|
|
* @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]
|