vault management
This commit is contained in:
parent
21952f8130
commit
335f9ee4b7
11 changed files with 495 additions and 93 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,5 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/shim-loader.js
|
dist/shim-loader.js
|
||||||
investigation/
|
investigation/
|
||||||
test-vault/
|
vaults/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Patches the extracted Obsidian asar for browser use:
|
// Patches the extracted Obsidian asar for browser use:
|
||||||
// 1. Removes Content-Security-Policy meta tag
|
// 1. Removes Content-Security-Policy meta tag
|
||||||
// 2. Injects shim-loader.js script (non-deferred, before all other scripts)
|
// 2. Injects shim-loader.js script (non-deferred, before all other scripts)
|
||||||
|
// Patches both index.html and starter.html.
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
@ -12,27 +13,29 @@ if (!asarDir) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexPath = path.join(asarDir, "index.html");
|
function patchHtml(filePath) {
|
||||||
if (!fs.existsSync(indexPath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
console.error(`Not found: ${indexPath}`);
|
console.warn(`[patch] Skipping (not found): ${filePath}`);
|
||||||
process.exit(1);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = fs.readFileSync(indexPath, "utf-8");
|
let html = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
// Remove CSP meta tag
|
// Remove CSP meta tag
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
/\s*<meta\s+http-equiv="Content-Security-Policy"[^>]*>\s*/g,
|
/\s*<meta\s+http-equiv="Content-Security-Policy"[^>]*>\s*/g,
|
||||||
"\n",
|
"\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Inject shim-loader before the first <script> tag
|
// Inject shim-loader before the first <script> tag
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
'<script type="text/javascript"',
|
'<script type="text/javascript"',
|
||||||
'<!-- Shim loader MUST load before everything else (no defer) -->\n' +
|
|
||||||
'<script type="text/javascript" src="shim-loader.js"></script>\n' +
|
'<script type="text/javascript" src="shim-loader.js"></script>\n' +
|
||||||
'<script type="text/javascript"',
|
'<script type="text/javascript"',
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.writeFileSync(indexPath, html);
|
fs.writeFileSync(filePath, html);
|
||||||
console.log(`[patch] Patched ${indexPath}`);
|
console.log(`[patch] Patched ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
patchHtml(path.join(asarDir, "index.html"));
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,48 @@
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
// VAULT_ROOT: a directory that contains vault folders.
|
||||||
|
// Each subdirectory is a vault. New vaults are created as new subdirs.
|
||||||
|
// Falls back to parent of VAULT_PATH (single-vault compat) or ./vaults.
|
||||||
|
const vaultRoot =
|
||||||
|
process.env.VAULT_ROOT ||
|
||||||
|
(process.env.VAULT_PATH
|
||||||
|
? path.dirname(process.env.VAULT_PATH)
|
||||||
|
: path.join(__dirname, "..", "vaults"));
|
||||||
|
|
||||||
|
function discoverVaults() {
|
||||||
|
const vaults = {};
|
||||||
|
try {
|
||||||
|
const entries = fs.readdirSync(vaultRoot, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
||||||
|
vaults[entry.name] = path.join(vaultRoot, entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[config] Failed to read VAULT_ROOT:", vaultRoot, e.message);
|
||||||
|
}
|
||||||
|
return vaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vaults = discoverVaults();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
port: process.env.PORT || 8080,
|
port: process.env.PORT || 8080,
|
||||||
vaultPath: process.env.VAULT_PATH || path.join(__dirname, "..", "test-vault"),
|
vaultRoot,
|
||||||
|
get vaults() {
|
||||||
|
return vaults;
|
||||||
|
},
|
||||||
|
get defaultVaultId() {
|
||||||
|
return Object.keys(vaults)[0] || null;
|
||||||
|
},
|
||||||
|
getVaultPath(id) {
|
||||||
|
return vaults[id] || null;
|
||||||
|
},
|
||||||
|
refreshVaults() {
|
||||||
|
vaults = discoverVaults();
|
||||||
|
return vaults;
|
||||||
|
},
|
||||||
obsidianAssetsPath:
|
obsidianAssetsPath:
|
||||||
process.env.OBSIDIAN_ASSETS_PATH ||
|
process.env.OBSIDIAN_ASSETS_PATH ||
|
||||||
path.join(__dirname, "..", "investigation", "obsidian.asar.unpacked"),
|
path.join(__dirname, "..", "investigation", "obsidian.asar.unpacked"),
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,19 @@ app.use("/api/fs", fsRoutes);
|
||||||
app.use("/api/vault", vaultRoutes);
|
app.use("/api/vault", vaultRoutes);
|
||||||
|
|
||||||
// Serve vault files for resource URLs (images, attachments, etc.)
|
// Serve vault files for resource URLs (images, attachments, etc.)
|
||||||
app.use("/vault-files", express.static(config.vaultPath));
|
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file
|
||||||
|
app.use("/vault-files", (req, res, next) => {
|
||||||
|
// Extract vault ID from the first path segment
|
||||||
|
const parts = req.path.split("/").filter(Boolean);
|
||||||
|
if (parts.length === 0)
|
||||||
|
return res.status(400).json({ error: "Missing vault ID" });
|
||||||
|
const vaultId = decodeURIComponent(parts[0]);
|
||||||
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
if (!vaultPath) return res.status(404).json({ error: "Vault not found" });
|
||||||
|
// Rewrite req.url to strip the vault ID prefix, then serve statically
|
||||||
|
req.url = "/" + parts.slice(1).join("/");
|
||||||
|
express.static(vaultPath)(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
// --- Static serving ---
|
// --- Static serving ---
|
||||||
// dist/ has shim-loader.js + patched index.html (dev mode).
|
// dist/ has shim-loader.js + patched index.html (dev mode).
|
||||||
|
|
@ -52,7 +64,10 @@ const server = app.listen(config.port, () => {
|
||||||
console.log(
|
console.log(
|
||||||
`[obsidian-bridge] Server running on http://localhost:${config.port}`,
|
`[obsidian-bridge] Server running on http://localhost:${config.port}`,
|
||||||
);
|
);
|
||||||
console.log(`[obsidian-bridge] Vault path: ${config.vaultPath}`);
|
console.log(`[obsidian-bridge] Vault root: ${config.vaultRoot}`);
|
||||||
|
console.log(
|
||||||
|
`[obsidian-bridge] Vaults: ${Object.keys(config.vaults).join(", ")}`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
setupWebSocket(server);
|
setupWebSocket(server);
|
||||||
|
|
|
||||||
|
|
@ -5,32 +5,44 @@ const config = require("../config");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Resolve a client-provided path to an absolute path within the vault.
|
// Resolve the vault root for a request. Reads vault ID from query or body.
|
||||||
|
function getVaultRoot(req, res) {
|
||||||
|
const vaultId = req.query.vault || req.body?.vault || config.defaultVaultId;
|
||||||
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
if (!vaultPath) {
|
||||||
|
res.status(404).json({ error: "Vault not found", id: vaultId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return vaultPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a client-provided path to an absolute path within a vault.
|
||||||
// Strips leading slashes so paths from the client are always treated as
|
// Strips leading slashes so paths from the client are always treated as
|
||||||
// relative to the vault root. Rejects path traversal attempts.
|
// relative to the vault root. Rejects path traversal attempts.
|
||||||
function resolveVaultPath(relativePath) {
|
function resolveVaultPath(vaultRoot, relativePath) {
|
||||||
// Strip leading slashes - client paths like "/.obsidian" should resolve
|
|
||||||
// relative to the vault, not as absolute filesystem paths.
|
|
||||||
const cleaned = (relativePath || "").replace(/^\/+/, "");
|
const cleaned = (relativePath || "").replace(/^\/+/, "");
|
||||||
const resolved = path.resolve(config.vaultPath, cleaned);
|
const resolved = path.resolve(vaultRoot, cleaned);
|
||||||
if (!resolved.startsWith(path.resolve(config.vaultPath))) {
|
if (!resolved.startsWith(path.resolve(vaultRoot))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
function guardPath(req, res) {
|
function guardPath(req, res) {
|
||||||
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
|
if (!vaultRoot) return null;
|
||||||
const p = req.query.path ?? req.body?.path;
|
const p = req.query.path ?? req.body?.path;
|
||||||
if (p === undefined || p === null) {
|
if (p === undefined || p === null) {
|
||||||
res.status(400).json({ error: "Missing path parameter" });
|
res.status(400).json({ error: "Missing path parameter" });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Empty string = vault root, which is valid
|
// Empty string = vault root, which is valid
|
||||||
const resolved = resolveVaultPath(p);
|
const resolved = resolveVaultPath(vaultRoot, p);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
res.status(403).json({ error: "Path traversal rejected" });
|
res.status(403).json({ error: "Path traversal rejected" });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
req._vaultRoot = vaultRoot;
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,9 +101,7 @@ router.get("/readFile", async (req, res) => {
|
||||||
// Check if path is a directory
|
// Check if path is a directory
|
||||||
const stat = await fs.promises.stat(resolved);
|
const stat = await fs.promises.stat(resolved);
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
return res
|
return res.status(400).json({
|
||||||
.status(400)
|
|
||||||
.json({
|
|
||||||
error: "EISDIR: illegal operation on a directory",
|
error: "EISDIR: illegal operation on a directory",
|
||||||
code: "EISDIR",
|
code: "EISDIR",
|
||||||
});
|
});
|
||||||
|
|
@ -111,9 +121,11 @@ router.get("/readFile", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/fs/writeFile { path, content, encoding? }
|
// POST /api/fs/writeFile { path, content, encoding?, vault? }
|
||||||
router.post("/writeFile", async (req, res) => {
|
router.post("/writeFile", async (req, res) => {
|
||||||
const resolved = resolveVaultPath(req.body?.path);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
|
if (!vaultRoot) return;
|
||||||
|
const resolved = resolveVaultPath(vaultRoot, req.body?.path);
|
||||||
if (!resolved) return res.status(403).json({ error: "Invalid path" });
|
if (!resolved) return res.status(403).json({ error: "Invalid path" });
|
||||||
try {
|
try {
|
||||||
// Ensure parent directory exists
|
// Ensure parent directory exists
|
||||||
|
|
@ -137,9 +149,11 @@ router.post("/writeFile", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/fs/appendFile { path, content }
|
// POST /api/fs/appendFile { path, content, vault? }
|
||||||
router.post("/appendFile", async (req, res) => {
|
router.post("/appendFile", async (req, res) => {
|
||||||
const resolved = resolveVaultPath(req.body?.path);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
|
if (!vaultRoot) return;
|
||||||
|
const resolved = resolveVaultPath(vaultRoot, req.body?.path);
|
||||||
if (!resolved) return res.status(403).json({ error: "Invalid path" });
|
if (!resolved) return res.status(403).json({ error: "Invalid path" });
|
||||||
try {
|
try {
|
||||||
await fs.promises.appendFile(resolved, req.body.content, "utf-8");
|
await fs.promises.appendFile(resolved, req.body.content, "utf-8");
|
||||||
|
|
@ -149,9 +163,11 @@ router.post("/appendFile", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/fs/mkdir { path, recursive? }
|
// POST /api/fs/mkdir { path, recursive?, vault? }
|
||||||
router.post("/mkdir", async (req, res) => {
|
router.post("/mkdir", async (req, res) => {
|
||||||
const resolved = resolveVaultPath(req.body?.path);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
|
if (!vaultRoot) return;
|
||||||
|
const resolved = resolveVaultPath(vaultRoot, req.body?.path);
|
||||||
if (!resolved) return res.status(403).json({ error: "Invalid path" });
|
if (!resolved) return res.status(403).json({ error: "Invalid path" });
|
||||||
try {
|
try {
|
||||||
await fs.promises.mkdir(resolved, { recursive: !!req.body.recursive });
|
await fs.promises.mkdir(resolved, { recursive: !!req.body.recursive });
|
||||||
|
|
@ -161,10 +177,12 @@ router.post("/mkdir", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/fs/rename { oldPath, newPath }
|
// POST /api/fs/rename { oldPath, newPath, vault? }
|
||||||
router.post("/rename", async (req, res) => {
|
router.post("/rename", async (req, res) => {
|
||||||
const oldResolved = resolveVaultPath(req.body?.oldPath);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
const newResolved = resolveVaultPath(req.body?.newPath);
|
if (!vaultRoot) return;
|
||||||
|
const oldResolved = resolveVaultPath(vaultRoot, req.body?.oldPath);
|
||||||
|
const newResolved = resolveVaultPath(vaultRoot, req.body?.newPath);
|
||||||
if (!oldResolved || !newResolved)
|
if (!oldResolved || !newResolved)
|
||||||
return res.status(403).json({ error: "Invalid path" });
|
return res.status(403).json({ error: "Invalid path" });
|
||||||
try {
|
try {
|
||||||
|
|
@ -175,10 +193,12 @@ router.post("/rename", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/fs/copyFile { src, dest }
|
// POST /api/fs/copyFile { src, dest, vault? }
|
||||||
router.post("/copyFile", async (req, res) => {
|
router.post("/copyFile", async (req, res) => {
|
||||||
const srcResolved = resolveVaultPath(req.body?.src);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
const destResolved = resolveVaultPath(req.body?.dest);
|
if (!vaultRoot) return;
|
||||||
|
const srcResolved = resolveVaultPath(vaultRoot, req.body?.src);
|
||||||
|
const destResolved = resolveVaultPath(vaultRoot, req.body?.dest);
|
||||||
if (!srcResolved || !destResolved)
|
if (!srcResolved || !destResolved)
|
||||||
return res.status(403).json({ error: "Invalid path" });
|
return res.status(403).json({ error: "Invalid path" });
|
||||||
try {
|
try {
|
||||||
|
|
@ -249,15 +269,17 @@ router.get("/realpath", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const real = await fs.promises.realpath(resolved);
|
const real = await fs.promises.realpath(resolved);
|
||||||
// Return path relative to vault root
|
// Return path relative to vault root
|
||||||
res.json({ path: path.relative(config.vaultPath, real) });
|
res.json({ path: path.relative(req._vaultRoot, real) });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/fs/utimes { path, atime, mtime }
|
// POST /api/fs/utimes { path, atime, mtime, vault? }
|
||||||
router.post("/utimes", async (req, res) => {
|
router.post("/utimes", async (req, res) => {
|
||||||
const resolved = resolveVaultPath(req.body?.path);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
|
if (!vaultRoot) return;
|
||||||
|
const resolved = resolveVaultPath(vaultRoot, req.body?.path);
|
||||||
if (!resolved) return res.status(403).json({ error: "Invalid path" });
|
if (!resolved) return res.status(403).json({ error: "Invalid path" });
|
||||||
try {
|
try {
|
||||||
await fs.promises.utimes(
|
await fs.promises.utimes(
|
||||||
|
|
@ -271,11 +293,13 @@ router.post("/utimes", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/fs/tree?path=... - returns full recursive file tree with metadata
|
// GET /api/fs/tree?path=...&vault=... - returns full recursive file tree with metadata
|
||||||
router.get("/tree", async (req, res) => {
|
router.get("/tree", async (req, res) => {
|
||||||
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
|
if (!vaultRoot) return;
|
||||||
const rootPath = req.query.path
|
const rootPath = req.query.path
|
||||||
? resolveVaultPath(req.query.path)
|
? resolveVaultPath(vaultRoot, req.query.path)
|
||||||
: config.vaultPath;
|
: vaultRoot;
|
||||||
if (!rootPath) return res.status(403).json({ error: "Invalid path" });
|
if (!rootPath) return res.status(403).json({ error: "Invalid path" });
|
||||||
try {
|
try {
|
||||||
const tree = {};
|
const tree = {};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,73 @@
|
||||||
const express = require('express');
|
const express = require("express");
|
||||||
const config = require('../config');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const config = require("../config");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// GET /api/vault/info
|
// GET /api/vault/list - returns all discovered vaults (re-scans on each call)
|
||||||
router.get('/info', (req, res) => {
|
router.get("/list", (req, res) => {
|
||||||
|
config.refreshVaults();
|
||||||
|
const list = Object.entries(config.vaults).map(([id, vaultPath]) => ({
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
path: vaultPath,
|
||||||
|
}));
|
||||||
|
res.json(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/vault/info?vault=<id> - returns info for a specific vault
|
||||||
|
router.get("/info", (req, res) => {
|
||||||
|
const vaultId = req.query.vault || config.defaultVaultId;
|
||||||
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
if (!vaultPath) {
|
||||||
|
return res.status(404).json({ error: "Vault not found", id: vaultId });
|
||||||
|
}
|
||||||
res.json({
|
res.json({
|
||||||
name: path.basename(config.vaultPath),
|
id: vaultId,
|
||||||
|
name: vaultId,
|
||||||
|
path: vaultPath,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
version: '0.1.0',
|
version: "0.1.0",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/vault/create { name } - create a new vault in VAULT_ROOT
|
||||||
|
router.post("/create", async (req, res) => {
|
||||||
|
const name = req.body?.name;
|
||||||
|
if (!name || /[\/\\:*?"<>|]/.test(name)) {
|
||||||
|
return res.status(400).json({ error: "Invalid vault name" });
|
||||||
|
}
|
||||||
|
const vaultPath = path.join(config.vaultRoot, name);
|
||||||
|
try {
|
||||||
|
await fs.promises.mkdir(vaultPath, { recursive: false });
|
||||||
|
await fs.promises.mkdir(path.join(vaultPath, ".obsidian"), {
|
||||||
|
recursive: false,
|
||||||
|
});
|
||||||
|
config.refreshVaults();
|
||||||
|
res.json({ ok: true, id: name, path: vaultPath });
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === "EEXIST") {
|
||||||
|
return res.status(409).json({ error: "Vault already exists" });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/vault/remove?vault=<id> - remove a vault from disk
|
||||||
|
router.delete("/remove", async (req, res) => {
|
||||||
|
const vaultId = req.query.vault;
|
||||||
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
if (!vaultPath) {
|
||||||
|
return res.status(404).json({ error: "Vault not found" });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fs.promises.rm(vaultPath, { recursive: true });
|
||||||
|
config.refreshVaults();
|
||||||
|
res.json({ ok: true });
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
// sandbox → void - open sandbox vault
|
// sandbox → void - open sandbox vault
|
||||||
// copy-asar → boolean - install update
|
// copy-asar → boolean - install update
|
||||||
|
|
||||||
|
import { showVaultManager } from "../ui/vault-manager.js";
|
||||||
|
|
||||||
const listeners = new Map();
|
const listeners = new Map();
|
||||||
|
|
||||||
// Sync channel handlers - must return values synchronously
|
// Sync channel handlers - must return values synchronously
|
||||||
|
|
@ -25,7 +27,8 @@ const syncHandlers = {
|
||||||
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
|
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
|
||||||
version: () => "1.8.9",
|
version: () => "1.8.9",
|
||||||
"is-dev": () => false,
|
"is-dev": () => false,
|
||||||
"file-url": () => "/vault-files/",
|
"file-url": () =>
|
||||||
|
"/vault-files/" + encodeURIComponent(window.__currentVaultId || "") + "/",
|
||||||
"disable-update": () => true,
|
"disable-update": () => true,
|
||||||
update: () => "",
|
update: () => "",
|
||||||
"disable-gpu": () => false,
|
"disable-gpu": () => false,
|
||||||
|
|
@ -36,7 +39,10 @@ const syncHandlers = {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
starter: () => null,
|
starter: () => {
|
||||||
|
showVaultManager();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
help: () => {
|
help: () => {
|
||||||
window.open("https://help.obsidian.md/", "_blank");
|
window.open("https://help.obsidian.md/", "_blank");
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -44,6 +50,55 @@ const syncHandlers = {
|
||||||
sandbox: () => null,
|
sandbox: () => null,
|
||||||
"copy-asar": () => false,
|
"copy-asar": () => false,
|
||||||
"check-update": () => null,
|
"check-update": () => null,
|
||||||
|
"vault-list": () => {
|
||||||
|
// Starter expects an object keyed by ID: {id: {path, ts, name}}
|
||||||
|
const result = {};
|
||||||
|
for (const v of window.__vaultList || []) {
|
||||||
|
result[v.id] = {
|
||||||
|
path: "/" + v.id,
|
||||||
|
ts: Date.now(),
|
||||||
|
open: v.id === (window.__currentVaultId || ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
"vault-open": (vaultPath, newWindow) => {
|
||||||
|
const id = (vaultPath || "").replace(/^\/+/, "");
|
||||||
|
const vault = (window.__vaultList || []).find((v) => v.id === id);
|
||||||
|
if (!vault && id) {
|
||||||
|
// New vault created by starter - create it on the server
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", "/api/vault/create", false);
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/json");
|
||||||
|
xhr.send(JSON.stringify({ name: id }));
|
||||||
|
if (xhr.status >= 400) return "Failed to create vault";
|
||||||
|
}
|
||||||
|
// Navigate - use parent if in iframe, otherwise current window
|
||||||
|
const target = window.parent !== window ? window.parent : window;
|
||||||
|
target.location.href = "/?vault=" + encodeURIComponent(id);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
"vault-remove": (vaultPath) => {
|
||||||
|
const id = (vaultPath || "").replace(/^\/+/, "");
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(
|
||||||
|
"DELETE",
|
||||||
|
"/api/vault/remove?vault=" + encodeURIComponent(id),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
xhr.send();
|
||||||
|
return xhr.status < 400;
|
||||||
|
},
|
||||||
|
"vault-move": (oldPath, newPath) => {
|
||||||
|
// Not supported in web context
|
||||||
|
return "Moving vaults is not supported in the web version";
|
||||||
|
},
|
||||||
|
"vault-message": () => null,
|
||||||
|
"get-default-vault-path": () => "/My Vault",
|
||||||
|
"get-documents-path": () => "/",
|
||||||
|
"desktop-dir": () => "/desktop",
|
||||||
|
"documents-dir": () => "/documents",
|
||||||
|
resources: () => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ipcRenderer = {
|
export const ipcRenderer = {
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,23 @@ function uint8ToBase64(bytes) {
|
||||||
return btoa(binary);
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vaultId() {
|
||||||
|
return window.__currentVaultId || "";
|
||||||
|
}
|
||||||
|
|
||||||
async function request(method, endpoint, params = {}) {
|
async function request(method, endpoint, params = {}) {
|
||||||
const url = new URL(API_BASE + endpoint, window.location.origin);
|
const url = new URL(API_BASE + endpoint, window.location.origin);
|
||||||
|
|
||||||
const options = { method };
|
const options = { method };
|
||||||
|
|
||||||
if (method === "GET" || method === "DELETE") {
|
if (method === "GET" || method === "DELETE") {
|
||||||
|
if (vaultId()) url.searchParams.set("vault", vaultId());
|
||||||
for (const [key, val] of Object.entries(params)) {
|
for (const [key, val] of Object.entries(params)) {
|
||||||
url.searchParams.set(key, val);
|
url.searchParams.set(key, val);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
options.headers = { "Content-Type": "application/json" };
|
options.headers = { "Content-Type": "application/json" };
|
||||||
options.body = JSON.stringify(params);
|
options.body = JSON.stringify({ vault: vaultId(), ...params });
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url.toString(), options);
|
const res = await fetch(url.toString(), options);
|
||||||
|
|
@ -57,6 +62,7 @@ function requestSync(method, endpoint, params = {}) {
|
||||||
const url = new URL(API_BASE + endpoint, window.location.origin);
|
const url = new URL(API_BASE + endpoint, window.location.origin);
|
||||||
|
|
||||||
if (method === "GET") {
|
if (method === "GET") {
|
||||||
|
if (vaultId()) url.searchParams.set("vault", vaultId());
|
||||||
for (const [key, val] of Object.entries(params)) {
|
for (const [key, val] of Object.entries(params)) {
|
||||||
url.searchParams.set(key, val);
|
url.searchParams.set(key, val);
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +73,7 @@ function requestSync(method, endpoint, params = {}) {
|
||||||
|
|
||||||
if (method !== "GET") {
|
if (method !== "GET") {
|
||||||
xhr.setRequestHeader("Content-Type", "application/json");
|
xhr.setRequestHeader("Content-Type", "application/json");
|
||||||
xhr.send(JSON.stringify(params));
|
xhr.send(JSON.stringify({ vault: vaultId(), ...params }));
|
||||||
} else {
|
} else {
|
||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,8 +114,14 @@ if (typeof window.Buffer === "undefined") {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent app.js from closing the window (browser blocks this anyway, but suppress the error)
|
// Prevent app.js from closing the window (browser blocks this anyway, but suppress the error)
|
||||||
|
// In an iframe (starter modal), close the modal overlay instead.
|
||||||
const _origClose = window.close;
|
const _origClose = window.close;
|
||||||
window.close = function () {
|
window.close = function () {
|
||||||
|
if (window.parent !== window) {
|
||||||
|
const modal = window.parent.document.getElementById("ignis-starter-modal");
|
||||||
|
if (modal) modal.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("[obsidian-bridge] window.close() blocked");
|
console.log("[obsidian-bridge] window.close() blocked");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -132,17 +138,60 @@ window.addEventListener(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Read vault ID from URL query param (?vault=my-notes)
|
||||||
|
const _urlParams = new URLSearchParams(window.location.search);
|
||||||
|
window.__currentVaultId = _urlParams.get("vault") || "";
|
||||||
|
|
||||||
|
// Fetch vault config from server synchronously (before metadata cache)
|
||||||
|
(function initVaultConfig() {
|
||||||
|
try {
|
||||||
|
const vaultParam = window.__currentVaultId
|
||||||
|
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
||||||
|
: "";
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/api/vault/info" + vaultParam, false);
|
||||||
|
xhr.send();
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const info = JSON.parse(xhr.responseText);
|
||||||
|
window.__currentVaultId = info.id;
|
||||||
|
window.__vaultConfig = {
|
||||||
|
id: info.id,
|
||||||
|
path: "/",
|
||||||
|
};
|
||||||
|
console.log("[obsidian-bridge] Vault:", window.__vaultConfig);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[obsidian-bridge] Failed to fetch vault config:", e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Fetch vault list for IPC handlers
|
||||||
|
(function initVaultList() {
|
||||||
|
try {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/api/vault/list", false);
|
||||||
|
xhr.send();
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
window.__vaultList = JSON.parse(xhr.responseText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
window.__vaultList = [];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Pre-populate fs metadata cache synchronously before app.js runs.
|
// Pre-populate fs metadata cache synchronously before app.js runs.
|
||||||
// This ensures existsSync() works for the vault path during startup.
|
// This ensures existsSync() works for the vault path during startup.
|
||||||
(function initMetadataCache() {
|
(function initMetadataCache() {
|
||||||
try {
|
try {
|
||||||
|
const vaultParam = window.__currentVaultId
|
||||||
|
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
||||||
|
: "";
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open("GET", "/api/fs/tree", false); // synchronous
|
xhr.open("GET", "/api/fs/tree" + vaultParam, false);
|
||||||
xhr.send();
|
xhr.send();
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
const tree = JSON.parse(xhr.responseText);
|
const tree = JSON.parse(xhr.responseText);
|
||||||
fsShim._metadataCache.populate(tree);
|
fsShim._metadataCache.populate(tree);
|
||||||
// Also add the root path itself
|
|
||||||
fsShim._metadataCache.set("", { type: "directory" });
|
fsShim._metadataCache.set("", { type: "directory" });
|
||||||
fsShim._metadataCache.set("/", { type: "directory" });
|
fsShim._metadataCache.set("/", { type: "directory" });
|
||||||
console.log(
|
console.log(
|
||||||
|
|
@ -161,24 +210,4 @@ window.addEventListener(
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Fetch vault config from server synchronously
|
|
||||||
(function initVaultConfig() {
|
|
||||||
try {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open("GET", "/api/vault/info", false); // synchronous
|
|
||||||
xhr.send();
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
const info = JSON.parse(xhr.responseText);
|
|
||||||
// Set the vault config that sendSync('vault') will return
|
|
||||||
window.__vaultConfig = {
|
|
||||||
id: info.name || "default-vault",
|
|
||||||
path: "/",
|
|
||||||
};
|
|
||||||
console.log("[obsidian-bridge] Vault config:", window.__vaultConfig);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[obsidian-bridge] Failed to fetch vault config:", e);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
console.log("[obsidian-bridge] Shim loader initialized");
|
console.log("[obsidian-bridge] Shim loader initialized");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
// Path shim - delegates to path-browserify (bundled via esbuild alias)
|
// Path shim - delegates to path-browserify (bundled via esbuild alias)
|
||||||
// Configured for posix mode since vault paths are normalized to forward slashes.
|
// Configured for posix mode since vault paths are normalized to forward slashes.
|
||||||
|
|
||||||
import pathBrowserify from 'path';
|
import pathBrowserify from "path";
|
||||||
|
|
||||||
export const pathShim = pathBrowserify;
|
const _origBasename = pathBrowserify.basename;
|
||||||
|
|
||||||
|
export const pathShim = {
|
||||||
|
...pathBrowserify,
|
||||||
|
basename(p, ext) {
|
||||||
|
// Vault root "/" should return the vault name for display purposes
|
||||||
|
if (p === "/" && window.__currentVaultId) {
|
||||||
|
return window.__currentVaultId;
|
||||||
|
}
|
||||||
|
return _origBasename(p, ext);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
163
shims/ui/vault-manager.js
Normal file
163
shims/ui/vault-manager.js
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
// Custom vault manager modal - vanilla JS (will migrate to Svelte later)
|
||||||
|
// Shows list of vaults, create new, delete, switch.
|
||||||
|
|
||||||
|
export function showVaultManager() {
|
||||||
|
if (!document.querySelector(".workspace")) return;
|
||||||
|
if (document.getElementById("ignis-starter-modal")) return;
|
||||||
|
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.id = "ignis-starter-modal";
|
||||||
|
overlay.style.cssText =
|
||||||
|
"position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.6);" +
|
||||||
|
"display:flex;align-items:center;justify-content:center;font-family:var(--font-interface);";
|
||||||
|
|
||||||
|
const modal = document.createElement("div");
|
||||||
|
modal.style.cssText =
|
||||||
|
"background:var(--background-primary);color:var(--text-normal);border-radius:12px;" +
|
||||||
|
"padding:24px;width:min(480px,90vw);max-height:80vh;overflow-y:auto;box-shadow:0 16px 48px rgba(0,0,0,0.4);";
|
||||||
|
|
||||||
|
const title = document.createElement("h2");
|
||||||
|
title.textContent = "Vaults";
|
||||||
|
title.style.cssText = "margin:0 0 16px 0;font-size:18px;font-weight:600;";
|
||||||
|
modal.appendChild(title);
|
||||||
|
|
||||||
|
const listEl = document.createElement("div");
|
||||||
|
listEl.style.cssText =
|
||||||
|
"display:flex;flex-direction:column;gap:4px;margin-bottom:16px;";
|
||||||
|
modal.appendChild(listEl);
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/api/vault/list", false);
|
||||||
|
xhr.send();
|
||||||
|
const vaults = xhr.status === 200 ? JSON.parse(xhr.responseText) : [];
|
||||||
|
listEl.innerHTML = "";
|
||||||
|
if (vaults.length === 0) {
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.textContent = "No vaults yet. Create one below.";
|
||||||
|
empty.style.cssText = "color:var(--text-muted);padding:12px 0;";
|
||||||
|
listEl.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const v of vaults) {
|
||||||
|
const isCurrent = v.id === (window.__currentVaultId || "");
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.cssText =
|
||||||
|
"display:flex;align-items:center;justify-content:space-between;" +
|
||||||
|
"padding:8px 12px;border-radius:6px;cursor:pointer;" +
|
||||||
|
"background:var(--background-secondary);";
|
||||||
|
row.addEventListener(
|
||||||
|
"mouseenter",
|
||||||
|
() => (row.style.background = "var(--background-modifier-hover)"),
|
||||||
|
);
|
||||||
|
row.addEventListener(
|
||||||
|
"mouseleave",
|
||||||
|
() => (row.style.background = "var(--background-secondary)"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.textContent = v.name;
|
||||||
|
name.style.cssText = "font-weight:500;flex:1;";
|
||||||
|
if (isCurrent) {
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.textContent = " current";
|
||||||
|
badge.style.cssText =
|
||||||
|
"font-size:11px;color:var(--text-muted);font-weight:400;";
|
||||||
|
name.appendChild(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
const del = document.createElement("button");
|
||||||
|
del.textContent = "Delete";
|
||||||
|
del.style.cssText =
|
||||||
|
"background:none;border:1px solid var(--background-modifier-border);" +
|
||||||
|
"color:var(--text-muted);border-radius:4px;padding:2px 8px;font-size:12px;cursor:pointer;";
|
||||||
|
del.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
'Delete vault "' + v.name + '"? This removes all files.',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const xhr2 = new XMLHttpRequest();
|
||||||
|
xhr2.open(
|
||||||
|
"DELETE",
|
||||||
|
"/api/vault/remove?vault=" + encodeURIComponent(v.id),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
xhr2.send();
|
||||||
|
renderList();
|
||||||
|
if (isCurrent) window.location.href = "/";
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
if (isCurrent) {
|
||||||
|
overlay.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = "/?vault=" + encodeURIComponent(v.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(name);
|
||||||
|
row.appendChild(del);
|
||||||
|
listEl.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList();
|
||||||
|
|
||||||
|
const form = document.createElement("div");
|
||||||
|
form.style.cssText = "display:flex;gap:8px;";
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.placeholder = "New vault name...";
|
||||||
|
input.style.cssText =
|
||||||
|
"flex:1;padding:6px 10px;border-radius:6px;border:1px solid var(--background-modifier-border);" +
|
||||||
|
"background:var(--background-secondary);color:var(--text-normal);font-size:14px;outline:none;";
|
||||||
|
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.textContent = "Create";
|
||||||
|
btn.style.cssText =
|
||||||
|
"padding:6px 16px;border-radius:6px;border:none;" +
|
||||||
|
"background:var(--interactive-accent);color:var(--text-on-accent);font-size:14px;cursor:pointer;font-weight:500;";
|
||||||
|
|
||||||
|
function createVault() {
|
||||||
|
const name = input.value.trim();
|
||||||
|
if (!name) return;
|
||||||
|
const xhr3 = new XMLHttpRequest();
|
||||||
|
xhr3.open("POST", "/api/vault/create", false);
|
||||||
|
xhr3.setRequestHeader("Content-Type", "application/json");
|
||||||
|
xhr3.send(JSON.stringify({ name }));
|
||||||
|
if (xhr3.status >= 400) {
|
||||||
|
alert(
|
||||||
|
"Failed to create vault: " +
|
||||||
|
(JSON.parse(xhr3.responseText).error || "Unknown error"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.value = "";
|
||||||
|
window.location.href = "/?vault=" + encodeURIComponent(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener("click", createVault);
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") createVault();
|
||||||
|
});
|
||||||
|
|
||||||
|
form.appendChild(input);
|
||||||
|
form.appendChild(btn);
|
||||||
|
modal.appendChild(form);
|
||||||
|
|
||||||
|
overlay.addEventListener("click", (e) => {
|
||||||
|
if (e.target === overlay) overlay.remove();
|
||||||
|
});
|
||||||
|
overlay.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") overlay.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
overlay.appendChild(modal);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue