New filelist format on frontend

This commit is contained in:
Leo Vasanko 2023-11-12 19:37:17 +00:00
parent ef5e37187d
commit 00a4297c0b
5 changed files with 82 additions and 100 deletions

View File

@ -1,13 +1,13 @@
<template> <template>
<LoginModal /> <LoginModal />
<header> <header>
<HeaderMain ref="headerMain" :path="path.pathList"> <HeaderMain ref="headerMain" :path="path.pathList" :query="path.query">
<HeaderSelected :path="path.pathList" /> <HeaderSelected :path="path.pathList" />
</HeaderMain> </HeaderMain>
<BreadCrumb :path="path.pathList" tabindex="-1"/> <BreadCrumb :path="path.pathList" tabindex="-1"/>
</header> </header>
<main> <main>
<RouterView :path="path.pathList" /> <RouterView :path="path.pathList" :query="path.query" />
</main> </main>
</template> </template>
@ -16,7 +16,7 @@ import { RouterView } from 'vue-router'
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import type HeaderMain from '@/components/HeaderMain.vue' import type HeaderMain from '@/components/HeaderMain.vue'
import { onMounted, onUnmounted, ref, watchEffect } from '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 { useDocumentStore } from '@/stores/documents'
import { computed } from 'vue' import { computed } from 'vue'
@ -25,19 +25,23 @@ import Router from '@/router/index'
interface Path { interface Path {
path: string path: string
pathList: string[] pathList: string[]
query: string
} }
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
const path: ComputedRef<Path> = computed(() => { const path: ComputedRef<Path> = computed(() => {
const p = decodeURIComponent(Router.currentRoute.value.path) const p = decodeURIComponent(Router.currentRoute.value.path).split('//')
const pathList = p.split('/').filter(value => value !== '') const pathList = p[0].split('/').filter(value => value !== '')
const query = p.slice(1).join('//')
return { return {
path: p, path: p[0],
pathList pathList,
query
} }
}) })
watchEffect(() => { watchEffect(() => {
document.title = path.value.path.replace(/\/$/, '').split('/').pop() || documentStore.server.name || 'Cista Storage' document.title = path.value.path.replace(/\/$/, '').split('/').pop() || documentStore.server.name || 'Cista Storage'
}) })
onMounted(loadSession)
onMounted(watchConnect) onMounted(watchConnect)
onUnmounted(watchDisconnect) onUnmounted(watchDisconnect)
// Update human-readable x seconds ago messages from mtimes // Update human-readable x seconds ago messages from mtimes

View File

