From 9d010f64ee2729a510a84fe28592d4d54fa988f9 Mon Sep 17 00:00:00 2001 From: nekosu Date: Wed, 9 Aug 2023 23:02:18 +0800 Subject: [PATCH] refactor: clean old fs --- src/App.vue | 9 +- src/components/TaskEdit.vue | 18 +- src/components/TaskTree.vue | 5 +- src/components/TaskTreeRender.tsx | 62 +++-- src/components/atomic/ImageHover.vue | 10 +- src/data.ts | 124 ---------- src/data/fs.ts | 64 ----- src/{ => data}/history.ts | 0 src/data/index.ts | 16 ++ src/data/task.ts | 202 ++++++++++------ src/filesystem/path.ts | 6 +- src/filesystem/tree.ts | 20 +- src/fs.ts | 342 --------------------------- src/loader.ts | 16 +- 14 files changed, 223 insertions(+), 671 deletions(-) delete mode 100644 src/data.ts delete mode 100644 src/data/fs.ts rename src/{ => data}/history.ts (100%) delete mode 100644 src/fs.ts diff --git a/src/App.vue b/src/App.vue index 5cb40bb..adb37c9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,17 +11,15 @@ import { produce } from 'immer' import { NButton, NCard, NIcon } from 'naive-ui' import { onMounted, ref } from 'vue' -import { active } from './data' -import { getTask, setTask } from './data/task' -import { history } from './history' import { loadFS, saveFS } from './loader' -import { fs } from '@/filesystem' +import { active, getTask, history, setTask } from '@/data' +import { type PathKey, fs } from '@/filesystem' import TaskEdit from '@/components/TaskEdit.vue' import TaskTree from '@/components/TaskTree.vue' -const expands = ref(['/']) +const expands = ref(['/' as PathKey]) onMounted(async () => { await loadFS() @@ -119,3 +117,4 @@ onMounted(async () => { +./data/history diff --git a/src/components/TaskEdit.vue b/src/components/TaskEdit.vue index 9419f13..86a4aae 100644 --- a/src/components/TaskEdit.vue +++ b/src/components/TaskEdit.vue @@ -19,9 +19,9 @@ import { } from 'naive-ui' import { computed, ref } from 'vue' -import { commitDelete, commitDuplicate, commitMove, navigate } from '@/data' +import { deleteTask, duplicateTask, moveTask, navigate } from '@/data' import { taskBackwardIndex, taskIndex } from '@/data/task' -import { Util } from '@/fs' +import { type PathKey, path } from '@/filesystem' import type { UseProducer } from '@/persis' import type { Task } from '@/types' @@ -35,13 +35,13 @@ import SingleNavigateEdit from '@/components/task/SingleNavigateEdit.vue' import FormLayout from '@/layout/FormLayout.vue' const props = defineProps<{ - name: string + name: PathKey value: Task edit: UseProducer }>() const hash = computed(() => { - const [, , hash] = Util.pathdiv(props.name) + const [, , hash] = path.divide(props.name) return hash! }) @@ -58,14 +58,14 @@ function tryRename() { return } showRename.value = false - const [dir, file] = Util.pathdiv(props.name) - const into = Util.pathjoin(dir, file, titleCache.value) - commitMove(props.name, into) + const [dir, file] = path.divide(props.name) + const into = path.joinkey(dir, file, titleCache.value) + moveTask(props.name, into) navigate(into) } function tryDuplicate() { - commitDuplicate(props.name) + duplicateTask(props.name) } const showDelete = ref(false) @@ -84,7 +84,7 @@ function tryDelete() { return } showDelete.value = false - commitDelete( + deleteTask( props.name, doTransfer.value ? taskIndex.value[transferTo.value] : null ) diff --git a/src/components/TaskTree.vue b/src/components/TaskTree.vue index 7a406c9..7c97c51 100644 --- a/src/components/TaskTree.vue +++ b/src/components/TaskTree.vue @@ -5,11 +5,10 @@ import { computed, ref } from 'vue' import { renderLabel, renderPrefix, renderSuffix } from './TaskTreeRender' -import { active, navigate } from '@/data' -import { filesystemTree } from '@/data/index' +import { active, filesystemTree, navigate } from '@/data' import { type PathKey, path } from '@/filesystem' -const expand = defineModel('expand', { +const expand = defineModel('expand', { required: true }) diff --git a/src/components/TaskTreeRender.tsx b/src/components/TaskTreeRender.tsx index 622f2a2..b982c3b 100644 --- a/src/components/TaskTreeRender.tsx +++ b/src/components/TaskTreeRender.tsx @@ -17,16 +17,14 @@ import { } from 'naive-ui' import { computed, ref } from 'vue' -import { commitDelete } from '@/data' -import { fs } from '@/data/fs' -import { setTask, taskIndex } from '@/data/task' -import { Util } from '@/fs' +import { deleteTask, setTask, taskIndex } from '@/data' +import { type PathKey, fs, path } from '@/filesystem' export function renderLabel({ option }: { option: TreeOption }) { - const key = option.key as string + const key = option.key as PathKey - if (!key.endsWith('/')) { - const [dir, file, hash] = Util.pathdiv(key) + if (!path.key_is_dir(key)) { + const [dir, file, hash] = path.divide(key) if (hash) { return {hash} } else { @@ -42,7 +40,7 @@ export function renderLabel({ option }: { option: TreeOption }) { } export function renderPrefix({ option }: { option: TreeOption }) { - const key = option.key as string + const key = option.key as PathKey if (key.endsWith('/')) { return ( @@ -51,7 +49,7 @@ export function renderPrefix({ option }: { option: TreeOption }) { ) } else { - const [dir, file, hash] = Util.pathdiv(key) + const [dir, file, hash] = path.divide(key) if (hash) { return ( @@ -77,8 +75,8 @@ export function renderPrefix({ option }: { option: TreeOption }) { export function renderSuffix({ option }: { option: TreeOption }) { const dialog = useDialog() - const key = option.key as string - if (key.endsWith('/')) { + const key = option.key as PathKey + if (path.key_is_dir(key)) { return (
name.value.endsWith('.json') ? name.value : `${name.value}.json` ) - const path = computed(() => Util.pathjoin(key, nameWithSfx.value)) + const dir = fs.tree.traceDir(fs.tree.root, key) const pathExists = computed(() => { - return !!fs.now().value?.getFile(path.value) + return !!fs.tree.traceFile(dir, nameWithSfx.value) }) const dlg = dialog.create({ title: '创建json', @@ -108,13 +106,7 @@ export function renderSuffix({ option }: { option: TreeOption }) { disabled={!name.value || pathExists.value} onClick={() => { if (!pathExists.value) { - fs.change(draft => { - draft?.addTextFileViaEntry( - draft.trace(key)!, - nameWithSfx.value, - '{}' - ) - }) + fs.tree.traceFile(dir, nameWithSfx.value, '{}') dlg.destroy() } }} @@ -139,10 +131,11 @@ export function renderSuffix({ option }: { option: TreeOption }) { e.stopPropagation() const name = ref('') - const path = computed(() => Util.pathjoin(key, name.value)) + const p = computed(() => path.join(key, name.value)) const pathExists = computed(() => { - return !!fs.now().value?.trace(path.value) + return !!fs.tree.traceDir(fs.tree.root, p.value) }) + const dlg = dialog.create({ title: '创建目录', content: () => ( @@ -157,9 +150,7 @@ export function renderSuffix({ option }: { option: TreeOption }) { disabled={pathExists.value} onClick={() => { if (!pathExists.value) { - fs.change(draft => { - draft?.trace(path.value, true) - }) + fs.tree.traceDir(fs.tree.root, p.value, true) dlg.destroy() } }} @@ -181,7 +172,7 @@ export function renderSuffix({ option }: { option: TreeOption }) {
) } else { - const [dir, file, hash] = Util.pathdiv(key) + const [dir, file, hash] = path.divide(key) if (!hash) { const isJson = file.endsWith('.json') return ( @@ -197,7 +188,7 @@ export function renderSuffix({ option }: { option: TreeOption }) { if (name in taskIndex.value) { continue } - setTask(Util.pathjoin(dir, file, name), {}) + setTask(path.joinkey(dir, file, name), {}) break } }} @@ -245,21 +236,22 @@ export function renderSuffix({ option }: { option: TreeOption }) { }, positiveText: '是', onPositiveClick: () => { - const path = Util.pathjoin(dir, file) + const p = path.joinkey(dir, file) - fs.enterBlock() + fs.history.pause() + const d = fs.tree.traceDir(fs.tree.root, dir) const obj = JSON.parse( - fs.now().value?.getFile(path)?.data ?? '{}' + fs.tree.traceFile(d, file)?.value ?? '{}' ) for (const name in obj) { - commitDelete(taskIndex.value[name], null) + deleteTask(taskIndex.value[name], null) } - fs.change(draft => { - draft!.removeFile(path) - }) - fs.leaveBlock() + fs.tree.delFile(d, file) + + fs.history.resume() + fs.history.commit() } }) }} diff --git a/src/components/atomic/ImageHover.vue b/src/components/atomic/ImageHover.vue index 654d5a4..fbf8fdc 100644 --- a/src/components/atomic/ImageHover.vue +++ b/src/components/atomic/ImageHover.vue @@ -2,8 +2,7 @@ import { NAvatar, NPopover } from 'naive-ui' import { computed } from 'vue' -import { fs } from '@/data/fs' -import { FS } from '@/fs' +import { type Path, fs, path, pool } from '@/filesystem' const props = defineProps<{ url: string | string[] @@ -12,9 +11,12 @@ const props = defineProps<{ function makeUrl(v: string | null) { const fallback = '/favicon-32x32.png' if (v && v.endsWith('.png')) { - const hash = fs.now().value?.getFile(v)?.ref + // TODO: maybe check? + const [dir, file] = path.divide(v as Path) + const hash = fs.tree.traceBinary(fs.tree.traceDir(fs.tree.root, dir), file) + ?.value if (hash) { - const url = FS.getBufferUrl(hash) + const url = pool.query(hash) if (url) { return url } diff --git a/src/data.ts b/src/data.ts deleted file mode 100644 index c99a18f..0000000 --- a/src/data.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { produce } from 'immer' -import { computed } from 'vue' - -import { fs } from './data/fs' -import { delTask, getTask, setTask, taskIndex } from './data/task' -import type { PathKey } from './filesystem' -import { Util } from './fs' -import { history } from './history' - -export const active = computed(() => { - return history.cur.value -}) - -export function navigate(task: string) { - history.push(task as PathKey) -} - -function performRename( - task: Record, - key: string, - from: string, - to: string | null -) { - if (!(key in task)) { - return - } - const val = task[key] - if (typeof val === 'string') { - if (val === from) { - if (to) { - task[key] = to - } else { - delete task[key] - } - return - } - } else if ( - val instanceof Array && - val.length > 0 && - typeof val[0] === 'string' - ) { - if (to) { - task[key] = val.map(x => { - if (x === from) { - return to - } else { - return x - } - }) - } else { - task[key] = val.filter(x => x !== from) - } - } -} - -export function commitMove(from: string, to: string) { - const keys = ['target', 'begin', 'end', 'next', 'timeout_next', 'runout_next'] - - const [fd, ff, fh] = Util.pathdiv(from) - const [td, tf, th] = Util.pathdiv(to) - - fs.enterBlock() - - for (const name in taskIndex.value) { - const task = getTask(taskIndex.value[name]) - if (!task) { - continue - } - const ntask = produce(task, dt => { - for (const key of keys) { - performRename(dt, key, fh!, th) - } - }) - if (name === fh) { - delTask(from) - setTask(to, ntask) - } else { - setTask(taskIndex.value[name], ntask) - } - } - - fs.leaveBlock() -} - -export function commitDuplicate(name: string) { - const [dir, file, hash] = Util.pathdiv(name) - const task = produce(getTask(name)!, draft => draft) - for (let i = 1; ; i++) { - const name2 = `${hash}${i}` - if (!(name2 in taskIndex.value)) { - setTask(Util.pathjoin(dir, file, name2), task) - return - } - } -} - -export function commitDelete(from: string, to: string | null) { - const keys = ['target', 'begin', 'end', 'next', 'timeout_next', 'runout_next'] - - const [fd, ff, fh] = Util.pathdiv(from) - const th = to ? Util.pathdiv(to)[2] : null - - fs.enterBlock() - - for (const name in taskIndex.value) { - const task = getTask(taskIndex.value[name]) - if (!task) { - continue - } - const ntask = produce(task, dt => { - for (const key of keys) { - performRename(dt, key, fh!, th) - } - }) - // console.log(name, ntask) - if (name === fh) { - delTask(from) - } else { - setTask(taskIndex.value[name], ntask) - } - } - - fs.leaveBlock() -} diff --git a/src/data/fs.ts b/src/data/fs.ts deleted file mode 100644 index 3d2f5bd..0000000 --- a/src/data/fs.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { TreeOption } from 'naive-ui' -import { computed } from 'vue' - -import { - type DirEntry, - type FS, - type FileEntry, - type PathSegment, - Util -} from '@/fs' -import { Persis } from '@/persis' -import { type TaskData } from '@/types' - -export const fs = new Persis(null) - -function buildFSTree(): TreeOption | null { - const f = fs.now() - if (f.value === null) { - return null - } - - const buildFileEntry = (sg: PathSegment, e: FileEntry): TreeOption => { - const curSeg = [...sg, e.name] - if (e.name.endsWith('.json')) { - const key = Util.fileseg2key(curSeg) - const obj = JSON.parse(e.data!) as TaskData - return { - key, - label: e.name, - children: Object.keys(obj) - .map(name => ({ - key: Util.pathjoin(sg, e.name, name!), - label: name! - })) - .sort((a, b) => a.key.localeCompare(b.key)) - } - } else { - return { - key: Util.fileseg2key(curSeg), - label: e.name - } - } - } - - const buildDirEntry = (sg: PathSegment, e: DirEntry): TreeOption => { - const curSeg = [...sg, e.name].filter(x => x) - return { - key: Util.dirseg2key(curSeg), - label: e.name, - children: [ - ...e.dir.map(se => { - return buildDirEntry(curSeg, se) - }), - ...e.file.map(se => { - return buildFileEntry(curSeg, se) - }) - ] - } - } - - return buildDirEntry([], f.value.root) -} - -export const fsTree = computed(buildFSTree) diff --git a/src/history.ts b/src/data/history.ts similarity index 100% rename from src/history.ts rename to src/data/history.ts diff --git a/src/data/index.ts b/src/data/index.ts index b0899e4..2904e7c 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1 +1,17 @@ +import { computed } from 'vue' + +import { history } from '@/data/history' +import type { PathKey } from '@/filesystem' + export * from './filesystem' +export * from './history' +export * from './image' +export * from './task' + +export const active = computed(() => { + return history.cur.value +}) + +export function navigate(task: PathKey) { + history.push(task) +} diff --git a/src/data/task.ts b/src/data/task.ts index a126469..88ddc9d 100644 --- a/src/data/task.ts +++ b/src/data/task.ts @@ -1,9 +1,7 @@ import { computed } from 'vue' -import { fs as oldfs } from '@/data/fs' import { fs, path } from '@/filesystem' import type { Path, PathKey } from '@/filesystem' -import { Util } from '@/fs' import type { Task, TaskData } from '@/types' export const taskIndex = computed(() => { @@ -54,90 +52,162 @@ export const taskBackwardIndex = computed(() => { return res }) -export function setTask(path: string | null, v: Task) { - if (!path) { +export function setTask(p: PathKey | null, v: Task) { + if (!p) { return } - const [dir, file, hash] = Util.pathdiv(path) + const [dir, file, hash] = path.divide(p) if (!hash) { return } - oldfs.change(draft => { - if (!draft) { - return - } - const de = draft.trace(dir) - if (!de) { - return - } - const json = draft.getFileViaEntry(de, file)?.[1]?.data - if (!json) { - return - } - const obj = JSON.parse(json) as TaskData - obj[hash] = v - draft.addTextFileViaEntry( - draft.trace(dir)!, - file, - JSON.stringify(obj, null, 4), - true - ) - }) + const f = fs.tree.traceFile(fs.tree.traceDir(fs.tree.root, dir), file) + if (!f) { + return + } + const obj = JSON.parse(f.value) as TaskData + obj[hash] = v + f.value = JSON.stringify(obj, null, 4) +} + +export function delTask(p: PathKey | null) { + if (!p) { + return + } + const [dir, file, hash] = path.divide(p) + if (!hash) { + return + } + const f = fs.tree.traceFile(fs.tree.traceDir(fs.tree.root, dir), file) + if (!f) { + return + } + const obj = JSON.parse(f.value) as TaskData + if (hash in obj) { + delete obj[hash] + } + f.value = JSON.stringify(obj, null, 4) } -export function delTask(path: string | null) { - if (!path) { +export function getTask(p: PathKey | null) { + if (!p) { return } - const [dir, file, hash] = Util.pathdiv(path) + const [dir, file, hash] = path.divide(p) if (!hash) { return } - oldfs.change(draft => { - if (!draft) { + const f = fs.tree.traceFile(fs.tree.traceDir(fs.tree.root, dir), file) + if (!f) { + return + } + const obj = JSON.parse(f.value) as TaskData + return obj[hash] ?? null +} + +function performRename( + task: Record, + key: string, + from: string, + to: string | null +) { + if (!(key in task)) { + return + } + const val = task[key] + if (typeof val === 'string') { + if (val === from) { + if (to) { + task[key] = to + } else { + delete task[key] + } return } - const de = draft.trace(dir) - if (!de) { - return + } else if ( + val instanceof Array && + val.length > 0 && + typeof val[0] === 'string' + ) { + if (to) { + task[key] = val.map(x => { + if (x === from) { + return to + } else { + return x + } + }) + } else { + task[key] = val.filter(x => x !== from) } - const json = draft.getFileViaEntry(de, file)?.[1]?.data - if (!json) { - return + } +} + +export function moveTask(from: PathKey, to: PathKey) { + const keys = ['target', 'begin', 'end', 'next', 'timeout_next', 'runout_next'] + + const [fd, ff, fh] = path.divide(from) + const [td, tf, th] = path.divide(to) + + fs.history.pause() + + for (const name in taskIndex.value) { + const task = getTask(taskIndex.value[name]) + if (!task) { + continue + } + for (const key of keys) { + performRename(task, key, fh!, th) } - const obj = JSON.parse(json) as TaskData - if (hash in obj) { - delete obj[hash] + if (name === fh) { + delTask(from) + setTask(to, task) + } else { + setTask(taskIndex.value[name], task) } - draft.addTextFileViaEntry( - draft.trace(dir)!, - file, - JSON.stringify(obj, null, 4), - true - ) - }) + } + + fs.history.pause() + fs.history.commit() } -export function getTask(path: string | null) { - if (!path) { - return null - } - const [dir, file, hash] = Util.pathdiv(path) - if (!hash) { - return null - } - const f = oldfs.now().value - if (!f) { - return null +export function duplicateTask(name: PathKey) { + const [dir, file, hash] = path.divide(name) + const task = getTask(name) + if (!task) { + return } - const de = f.trace(dir) - if (!de) { - return null + for (let i = 1; ; i++) { + const name2 = `${hash}${i}` + if (!(name2 in taskIndex.value)) { + setTask(path.joinkey(dir, file, name2), task) + return + } } - const json = f.getFileViaEntry(de, file)?.[1]?.data - if (!json) { - return null +} + +export function deleteTask(from: PathKey, to: PathKey | null) { + const keys = ['target', 'begin', 'end', 'next', 'timeout_next', 'runout_next'] + + const [fd, ff, fh] = path.divide(from) + const th = to ? path.divide(to)[2] : null + + fs.history.pause() + + for (const name in taskIndex.value) { + if (name === fh) { + delTask(from) + } else { + const task = getTask(taskIndex.value[name]) + if (!task) { + continue + } + for (const key of keys) { + performRename(task, key, fh!, th) + } + setTask(taskIndex.value[name], task) + } } - const obj = JSON.parse(json) as TaskData - return obj[hash] ?? null + + fs.history.pause() + fs.history.commit() } diff --git a/src/filesystem/path.ts b/src/filesystem/path.ts index c4cf6e0..c44baa4 100644 --- a/src/filesystem/path.ts +++ b/src/filesystem/path.ts @@ -35,7 +35,7 @@ export function zip_to_path(zip: PathZip): Path { // cannot perform on root export function divide( - path: Path | PathKey + path: Path | PathKey | PathZip ): [PathSegments, string, string | null] { const seg = to_seg(path) const file = seg.pop()! @@ -48,7 +48,7 @@ export function divide( } export function join( - dir: PathSegments | Path, + dir: PathSegments | Path | PathKey, file: string | null ): PathSegments { if (typeof dir === 'string') { @@ -63,7 +63,7 @@ export function join( // only file export function joinkey( - dir: PathSegments | Path, + dir: PathSegments | Path | PathKey, file: string, hash?: string ): PathKey { diff --git a/src/filesystem/tree.ts b/src/filesystem/tree.ts index 53b7f28..bcfa6fc 100644 --- a/src/filesystem/tree.ts +++ b/src/filesystem/tree.ts @@ -1,7 +1,13 @@ import { type Ref, ref, toRef } from 'vue' import * as path from './path' -import type { DirEntry, FileContentRef, Path, PathSegments } from './types' +import type { + DirEntry, + FileContentRef, + Path, + PathKey, + PathSegments +} from './types' export function useTree() { const root: Ref = ref({ @@ -20,7 +26,7 @@ export function useTree() { function traceDir( root: Ref, - dir: Path | PathSegments, + dir: PathSegments | Path | PathKey, create = false ) { const segs = dir instanceof Array ? dir : path.to_seg(dir) @@ -45,10 +51,13 @@ export function useTree() { } function traceFile( - entry: Ref, + entry: Ref | null, name: string, create: string | null = null ) { + if (!entry) { + return null + } if (!(name in entry.value.file)) { if (create !== null) { entry.value.file[name] = create @@ -60,10 +69,13 @@ export function useTree() { } function traceBinary( - entry: Ref, + entry: Ref | null, name: string, create: FileContentRef | null = null ) { + if (!entry) { + return null + } if (!(name in entry.value.bin)) { if (create !== null) { entry.value.bin[name] = create diff --git a/src/fs.ts b/src/fs.ts deleted file mode 100644 index 517a0ea..0000000 --- a/src/fs.ts +++ /dev/null @@ -1,342 +0,0 @@ -import CryptoES from 'crypto-es' -import { immerable } from 'immer' -import JSZip from 'jszip' - -export type DirEntry = { - name: string - dir: DirEntry[] - file: FileEntry[] -} - -export type TextFileEntry = { - name: string - data: string - ref?: never -} - -export type BinFileEntry = { - name: string - data?: never - ref: string -} - -export type FileEntry = TextFileEntry | BinFileEntry - -export type PathSegment = string[] // ['abc', 'def', 'ghi'] -export type Path = string // /abc/def/ghi -/** - * file: /abc/def/ghi - * dir: /abc/def/ghi/ - * task: /abc/def/ghi#jkl - */ -export type PathKey = string -export type PathZip = string // abc/def/ghi - -export const Util = { - seg2path(seg: PathSegment): Path { - return `/${seg.filter(x => x).join('/')}` - }, - path2seg(path: Path): PathSegment { - return path.split('/').filter(x => x) - }, - zip2path(path: PathZip): Path { - return `/${path}`.replace(/\/+/g, '/') - }, - path2zip(path: Path): PathZip { - return path.replace(/\/+/g, '/').replace(/^\//, '') - }, - fileseg2key(seg: PathSegment): PathKey { - return Util.seg2path(seg) - }, - dirseg2key(seg: PathSegment): PathKey { - return (Util.seg2path(seg) + '/').replace(/\/+/g, '/') - }, - keyIsDir(key: PathKey) { - return key.endsWith('/') - }, - keyIsFile(key: PathKey) { - return !Util.keyIsDir(key) - }, - - pathdiv(path: Path): [Path, string, string | null] { - const seg = Util.path2seg(path) - if (seg.length > 0) { - const file = seg.pop()! - const fh = file.split('#') - if (fh.length === 2) { - return [Util.seg2path(seg), fh[0], fh[1]] - } else { - return [Util.seg2path(seg), file, null] - } - } else { - throw 'dividing root!' - } - }, - pathjoin(dir: PathSegment | Path, file: string, hash?: string): string { - if (typeof dir === 'string') { - dir = Util.path2seg(dir) - } - const seg = [...dir] - if (file !== '') { - seg.push(file) - } - return Util.seg2path(seg) + (hash ? `#${hash}` : '') - } -} - -// TODO: maybe change to full compare -function naiveCheck(a: ArrayBuffer, b: ArrayBuffer) { - if (a.byteLength !== b.byteLength) { - return false - } - const len = a.byteLength - if (len === 0) { - return true - } - const va = new Uint8Array(a) - const vb = new Uint8Array(b) - return va[0] === vb[0] && va[len - 1] === vb[len - 1] -} - -export class FS { - static pool: Record = {} - static objectUrlPool: Record = {}; - - [immerable] = true - root: DirEntry - - static addBuffer(data: ArrayBuffer): string { - const hash = CryptoES.SHA256(CryptoES.lib.WordArray.create(data)).toString( - CryptoES.enc.Base64 - ) - if (hash in FS.pool) { - const oldData = FS.pool[hash] - if (!naiveCheck(data, oldData)) { - throw 'SHA256 hash same for different data!' - } - } else { - FS.pool[hash] = data - } - - return hash - } - - static getBufferUrl(hash: string): string | null { - if (!(hash in FS.pool)) { - return null - } - if (hash in FS.objectUrlPool) { - return FS.objectUrlPool[hash] - } else { - const buf = FS.pool[hash] - const url = URL.createObjectURL( - new Blob([new Uint8Array(buf, 0, buf.byteLength)]) - ) - FS.objectUrlPool[hash] = url - return url - } - } - - static async fromZip(data: ArrayBuffer) { - const fs = new FS() - const zip = new JSZip() - await zip.loadAsync(data) - const pros: Promise[] = [] - zip.forEach((p, f) => { - pros.push( - (async () => { - if (f.dir) { - fs.trace(Util.zip2path(p), true) - } else { - const [dir, file] = Util.pathdiv(Util.zip2path(p)) - let de = fs.trace(dir) - if (!de) { - console.warn('found file', p, 'but dir not created before') - de = fs.trace(dir, true)! - } - if (file.endsWith('.json')) { - fs.addTextFileViaEntry(de, file, await f.async('string'), true) - } else { - fs.addBinFileViaEntry( - de, - file, - await f.async('arraybuffer'), - true - ) - } - } - })() - ) - }) - await Promise.all(pros) - return fs - } - - constructor() { - this.root = { - name: '', - dir: [], - file: [] - } - } - - async zip() { - const zip = new JSZip() - this.enumAll( - (dir, entry, parent) => { - if (entry.data) { - parent.file(entry.name, entry.data) - } else if (entry.ref) { - parent.file(entry.name, FS.pool[entry.ref]) - } - }, - (dir, entry, parent) => { - return parent.folder(entry.name)! - }, - zip - ) - return await zip.generateAsync({ - type: 'blob' - }) - } - - trace(path: Path, create = false) { - const segs = Util.path2seg(path) - - let now: DirEntry = this.root - for (const seg of segs) { - const idx = now.dir.findIndex(entry => entry.name === seg) - if (idx === -1) { - if (create) { - const ndir: DirEntry = { - name: seg, - dir: [], - file: [] - } - now.dir.push(ndir) - now = ndir - } else { - return null - } - } else { - now = now.dir[idx] - } - } - return now - } - - getFileViaEntry( - entry: DirEntry, - name: string - ): [number, FileEntry] | [-1, null] { - for (const [i, f] of entry.file.entries()) { - if (f.name === name) { - return [i, entry.file[i]] - } - } - return [-1, null] - } - - addTextFileViaEntry( - entry: DirEntry, - name: string, - data: string, - override = false - ) { - const [idx] = this.getFileViaEntry(entry, name) - if (idx !== -1) { - if (override) { - // console.log(JSON.parse(entry.file[idx].data ?? '{}'), JSON.parse(data)) - const fe = entry.file[idx] as TextFileEntry - fe.name = name - fe.data = data - if (fe.ref) { - delete fe.ref - } - } - return entry.file[idx] - } else { - const fe: FileEntry = { - name, - data - } - entry.file.push(fe) - return fe - } - } - - addBinFileViaEntry( - entry: DirEntry, - name: string, - data: ArrayBuffer, - override = false - ) { - const [idx] = this.getFileViaEntry(entry, name) - if (idx !== -1) { - if (override) { - const fe = entry.file[idx] as BinFileEntry - fe.name = name - fe.ref = FS.addBuffer(data) - if (fe.data) { - delete fe.data - } - } - return entry.file[idx] - } else { - const fe: FileEntry = { - name, - ref: FS.addBuffer(data) - } - entry.file.push(fe) - return fe - } - } - - getFile(path: Path) { - const [dir, file] = Util.pathdiv(path) - const de = this.trace(dir) - if (!de) { - return null - } - const [, fe] = this.getFileViaEntry(de, file) - return fe - } - - removeFile(path: Path) { - const [dir, file] = Util.pathdiv(path) - const de = this.trace(dir) - if (!de) { - return null - } - const [idx, fe] = this.getFileViaEntry(de, file) - if (idx !== -1) { - de.file.splice(idx, 1) - } - return fe - } - - enumFile(func: (dir: PathSegment, entry: FileEntry) => void) { - const enumUnderDir = (prf: PathSegment, dir: DirEntry) => { - const cur = [...prf, dir.name].filter(x => x) - dir.file.forEach(fe => func(cur, fe)) - dir.dir.forEach(de => enumUnderDir(cur, de)) - } - - enumUnderDir([], this.root) - } - - enumAll( - func: (dir: PathSegment, entry: FileEntry, param: T) => void, - dirfunc: (dir: PathSegment, entry: DirEntry, param: T) => T, - init: T - ) { - const enumUnderDir = (prf: PathSegment, dir: DirEntry, param: T) => { - const cur = [...prf, dir.name].filter(x => x) - const val = dirfunc(prf, dir, param) - dir.file.forEach(fe => func(cur, fe, val)) - dir.dir.forEach(de => enumUnderDir(cur, de, val)) - } - - enumUnderDir([], this.root, init) - } -} diff --git a/src/loader.ts b/src/loader.ts index bea406d..3de9df1 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,20 +1,12 @@ -import { fs } from './data/fs' -import { FS } from './fs' - import * as api from '@/api' -import { fs as newfs } from '@/filesystem' - -async function extraInit(zip: ArrayBuffer) { - await newfs.loadZip(zip) -} +import { fs } from '@/filesystem' export async function loadFS() { const zip = await api.load() - const newfs = await FS.fromZip(zip) - fs.change(() => newfs) - await extraInit(zip) + await fs.loadZip(zip) } export async function saveFS() { - await api.save(await fs.now().value!.zip()) + const zip = await fs.saveZip() + await api.save(zip) }