export, import: an export that pours back into a workspace whole

An export used to lose what plain files cannot carry: dates, names as
typed, the boards. It now writes .workspace.json beside the tree (format
1), and an import that finds one restores them. The markdown stays
plain, so the archive still reads without the application.

POST /api/import (:import! / in the UI) imports at the root: each
top-level directory becomes a space, or merges into the space already
there by that name, with its board and cards and the diary role. With a
manifest, media/ comes back as embedded files rather than a folder, a
file whose name ends in .md stays a file, and empty folders come back.

Export claims every path in one set, folders, then documents, then
files, so a file named notes.md beside a document notes no longer makes
a duplicate entry that failed the whole zip. Links to folders are
rewritten like links to documents, in one regex pass per kind instead
of a replace per file and document. A sealed file that will not open
now fails the export instead of being left out silently. The temporary
zip is made in the data directory, not in /tmp.
This commit is contained in:
nvrl committed 2026-10-20 01:38:42 +02:00
1 parent b1ba63dc61
commit aa34be8663
10 files changed
+799 -199

No files matched your search

+10
View File
@@ -72,10 +72,13 @@ export interface SharedDocument {
/** What an import did, as `POST /api/nodes/{id}/import` reports it. */
export interface ImportReport {
/** New spaces, made by an import at the root. */
spaces: number
folders: number
documents: number
files: number
links: number
cards: number
skipped: string[]
}
@@ -230,6 +233,13 @@ export const api = {
return request<ImportReport>(`/nodes/${id}/import`, { method: 'POST', body: form })
},
/** An `:export!` poured back in whole: its directories become spaces. */
importRoot: (files: File[]) => {
const form = new FormData()
for (const file of files) form.append('file', file, file.webkitRelativePath || file.name)
return request<ImportReport>('/import', { method: 'POST', body: form })
},
createNode: (input: {
parent_id: number
kind?: NodeKind