@ -22,29 +22,16 @@ export type errorEvent = {
// Raw types the backend /api/watch sends us // Raw types the backend /api/watch sends us
export type FileEntry = { export type FileEntry = [
key: FUID number, // level
size: number string, // name
mtime: number FUID,
} number, //mtime
number, // size
number, // isfile
]
export type DirEntry = { export type UpdateEntry = ['k', number] | ['d', number] | ['i', Array<FileEntry>]
key: FUID
size: number
mtime: number
dir: DirList
}
export type DirList = Record<string, FileEntry | DirEntry>
export type UpdateEntry = {
name: string
deleted?: boolean
key?: FUID
size?: number
mtime?: number
dir?: DirList
}
// Helper structure for selections // Helper structure for selections
export interface SelectedItems { export interface SelectedItems {

View File

@ -1,14 +1,29 @@
import { useDocumentStore } from "@/stores/documents" 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 controlUrl = '/api/control'
export const uploadUrl = '/api/upload' export const uploadUrl = '/api/upload'
export const watchUrl = '/api/watch' export const watchUrl = '/api/watch'
let tree = null as DirEntry | null let tree = [] as FileEntry[]
let reconnectDuration = 500 let reconnectDuration = 500
let wsWatch = null as WebSocket | null 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<Record<keyof WebSocketEventMap, any>>) => { export const connect = (path: string, handlers: Partial<Record<keyof WebSocketEventMap, any>>) => {
const webSocket = new WebSocket(new URL(path, location.origin.replace(/^http/, 'ws'))) const webSocket = new WebSocket(new URL(path, location.origin.replace(/^http/, 'ws')))
for (const [event, handler] of Object.entries(handlers)) webSocket.addEventListener(event, handler) 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) console.log('Watch root', root)
store.updateRoot(root) store.updateRoot(root)
tree = root tree = root
saveSession()
} }
function handleUpdateMessage(updateData: { update: UpdateEntry[] }) { function handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
const store = useDocumentStore() 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') if (!tree) return console.error('Watch update before root')
let node: DirEntry = tree let newtree = []
for (const elem of updateData.update) { let oidx = 0
if (elem.deleted) {
delete node.dir[elem.name] for (const [action, arg] of update) {
break // Deleted elements can't have further children if (action === 'k') {
newtree.push(...tree.slice(oidx, oidx + arg))
oidx += arg
} }
if (elem.name) { else if (action === 'd') oidx += arg
// @ts-ignore else if (action === 'i') newtree.push(...arg)
console.log(node, elem.name) else console.log("Unknown update action", action, arg)
node = node.dir[elem.name] ||= {}
} }
if (elem.key !== undefined) node.key = elem.key if (oidx != tree.length)
if (elem.size !== undefined) node.size = elem.size throw Error(`Tree update out of sync, number of entries mismatch: got ${oidx}, expected ${tree.length}`)
if (elem.mtime !== undefined) node.mtime = elem.mtime store.updateRoot(newtree)
if (elem.dir !== undefined) node.dir = elem.dir tree = newtree
} saveSession()
store.updateRoot(tree)
} }
function handleError(msg: errorEvent) { function handleError(msg: errorEvent) {

View File

@ -1,10 +1,4 @@
import type { import type { Document, FileEntry, FUID, SelectedItems } from '@/repositories/Document'
Document,
DirEntry,
FileEntry,
FUID,
SelectedItems
} from '@/repositories/Document'
import { formatSize, formatUnixDate, haystackFormat } from '@/utils' import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { collator } from '@/utils' import { collator } from '@/utils'
@ -26,7 +20,6 @@ export const useDocumentStore = defineStore({
id: 'documents', id: 'documents',
state: () => ({ state: () => ({
document: [] as Document[], document: [] as Document[],
search: "" as string,
selected: new Set<FUID>(), selected: new Set<FUID>(),
uploadingDocuments: [], uploadingDocuments: [],
uploadCount: 0 as number, uploadCount: 0 as number,
@ -41,46 +34,27 @@ export const useDocumentStore = defineStore({
isOpenLoginModal: false isOpenLoginModal: false
} as User } as User
}), }),
persist: {
storage: sessionStorage,
paths: ['document'],
},
actions: { actions: {
updateRoot(root: DirEntry | null = null) { updateRoot(root: FileEntry[]) {
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)]
const docs = [] const docs = []
for (let doc; (doc = queue.shift()) !== undefined;) { let loc = [] as string[]
docs.push(doc) for (const [level, name, key, mtime, size, isfile] of root) {
if ("dir" in doc) { if (level === 0) continue
// Recurse but replace recursive structure with boolean loc = loc.slice(0, level - 1)
loc = doc.loc ? `${doc.loc}/${doc.name}` : doc.name docs.push({
queue.push(...Object.entries(doc.dir).map(mapper)) name,
// @ts-ignore loc: loc.join('/'),
doc.dir = true key,
size,
sizedisp: formatSize(size),
mtime,
modified: formatUnixDate(mtime),
haystack: haystackFormat(name),
dir: !isfile,
})
loc.push(name)
} }
// @ts-ignore console.log("Documents", docs)
else doc.dir = false
}
// 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)
)
this.document = docs as Document[] this.document = docs as Document[]
}, },
login(username: string, privileged: boolean) { login(username: string, privileged: boolean) {

View File

@ -16,17 +16,17 @@ import { needleFormat, localeIncludes, collator } from '@/utils';
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
const fileExplorer = ref() const fileExplorer = ref()
const props = defineProps({ const props = defineProps<{
path: Array<string> path: Array<string>
}) query: string
}>()
const documents = computed(() => { const documents = computed(() => {
if (!props.path) return []
const loc = props.path.join('/') const loc = props.path.join('/')
const query = props.query
// List the current location // 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 // Find up to 100 newest documents that match the search
const search = documentStore.search const needle = needleFormat(query)
const needle = needleFormat(search)
let limit = 100 let limit = 100
let docs = [] let docs = []
for (const doc of documentStore.recentDocuments) { for (const doc of documentStore.recentDocuments) {
@ -46,7 +46,7 @@ const documents = computed(() => {
// @ts-ignore // @ts-ignore
(a.type === 'file') - (b.type === 'file') || (a.type === 'file') - (b.type === 'file') ||
// @ts-ignore // @ts-ignore
b.name.includes(search) - a.name.includes(search) || b.name.includes(query) - a.name.includes(query) ||
collator.compare(a.name, b.name) collator.compare(a.name, b.name)
)) ))
return docs return docs