diff --git a/frontend/src/stores/files.ts b/frontend/src/stores/files.ts new file mode 100644 index 0000000..085d2bc --- /dev/null +++ b/frontend/src/stores/files.ts @@ -0,0 +1,136 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { useApi } from '@/composables/useApi' +import { useInstancesStore } from '@/stores/instances' + +export interface FileEntry { + name: string + path: string + is_dir: boolean + size: number + modified: string +} + +/** + * Per-instance file browser store. + * All operations target `/api/instances/{id}/...` — jailed to instance root. + * Guard: if no current instance, list() sets error and bails out early. + */ +export const useFilesStore = defineStore('files', () => { + const api = useApi() + + const cwd = ref('') + const entries = ref([]) + const loading = ref(false) + const error = ref(null) + + /** Join two relative path segments with a single forward slash. */ + function joinPath(base: string, name: string): string { + if (!base) return name + return `${base}/${name}` + } + + function currentId(): string | null { + // Retrieve fresh from the store each call — avoids stale closure. + return useInstancesStore().currentId + } + + /** List a directory. Sets cwd + entries. Does NOT throw — sets error. */ + async function list(path: string): Promise { + const id = currentId() + if (!id) { + error.value = 'No instance — connect the host agent' + entries.value = [] + return + } + loading.value = true + error.value = null + try { + const data = await api.get<{ entries: FileEntry[] }>( + `/instances/${id}/files?path=${encodeURIComponent(path)}`, + ) + cwd.value = path + entries.value = data.entries ?? [] + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load directory' + } finally { + loading.value = false + } + } + + /** Read a text file. Returns content string. Throws on error (binary/too big/not found). */ + async function readFile(path: string): Promise { + const id = currentId() + if (!id) throw new Error('No instance selected') + const data = await api.get<{ content: string }>( + `/instances/${id}/file?path=${encodeURIComponent(path)}`, + ) + return data.content ?? '' + } + + /** Write / overwrite a text file. Throws on error. */ + async function writeFile(path: string, content: string): Promise { + const id = currentId() + if (!id) throw new Error('No instance selected') + await api.put(`/instances/${id}/file`, { path, content }) + } + + /** Delete a file or directory (recursive). Throws on error. */ + async function del(path: string): Promise { + const id = currentId() + if (!id) throw new Error('No instance selected') + await api.post(`/instances/${id}/files/delete`, { path }) + } + + /** Rename within the same parent. `name` is the bare new filename. Throws on error. */ + async function rename(path: string, name: string): Promise { + const id = currentId() + if (!id) throw new Error('No instance selected') + await api.post(`/instances/${id}/files/rename`, { path, name }) + } + + /** Create a directory (and all missing ancestors). Throws on error. */ + async function mkdir(path: string): Promise { + const id = currentId() + if (!id) throw new Error('No instance selected') + await api.post(`/instances/${id}/files/mkdir`, { path }) + } + + /** Create an empty file. Throws on error. */ + async function mkfile(path: string): Promise { + const id = currentId() + if (!id) throw new Error('No instance selected') + await api.post(`/instances/${id}/files/mkfile`, { path }) + } + + /** Move a file or directory. Both paths are relative to the instance root. Throws on error. */ + async function move(path: string, dest: string): Promise { + const id = currentId() + if (!id) throw new Error('No instance selected') + await api.post(`/instances/${id}/files/move`, { path, dest }) + } + + /** Copy a file or directory. Both paths are relative to the instance root. Throws on error. */ + async function copy(path: string, dest: string): Promise { + const id = currentId() + if (!id) throw new Error('No instance selected') + await api.post(`/instances/${id}/files/copy`, { path, dest }) + } + + return { + cwd, + entries, + loading, + error, + joinPath, + list, + readFile, + writeFile, + del, + rename, + mkdir, + mkfile, + move, + copy, + } +}) diff --git a/frontend/src/views/admin/FileManagerView.vue b/frontend/src/views/admin/FileManagerView.vue index 098d3b2..e018e70 100644 --- a/frontend/src/views/admin/FileManagerView.vue +++ b/frontend/src/views/admin/FileManagerView.vue @@ -1,28 +1,176 @@ @@ -41,15 +189,232 @@ const finderConfig = { - -
- -
+ + + + + + + +