laden von erweiterungen implementiert

This commit is contained in:
Martin Drechsel
2025-05-15 09:28:45 +02:00
parent a653111071
commit ad3aa4293a
40 changed files with 1454 additions and 784 deletions

View File

@ -1,47 +1,32 @@
import { invoke } from "@tauri-apps/api/core";
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { join, resourceDir } from "@tauri-apps/api/path";
import { readTextFile, readDir } from "@tauri-apps/plugin-fs";
import { convertFileSrc } from "@tauri-apps/api/core";
import { exists, readDir, readTextFile, remove } from "@tauri-apps/plugin-fs";
import { and, eq } from "drizzle-orm";
import type {
IHaexHubExtension,
IHaexHubExtensionLink,
IHaexHubExtensionManifest,
} from "~/types/haexhub";
import { haexExtensions } from "~~/src-tauri/database/schemas/vault";
import { eq } from "drizzle-orm";
const manifestFileName = "manifest.json";
const logoFileName = "logo.svg";
export interface IHaexHubExtensionLink {
name: string;
icon: string;
tooltip?: string;
id: string;
version: string;
manifest?: IHaexHubExtensionManifest;
}
export interface IHaexHubExtensionManifest {
name: string;
id: string;
entry: string;
author: string;
url: string;
version: string;
icon: string;
permissions: {
database?: {
read?: string[];
write?: string[];
create?: string[];
};
http?: string[];
filesystem?: {
read?: string[];
write?: string[];
};
};
}
export const useExtensionsStore = defineStore("extensionsStore", () => {
const availableExtensions = ref<IHaexHubExtensionLink[]>([]);
const extensionLinks = computed<ISidebarItem[]>(() =>
availableExtensions.value
.filter((extension) => extension.enabled && extension.installed)
.map((extension) => ({
icon: extension.icon ?? "",
id: extension.id,
name: extension.name ?? "",
tooltip: extension.name ?? "",
to: { name: "haexExtension", params: { extensionId: extension.id } },
}))
);
const currentRoute = useRouter().currentRoute;
const isActive = (id: string) =>
@ -82,6 +67,16 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
}
};
const isExtensionInstalledAsync = async (extension: Partial<IHaexHubExtension>) => {
try {
const extensionPath = await getExtensionPathAsync(extension.id, `${extension.version}`);
console.log(`extension ${extension.id} is installed ${await exists(extensionPath)}`);
return await exists(extensionPath);
} catch (error) {
return false;
}
};
const checkManifest = (manifestFile: unknown): manifestFile is IHaexHubExtensionManifest => {
const errors = [];
@ -130,8 +125,10 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
return true;
};
const readManifestFileAsync = async (extensionId: string, version: string | number) => {
const readManifestFileAsync = async (extensionId: string, version: string) => {
try {
if (!(await isExtensionInstalledAsync({ id: extensionId, version }))) return null;
const extensionPath = await getExtensionPathAsync(extensionId, `${version}`);
const manifestPath = await join(extensionPath, manifestFileName);
const manifest = (await JSON.parse(
@ -139,7 +136,7 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
)) as IHaexHubExtensionManifest;
/*
TODO implement await checkManifets(manifest);
TODO implement check, that manifest has valid data
*/
return manifest;
} catch (error) {
@ -287,77 +284,83 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
const extensionEntry = computedAsync(
async () => {
console.log("extensionEntry start", currentExtension.value);
const regex = /((href|src)=["'])([^"']+)(["'])/g;
if (!currentExtension.value?.id || !currentExtension.value.version) {
console.log("extension id or entry missing", currentExtension.value);
return "no mani: " + currentExtension.value;
}
//return "wadahadedzdz";
try {
console.log("extensionEntry start", currentExtension.value);
const regex = /((href|src)=["'])([^"']+)(["'])/g;
const extensionPath = await getExtensionPathAsync(
currentExtension.value?.id,
currentExtension.value?.version
); //await join(await resourceDir(), currentExtension.value.. extensionDir, entryFileName);
console.log("extensionEntry extensionPath", extensionPath);
const manifest = await readManifestFileAsync(
currentExtension.value.id,
currentExtension.value.version
);
if (!manifest) return "no manifest readable";
const entryPath = await join(extensionPath, manifest.entry);
return `asset://localhost/${entryPath}`;
let entryHtml = await readTextFile(entryPath);
console.log("entryHtml", entryHtml);
const replacements = [];
let match;
while ((match = regex.exec(entryHtml)) !== null) {
const [fullMatch, prefix, attr, resource, suffix] = match;
if (!resource.startsWith("http")) {
replacements.push({ match: fullMatch, resource, prefix, suffix });
if (!currentExtension.value?.id || !currentExtension.value.version) {
console.log("extension id or entry missing", currentExtension.value);
return "no mani: " + currentExtension.value;
}
}
for (const { match, resource, prefix, suffix } of replacements) {
const fileContent = await readTextFile(await join(extensionPath, resource));
const blob = new Blob([fileContent], { type: getMimeType(resource) });
const blobUrl = URL.createObjectURL(blob);
console.log("blob", resource, blobUrl);
entryHtml = entryHtml.replace(match, `${prefix}${blobUrl}${suffix}`);
}
const extensionPath = await getExtensionPathAsync(
currentExtension.value?.id,
currentExtension.value?.version
); //await join(await resourceDir(), currentExtension.value.. extensionDir, entryFileName);
console.log("entryHtml", entryHtml);
console.log("extensionEntry extensionPath", extensionPath);
const manifest = await readManifestFileAsync(
currentExtension.value.id,
currentExtension.value.version
);
const blob = new Blob([entryHtml], { type: "text/html" });
const iframeSrc = URL.createObjectURL(blob);
if (!manifest) return "no manifest readable";
console.log("iframeSrc", iframeSrc);
const entryPath = await join(extensionPath, manifest.entry);
/* const path = convertFileSrc(extensionDir, manifest.entry);
const hexName = stringToHex(
JSON.stringify({
id: currentExtension.value.id,
version: currentExtension.value.version,
})
);
return `haex-extension://${hexName}/index.html`;
return convertFileSrc(entryPath); //`asset://localhost/${entryPath}`;
let entryHtml = await readTextFile(entryPath);
console.log("entryHtml", entryHtml);
const replacements = [];
let match;
while ((match = regex.exec(entryHtml)) !== null) {
const [fullMatch, prefix, attr, resource, suffix] = match;
if (!resource.startsWith("http")) {
replacements.push({ match: fullMatch, resource, prefix, suffix });
}
}
for (const { match, resource, prefix, suffix } of replacements) {
const srcFile = convertFileSrc(await join(extensionPath, resource));
entryHtml = entryHtml.replace(match, `${prefix}${srcFile}${suffix}`);
}
console.log("entryHtml", entryHtml);
const blob = new Blob([entryHtml], { type: "text/html" });
const iframeSrc = URL.createObjectURL(blob);
console.log("iframeSrc", iframeSrc);
/* const path = convertFileSrc(extensionDir, manifest.entry);
console.log("final path", path); */
//manifest.entry = iframeSrc;
return iframeSrc;
/* await join(
//manifest.entry = iframeSrc;
return iframeSrc;
/* await join(
path, //`file:/${extensionDirectory}`,
manifest.entry
); */
// Modul-Datei laden
//const modulePathFull = await join(basePath, manifest.main);
/* const manifest: PluginManifest = await invoke('load_plugin', {
// Modul-Datei laden
//const modulePathFull = await join(basePath, manifest.main);
/* const manifest: PluginManifest = await invoke('load_plugin', {
manifestPath,
}); */
/* const iframe = document.createElement('iframe');
/* const iframe = document.createElement('iframe');
iframe.src = manifest.entry;
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none'; */
/* const addonApi = {
/* const addonApi = {
db_execute: async (sql: string, params: string[] = []) => {
return invoke('db_execute', {
addonId: manifest.name,
@ -373,24 +376,27 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
});
},
}; */
/* iframe.onload = () => {
/* iframe.onload = () => {
iframe.contentWindow?.postMessage(
{ type: 'init', payload: addonApi },
'*'
);
};
window.addEventListener('message', (event) => {
if (event.source === iframe.contentWindow) {
const { type } = event.data;
if (type === 'ready') {
console.log(`Plugin ${manifest.name} ist bereit`);
}
}
}); */
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
console.log(`Plugin ${manifest.name} geladen.`); */
);
};
window.addEventListener('message', (event) => {
if (event.source === iframe.contentWindow) {
const { type } = event.data;
if (type === 'ready') {
console.log(`Plugin ${manifest.name} ist bereit`);
}
}
}); */
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
console.log(`Plugin ${manifest.name} geladen.`); */
} catch (error) {
console.error("ERROR extensionEntry", error);
}
},
null,
{ lazy: true }
@ -399,28 +405,36 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
const loadExtensionsAsync = async () => {
const { currentVault } = storeToRefs(useVaultStore());
/* const query = db
.select()
.from(haexExtensions)
//.where(sql`${haexExtensions.enabled} = "1"`);
.where(eq(haexExtensions.enabled, true)); */
const extensions = await currentVault.value?.drizzle
.select()
.from(haexExtensions)
.where(eq(haexExtensions.enabled, true));
const extensions = (await currentVault.value?.drizzle.select().from(haexExtensions)) ?? [];
//if (!extensions?.length) return false;
const installedExtensions = await filterAsync(extensions, isExtensionInstalledAsync);
console.log("loadExtensionsAsync installedExtensions", installedExtensions);
//const manifest = readTextFile(manifestFileName)
//const { sql, params } = query.toSQL();
//const extensions = await invoke<any>("sql_select", { sql, params });
console.log("loadExtensionsAsync ", extensions);
availableExtensions.value =
extensions?.map((extension) => ({
extensions.map((extension) => ({
id: extension.id,
name: extension.name ?? "",
icon: extension.icon ?? "",
tooltip: extension.name ?? "",
author: extension.author ?? "",
version: extension.version ?? "",
enabled: extension.enabled ? true : false,
installed: installedExtensions.includes(extension),
})) ?? [];
console.log("loadExtensionsAsync", availableExtensions.value);
return true;
};
const removeExtensionAsync = async (id: string, version: string) => {
try {
console.log("remove extension", id, version);
await removeExtensionFromVaultAsync(id, version);
await removeExtensionFilesAsync(id, version);
} catch (error) {
throw new Error(JSON.stringify(error));
}
};
return {
@ -428,10 +442,13 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
checkManifest,
currentExtension,
extensionEntry,
extensionLinks,
installAsync,
isActive,
loadExtensionsAsync,
readManifestFileAsync,
removeExtensionAsync,
getExtensionPathAsync,
};
});
@ -440,3 +457,36 @@ const getMimeType = (file: string) => {
if (file.endsWith(".js")) return "text/javascript";
return "text/plain";
};
const removeExtensionFromVaultAsync = async (id: string | null, version: string | null) => {
if (!id) throw new Error("Erweiterung kann nicht gelöscht werden. Es keine ID angegeben");
if (!version)
throw new Error("Erweiterung kann nicht gelöscht werden. Es wurde keine Version angegeben");
const { currentVault } = useVaultStore();
const removedExtensions = await currentVault?.drizzle
.delete(haexExtensions)
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)));
return removedExtensions;
};
const removeExtensionFilesAsync = async (id: string | null, version: string | null) => {
try {
const { getExtensionPathAsync } = useExtensionsStore();
if (!id) throw new Error("Erweiterung kann nicht gelöscht werden. Es keine ID angegeben");
if (!version)
throw new Error("Erweiterung kann nicht gelöscht werden. Es wurde keine Version angegeben");
const extensionDirectory = await getExtensionPathAsync(id, version);
await remove(extensionDirectory, {
recursive: true,
});
} catch (error) {
console.error("ERROR removeExtensionFilesAsync", error);
throw new Error(JSON.stringify(error));
}
};
const replaceUrlWithAssetProtocolAsync = () => { };

View File

@ -1,5 +1,4 @@
import { getSingleRouteParam } from "~/composables/helper";
import type { RouteLocationRaw, RouteLocationAsRelativeGeneric } from "vue-router";
import type { RouteLocationAsRelativeGeneric } from "vue-router";
export interface ISidebarItem {
name: string;
@ -7,6 +6,7 @@ export interface ISidebarItem {
tooltip?: string;
id: string;
to?: RouteLocationAsRelativeGeneric;
iconType?: "icon" | "svg";
}
export const useSidebarStore = defineStore("sidebarStore", () => {

View File

@ -4,18 +4,15 @@ import { drizzle, SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy";
import * as schema from "@/../src-tauri/database/schemas/vault";
import { invoke } from "@tauri-apps/api/core";
import { count } from "drizzle-orm";
import { and, count, eq } from "drizzle-orm";
import { platform } from "@tauri-apps/plugin-os";
interface IVault {
//database: Database;
path: string;
password: string;
name: string;
drizzle: SqliteRemoteDatabase<typeof schema>;
}
interface IOpenVaults {
[vaultPath: string]: IVault;
[vaultId: string]: IVault;
}
export const useVaultStore = defineStore("vaultStore", () => {
@ -58,27 +55,20 @@ export const useVaultStore = defineStore("vaultStore", () => {
const openAsync = async ({ path = "", password }: { path: string; password: string }) => {
try {
console.log("try to open db", path, password);
const result = await invoke<string>("open_encrypted_database", {
path,
key: password,
});
console.log("open vault from store", result);
//const db = await Database.load(sqlitePath);
if (result !== "success") throw new Error(result);
const vaultId = await getVaultIdAsync(path);
const seperator = platform() === "windows" ? "\\" : "/";
const fileName = path.split(seperator).pop();
console.log("opened db fileName", fileName, vaultId);
openVaults.value = {
...openVaults.value,
[vaultId]: {
//database: db,
path,
password,
name: fileName ?? path,
drizzle: drizzle<typeof schema>(
async (sql, params: unknown[], method) => {
@ -102,24 +92,15 @@ export const useVaultStore = defineStore("vaultStore", () => {
return { rows: [] };
}
/* rows = rows.map((row: any) => {
return Object.values(row);
}); */
console.log("select after map", rows);
// If the method is "all", return all rows
results = method === "all" ? rows : rows[0];
return { rows: results };
},
// Pass the schema to the drizzle instance
{ schema: schema, logger: true }
),
},
};
//if (!(await testDatabaseReadAsync())) throw new Error("Passwort falsch");
const { addVaultAsync } = useLastVaultStore();
await addVaultAsync({ path });
@ -127,15 +108,6 @@ export const useVaultStore = defineStore("vaultStore", () => {
} catch (error) {
console.error("Error openAsync ", error);
return false;
//if (error === "file is not a database") throw new Error("Passwort falsch");
}
};
const testDatabaseReadAsync = async () => {
try {
return currentVault.value?.drizzle.select({ count: count() }).from(schema.haexExtensions);
} catch (error) {
return false;
}
};
@ -198,7 +170,7 @@ const getVaultIdAsync = async (path: string) => {
return hashHex;
};
function isSelectQuery(sql: string): boolean {
const isSelectQuery = (sql: string) => {
const selectRegex = /^\s*SELECT\b/i;
return selectRegex.test(sql);
}
};