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 DATA_PATH = "/data/";
export const ASSETS_PATH = "assets/";
export const STORAGE_PATH = DATA_PATH + "storage/petal/siyuan-jsdraw-plugin";
export const TOOLBAR_PATH = STORAGE_PATH + "/toolbar.json";
export const CONFIG_PATH = STORAGE_PATH + "/conf.json";
export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin/";
export const TOOLBAR_FILENAME = "toolbar.json";
export const CONFIG_FILENAME = "conf.json";
export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path=";
export const DUMMY_HOST = "https://dummy.host/";

View file

@ -1,17 +1,18 @@
import {MaterialIconProvider} from "@js-draw/material-icons";
import {getFile, saveFile, uploadAsset} from "@/file";
import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const";
import {IDsToAssetPath} from "@/helper";
import {PluginAsset, PluginFile} from "@/file";
import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const";
import Editor, {BaseWidget, EditorEventType} from "js-draw";
import {Dialog, Plugin, openTab, getFrontend} from "siyuan";
import {replaceSyncID} from "@/protyle";
import {removeFile} from "@/api";
export class PluginEditor {
private readonly element: HTMLElement;
private readonly editor: Editor;
private drawingFile: PluginAsset;
private toolbarFile: PluginFile;
private readonly fileID: string;
private syncID: string;
private readonly initialSyncID: string;
@ -34,12 +35,13 @@ export class PluginEditor {
this.initialSyncID = initialSyncID;
this.syncID = initialSyncID;
this.genToolbar()
this.genToolbar();
// restore drawing
getFile(DATA_PATH +IDsToAssetPath(fileID, initialSyncID)).then(svg => {
if(svg != null) {
this.editor.loadFromSVG(svg);
this.drawingFile = new PluginAsset(fileID, initialSyncID, SVG_MIME);
this.drawingFile.loadFromSiYuanFS().then(() => {
if(this.drawingFile.getContent() != null) {
this.editor.loadFromSVG(this.drawingFile.getContent());
}
});
@ -52,11 +54,13 @@ export class PluginEditor {
const toolbar = this.editor.addToolbar();
// restore toolbar state
const toolbarState = await getFile(TOOLBAR_PATH);
if (toolbarState != null) {
toolbar.deserializeState(toolbarState);
// restore toolbarFile state
this.toolbarFile = new PluginFile(STORAGE_PATH, TOOLBAR_FILENAME, JSON_MIME);
this.toolbarFile.loadFromSiYuanFS().then(() => {
if(this.toolbarFile.getContent() != null) {
toolbar.deserializeState(this.toolbarFile.getContent());
}
});
// save button
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!)
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;
try {
newSyncID = (await uploadAsset(this.fileID, SVG_MIME, svgElem.outerHTML)).syncID;
if(newSyncID != oldSyncID) {
const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID);
this.drawingFile.setContent(svgElem.outerHTML);
await this.drawingFile.save();
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) {
alert(
"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." +
"\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);
setTimeout(() => { // @todo improve save button feedback

View file

@ -1,52 +1,108 @@
import {getFileBlob, putFile, upload} from "@/api";
import {ASSETS_PATH} from "@/const";
import {assetPathToIDs} from "@/helper";
import {getFileBlob, putFile, removeFile, upload} from "@/api";
import {ASSETS_PATH, DATA_PATH} from "@/const";
import {assetPathToIDs, IDsToAssetName} from "@/helper";
function toFile(title: string, content: string, mimeType: string){
const blob = new Blob([content], { type: mimeType });
return new File([blob], title, { type: mimeType });
}
abstract class PluginFileBase {
// upload asset to the assets folder, return fileID and syncID
export async function uploadAsset(fileID: string, mimeType: string, content: string) {
protected content: string | null;
const file = toFile(fileID + ".svg", content, mimeType);
protected fileName: string;
protected folderPath: string;
protected mimeType: string;
let r = await upload('/' + ASSETS_PATH, [file]);
if(r.errFiles) {
throw new Error("Failed to upload file");
getContent() { return this.content; }
setContent(content: string) { this.content = content; }
setFileName(fileName: string) { this.fileName = fileName; }
private setFolderPath(folderPath: string) {
if(folderPath.startsWith('/') && folderPath.endsWith('/')) {
this.folderPath = folderPath;
}else{
throw new Error("folderPath must start and end with /");
}
}
return assetPathToIDs(r.succMap[file.name]);
}
// folderPath must start and end with /
constructor(folderPath: string, fileName: string, mimeType: string) {
this.setFolderPath(folderPath);
this.fileName = fileName;
this.mimeType = mimeType;
}
export function saveFile(path: string, mimeType: string, content: string) {
const file = toFile(path.split('/').pop(), content, mimeType);
async loadFromSiYuanFS() {
const blob = await getFileBlob(this.folderPath + this.fileName);
const text = await blob.text();
try {
putFile(path, false, file);
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 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!
return jsonText;
}
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) {
return `${ASSETS_PATH}${fileID}-${syncID}.svg`
return `${ASSETS_PATH}${IDsToAssetName(fileID, syncID)}`
}
export function assetPathToIDs(assetPath: string): { fileID: string; syncID: string } | null {

View file

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