File refactoring

This commit is contained in:
MassiveBox 2025-04-11 18:42:45 +02:00
parent e23cc424f8
commit 6bca12c934
Signed by: massivebox
GPG key ID: 9B74D3A59181947D
5 changed files with 142 additions and 72 deletions

View file

@ -2,8 +2,8 @@ export const SVG_MIME = "image/svg+xml";
export const JSON_MIME = "application/json"; export const JSON_MIME = "application/json";
export const DATA_PATH = "/data/"; export const DATA_PATH = "/data/";
export const ASSETS_PATH = "assets/"; export const ASSETS_PATH = "assets/";
export const STORAGE_PATH = DATA_PATH + "storage/petal/siyuan-jsdraw-plugin"; export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin/";
export const TOOLBAR_PATH = STORAGE_PATH + "/toolbar.json"; export const TOOLBAR_FILENAME = "toolbar.json";
export const CONFIG_PATH = STORAGE_PATH + "/conf.json"; export const CONFIG_FILENAME = "conf.json";
export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path="; export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path=";
export const DUMMY_HOST = "https://dummy.host/"; export const DUMMY_HOST = "https://dummy.host/";

View file

@ -1,17 +1,18 @@
import {MaterialIconProvider} from "@js-draw/material-icons"; import {MaterialIconProvider} from "@js-draw/material-icons";
import {getFile, saveFile, uploadAsset} from "@/file"; import {PluginAsset, PluginFile} from "@/file";
import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const";
import {IDsToAssetPath} from "@/helper";
import Editor, {BaseWidget, EditorEventType} from "js-draw"; import Editor, {BaseWidget, EditorEventType} from "js-draw";
import {Dialog, Plugin, openTab, getFrontend} from "siyuan"; import {Dialog, Plugin, openTab, getFrontend} from "siyuan";
import {replaceSyncID} from "@/protyle"; import {replaceSyncID} from "@/protyle";
import {removeFile} from "@/api";
export class PluginEditor { export class PluginEditor {
private readonly element: HTMLElement; private readonly element: HTMLElement;
private readonly editor: Editor; private readonly editor: Editor;
private drawingFile: PluginAsset;
private toolbarFile: PluginFile;
private readonly fileID: string; private readonly fileID: string;
private syncID: string; private syncID: string;
private readonly initialSyncID: string; private readonly initialSyncID: string;
@ -34,12 +35,13 @@ export class PluginEditor {
this.initialSyncID = initialSyncID; this.initialSyncID = initialSyncID;
this.syncID = initialSyncID; this.syncID = initialSyncID;
this.genToolbar() this.genToolbar();
// restore drawing // restore drawing
getFile(DATA_PATH +IDsToAssetPath(fileID, initialSyncID)).then(svg => { this.drawingFile = new PluginAsset(fileID, initialSyncID, SVG_MIME);
if(svg != null) { this.drawingFile.loadFromSiYuanFS().then(() => {
this.editor.loadFromSVG(svg); if(this.drawingFile.getContent() != null) {
this.editor.loadFromSVG(this.drawingFile.getContent());
} }
}); });
@ -52,11 +54,13 @@ export class PluginEditor {
const toolbar = this.editor.addToolbar(); const toolbar = this.editor.addToolbar();
// restore toolbar state // restore toolbarFile state
const toolbarState = await getFile(TOOLBAR_PATH); this.toolbarFile = new PluginFile(STORAGE_PATH, TOOLBAR_FILENAME, JSON_MIME);
if (toolbarState != null) { this.toolbarFile.loadFromSiYuanFS().then(() => {
toolbar.deserializeState(toolbarState); if(this.toolbarFile.getContent() != null) {
} toolbar.deserializeState(this.toolbarFile.getContent());
}
});
// save button // save button
const saveButton = toolbar.addSaveButton(async () => { const saveButton = toolbar.addSaveButton(async () => {
@ -65,7 +69,8 @@ export class PluginEditor {
// save toolbar config on tool change (toolbar state is not saved in SVGs!) // save toolbar config on tool change (toolbar state is not saved in SVGs!)
this.editor.notifier.on(EditorEventType.ToolUpdated, () => { this.editor.notifier.on(EditorEventType.ToolUpdated, () => {
saveFile(TOOLBAR_PATH, JSON_MIME, toolbar.serializeState()); this.toolbarFile.setContent(toolbar.serializeState());
this.toolbarFile.save();
}); });
} }
@ -77,18 +82,20 @@ export class PluginEditor {
const oldSyncID = this.syncID; const oldSyncID = this.syncID;
try { try {
newSyncID = (await uploadAsset(this.fileID, SVG_MIME, svgElem.outerHTML)).syncID; this.drawingFile.setContent(svgElem.outerHTML);
if(newSyncID != oldSyncID) { await this.drawingFile.save();
const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); newSyncID = this.drawingFile.getSyncID();
if(newSyncID != oldSyncID) { // supposed to replace protyle
const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); // try to change protyle
if(!changed) { if(!changed) {
alert( alert(
"Error replacing old sync ID with new one! You may need to manually replace the file path." + "Error replacing old sync ID with new one! You may need to manually replace the file path." +
"\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." + "\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." +
"\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory." "\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory."
); );
return; return; // don't delete old drawing if protyle unchanged (could cause confusion)
} }
await removeFile(DATA_PATH + IDsToAssetPath(this.fileID, oldSyncID)); await this.drawingFile.removeOld(oldSyncID);
} }
saveButton.setDisabled(true); saveButton.setDisabled(true);
setTimeout(() => { // @todo improve save button feedback setTimeout(() => { // @todo improve save button feedback

View file

@ -1,52 +1,108 @@
import {getFileBlob, putFile, upload} from "@/api"; import {getFileBlob, putFile, removeFile, upload} from "@/api";
import {ASSETS_PATH} from "@/const"; import {ASSETS_PATH, DATA_PATH} from "@/const";
import {assetPathToIDs} from "@/helper"; import {assetPathToIDs, IDsToAssetName} from "@/helper";
function toFile(title: string, content: string, mimeType: string){ abstract class PluginFileBase {
const blob = new Blob([content], { type: mimeType });
return new File([blob], title, { type: mimeType });
}
// upload asset to the assets folder, return fileID and syncID protected content: string | null;
export async function uploadAsset(fileID: string, mimeType: string, content: string) {
const file = toFile(fileID + ".svg", content, mimeType); protected fileName: string;
protected folderPath: string;
protected mimeType: string;
let r = await upload('/' + ASSETS_PATH, [file]); getContent() { return this.content; }
if(r.errFiles) { setContent(content: string) { this.content = content; }
throw new Error("Failed to upload file"); setFileName(fileName: string) { this.fileName = fileName; }
}
return assetPathToIDs(r.succMap[file.name]);
} private setFolderPath(folderPath: string) {
if(folderPath.startsWith('/') && folderPath.endsWith('/')) {
export function saveFile(path: string, mimeType: string, content: string) { this.folderPath = folderPath;
}else{
const file = toFile(path.split('/').pop(), content, mimeType); throw new Error("folderPath must start and end with /");
try {
putFile(path, false, file);
} catch (error) {
console.error("Error saving file:", error);
throw error;
}
}
export async function getFile(path: string) {
const blob = await getFileBlob(path);
const jsonText = await blob.text();
// if we got a 404 api response, we will return null
try {
const res = JSON.parse(jsonText);
if(res.code == 404) {
return null;
} }
}catch {} }
// js-draw expects a string! // folderPath must start and end with /
return jsonText; constructor(folderPath: string, fileName: string, mimeType: string) {
this.setFolderPath(folderPath);
this.fileName = fileName;
this.mimeType = mimeType;
}
async loadFromSiYuanFS() {
const blob = await getFileBlob(this.folderPath + this.fileName);
const text = await blob.text();
try {
const res = JSON.parse(text);
if(res.code == 404) {
this.content = null;
return;
}
}catch {}
this.content = text;
}
async remove(customFilename?: string) {
let filename = customFilename || this.fileName;
await removeFile(this.folderPath + filename);
}
protected toFile(customFilename?: string): File {
let filename = customFilename || this.fileName;
const blob = new Blob([this.content], { type: this.mimeType });
return new File([blob], filename, { type: this.mimeType });
}
} }
export class PluginFile extends PluginFileBase {
async save() {
const file = this.toFile();
try {
await putFile(this.folderPath + this.fileName, false, file);
} catch (error) {
console.error("Error saving file:", error);
throw error;
}
}
}
export class PluginAsset extends PluginFileBase {
private fileID: string
private syncID: string
getFileID() { return this.fileID; }
getSyncID() { return this.syncID; }
constructor(fileID: string, syncID: string, mimeType: string) {
super(DATA_PATH + ASSETS_PATH, IDsToAssetName(fileID, syncID), mimeType);
this.fileID = fileID;
this.syncID = syncID;
}
async save() {
const file = this.toFile(this.fileID + '.svg');
let r = await upload('/' + ASSETS_PATH, [file]);
if (r.errFiles) {
throw new Error("Failed to upload file");
}
const ids = assetPathToIDs(r.succMap[file.name])
this.fileID = ids.fileID;
this.syncID = ids.syncID;
super.setFileName(IDsToAssetName(this.fileID, this.syncID));
}
async removeOld(oldSyncID: string) {
await super.remove(IDsToAssetName(this.fileID, oldSyncID));
}
}

View file

@ -47,8 +47,11 @@ export function generateRandomString() {
} }
export function IDsToAssetName(fileID: string, syncID: string) {
return `${fileID}-${syncID}.svg`;
}
export function IDsToAssetPath(fileID: string, syncID: string) { export function IDsToAssetPath(fileID: string, syncID: string) {
return `${ASSETS_PATH}${fileID}-${syncID}.svg` return `${ASSETS_PATH}${IDsToAssetName(fileID, syncID)}`
} }
export function assetPathToIDs(assetPath: string): { fileID: string; syncID: string } | null { export function assetPathToIDs(assetPath: string): { fileID: string; syncID: string } | null {

View file

@ -1,5 +1,5 @@
import {sql} from "@/api"; import {sql} from "@/api";
import {getFile, uploadAsset} from "@/file"; import {PluginAsset, PluginFile} from "@/file";
import {ASSETS_PATH, DATA_PATH, SVG_MIME} from "@/const"; import {ASSETS_PATH, DATA_PATH, SVG_MIME} from "@/const";
import {replaceBlockContent} from "@/protyle"; import {replaceBlockContent} from "@/protyle";
import {generateRandomString, getMarkdownBlock} from "@/helper"; import {generateRandomString, getMarkdownBlock} from "@/helper";
@ -13,11 +13,15 @@ export async function migrate() {
for(const block of blocks) { for(const block of blocks) {
const oldFileID = extractID(block.markdown); const oldFileID = extractID(block.markdown);
if(oldFileID) { if(oldFileID) {
const newFileID = generateRandomString() + "-" + oldFileID; const oldFile = new PluginFile(DATA_PATH + ASSETS_PATH, oldFileID + '.svg', SVG_MIME);
const file = await getFile(DATA_PATH + ASSETS_PATH + oldFileID + ".svg"); await oldFile.loadFromSiYuanFS();
const r = await uploadAsset(newFileID, SVG_MIME, file); const newFile = new PluginAsset(generateRandomString(), oldFileID, SVG_MIME);
const newMarkdown = getMarkdownBlock(r.fileID, r.syncID); newFile.setContent(oldFile.getContent());
await replaceBlockContent(block.id, block.markdown, newMarkdown); await newFile.save();
const newMarkdown = getMarkdownBlock(newFile.getFileID(), newFile.getSyncID());
if(await replaceBlockContent(block.id, block.markdown, newMarkdown)) {
await oldFile.remove();
}
} }
} }