Files
ss-tools/frontend/src/routes/tools/storage/+page.svelte

219 lines
7.5 KiB
Svelte

<!-- [DEF:StoragePage:Component] -->
<!--
@TIER: STANDARD
@SEMANTICS: storage, files, management
@PURPOSE: Main page for unified file storage management.
@LAYER: UI
@RELATION: DEPENDS_ON -> storageService
@RELATION: CONTAINS -> FileList
@RELATION: CONTAINS -> FileUpload
@INVARIANT: Always displays a unified storage view without category tabs.
-->
<script lang="ts">
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { listFiles, deleteFile } from '../../../services/storageService';
import { addToast } from '../../../lib/toasts';
import { t } from '../../../lib/i18n';
import FileList from '../../../components/storage/FileList.svelte';
import FileUpload from '../../../components/storage/FileUpload.svelte';
// [/SECTION: IMPORTS]
// [DEF:loadFiles:Function]
/**
* @purpose Fetches the list of files from the server.
* @pre currentPath is a valid storage path or empty for root.
* @post Updates the `files` array with the latest data.
*/
let files = [];
let isLoading = false;
let currentPath = '';
let uploadCategory = 'backups';
let uploadSubpath = '';
// [DEF:resolveStorageQueryFromPath:Function]
/**
* @purpose Splits UI path into storage API category and category-local subpath.
* @pre uiPath may be empty or start with backups/repositorys.
* @post Returns {category, subpath} compatible with /api/storage/files.
*/
function resolveStorageQueryFromPath(uiPath: string): { category?: string; subpath?: string } {
const segments = String(uiPath || '').split('/').filter(Boolean);
if (segments.length === 0) return {};
const topLevel = segments[0];
if (topLevel !== 'backups' && topLevel !== 'repositorys') {
return {};
}
const subpath = segments.slice(1).join('/');
return {
category: topLevel,
subpath: subpath || undefined,
};
}
// [/DEF:resolveStorageQueryFromPath:Function]
async function loadFiles() {
console.log('[STORAGE-PAGE][LOAD_START] path=%s', currentPath);
isLoading = true;
try {
const query = resolveStorageQueryFromPath(currentPath);
files = await listFiles(query.category, query.subpath);
console.log('[STORAGE-PAGE][LOAD_OK] count=%d', files.length);
} catch (error) {
console.log('[STORAGE-PAGE][LOAD_ERR] error=%s', error.message);
addToast($t.storage.messages.load_failed.replace('{error}', error.message), 'error');
} finally {
isLoading = false;
}
}
// [/DEF:loadFiles:Function]
// [DEF:handleDelete:Function]
/**
* @purpose Handles the file deletion process.
* @pre The event contains valid category and path.
* @post File is deleted and file list is refreshed.
* @param {CustomEvent} event - The delete event containing category and path.
*/
async function handleDelete(event) {
const { category, path, name } = event.detail;
console.log('[STORAGE-PAGE][DELETE_START] category=%s path=%s', category, path);
if (!confirm($t.storage.messages.delete_confirm.replace('{name}', name))) return;
try {
await deleteFile(category, path);
console.log('[STORAGE-PAGE][DELETE_OK] name=%s', name);
addToast($t.storage.messages.delete_success.replace('{name}', name), 'success');
await loadFiles();
} catch (error) {
console.log('[STORAGE-PAGE][DELETE_ERR] error=%s', error.message);
addToast($t.storage.messages.delete_failed.replace('{error}', error.message), 'error');
}
}
// [/DEF:handleDelete:Function]
// [DEF:handleNavigate:Function]
/**
* @purpose Updates the current path and reloads files when navigating into a directory.
* @pre The event contains a valid path string.
* @post currentPath is updated and files are reloaded.
* @param {CustomEvent} event - The navigation event containing the new path.
*/
function handleNavigate(event) {
console.log('[STORAGE-PAGE][NAVIGATE] path=%s', event.detail);
currentPath = event.detail;
loadFiles();
}
// [/DEF:handleNavigate:Function]
// [DEF:navigateUp:Function]
/**
* @purpose Navigates one level up in the directory structure.
* @pre currentPath is not root.
* @post currentPath is moved up one directory level.
*/
function navigateUp() {
if (!currentPath) return;
const parts = currentPath.split('/');
parts.pop();
currentPath = parts.join('/') || '';
loadFiles();
}
// [/DEF:navigateUp:Function]
// [DEF:updateUploadCategory:Function]
/**
* @purpose Keeps upload category aligned with the currently viewed top-level folder.
* @pre currentPath can be empty or a slash-delimited path.
* @post uploadCategory is either backups or repositorys.
*/
function updateUploadCategory() {
const [topLevel, ...rest] = currentPath.split('/').filter(Boolean);
uploadCategory = topLevel === 'repositorys' ? 'repositorys' : 'backups';
uploadSubpath = topLevel === 'repositorys' || topLevel === 'backups'
? rest.join('/')
: '';
}
// [/DEF:updateUploadCategory:Function]
onMount(() => {
const pathParam = $page.url.searchParams.get('path');
if (pathParam) {
currentPath = pathParam;
}
updateUploadCategory();
loadFiles();
});
$: currentPath, updateUploadCategory();
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="container mx-auto w-full p-4 min-[1280px]:max-w-[1400px] min-[1536px]:max-w-[1680px]">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">{$t.storage.management}</h1>
<div class="flex items-center mt-2 text-sm text-gray-500">
<button on:click={() => { currentPath = ''; loadFiles(); }} class="hover:text-indigo-600">{$t.storage.root}</button>
{#each currentPath.split('/').filter(Boolean) as part, i}
<span class="mx-2">/</span>
<button
on:click={() => {
currentPath = currentPath.split('/').filter(Boolean).slice(0, i + 1).join('/');
loadFiles();
}}
class="hover:text-indigo-600 capitalize"
>
{part}
</button>
{/each}
</div>
</div>
<div class="flex justify-end mb-4">
<button
on:click={loadFiles}
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
disabled={isLoading}
>
{isLoading ? $t.storage.refreshing : $t.storage.refresh}
</button>
</div>
<div class="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_360px] 2xl:grid-cols-[minmax(0,1fr)_420px] gap-6">
<!-- Main Content: File List -->
<div class="space-y-4 min-w-0">
<div class="flex items-center mb-2">
{#if currentPath}
<button
on:click={navigateUp}
class="mr-4 inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50"
>
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
{$t.common?.back}
</button>
{/if}
</div>
<FileList {files} on:delete={handleDelete} on:navigate={handleNavigate} />
</div>
<!-- Sidebar: Upload -->
<div class="lg:col-span-1">
<FileUpload
category={uploadCategory}
path={uploadSubpath}
on:uploaded={loadFiles}
/>
</div>
</div>
</div>
<!-- [/SECTION: TEMPLATE] -->
<!-- [/DEF:StoragePage:Component] -->