Compare commits
	
		
			13 Commits
		
	
	
		
			v0.2.0
			...
			19a5c4ad8a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 19a5c4ad8a | ||
|   | 3d3b078e60 | ||
|   | d4e91ea9a6 | ||
|   | dc4bb494f3 | ||
|   | 9b58b887b4 | ||
|   | 07848907f3 | ||
|   | 7a08f7cbe2 | ||
|   | dd37238510 | ||
|   | c8d5f335b1 | ||
|   | bb80b3ee54 | ||
|   | 06d860c601 | ||
|   | c321de13fd | ||
|   | 278e8303c4 | 
							
								
								
									
										13
									
								
								cista/api.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								cista/api.py
									
									
									
									
									
								
							| @@ -37,16 +37,23 @@ async def upload(req, ws): | |||||||
|             ) |             ) | ||||||
|         req = msgspec.json.decode(text, type=FileRange) |         req = msgspec.json.decode(text, type=FileRange) | ||||||
|         pos = req.start |         pos = req.start | ||||||
|         data = None |         while True: | ||||||
|         while pos < req.end and (data := await ws.recv()) and isinstance(data, bytes): |             data = await ws.recv() | ||||||
|  |             if not isinstance(data, bytes): | ||||||
|  |                 break | ||||||
|  |             if len(data) > req.end - pos: | ||||||
|  |                 raise ValueError( | ||||||
|  |                     f"Expected up to {req.end - pos} bytes, got {len(data)} bytes" | ||||||
|  |                 ) | ||||||
|             sentsize = await alink(("upload", req.name, pos, data, req.size)) |             sentsize = await alink(("upload", req.name, pos, data, req.size)) | ||||||
|             pos += typing.cast(int, sentsize) |             pos += typing.cast(int, sentsize) | ||||||
|  |             if pos >= req.end: | ||||||
|  |                 break | ||||||
|         if pos != req.end: |         if pos != req.end: | ||||||
|             d = f"{len(data)} bytes" if isinstance(data, bytes) else data |             d = f"{len(data)} bytes" if isinstance(data, bytes) else data | ||||||
|             raise ValueError(f"Expected {req.end - pos} more bytes, got {d}") |             raise ValueError(f"Expected {req.end - pos} more bytes, got {d}") | ||||||
|         # Report success |         # Report success | ||||||
|         res = StatusMsg(status="ack", req=req) |         res = StatusMsg(status="ack", req=req) | ||||||
|         print("ack", res) |  | ||||||
|         await asend(ws, res) |         await asend(ws, res) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -34,9 +34,11 @@ class File: | |||||||
|             self.open_rw() |             self.open_rw() | ||||||
|         assert self.fd is not None |         assert self.fd is not None | ||||||
|         if file_size is not None: |         if file_size is not None: | ||||||
|  |             assert pos + len(buffer) <= file_size | ||||||
|             os.ftruncate(self.fd, file_size) |             os.ftruncate(self.fd, file_size) | ||||||
|         os.lseek(self.fd, pos, os.SEEK_SET) |         if buffer: | ||||||
|         os.write(self.fd, buffer) |             os.lseek(self.fd, pos, os.SEEK_SET) | ||||||
|  |             os.write(self.fd, buffer) | ||||||
|  |  | ||||||
|     def __getitem__(self, slice): |     def __getitem__(self, slice): | ||||||
|         if self.fd is None: |         if self.fd is None: | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import os | import os | ||||||
| import re | import re | ||||||
| from pathlib import Path, PurePath | from pathlib import Path | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
|  |  | ||||||
| @@ -15,7 +15,6 @@ def run(*, dev=False): | |||||||
|     # Silence Sanic's warning about running in production rather than debug |     # Silence Sanic's warning about running in production rather than debug | ||||||
|     os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1" |     os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1" | ||||||
|     confdir = config.conffile.parent |     confdir = config.conffile.parent | ||||||
|     wwwroot = PurePath(__file__).parent / "wwwroot" |  | ||||||
|     if opts.get("ssl"): |     if opts.get("ssl"): | ||||||
|         # Run plain HTTP redirect/acme server on port 80 |         # Run plain HTTP redirect/acme server on port 80 | ||||||
|         server80.app.prepare(port=80, motd=False) |         server80.app.prepare(port=80, motd=False) | ||||||
| @@ -27,7 +26,7 @@ def run(*, dev=False): | |||||||
|         motd=False, |         motd=False, | ||||||
|         dev=dev, |         dev=dev, | ||||||
|         auto_reload=dev, |         auto_reload=dev, | ||||||
|         reload_dir={confdir, wwwroot}, |         reload_dir={confdir}, | ||||||
|         access_log=True, |         access_log=True, | ||||||
|     )  # type: ignore |     )  # type: ignore | ||||||
|     if dev: |     if dev: | ||||||
|   | |||||||
| @@ -44,8 +44,6 @@ watchEffect(() => { | |||||||
| onMounted(loadSession) | onMounted(loadSession) | ||||||
| onMounted(watchConnect) | onMounted(watchConnect) | ||||||
| onUnmounted(watchDisconnect) | onUnmounted(watchDisconnect) | ||||||
| // Update human-readable x seconds ago messages from mtimes |  | ||||||
| setInterval(documentStore.updateModified, 1000) |  | ||||||
| const headerMain = ref<typeof HeaderMain | null>(null) | const headerMain = ref<typeof HeaderMain | null>(null) | ||||||
| let vert = 0 | let vert = 0 | ||||||
| let timer: any = null | let timer: any = null | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
|         <td class="name"> |         <td class="name"> | ||||||
|           <FileRenameInput :doc="editing" :rename="mkdir" :exit="() => {editing = null}" /> |           <FileRenameInput :doc="editing" :rename="mkdir" :exit="() => {editing = null}" /> | ||||||
|         </td> |         </td> | ||||||
|         <FileModified :doc=editing /> |         <FileModified :doc=editing :key=nowkey /> | ||||||
|         <FileSize :doc=editing /> |         <FileSize :doc=editing /> | ||||||
|         <td class="menu"></td> |         <td class="menu"></td> | ||||||
|       </tr> |       </tr> | ||||||
| @@ -61,7 +61,7 @@ | |||||||
|               <button v-if="cursor == doc" class="rename-button" @click="() => (editing = doc)">🖊️</button> |               <button v-if="cursor == doc" class="rename-button" @click="() => (editing = doc)">🖊️</button> | ||||||
|             </template> |             </template> | ||||||
|           </td> |           </td> | ||||||
|           <FileModified :doc=doc /> |           <FileModified :doc=doc :key=nowkey /> | ||||||
|           <FileSize :doc=doc /> |           <FileSize :doc=doc /> | ||||||
|           <td class="menu"> |           <td class="menu"> | ||||||
|             <button tabindex="-1" @click.stop="contextMenu($event, doc)">⋮</button> |             <button tabindex="-1" @click.stop="contextMenu($event, doc)">⋮</button> | ||||||
| @@ -79,28 +79,28 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref, computed, watchEffect } from 'vue' | import { ref, computed, watchEffect, shallowRef, onMounted, onUnmounted } from 'vue' | ||||||
| import { useDocumentStore } from '@/stores/documents' | import { useDocumentStore } from '@/stores/documents' | ||||||
| import type { Document } from '@/repositories/Document' | import { Doc } from '@/repositories/Document' | ||||||
| import FileRenameInput from './FileRenameInput.vue' | import FileRenameInput from './FileRenameInput.vue' | ||||||
| import { connect, controlUrl } from '@/repositories/WS' | import { connect, controlUrl } from '@/repositories/WS' | ||||||
| import { collator, formatSize, formatUnixDate } from '@/utils' | import { collator, formatSize } from '@/utils' | ||||||
| import { useRouter } from 'vue-router' | import { useRouter } from 'vue-router' | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|   path: Array<string> |   path: Array<string> | ||||||
|   documents: Document[] |   documents: Doc[] | ||||||
| }>() | }>() | ||||||
| const documentStore = useDocumentStore() | const documentStore = useDocumentStore() | ||||||
| const router = useRouter() | const router = useRouter() | ||||||
| const url_for = (doc: Document) => { | const url_for = (doc: Doc) => { | ||||||
|   const p = doc.loc ? `${doc.loc}/${doc.name}` : doc.name |   const p = doc.loc ? `${doc.loc}/${doc.name}` : doc.name | ||||||
|   return doc.dir ? `#/${p}/` : `/files/${p}` |   return doc.dir ? `#/${p}/` : `/files/${p}` | ||||||
| } | } | ||||||
| const cursor = ref<Document | null>(null) | const cursor = shallowRef<Doc | null>(null) | ||||||
| // File rename | // File rename | ||||||
| const editing = ref<Document | null>(null) | const editing = shallowRef<Doc | null>(null) | ||||||
| const rename = (doc: Document, newName: string) => { | const rename = (doc: Doc, newName: string) => { | ||||||
|   const oldName = doc.name |   const oldName = doc.name | ||||||
|   const control = connect(controlUrl, { |   const control = connect(controlUrl, { | ||||||
|     message(ev: MessageEvent) { |     message(ev: MessageEvent) { | ||||||
| @@ -124,7 +124,7 @@ const rename = (doc: Document, newName: string) => { | |||||||
|   } |   } | ||||||
|   doc.name = newName // We should get an update from watch but this is quicker |   doc.name = newName // We should get an update from watch but this is quicker | ||||||
| } | } | ||||||
| const sortedDocuments = computed(() => sorted(props.documents as Document[])) | const sortedDocuments = computed(() => sorted(props.documents)) | ||||||
| const showFolderBreadcrumb = (i: number) => { | const showFolderBreadcrumb = (i: number) => { | ||||||
|   const docs = sortedDocuments.value |   const docs = sortedDocuments.value | ||||||
|   const docloc = docs[i].loc |   const docloc = docs[i].loc | ||||||
| @@ -132,19 +132,15 @@ const showFolderBreadcrumb = (i: number) => { | |||||||
| } | } | ||||||
| defineExpose({ | defineExpose({ | ||||||
|   newFolder() { |   newFolder() { | ||||||
|     const now = Date.now() / 1000 |     const now = Math.floor(Date.now() / 1000) | ||||||
|     editing.value = { |     editing.value = new Doc({ | ||||||
|       loc: loc.value, |       loc: loc.value, | ||||||
|       key: 'new', |       key: 'new', | ||||||
|       name: 'New Folder', |       name: 'New Folder', | ||||||
|       dir: true, |       dir: true, | ||||||
|       mtime: now, |       mtime: now, | ||||||
|       size: 0, |       size: 0, | ||||||
|       sizedisp: formatSize(0), |     }) | ||||||
|       modified: formatUnixDate(now), |  | ||||||
|       haystack: '', |  | ||||||
|     } |  | ||||||
|     console.log("New") |  | ||||||
|   }, |   }, | ||||||
|   toggleSelectAll() { |   toggleSelectAll() { | ||||||
|     console.log('Select') |     console.log('Select') | ||||||
| @@ -229,7 +225,14 @@ watchEffect(() => { | |||||||
|     focusBreadcrumb() |     focusBreadcrumb() | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
| const mkdir = (doc: Document, name: string) => { | let nowkey = ref(0) | ||||||
|  | let modifiedTimer: any = null | ||||||
|  | const updateModified = () => { | ||||||
|  |   nowkey.value = Math.floor(Date.now() / 1000) | ||||||
|  | } | ||||||
|  | onMounted(() => { updateModified(); modifiedTimer = setInterval(updateModified, 1000) }) | ||||||
|  | onUnmounted(() => { clearInterval(modifiedTimer) }) | ||||||
|  | const mkdir = (doc: Doc, name: string) => { | ||||||
|   const control = connect(controlUrl, { |   const control = connect(controlUrl, { | ||||||
|     open() { |     open() { | ||||||
|       control.send( |       control.send( | ||||||
| @@ -250,7 +253,9 @@ const mkdir = (doc: Document, name: string) => { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
|   doc.name = name // We should get an update from watch but this is quicker |   // We should get an update from watch but this is quicker | ||||||
|  |   doc.name = name | ||||||
|  |   doc.key = crypto.randomUUID() | ||||||
| } | } | ||||||
|  |  | ||||||
| // Column sort | // Column sort | ||||||
| @@ -259,11 +264,11 @@ const toggleSort = (name: string) => { | |||||||
| } | } | ||||||
| const sort = ref<string>('') | const sort = ref<string>('') | ||||||
| const sortCompare = { | const sortCompare = { | ||||||
|   name: (a: Document, b: Document) => collator.compare(a.name, b.name), |   name: (a: Doc, b: Doc) => collator.compare(a.name, b.name), | ||||||
|   modified: (a: Document, b: Document) => b.mtime - a.mtime, |   modified: (a: Doc, b: Doc) => b.mtime - a.mtime, | ||||||
|   size: (a: Document, b: Document) => b.size - a.size |   size: (a: Doc, b: Doc) => b.size - a.size | ||||||
| } | } | ||||||
| const sorted = (documents: Document[]) => { | const sorted = (documents: Doc[]) => { | ||||||
|   const cmp = sortCompare[sort.value as keyof typeof sortCompare] |   const cmp = sortCompare[sort.value as keyof typeof sortCompare] | ||||||
|   const sorted = [...documents] |   const sorted = [...documents] | ||||||
|   if (cmp) sorted.sort(cmp) |   if (cmp) sorted.sort(cmp) | ||||||
| @@ -273,7 +278,7 @@ const selectionIndeterminate = computed({ | |||||||
|   get: () => { |   get: () => { | ||||||
|     return ( |     return ( | ||||||
|       props.documents.length > 0 && |       props.documents.length > 0 && | ||||||
|       props.documents.some((doc: Document) => documentStore.selected.has(doc.key)) && |       props.documents.some((doc: Doc) => documentStore.selected.has(doc.key)) && | ||||||
|       !allSelected.value |       !allSelected.value | ||||||
|     ) |     ) | ||||||
|   }, |   }, | ||||||
| @@ -284,7 +289,7 @@ const allSelected = computed({ | |||||||
|   get: () => { |   get: () => { | ||||||
|     return ( |     return ( | ||||||
|       props.documents.length > 0 && |       props.documents.length > 0 && | ||||||
|       props.documents.every((doc: Document) => documentStore.selected.has(doc.key)) |       props.documents.every((doc: Doc) => documentStore.selected.has(doc.key)) | ||||||
|     ) |     ) | ||||||
|   }, |   }, | ||||||
|   set: (value: boolean) => { |   set: (value: boolean) => { | ||||||
| @@ -301,7 +306,7 @@ const allSelected = computed({ | |||||||
|  |  | ||||||
| const loc = computed(() => props.path.join('/')) | const loc = computed(() => props.path.join('/')) | ||||||
|  |  | ||||||
| const contextMenu = (ev: Event, doc: Document) => { | const contextMenu = (ev: Event, doc: Doc) => { | ||||||
|   cursor.value = doc |   cursor.value = doc | ||||||
|   console.log('Context menu', ev, doc) |   console.log('Context menu', ev, doc) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { Document } from '@/repositories/Document' | import { Doc } from '@/repositories/Document' | ||||||
| import { computed } from 'vue' | import { computed } from 'vue' | ||||||
|  |  | ||||||
| const datetime = computed(() => | const datetime = computed(() => | ||||||
| @@ -17,6 +17,6 @@ const tooltip = computed(() => | |||||||
| ) | ) | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|     doc: Document |     doc: Doc | ||||||
| }>() | }>() | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { Document } from '@/repositories/Document' | import { Doc } from '@/repositories/Document' | ||||||
| import { ref, onMounted, nextTick } from 'vue' | import { ref, onMounted, nextTick } from 'vue' | ||||||
|  |  | ||||||
| const input = ref<HTMLInputElement | null>(null) | const input = ref<HTMLInputElement | null>(null) | ||||||
| @@ -28,8 +28,8 @@ onMounted(() => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|   doc: Document |   doc: Doc | ||||||
|   rename: (doc: Document, newName: string) => void |   rename: (doc: Doc, newName: string) => void | ||||||
|   exit: () => void |   exit: () => void | ||||||
| }>() | }>() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { Document } from '@/repositories/Document' | import { Doc } from '@/repositories/Document' | ||||||
| import { computed } from 'vue' | import { computed } from 'vue' | ||||||
|  |  | ||||||
| const sizeClass = computed(() => { | const sizeClass = computed(() => { | ||||||
| @@ -12,7 +12,7 @@ const sizeClass = computed(() => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|     doc: Document |     doc: Doc | ||||||
| }>() | }>() | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,13 +16,51 @@ type CloudFile = { | |||||||
|   cloudName: string |   cloudName: string | ||||||
|   cloudPos: number |   cloudPos: number | ||||||
| } | } | ||||||
|  | function pasteHandler(event: ClipboardEvent) { | ||||||
|  |   const items = Array.from(event.clipboardData?.items ?? []) | ||||||
|  |   const infiles = [] as File[] | ||||||
|  |   const dirs = [] as FileSystemDirectoryEntry[] | ||||||
|  |   for (const item of items) { | ||||||
|  |     if (item.kind !== 'file') continue | ||||||
|  |     const entry = item.webkitGetAsEntry() | ||||||
|  |     if (entry?.isFile) { | ||||||
|  |       const file = item.getAsFile() | ||||||
|  |       if (file) infiles.push(file) | ||||||
|  |     } else if (entry?.isDirectory) { | ||||||
|  |       dirs.push(entry as FileSystemDirectoryEntry) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (infiles.length || dirs.length) { | ||||||
|  |     event.preventDefault() | ||||||
|  |     uploadFiles(infiles) | ||||||
|  |     for (const entry of dirs) pasteDirectory(entry, `${props.path!.join('/')}/${entry.name}`) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | const pasteDirectory = async (entry: FileSystemDirectoryEntry, loc: string) => { | ||||||
|  |   const reader = entry.createReader() | ||||||
|  |   const entries = await new Promise<any[]>(resolve => reader.readEntries(resolve)) | ||||||
|  |   const cloudfiles = [] as CloudFile[] | ||||||
|  |   for (const entry of entries) { | ||||||
|  |     const cloudName = `${loc}/${entry.name}` | ||||||
|  |     if (entry.isFile) { | ||||||
|  |       const file = await new Promise(resolve => entry.file(resolve)) as File | ||||||
|  |       cloudfiles.push({file, cloudName, cloudPos: 0}) | ||||||
|  |     } else if (entry.isDirectory) { | ||||||
|  |       await pasteDirectory(entry, cloudName) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (cloudfiles.length) uploadCloudFiles(cloudfiles) | ||||||
|  | } | ||||||
| function uploadHandler(event: Event) { | function uploadHandler(event: Event) { | ||||||
|   event.preventDefault() |   event.preventDefault() | ||||||
|   event.stopPropagation() |  | ||||||
|   // @ts-ignore |   // @ts-ignore | ||||||
|   const infiles = Array.from(event.dataTransfer?.files || event.target.files) as File[] |   const input = event.target as HTMLInputElement | null | ||||||
|   if (!infiles.length) return |   const infiles = Array.from((input ?? (event as DragEvent).dataTransfer)?.files ?? []) as File[] | ||||||
|  |   if (input) input.value = '' | ||||||
|  |   if (infiles.length) uploadFiles(infiles) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const uploadFiles = (infiles: File[]) => { | ||||||
|   const loc = props.path!.join('/') |   const loc = props.path!.join('/') | ||||||
|   let files = [] |   let files = [] | ||||||
|   for (const file of infiles) { |   for (const file of infiles) { | ||||||
| @@ -32,6 +70,9 @@ function uploadHandler(event: Event) { | |||||||
|       cloudPos: 0, |       cloudPos: 0, | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |   uploadCloudFiles(files) | ||||||
|  | } | ||||||
|  | const uploadCloudFiles = (files: CloudFile[]) => { | ||||||
|   const dotfiles = files.filter(f => f.cloudName.includes('/.')) |   const dotfiles = files.filter(f => f.cloudName.includes('/.')) | ||||||
|   if (dotfiles.length) { |   if (dotfiles.length) { | ||||||
|     documentStore.error = "Won't upload dotfiles" |     documentStore.error = "Won't upload dotfiles" | ||||||
| @@ -76,7 +117,7 @@ const speed = computed(() => { | |||||||
|   if (tsince > 1 / s) return 1 / tsince  // Next block is late or not coming, decay |   if (tsince > 1 / s) return 1 / tsince  // Next block is late or not coming, decay | ||||||
|   return s  // "Current speed" |   return s  // "Current speed" | ||||||
| }) | }) | ||||||
| const speeddisp = computed(() => speed.value ? speed.value.toFixed(speed.value < 100 ? 1 : 0) + '\u202FMB/s': 'stalled') | const speeddisp = computed(() => speed.value ? speed.value.toFixed(speed.value < 10 ? 1 : 0) + '\u202FMB/s': 'stalled') | ||||||
| setInterval(() => { | setInterval(() => { | ||||||
|   if (Date.now() - uprogress.tlast > 3000) { |   if (Date.now() - uprogress.tlast > 3000) { | ||||||
|     // Reset |     // Reset | ||||||
| @@ -165,10 +206,6 @@ const worker = async () => { | |||||||
|   const ws = await WSCreate() |   const ws = await WSCreate() | ||||||
|   while (upqueue.length) { |   while (upqueue.length) { | ||||||
|     const f = upqueue[0] |     const f = upqueue[0] | ||||||
|     if (f.cloudPos === f.file.size) { |  | ||||||
|       upqueue.shift() |  | ||||||
|       continue |  | ||||||
|     } |  | ||||||
|     const start = f.cloudPos |     const start = f.cloudPos | ||||||
|     const end = Math.min(f.file.size, start + (1<<20)) |     const end = Math.min(f.file.size, start + (1<<20)) | ||||||
|     const control = { name: f.cloudName, size: f.file.size, start, end } |     const control = { name: f.cloudName, size: f.file.size, start, end } | ||||||
| @@ -179,6 +216,7 @@ const worker = async () => { | |||||||
|     ws.sendMsg(control) |     ws.sendMsg(control) | ||||||
|     // @ts-ignore |     // @ts-ignore | ||||||
|     await ws.sendData(data) |     await ws.sendData(data) | ||||||
|  |     if (f.cloudPos === f.file.size) upqueue.shift() | ||||||
|   } |   } | ||||||
|   if (upqueue.length) startWorker() |   if (upqueue.length) startWorker() | ||||||
|   uprogress.status = "idle" |   uprogress.status = "idle" | ||||||
| @@ -196,8 +234,10 @@ onMounted(() => { | |||||||
|   // Need to prevent both to prevent browser from opening the file |   // Need to prevent both to prevent browser from opening the file | ||||||
|   addEventListener('dragover', uploadHandler) |   addEventListener('dragover', uploadHandler) | ||||||
|   addEventListener('drop', uploadHandler) |   addEventListener('drop', uploadHandler) | ||||||
|  |   addEventListener('paste', pasteHandler) | ||||||
| }) | }) | ||||||
| onUnmounted(() => { | onUnmounted(() => { | ||||||
|  |   removeEventListener('paste', pasteHandler) | ||||||
|   removeEventListener('dragover', uploadHandler) |   removeEventListener('dragover', uploadHandler) | ||||||
|   removeEventListener('drop', uploadHandler) |   removeEventListener('drop', uploadHandler) | ||||||
| }) | }) | ||||||
| @@ -219,7 +259,7 @@ onUnmounted(() => { | |||||||
|           {{ (uprogress.filepos / uprogress.filesize * 100).toFixed(0) + '\u202F%' }} |           {{ (uprogress.filepos / uprogress.filesize * 100).toFixed(0) + '\u202F%' }} | ||||||
|         </span> |         </span> | ||||||
|       </span> |       </span> | ||||||
|       <span class="position" v-if="uprogress.filesize > 1e7"> |       <span class="position" v-if="uprogress.total > 1e7"> | ||||||
|         {{ (uprogress.uploaded / 1e6).toFixed(0) + '\u202F/\u202F' + (uprogress.total / 1e6).toFixed(0) + '\u202FMB' }} |         {{ (uprogress.uploaded / 1e6).toFixed(0) + '\u202F/\u202F' + (uprogress.total / 1e6).toFixed(0) + '\u202FMB' }} | ||||||
|       </span> |       </span> | ||||||
|       <span class="speed">{{ speeddisp }}</span> |       <span class="speed">{{ speeddisp }}</span> | ||||||
|   | |||||||
| @@ -1,17 +1,34 @@ | |||||||
|  | import { formatSize, formatUnixDate, haystackFormat } from "@/utils" | ||||||
|  |  | ||||||
| export type FUID = string | export type FUID = string | ||||||
|  |  | ||||||
| export type Document = { | export type DocProps = { | ||||||
|   loc: string |   loc: string | ||||||
|   name: string |   name: string | ||||||
|   key: FUID |   key: FUID | ||||||
|   size: number |   size: number | ||||||
|   sizedisp: string |  | ||||||
|   mtime: number |   mtime: number | ||||||
|   modified: string |  | ||||||
|   haystack: string |  | ||||||
|   dir: boolean |   dir: boolean | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export class Doc { | ||||||
|  |   private _name: string = "" | ||||||
|  |   public loc: string = "" | ||||||
|  |   public key: FUID = "" | ||||||
|  |   public size: number = 0 | ||||||
|  |   public mtime: number = 0 | ||||||
|  |   public haystack: string = "" | ||||||
|  |   public dir: boolean = false | ||||||
|  |  | ||||||
|  |   constructor(props: Partial<DocProps> = {}) { Object.assign(this, props) } | ||||||
|  |   get name() { return this._name } | ||||||
|  |   set name(name: string) { | ||||||
|  |     this._name = name | ||||||
|  |     this.haystack = haystackFormat(name) | ||||||
|  |   } | ||||||
|  |   get sizedisp(): string { return formatSize(this.size) } | ||||||
|  |   get modified(): string { return formatUnixDate(this.mtime) } | ||||||
|  | } | ||||||
| export type errorEvent = { | export type errorEvent = { | ||||||
|   error: { |   error: { | ||||||
|     code: number |     code: number | ||||||
| @@ -36,7 +53,7 @@ export type UpdateEntry = ['k', number] | ['d', number] | ['i', Array<FileEntry> | |||||||
| // Helper structure for selections | // Helper structure for selections | ||||||
| export interface SelectedItems { | export interface SelectedItems { | ||||||
|   keys: FUID[] |   keys: FUID[] | ||||||
|   docs: Record<FUID, Document> |   docs: Record<FUID, Doc> | ||||||
|   recursive: Array<[string, string, Document]> |   recursive: Array<[string, string, Doc]> | ||||||
|   missing: Set<FUID> |   missing: Set<FUID> | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,22 +6,26 @@ export const uploadUrl = '/api/upload' | |||||||
| export const watchUrl = '/api/watch' | export const watchUrl = '/api/watch' | ||||||
|  |  | ||||||
| let tree = [] as FileEntry[] | let tree = [] as FileEntry[] | ||||||
| let reconnectDuration = 500 | let reconnDelay = 500 | ||||||
| let wsWatch = null as WebSocket | null | let wsWatch = null as WebSocket | null | ||||||
|  |  | ||||||
| export const loadSession = () => { | export const loadSession = () => { | ||||||
|  |   const s = localStorage['cista-files'] | ||||||
|  |   if (!s) return false | ||||||
|   const store = useDocumentStore() |   const store = useDocumentStore() | ||||||
|   try { |   try { | ||||||
|     tree = JSON.parse(sessionStorage["cista-files"]) |     tree = JSON.parse(s) | ||||||
|     store.updateRoot(tree) |     store.updateRoot(tree) | ||||||
|  |     console.log(`Loaded session with ${tree.length} items cached`) | ||||||
|     return true |     return true | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|  |     console.log("Loading session failed", error) | ||||||
|     return false |     return false | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| const saveSession = () => { | const saveSession = () => { | ||||||
|   sessionStorage["cista-files"] = JSON.stringify(tree) |   localStorage["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>>) => { | ||||||
| @@ -59,7 +63,7 @@ export const watchConnect = () => { | |||||||
|       console.log('Connected to backend', msg) |       console.log('Connected to backend', msg) | ||||||
|       store.server = msg.server |       store.server = msg.server | ||||||
|       store.connected = true |       store.connected = true | ||||||
|       reconnectDuration = 500 |       reconnDelay = 500 | ||||||
|       store.error = '' |       store.error = '' | ||||||
|       if (msg.user) store.login(msg.user.username, msg.user.privileged) |       if (msg.user) store.login(msg.user.username, msg.user.privileged) | ||||||
|       else if (store.isUserLogged) store.logout() |       else if (store.isUserLogged) store.logout() | ||||||
| @@ -83,10 +87,10 @@ const watchReconnect = (event: MessageEvent) => { | |||||||
|     store.connected = false |     store.connected = false | ||||||
|     store.error = 'Reconnecting...' |     store.error = 'Reconnecting...' | ||||||
|   } |   } | ||||||
|   reconnectDuration = Math.min(5000, reconnectDuration + 500) |   reconnDelay = Math.min(5000, reconnDelay + 500) | ||||||
|   // The server closes the websocket after errors, so we need to reopen it |   // The server closes the websocket after errors, so we need to reopen it | ||||||
|   if (watchTimeout !== null) clearTimeout(watchTimeout) |   if (watchTimeout !== null) clearTimeout(watchTimeout) | ||||||
|   watchTimeout = setTimeout(watchConnect, reconnectDuration) |   watchTimeout = setTimeout(watchConnect, reconnDelay) | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,14 +1,11 @@ | |||||||
| import type { Document, FileEntry, FUID, SelectedItems } from '@/repositories/Document' | import type { FileEntry, FUID, SelectedItems } from '@/repositories/Document' | ||||||
| import { formatSize, formatUnixDate, haystackFormat } from '@/utils' | import { Doc } from '@/repositories/Document' | ||||||
| import { defineStore } from 'pinia' | import { defineStore } from 'pinia' | ||||||
| import { collator } from '@/utils' | import { collator } from '@/utils' | ||||||
| import { logoutUser } from '@/repositories/User' | import { logoutUser } from '@/repositories/User' | ||||||
| import { watchConnect } from '@/repositories/WS' | import { watchConnect } from '@/repositories/WS' | ||||||
|  | import { shallowRef } from 'vue' | ||||||
|  |  | ||||||
| type FileData = { id: string; mtime: number; size: number; dir: DirectoryData } |  | ||||||
| type DirectoryData = { |  | ||||||
|   [filename: string]: FileData |  | ||||||
| } |  | ||||||
| type User = { | type User = { | ||||||
|   username: string |   username: string | ||||||
|   privileged: boolean |   privileged: boolean | ||||||
| @@ -19,7 +16,7 @@ type User = { | |||||||
| export const useDocumentStore = defineStore({ | export const useDocumentStore = defineStore({ | ||||||
|   id: 'documents', |   id: 'documents', | ||||||
|   state: () => ({ |   state: () => ({ | ||||||
|     document: [] as Document[], |     document: shallowRef<Doc[]>([]), | ||||||
|     selected: new Set<FUID>(), |     selected: new Set<FUID>(), | ||||||
|     fileExplorer: null as any, |     fileExplorer: null as any, | ||||||
|     error: '' as string, |     error: '' as string, | ||||||
| @@ -38,23 +35,17 @@ export const useDocumentStore = defineStore({ | |||||||
|       let loc = [] as string[] |       let loc = [] as string[] | ||||||
|       for (const [level, name, key, mtime, size, isfile] of root) { |       for (const [level, name, key, mtime, size, isfile] of root) { | ||||||
|         loc = loc.slice(0, level - 1) |         loc = loc.slice(0, level - 1) | ||||||
|         docs.push({ |         docs.push(new Doc({ | ||||||
|           name, |           name, | ||||||
|           loc: level ? loc.join('/') : '/', |           loc: level ? loc.join('/') : '/', | ||||||
|           key, |           key, | ||||||
|           size, |           size, | ||||||
|           sizedisp: formatSize(size), |  | ||||||
|           mtime, |           mtime, | ||||||
|           modified: formatUnixDate(mtime), |  | ||||||
|           haystack: haystackFormat(name), |  | ||||||
|           dir: !isfile, |           dir: !isfile, | ||||||
|         }) |         })) | ||||||
|         loc.push(name) |         loc.push(name) | ||||||
|       } |       } | ||||||
|       this.document = docs as Document[] |       this.document = docs | ||||||
|     }, |  | ||||||
|     updateModified() { |  | ||||||
|       for (const doc of this.document) doc.modified = formatUnixDate(doc.mtime) |  | ||||||
|     }, |     }, | ||||||
|     login(username: string, privileged: boolean) { |     login(username: string, privileged: boolean) { | ||||||
|       this.user.username = username |       this.user.username = username | ||||||
| @@ -70,6 +61,7 @@ export const useDocumentStore = defineStore({ | |||||||
|       console.log("Logout") |       console.log("Logout") | ||||||
|       await logoutUser() |       await logoutUser() | ||||||
|       this.$reset() |       this.$reset() | ||||||
|  |       localStorage.clear() | ||||||
|       history.go() // Reload page |       history.go() // Reload page | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
| @@ -77,12 +69,12 @@ export const useDocumentStore = defineStore({ | |||||||
|     isUserLogged(): boolean { |     isUserLogged(): boolean { | ||||||
|       return this.user.isLoggedIn |       return this.user.isLoggedIn | ||||||
|     }, |     }, | ||||||
|     recentDocuments(): Document[] { |     recentDocuments(): Doc[] { | ||||||
|       const ret = [...this.document] |       const ret = [...this.document] | ||||||
|       ret.sort((a, b) => b.mtime - a.mtime) |       ret.sort((a, b) => b.mtime - a.mtime) | ||||||
|       return ret |       return ret | ||||||
|     }, |     }, | ||||||
|     largeDocuments(): Document[] { |     largeDocuments(): Doc[] { | ||||||
|       const ret = [...this.document] |       const ret = [...this.document] | ||||||
|       ret.sort((a, b) => b.size - a.size) |       ret.sort((a, b) => b.size - a.size) | ||||||
|       return ret |       return ret | ||||||
| @@ -107,7 +99,7 @@ export const useDocumentStore = defineStore({ | |||||||
|       for (const key of selected) if (!found.has(key)) ret.missing.add(key) |       for (const key of selected) if (!found.has(key)) ret.missing.add(key) | ||||||
|       // Build a flat list including contents recursively |       // Build a flat list including contents recursively | ||||||
|       const relnames = new Set<string>() |       const relnames = new Set<string>() | ||||||
|       function add(rel: string, full: string, doc: Document) { |       function add(rel: string, full: string, doc: Doc) { | ||||||
|         if (!doc.dir && relnames.has(rel)) throw Error(`Multiple selections conflict for: ${rel}`) |         if (!doc.dir && relnames.has(rel)) throw Error(`Multiple selections conflict for: ${rel}`) | ||||||
|         relnames.add(rel) |         relnames.add(rel) | ||||||
|         ret.recursive.push([rel, full, doc]) |         ret.recursive.push([rel, full, doc]) | ||||||
|   | |||||||
| @@ -86,7 +86,7 @@ export function haystackFormat(str: string) { | |||||||
| // Preformat search string for faster search | // Preformat search string for faster search | ||||||
| export function needleFormat(query: string) { | export function needleFormat(query: string) { | ||||||
|   const based = query.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase() |   const based = query.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase() | ||||||
|   return {based, words: based.split(/\W+/)} |   return {based, words: based.split(/\s+/)} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Test if haystack includes needle | // Test if haystack includes needle | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user