From 00a4297c0bade3ccd5a378c0be601af554764c1e Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 12 Nov 2023 19:37:17 +0000 Subject: [PATCH] New filelist format on frontend --- frontend/src/App.vue | 18 +++++--- frontend/src/repositories/Document.ts | 31 ++++--------- frontend/src/repositories/WS.ts | 55 +++++++++++++++-------- frontend/src/stores/documents.ts | 64 ++++++++------------------- frontend/src/views/ExplorerView.vue | 14 +++--- 5 files changed, 82 insertions(+), 100 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d7a91d5..a862b9a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,13 +1,13 @@ @@ -16,7 +16,7 @@ import { RouterView } from 'vue-router' import type { ComputedRef } from 'vue' import type HeaderMain from '@/components/HeaderMain.vue' import { onMounted, onUnmounted, ref, watchEffect } from 'vue' -import { watchConnect, watchDisconnect } from '@/repositories/WS' +import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS' import { useDocumentStore } from '@/stores/documents' import { computed } from 'vue' @@ -25,19 +25,23 @@ import Router from '@/router/index' interface Path { path: string pathList: string[] + query: string } const documentStore = useDocumentStore() const path: ComputedRef = computed(() => { - const p = decodeURIComponent(Router.currentRoute.value.path) - const pathList = p.split('/').filter(value => value !== '') + const p = decodeURIComponent(Router.currentRoute.value.path).split('//') + const pathList = p[0].split('/').filter(value => value !== '') + const query = p.slice(1).join('//') return { - path: p, - pathList + path: p[0], + pathList, + query } }) watchEffect(() => { document.title = path.value.path.replace(/\/$/, '').split('/').pop() || documentStore.server.name || 'Cista Storage' }) +onMounted(loadSession) onMounted(watchConnect) onUnmounted(watchDisconnect) // Update human-readable x seconds ago messages from mtimes diff --git a/frontend/src/repositories/Document.ts b/frontend/src/repositories/Document.ts index 57355b4..022b260 100644 --- a/frontend/src/repositories/Document.ts +++ b/frontend/src/repositories/Document.ts @@ -22,29 +22,16 @@ export type errorEvent = { // Raw types the backend /api/watch sends us -export type FileEntry = { - key: FUID - size: number - mtime: number -} +export type FileEntry = [ + number, // level + string, // name + FUID, + number, //mtime + number, // size + number, // isfile +] -export type DirEntry = { - key: FUID - size: number - mtime: number - dir: DirList -} - -export type DirList = Record - -export type UpdateEntry = { - name: string - deleted?: boolean - key?: FUID - size?: number - mtime?: number - dir?: DirList -} +export type UpdateEntry = ['k', number] | ['d', number] | ['i', Array] // Helper structure for selections export interface SelectedItems { diff --git a/frontend/src/repositories/WS.ts b/frontend/src/repositories/WS.ts index d5ebf8a..0570921 100644 --- a/frontend/src/repositories/WS.ts +++ b/frontend/src/repositories/WS.ts @@ -1,14 +1,29 @@ import { useDocumentStore } from "@/stores/documents" -import type { DirEntry, UpdateEntry, errorEvent } from "./Document" +import type { FileEntry, UpdateEntry, errorEvent } from "./Document" export const controlUrl = '/api/control' export const uploadUrl = '/api/upload' export const watchUrl = '/api/watch' -let tree = null as DirEntry | null +let tree = [] as FileEntry[] let reconnectDuration = 500 let wsWatch = null as WebSocket | null +export const loadSession = () => { + const store = useDocumentStore() + try { + tree = JSON.parse(sessionStorage["cista-files"]) + store.updateRoot(tree) + return true + } catch (error) { + return false + } +} + +const saveSession = () => { + sessionStorage["cista-files"] = JSON.stringify(tree) +} + export const connect = (path: string, handlers: Partial>) => { const webSocket = new WebSocket(new URL(path, location.origin.replace(/^http/, 'ws'))) for (const [event, handler] of Object.entries(handlers)) webSocket.addEventListener(event, handler) @@ -99,29 +114,31 @@ function handleRootMessage({ root }: { root: DirEntry }) { console.log('Watch root', root) store.updateRoot(root) tree = root + saveSession() } function handleUpdateMessage(updateData: { update: UpdateEntry[] }) { const store = useDocumentStore() - console.log('Watch update', updateData.update) + const update = updateData.update + console.log('Watch update', update) if (!tree) return console.error('Watch update before root') - let node: DirEntry = tree - for (const elem of updateData.update) { - if (elem.deleted) { - delete node.dir[elem.name] - break // Deleted elements can't have further children - } - if (elem.name) { - // @ts-ignore - console.log(node, elem.name) - node = node.dir[elem.name] ||= {} - } - if (elem.key !== undefined) node.key = elem.key - if (elem.size !== undefined) node.size = elem.size - if (elem.mtime !== undefined) node.mtime = elem.mtime - if (elem.dir !== undefined) node.dir = elem.dir + let newtree = [] + let oidx = 0 + + for (const [action, arg] of update) { + if (action === 'k') { + newtree.push(...tree.slice(oidx, oidx + arg)) + oidx += arg + } + else if (action === 'd') oidx += arg + else if (action === 'i') newtree.push(...arg) + else console.log("Unknown update action", action, arg) } - store.updateRoot(tree) + if (oidx != tree.length) + throw Error(`Tree update out of sync, number of entries mismatch: got ${oidx}, expected ${tree.length}`) + store.updateRoot(newtree) + tree = newtree + saveSession() } function handleError(msg: errorEvent) { diff --git a/frontend/src/stores/documents.ts b/frontend/src/stores/documents.ts index 97c6ca6..985d5fd 100644 --- a/frontend/src/stores/documents.ts +++ b/frontend/src/stores/documents.ts @@ -1,10 +1,4 @@ -import type { - Document, - DirEntry, - FileEntry, - FUID, - SelectedItems -} from '@/repositories/Document' +import type { Document, FileEntry, FUID, SelectedItems } from '@/repositories/Document' import { formatSize, formatUnixDate, haystackFormat } from '@/utils' import { defineStore } from 'pinia' import { collator } from '@/utils' @@ -26,7 +20,6 @@ export const useDocumentStore = defineStore({ id: 'documents', state: () => ({ document: [] as Document[], - search: "" as string, selected: new Set(), uploadingDocuments: [], uploadCount: 0 as number, @@ -41,46 +34,27 @@ export const useDocumentStore = defineStore({ isOpenLoginModal: false } as User }), - persist: { - storage: sessionStorage, - paths: ['document'], - }, actions: { - updateRoot(root: DirEntry | null = null) { - if (!root) { - this.document = [] - return - } - // Transform tree data to flat documents array - let loc = "" - const mapper = ([name, attr]: [string, FileEntry | DirEntry]) => ({ - ...attr, - loc, - name, - sizedisp: formatSize(attr.size), - modified: formatUnixDate(attr.mtime), - haystack: haystackFormat(name), - }) - const queue = [...Object.entries(root.dir ?? {}).map(mapper)] + updateRoot(root: FileEntry[]) { const docs = [] - for (let doc; (doc = queue.shift()) !== undefined;) { - docs.push(doc) - if ("dir" in doc) { - // Recurse but replace recursive structure with boolean - loc = doc.loc ? `${doc.loc}/${doc.name}` : doc.name - queue.push(...Object.entries(doc.dir).map(mapper)) - // @ts-ignore - doc.dir = true - } - // @ts-ignore - else doc.dir = false + let loc = [] as string[] + for (const [level, name, key, mtime, size, isfile] of root) { + if (level === 0) continue + loc = loc.slice(0, level - 1) + docs.push({ + name, + loc: loc.join('/'), + key, + size, + sizedisp: formatSize(size), + mtime, + modified: formatUnixDate(mtime), + haystack: haystackFormat(name), + dir: !isfile, + }) + loc.push(name) } - // Pre sort directory entries folders first then files, names in natural ordering - docs.sort((a, b) => - // @ts-ignore - b.dir - a.dir || - collator.compare(a.name, b.name) - ) + console.log("Documents", docs) this.document = docs as Document[] }, login(username: string, privileged: boolean) { diff --git a/frontend/src/views/ExplorerView.vue b/frontend/src/views/ExplorerView.vue index b163caa..773d2ee 100644 --- a/frontend/src/views/ExplorerView.vue +++ b/frontend/src/views/ExplorerView.vue @@ -16,17 +16,17 @@ import { needleFormat, localeIncludes, collator } from '@/utils'; const documentStore = useDocumentStore() const fileExplorer = ref() -const props = defineProps({ +const props = defineProps<{ path: Array -}) + query: string +}>() const documents = computed(() => { - if (!props.path) return [] const loc = props.path.join('/') + const query = props.query // List the current location - if (!documentStore.search) return documentStore.document.filter(doc => doc.loc === loc) + if (!query) return documentStore.document.filter(doc => doc.loc === loc) // Find up to 100 newest documents that match the search - const search = documentStore.search - const needle = needleFormat(search) + const needle = needleFormat(query) let limit = 100 let docs = [] for (const doc of documentStore.recentDocuments) { @@ -46,7 +46,7 @@ const documents = computed(() => { // @ts-ignore (a.type === 'file') - (b.type === 'file') || // @ts-ignore - b.name.includes(search) - a.name.includes(search) || + b.name.includes(query) - a.name.includes(query) || collator.compare(a.name, b.name) )) return docs