some styling cleanup
This commit is contained in:
parent
c70b9e9d0f
commit
0738c47ac5
27 changed files with 479 additions and 105 deletions
|
|
@ -3,7 +3,7 @@ const fs = require("fs");
|
||||||
|
|
||||||
// VAULT_ROOT: a directory that contains vault folders.
|
// VAULT_ROOT: a directory that contains vault folders.
|
||||||
// Each subdirectory is a vault. New vaults are created as new subdirs.
|
// Each subdirectory is a vault. New vaults are created as new subdirs.
|
||||||
// Falls back to parent of VAULT_PATH (single-vault compat) or ./vaults.
|
// Falls back to parent of VAULT_PATH (single-vault compatibility) or ./vaults.
|
||||||
const vaultRoot =
|
const vaultRoot =
|
||||||
process.env.VAULT_ROOT ||
|
process.env.VAULT_ROOT ||
|
||||||
(process.env.VAULT_PATH
|
(process.env.VAULT_PATH
|
||||||
|
|
@ -19,8 +19,10 @@ try {
|
||||||
|
|
||||||
function discoverVaults() {
|
function discoverVaults() {
|
||||||
const vaults = {};
|
const vaults = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = fs.readdirSync(vaultRoot, { withFileTypes: true });
|
const entries = fs.readdirSync(vaultRoot, { withFileTypes: true });
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
||||||
vaults[entry.name] = path.join(vaultRoot, entry.name);
|
vaults[entry.name] = path.join(vaultRoot, entry.name);
|
||||||
|
|
@ -29,12 +31,15 @@ function discoverVaults() {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[config] Failed to read VAULT_ROOT:", vaultRoot, e.message);
|
console.error("[config] Failed to read VAULT_ROOT:", vaultRoot, e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a default vault if none exist
|
// Create a default vault if none exist
|
||||||
if (Object.keys(vaults).length === 0) {
|
if (Object.keys(vaults).length === 0) {
|
||||||
const defaultPath = path.join(vaultRoot, "My Vault");
|
const defaultPath = path.join(vaultRoot, "My Vault");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.join(defaultPath, ".obsidian"), { recursive: true });
|
fs.mkdirSync(path.join(defaultPath, ".obsidian"), { recursive: true });
|
||||||
vaults["My Vault"] = defaultPath;
|
vaults["My Vault"] = defaultPath;
|
||||||
|
|
||||||
console.log("[config] Created default vault: My Vault");
|
console.log("[config] Created default vault: My Vault");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[config] Failed to create default vault:", e.message);
|
console.error("[config] Failed to create default vault:", e.message);
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,39 @@ const path = require("path");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { setupWebSocket } = require("./ws");
|
const { setupWebSocket } = require("./ws");
|
||||||
|
|
||||||
|
const ANSI_RED = "\x1b[31m";
|
||||||
|
const ANSI_YELLOW = "\x1b[33m";
|
||||||
|
const ANSI_GREEN = "\x1b[32m";
|
||||||
|
const ANSI_RESET = "\x1b[0m";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(express.json({ limit: "50mb" }));
|
app.use(express.json({ limit: "50mb" }));
|
||||||
|
|
||||||
|
// logger middleware
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const origEnd = res.end;
|
const origEnd = res.end;
|
||||||
|
|
||||||
res.end = function (...args) {
|
res.end = function (...args) {
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
const status = res.statusCode;
|
const status = res.statusCode;
|
||||||
|
|
||||||
const color =
|
const color =
|
||||||
status >= 500 ? "\x1b[31m" : status >= 400 ? "\x1b[33m" : "\x1b[32m";
|
status >= 500 ? ANSI_RED : status >= 400 ? ANSI_YELLOW : ANSI_GREEN;
|
||||||
const reset = "\x1b[0m";
|
|
||||||
const path =
|
const path =
|
||||||
req.originalUrl.length > 80
|
req.originalUrl.length > 80
|
||||||
? req.originalUrl.slice(0, 80) + "..."
|
? req.originalUrl.slice(0, 80) + "..."
|
||||||
: req.originalUrl;
|
: req.originalUrl;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${color}${req.method} ${status}${reset} ${path} (${duration}ms)`,
|
`${color}${req.method} ${status}${ANSI_RESET} ${path} (${duration}ms)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
origEnd.apply(this, args);
|
origEnd.apply(this, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -41,11 +52,18 @@ app.use("/api/proxy", proxyRoutes);
|
||||||
app.use("/vault-files", (req, res, next) => {
|
app.use("/vault-files", (req, res, next) => {
|
||||||
// Extract vault ID from the first path segment
|
// Extract vault ID from the first path segment
|
||||||
const parts = req.path.split("/").filter(Boolean);
|
const parts = req.path.split("/").filter(Boolean);
|
||||||
if (parts.length === 0)
|
|
||||||
|
if (parts.length === 0) {
|
||||||
return res.status(400).json({ error: "Missing vault ID" });
|
return res.status(400).json({ error: "Missing vault ID" });
|
||||||
|
}
|
||||||
|
|
||||||
const vaultId = decodeURIComponent(parts[0]);
|
const vaultId = decodeURIComponent(parts[0]);
|
||||||
const vaultPath = config.getVaultPath(vaultId);
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
if (!vaultPath) return res.status(404).json({ error: "Vault not found" });
|
|
||||||
|
if (!vaultPath) {
|
||||||
|
return res.status(404).json({ error: "Vault not found" });
|
||||||
|
}
|
||||||
|
|
||||||
// Rewrite req.url to strip the vault ID prefix, then serve statically
|
// Rewrite req.url to strip the vault ID prefix, then serve statically
|
||||||
req.url = "/" + parts.slice(1).join("/");
|
req.url = "/" + parts.slice(1).join("/");
|
||||||
express.static(vaultPath)(req, res, next);
|
express.static(vaultPath)(req, res, next);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const router = express.Router();
|
||||||
function getVaultRoot(req, res) {
|
function getVaultRoot(req, res) {
|
||||||
const vaultId = req.query.vault || req.body?.vault || config.defaultVaultId;
|
const vaultId = req.query.vault || req.body?.vault || config.defaultVaultId;
|
||||||
const vaultPath = config.getVaultPath(vaultId);
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
|
||||||
if (!vaultPath) {
|
if (!vaultPath) {
|
||||||
res.status(404).json({ error: "Vault not found", id: vaultId });
|
res.status(404).json({ error: "Vault not found", id: vaultId });
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -21,39 +22,35 @@ function getVaultRoot(req, res) {
|
||||||
function resolveVaultPath(vaultRoot, relativePath) {
|
function resolveVaultPath(vaultRoot, relativePath) {
|
||||||
const cleaned = (relativePath || "").replace(/^\/+/, "");
|
const cleaned = (relativePath || "").replace(/^\/+/, "");
|
||||||
const resolved = path.resolve(vaultRoot, cleaned);
|
const resolved = path.resolve(vaultRoot, cleaned);
|
||||||
|
|
||||||
if (!resolved.startsWith(path.resolve(vaultRoot))) {
|
if (!resolved.startsWith(path.resolve(vaultRoot))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
function guardPath(req, res) {
|
function guardPath(req, res, source = "query") {
|
||||||
const vaultRoot = getVaultRoot(req, res);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
if (!vaultRoot) return null;
|
|
||||||
const p = req.query.path ?? req.body?.path;
|
if (!vaultRoot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = source === "body" ? req.body?.path : req.query.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(vaultRoot, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same as guardPath but reads path from req.body (POST routes)
|
|
||||||
function guardBodyPath(req, res) {
|
|
||||||
const vaultRoot = getVaultRoot(req, res);
|
|
||||||
if (!vaultRoot) return null;
|
|
||||||
const resolved = resolveVaultPath(vaultRoot, req.body?.path);
|
|
||||||
if (!resolved) {
|
|
||||||
res.status(403).json({ error: "Invalid path" });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
req._vaultRoot = vaultRoot;
|
req._vaultRoot = vaultRoot;
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
@ -61,9 +58,14 @@ function guardBodyPath(req, res) {
|
||||||
// GET /api/fs/stat?path=...
|
// GET /api/fs/stat?path=...
|
||||||
router.get("/stat", async (req, res) => {
|
router.get("/stat", async (req, res) => {
|
||||||
const resolved = guardPath(req, res);
|
const resolved = guardPath(req, res);
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stat = await fs.promises.stat(resolved);
|
const stat = await fs.promises.stat(resolved);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
type: stat.isDirectory() ? "directory" : "file",
|
type: stat.isDirectory() ? "directory" : "file",
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
|
|
@ -80,18 +82,25 @@ router.get("/stat", async (req, res) => {
|
||||||
// GET /api/fs/readdir?path=...
|
// GET /api/fs/readdir?path=...
|
||||||
router.get("/readdir", async (req, res) => {
|
router.get("/readdir", async (req, res) => {
|
||||||
const resolved = guardPath(req, res);
|
const resolved = guardPath(req, res);
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if path is a file. return ENOTDIR instead of crashing
|
// Check if path is a file. return ENOTDIR instead of crashing
|
||||||
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)
|
.status(400)
|
||||||
.json({ error: "ENOTDIR: not a directory", code: "ENOTDIR" });
|
.json({ error: "ENOTDIR: not a directory", code: "ENOTDIR" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = await fs.promises.readdir(resolved, {
|
const entries = await fs.promises.readdir(resolved, {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
entries.map((e) => ({
|
entries.map((e) => ({
|
||||||
name: e.name,
|
name: e.name,
|
||||||
|
|
@ -108,22 +117,30 @@ router.get("/readdir", async (req, res) => {
|
||||||
// GET /api/fs/readFile?path=...&encoding=...
|
// GET /api/fs/readFile?path=...&encoding=...
|
||||||
router.get("/readFile", async (req, res) => {
|
router.get("/readFile", async (req, res) => {
|
||||||
const resolved = guardPath(req, res);
|
const resolved = guardPath(req, res);
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 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.status(400).json({
|
return res.status(400).json({
|
||||||
error: "EISDIR: illegal operation on a directory",
|
error: "EISDIR: illegal operation on a directory",
|
||||||
code: "EISDIR",
|
code: "EISDIR",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const encoding = req.query.encoding;
|
const encoding = req.query.encoding;
|
||||||
|
|
||||||
if (encoding === "utf8" || encoding === "utf-8") {
|
if (encoding === "utf8" || encoding === "utf-8") {
|
||||||
const data = await fs.promises.readFile(resolved, "utf-8");
|
const data = await fs.promises.readFile(resolved, "utf-8");
|
||||||
|
|
||||||
res.type("text/plain").send(data);
|
res.type("text/plain").send(data);
|
||||||
} else {
|
} else {
|
||||||
const data = await fs.promises.readFile(resolved);
|
const data = await fs.promises.readFile(resolved);
|
||||||
|
|
||||||
res.type("application/octet-stream").send(data);
|
res.type("application/octet-stream").send(data);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -135,8 +152,12 @@ router.get("/readFile", async (req, res) => {
|
||||||
|
|
||||||
// POST /api/fs/writeFile { path, content, encoding?, vault? }
|
// POST /api/fs/writeFile { path, content, encoding?, vault? }
|
||||||
router.post("/writeFile", async (req, res) => {
|
router.post("/writeFile", async (req, res) => {
|
||||||
const resolved = guardBodyPath(req, res);
|
const resolved = guardPath(req, res, "body");
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure parent directory exists
|
// Ensure parent directory exists
|
||||||
const dir = path.dirname(resolved);
|
const dir = path.dirname(resolved);
|
||||||
|
|
@ -144,15 +165,19 @@ router.post("/writeFile", async (req, res) => {
|
||||||
|
|
||||||
const encoding = req.body.encoding || "utf-8";
|
const encoding = req.body.encoding || "utf-8";
|
||||||
let data = req.body.content;
|
let data = req.body.content;
|
||||||
|
|
||||||
if (req.body.base64) {
|
if (req.body.base64) {
|
||||||
data = Buffer.from(req.body.content, "base64");
|
data = Buffer.from(req.body.content, "base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.writeFile(
|
await fs.promises.writeFile(
|
||||||
resolved,
|
resolved,
|
||||||
data,
|
data,
|
||||||
encoding === "binary" ? undefined : encoding,
|
encoding === "binary" ? undefined : encoding,
|
||||||
);
|
);
|
||||||
|
|
||||||
const stat = await fs.promises.stat(resolved);
|
const stat = await fs.promises.stat(resolved);
|
||||||
|
|
||||||
res.json({ ok: true, mtime: stat.mtimeMs, size: stat.size });
|
res.json({ ok: true, mtime: stat.mtimeMs, size: stat.size });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -161,10 +186,15 @@ router.post("/writeFile", async (req, res) => {
|
||||||
|
|
||||||
// POST /api/fs/appendFile { path, content, vault? }
|
// POST /api/fs/appendFile { path, content, vault? }
|
||||||
router.post("/appendFile", async (req, res) => {
|
router.post("/appendFile", async (req, res) => {
|
||||||
const resolved = guardBodyPath(req, res);
|
const resolved = guardPath(req, res, "body");
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.appendFile(resolved, req.body.content, "utf-8");
|
await fs.promises.appendFile(resolved, req.body.content, "utf-8");
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -173,10 +203,15 @@ router.post("/appendFile", async (req, res) => {
|
||||||
|
|
||||||
// POST /api/fs/mkdir { path, recursive?, vault? }
|
// POST /api/fs/mkdir { path, recursive?, vault? }
|
||||||
router.post("/mkdir", async (req, res) => {
|
router.post("/mkdir", async (req, res) => {
|
||||||
const resolved = guardBodyPath(req, res);
|
const resolved = guardPath(req, res, "body");
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.mkdir(resolved, { recursive: !!req.body.recursive });
|
await fs.promises.mkdir(resolved, { recursive: !!req.body.recursive });
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -186,13 +221,21 @@ router.post("/mkdir", async (req, res) => {
|
||||||
// POST /api/fs/rename { oldPath, newPath, vault? }
|
// POST /api/fs/rename { oldPath, newPath, vault? }
|
||||||
router.post("/rename", async (req, res) => {
|
router.post("/rename", async (req, res) => {
|
||||||
const vaultRoot = getVaultRoot(req, res);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
if (!vaultRoot) return;
|
|
||||||
|
if (!vaultRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const oldResolved = resolveVaultPath(vaultRoot, req.body?.oldPath);
|
const oldResolved = resolveVaultPath(vaultRoot, req.body?.oldPath);
|
||||||
const newResolved = resolveVaultPath(vaultRoot, req.body?.newPath);
|
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 {
|
||||||
await fs.promises.rename(oldResolved, newResolved);
|
await fs.promises.rename(oldResolved, newResolved);
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -202,13 +245,21 @@ router.post("/rename", async (req, res) => {
|
||||||
// POST /api/fs/copyFile { src, dest, vault? }
|
// POST /api/fs/copyFile { src, dest, vault? }
|
||||||
router.post("/copyFile", async (req, res) => {
|
router.post("/copyFile", async (req, res) => {
|
||||||
const vaultRoot = getVaultRoot(req, res);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
if (!vaultRoot) return;
|
|
||||||
|
if (!vaultRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const srcResolved = resolveVaultPath(vaultRoot, req.body?.src);
|
const srcResolved = resolveVaultPath(vaultRoot, req.body?.src);
|
||||||
const destResolved = resolveVaultPath(vaultRoot, req.body?.dest);
|
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 {
|
||||||
await fs.promises.copyFile(srcResolved, destResolved);
|
await fs.promises.copyFile(srcResolved, destResolved);
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -218,12 +269,18 @@ router.post("/copyFile", async (req, res) => {
|
||||||
// DELETE /api/fs/unlink?path=...
|
// DELETE /api/fs/unlink?path=...
|
||||||
router.delete("/unlink", async (req, res) => {
|
router.delete("/unlink", async (req, res) => {
|
||||||
const resolved = guardPath(req, res);
|
const resolved = guardPath(req, res);
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.unlink(resolved);
|
await fs.promises.unlink(resolved);
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const status = e.code === "ENOENT" ? 404 : 500;
|
const status = e.code === "ENOENT" ? 404 : 500;
|
||||||
|
|
||||||
res.status(status).json({ error: e.message, code: e.code });
|
res.status(status).json({ error: e.message, code: e.code });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -231,9 +288,14 @@ router.delete("/unlink", async (req, res) => {
|
||||||
// DELETE /api/fs/rmdir?path=...
|
// DELETE /api/fs/rmdir?path=...
|
||||||
router.delete("/rmdir", async (req, res) => {
|
router.delete("/rmdir", async (req, res) => {
|
||||||
const resolved = guardPath(req, res);
|
const resolved = guardPath(req, res);
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.rmdir(resolved);
|
await fs.promises.rmdir(resolved);
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -243,7 +305,11 @@ router.delete("/rmdir", async (req, res) => {
|
||||||
// DELETE /api/fs/rm?path=...&recursive=true
|
// DELETE /api/fs/rm?path=...&recursive=true
|
||||||
router.delete("/rm", async (req, res) => {
|
router.delete("/rm", async (req, res) => {
|
||||||
const resolved = guardPath(req, res);
|
const resolved = guardPath(req, res);
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.rm(resolved, {
|
await fs.promises.rm(resolved, {
|
||||||
recursive: req.query.recursive === "true",
|
recursive: req.query.recursive === "true",
|
||||||
|
|
@ -254,12 +320,16 @@ router.delete("/rm", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/fs/access?path=...
|
|
||||||
router.get("/access", async (req, res) => {
|
router.get("/access", async (req, res) => {
|
||||||
const resolved = guardPath(req, res);
|
const resolved = guardPath(req, res);
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.access(resolved);
|
await fs.promises.access(resolved);
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res
|
res
|
||||||
|
|
@ -268,13 +338,16 @@ router.get("/access", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/fs/realpath?path=...
|
|
||||||
router.get("/realpath", async (req, res) => {
|
router.get("/realpath", async (req, res) => {
|
||||||
const resolved = guardPath(req, res);
|
const resolved = guardPath(req, res);
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const real = await fs.promises.realpath(resolved);
|
const real = await fs.promises.realpath(resolved);
|
||||||
// Return path relative to vault root
|
|
||||||
res.json({ path: path.relative(req._vaultRoot, 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 });
|
||||||
|
|
@ -283,8 +356,12 @@ router.get("/realpath", async (req, res) => {
|
||||||
|
|
||||||
// POST /api/fs/utimes { path, atime, mtime, vault? }
|
// POST /api/fs/utimes { path, atime, mtime, vault? }
|
||||||
router.post("/utimes", async (req, res) => {
|
router.post("/utimes", async (req, res) => {
|
||||||
const resolved = guardBodyPath(req, res);
|
const resolved = guardPath(req, res, "body");
|
||||||
if (!resolved) return;
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.utimes(
|
await fs.promises.utimes(
|
||||||
resolved,
|
resolved,
|
||||||
|
|
@ -300,23 +377,36 @@ router.post("/utimes", async (req, res) => {
|
||||||
// GET /api/fs/tree?path=...&vault=... 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);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
if (!vaultRoot) return;
|
|
||||||
|
if (!vaultRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rootPath = req.query.path
|
const rootPath = req.query.path
|
||||||
? resolveVaultPath(vaultRoot, req.query.path)
|
? resolveVaultPath(vaultRoot, req.query.path)
|
||||||
: vaultRoot;
|
: 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 = {};
|
||||||
|
|
||||||
async function walk(dir, prefix) {
|
async function walk(dir, prefix) {
|
||||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const rel = prefix ? prefix + "/" + entry.name : entry.name;
|
const rel = prefix ? prefix + "/" + entry.name : entry.name;
|
||||||
const full = path.join(dir, entry.name);
|
const full = path.join(dir, entry.name);
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
tree[rel] = { type: "directory" };
|
tree[rel] = { type: "directory" };
|
||||||
|
|
||||||
await walk(full, rel);
|
await walk(full, rel);
|
||||||
} else {
|
} else {
|
||||||
const stat = await fs.promises.stat(full);
|
const stat = await fs.promises.stat(full);
|
||||||
|
|
||||||
tree[rel] = {
|
tree[rel] = {
|
||||||
type: "file",
|
type: "file",
|
||||||
size: stat.size,
|
size: stat.size,
|
||||||
|
|
@ -326,7 +416,9 @@ router.get("/tree", async (req, res) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await walk(rootPath, "");
|
await walk(rootPath, "");
|
||||||
|
|
||||||
res.json(tree);
|
res.json(tree);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ const express = require("express");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// POST /api/proxy - forward a request to an external URL (bypasses browser CORS)
|
// POST /api/proxy - forward a request to an external URL to bypass CORS
|
||||||
// Used by the requestUrl shim for plugin installation, update checks, etc.
|
// Used by the requestUrl shim for plugin installation, etc.
|
||||||
router.post("/", async (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
const { url, method, headers, body, binary } = req.body;
|
const { url, method, headers, body, binary } = req.body;
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return res.status(400).json({ error: "Missing url" });
|
return res.status(400).json({ error: "Missing url" });
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +16,7 @@ router.post("/", async (req, res) => {
|
||||||
method: method || "GET",
|
method: method || "GET",
|
||||||
headers: headers || {},
|
headers: headers || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (body && method !== "GET" && method !== "HEAD") {
|
if (body && method !== "GET" && method !== "HEAD") {
|
||||||
if (binary && typeof body === "string") {
|
if (binary && typeof body === "string") {
|
||||||
fetchOpts.body = Buffer.from(body, "base64");
|
fetchOpts.body = Buffer.from(body, "base64");
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ const router = express.Router();
|
||||||
// GET /api/vault/list - returns all discovered vaults (re-scans on each call)
|
// GET /api/vault/list - returns all discovered vaults (re-scans on each call)
|
||||||
router.get("/list", (req, res) => {
|
router.get("/list", (req, res) => {
|
||||||
config.refreshVaults();
|
config.refreshVaults();
|
||||||
|
|
||||||
const list = Object.entries(config.vaults).map(([id, vaultPath]) => ({
|
const list = Object.entries(config.vaults).map(([id, vaultPath]) => ({
|
||||||
id,
|
id,
|
||||||
name: id,
|
name: id,
|
||||||
path: vaultPath,
|
path: vaultPath,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(list);
|
res.json(list);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -20,9 +22,11 @@ router.get("/list", (req, res) => {
|
||||||
router.get("/info", (req, res) => {
|
router.get("/info", (req, res) => {
|
||||||
const vaultId = req.query.vault || config.defaultVaultId;
|
const vaultId = req.query.vault || config.defaultVaultId;
|
||||||
const vaultPath = config.getVaultPath(vaultId);
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
|
||||||
if (!vaultPath) {
|
if (!vaultPath) {
|
||||||
return res.status(404).json({ error: "Vault not found", id: vaultId });
|
return res.status(404).json({ error: "Vault not found", id: vaultId });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
id: vaultId,
|
id: vaultId,
|
||||||
name: vaultId,
|
name: vaultId,
|
||||||
|
|
@ -35,21 +39,27 @@ router.get("/info", (req, res) => {
|
||||||
// POST /api/vault/create { name } - create a new vault in VAULT_ROOT
|
// POST /api/vault/create { name } - create a new vault in VAULT_ROOT
|
||||||
router.post("/create", async (req, res) => {
|
router.post("/create", async (req, res) => {
|
||||||
const name = req.body?.name;
|
const name = req.body?.name;
|
||||||
|
|
||||||
if (!name || /[\/\\:*?"<>|]/.test(name)) {
|
if (!name || /[\/\\:*?"<>|]/.test(name)) {
|
||||||
return res.status(400).json({ error: "Invalid vault name" });
|
return res.status(400).json({ error: "Invalid vault name" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaultPath = path.join(config.vaultRoot, name);
|
const vaultPath = path.join(config.vaultRoot, name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.mkdir(vaultPath, { recursive: false });
|
await fs.promises.mkdir(vaultPath, { recursive: false });
|
||||||
await fs.promises.mkdir(path.join(vaultPath, ".obsidian"), {
|
await fs.promises.mkdir(path.join(vaultPath, ".obsidian"), {
|
||||||
recursive: false,
|
recursive: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
config.refreshVaults();
|
config.refreshVaults();
|
||||||
|
|
||||||
res.json({ ok: true, id: name, path: vaultPath });
|
res.json({ ok: true, id: name, path: vaultPath });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === "EEXIST") {
|
if (e.code === "EEXIST") {
|
||||||
return res.status(409).json({ error: "Vault already exists" });
|
return res.status(409).json({ error: "Vault already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -58,12 +68,16 @@ router.post("/create", async (req, res) => {
|
||||||
router.delete("/remove", async (req, res) => {
|
router.delete("/remove", async (req, res) => {
|
||||||
const vaultId = req.query.vault;
|
const vaultId = req.query.vault;
|
||||||
const vaultPath = config.getVaultPath(vaultId);
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
|
||||||
if (!vaultPath) {
|
if (!vaultPath) {
|
||||||
return res.status(404).json({ error: "Vault not found" });
|
return res.status(404).json({ error: "Vault not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.rm(vaultPath, { recursive: true });
|
await fs.promises.rm(vaultPath, { recursive: true });
|
||||||
|
|
||||||
config.refreshVaults();
|
config.refreshVaults();
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
|
||||||
20
server/ws.js
20
server/ws.js
|
|
@ -1,24 +1,24 @@
|
||||||
const { WebSocketServer } = require('ws');
|
const { WebSocketServer } = require("ws");
|
||||||
|
|
||||||
|
//currently unused
|
||||||
function setupWebSocket(server) {
|
function setupWebSocket(server) {
|
||||||
const wss = new WebSocketServer({ server, path: '/ws' });
|
const wss = new WebSocketServer({ server, path: "/ws" });
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on("connection", (ws) => {
|
||||||
console.log('[ws] Client connected');
|
console.log("[ws] Client connected");
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on("message", (data) => {
|
||||||
// TODO: handle watch/unwatch subscriptions from client
|
// TODO: handle watch/unwatch subscriptions from client
|
||||||
const msg = JSON.parse(data);
|
const msg = JSON.parse(data);
|
||||||
console.log('[ws] Received:', msg);
|
console.log("[ws] Received:", msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on("close", () => {
|
||||||
console.log('[ws] Client disconnected');
|
console.log("[ws] Client disconnected");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: integrate chokidar file watching and broadcast changes
|
// TODO: maybe integrate chokidar file watching and broadcast changes
|
||||||
// This will be implemented once the sync strategy is finalized.
|
|
||||||
|
|
||||||
return wss;
|
return wss;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export function createHash(algorithm) {
|
export function createHash(algorithm) {
|
||||||
const alg = algorithm.toUpperCase().replace("-", "");
|
const alg = algorithm.toUpperCase().replace("-", "");
|
||||||
|
|
||||||
const subtleAlg =
|
const subtleAlg =
|
||||||
alg === "SHA256"
|
alg === "SHA256"
|
||||||
? "SHA-256"
|
? "SHA-256"
|
||||||
|
|
@ -16,32 +17,47 @@ export function createHash(algorithm) {
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
data = new TextEncoder().encode(data);
|
data = new TextEncoder().encode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const merged = new Uint8Array(inputData.length + data.length);
|
const merged = new Uint8Array(inputData.length + data.length);
|
||||||
|
|
||||||
merged.set(inputData);
|
merged.set(inputData);
|
||||||
merged.set(data, inputData.length);
|
merged.set(data, inputData.length);
|
||||||
|
|
||||||
inputData = merged;
|
inputData = merged;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
digest(encoding) {
|
digest(encoding) {
|
||||||
console.warn("[shim:crypto] createHash.digest - using placeholder");
|
console.warn("[shim:crypto] createHash.digest - using placeholder");
|
||||||
|
|
||||||
const hash = simpleHash(inputData);
|
const hash = simpleHash(inputData);
|
||||||
if (encoding === "hex") return hash;
|
|
||||||
if (encoding === "base64") return btoa(hash);
|
if (encoding === "hex") {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === "base64") {
|
||||||
|
return btoa(hash);
|
||||||
|
}
|
||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
},
|
},
|
||||||
|
|
||||||
async digestAsync(encoding) {
|
async digestAsync(encoding) {
|
||||||
const hashBuffer = await crypto.subtle.digest(subtleAlg, inputData);
|
const hashBuffer = await crypto.subtle.digest(subtleAlg, inputData);
|
||||||
const hashArray = new Uint8Array(hashBuffer);
|
const hashArray = new Uint8Array(hashBuffer);
|
||||||
|
|
||||||
if (encoding === "hex") {
|
if (encoding === "hex") {
|
||||||
return Array.from(hashArray)
|
return Array.from(hashArray)
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encoding === "base64") {
|
if (encoding === "base64") {
|
||||||
return btoa(String.fromCharCode(...hashArray));
|
return btoa(String.fromCharCode(...hashArray));
|
||||||
}
|
}
|
||||||
|
|
||||||
return hashArray;
|
return hashArray;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -49,8 +65,10 @@ export function createHash(algorithm) {
|
||||||
|
|
||||||
function simpleHash(data) {
|
function simpleHash(data) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
hash = ((hash << 5) - hash + data[i]) | 0;
|
hash = ((hash << 5) - hash + data[i]) | 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.abs(hash).toString(16).padStart(8, "0");
|
return Math.abs(hash).toString(16).padStart(8, "0");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ export function randomBytes(size) {
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encoding === "base64") {
|
if (encoding === "base64") {
|
||||||
return btoa(String.fromCharCode(...this));
|
return btoa(String.fromCharCode(...this));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TextDecoder().decode(this);
|
return new TextDecoder().decode(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,31 +6,40 @@ const syncHandlers = {
|
||||||
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
|
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
|
||||||
version: () => window.__obsidianVersion || "0.0.0",
|
version: () => window.__obsidianVersion || "0.0.0",
|
||||||
"is-dev": () => false,
|
"is-dev": () => false,
|
||||||
|
|
||||||
"file-url": () =>
|
"file-url": () =>
|
||||||
"/vault-files/" + encodeURIComponent(window.__currentVaultId || "") + "/",
|
"/vault-files/" + encodeURIComponent(window.__currentVaultId || "") + "/",
|
||||||
|
|
||||||
"disable-update": () => true,
|
"disable-update": () => true,
|
||||||
update: () => "",
|
update: () => "",
|
||||||
"disable-gpu": () => false,
|
"disable-gpu": () => false,
|
||||||
frame: () => null,
|
frame: () => null,
|
||||||
"set-icon": () => null,
|
"set-icon": () => null,
|
||||||
"get-icon": () => null,
|
"get-icon": () => null,
|
||||||
|
|
||||||
relaunch: () => {
|
relaunch: () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
starter: () => {
|
starter: () => {
|
||||||
showVaultManager();
|
showVaultManager();
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
help: () => {
|
help: () => {
|
||||||
window.open("https://help.obsidian.md/", "_blank");
|
window.open("https://help.obsidian.md/", "_blank");
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
sandbox: () => null,
|
sandbox: () => null,
|
||||||
|
|
||||||
"copy-asar": () => false,
|
"copy-asar": () => false,
|
||||||
"check-update": () => null,
|
"check-update": () => null,
|
||||||
|
|
||||||
"vault-list": () => {
|
"vault-list": () => {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
for (const v of window.__vaultList || []) {
|
for (const v of window.__vaultList || []) {
|
||||||
result[v.id] = {
|
result[v.id] = {
|
||||||
path: "/" + v.id,
|
path: "/" + v.id,
|
||||||
|
|
@ -38,36 +47,51 @@ const syncHandlers = {
|
||||||
open: v.id === (window.__currentVaultId || ""),
|
open: v.id === (window.__currentVaultId || ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
"vault-open": (vaultPath, newWindow) => {
|
"vault-open": (vaultPath, newWindow) => {
|
||||||
const id = (vaultPath || "").replace(/^\/+/, "");
|
const id = (vaultPath || "").replace(/^\/+/, "");
|
||||||
const vault = (window.__vaultList || []).find((v) => v.id === id);
|
const vault = (window.__vaultList || []).find((v) => v.id === id);
|
||||||
|
|
||||||
if (!vault && id) {
|
if (!vault && id) {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
xhr.open("POST", "/api/vault/create", false);
|
xhr.open("POST", "/api/vault/create", false);
|
||||||
xhr.setRequestHeader("Content-Type", "application/json");
|
xhr.setRequestHeader("Content-Type", "application/json");
|
||||||
xhr.send(JSON.stringify({ name: id }));
|
xhr.send(JSON.stringify({ name: id }));
|
||||||
if (xhr.status >= 400) return "Failed to create vault";
|
|
||||||
|
if (xhr.status >= 400) {
|
||||||
|
return "Failed to create vault";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const target = window.parent !== window ? window.parent : window;
|
const target = window.parent !== window ? window.parent : window;
|
||||||
target.location.href = "/?vault=" + encodeURIComponent(id);
|
target.location.href = "/?vault=" + encodeURIComponent(id);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
"vault-remove": (vaultPath) => {
|
"vault-remove": (vaultPath) => {
|
||||||
const id = (vaultPath || "").replace(/^\/+/, "");
|
const id = (vaultPath || "").replace(/^\/+/, "");
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
xhr.open(
|
xhr.open(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
"/api/vault/remove?vault=" + encodeURIComponent(id),
|
"/api/vault/remove?vault=" + encodeURIComponent(id),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
|
||||||
return xhr.status < 400;
|
return xhr.status < 400;
|
||||||
},
|
},
|
||||||
|
|
||||||
"vault-move": (oldPath, newPath) => {
|
"vault-move": (oldPath, newPath) => {
|
||||||
return "Moving vaults is not supported in the web version";
|
return "Moving vaults is not supported in the web version";
|
||||||
},
|
},
|
||||||
|
|
||||||
"vault-message": () => null,
|
"vault-message": () => null,
|
||||||
"get-default-vault-path": () => "/My Vault",
|
"get-default-vault-path": () => "/My Vault",
|
||||||
"get-documents-path": () => "/",
|
"get-documents-path": () => "/",
|
||||||
|
|
@ -80,18 +104,22 @@ function arrayBufferToBase64(buf) {
|
||||||
const bytes = new Uint8Array(buf);
|
const bytes = new Uint8Array(buf);
|
||||||
let binary = "";
|
let binary = "";
|
||||||
const chunk = 8192;
|
const chunk = 8192;
|
||||||
|
|
||||||
for (let i = 0; i < bytes.length; i += chunk) {
|
for (let i = 0; i < bytes.length; i += chunk) {
|
||||||
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
||||||
}
|
}
|
||||||
|
|
||||||
return btoa(binary);
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
function base64ToArrayBuffer(base64) {
|
function base64ToArrayBuffer(base64) {
|
||||||
const binary = atob(base64);
|
const binary = atob(base64);
|
||||||
const bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
|
||||||
for (let i = 0; i < binary.length; i++) {
|
for (let i = 0; i < binary.length; i++) {
|
||||||
bytes[i] = binary.charCodeAt(i);
|
bytes[i] = binary.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes.buffer;
|
return bytes.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +127,7 @@ async function handleRequestUrl(requestId, request) {
|
||||||
try {
|
try {
|
||||||
let body = request.body;
|
let body = request.body;
|
||||||
let binary = false;
|
let binary = false;
|
||||||
|
|
||||||
if (body instanceof ArrayBuffer) {
|
if (body instanceof ArrayBuffer) {
|
||||||
body = arrayBufferToBase64(body);
|
body = arrayBufferToBase64(body);
|
||||||
binary = true;
|
binary = true;
|
||||||
|
|
@ -118,6 +147,7 @@ async function handleRequestUrl(requestId, request) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const proxyResult = await res.json();
|
const proxyResult = await res.json();
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
ipcRenderer._emit(requestId, {
|
ipcRenderer._emit(requestId, {
|
||||||
error: proxyResult.error || "Proxy request failed",
|
error: proxyResult.error || "Proxy request failed",
|
||||||
|
|
@ -160,6 +190,7 @@ export const ipcRenderer = {
|
||||||
|
|
||||||
if (channel === "print-to-pdf") {
|
if (channel === "print-to-pdf") {
|
||||||
const iframe = window.__popupIframe;
|
const iframe = window.__popupIframe;
|
||||||
|
|
||||||
if (iframe) {
|
if (iframe) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
iframe.contentWindow.print();
|
iframe.contentWindow.print();
|
||||||
|
|
@ -170,6 +201,7 @@ export const ipcRenderer = {
|
||||||
}, 200);
|
}, 200);
|
||||||
} else {
|
} else {
|
||||||
window.print();
|
window.print();
|
||||||
|
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
ipcRenderer._emit("print-to-pdf", { success: true });
|
ipcRenderer._emit("print-to-pdf", { success: true });
|
||||||
});
|
});
|
||||||
|
|
@ -180,9 +212,11 @@ export const ipcRenderer = {
|
||||||
|
|
||||||
sendSync(channel, ...args) {
|
sendSync(channel, ...args) {
|
||||||
console.log("[shim:ipcRenderer] sendSync:", channel, args);
|
console.log("[shim:ipcRenderer] sendSync:", channel, args);
|
||||||
|
|
||||||
if (syncHandlers[channel]) {
|
if (syncHandlers[channel]) {
|
||||||
return syncHandlers[channel](...args);
|
return syncHandlers[channel](...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn("[shim:ipcRenderer] Unhandled sendSync channel:", channel);
|
console.warn("[shim:ipcRenderer] Unhandled sendSync channel:", channel);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
@ -191,7 +225,9 @@ export const ipcRenderer = {
|
||||||
if (!listeners.has(channel)) {
|
if (!listeners.has(channel)) {
|
||||||
listeners.set(channel, []);
|
listeners.set(channel, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners.get(channel).push(listener);
|
listeners.get(channel).push(listener);
|
||||||
|
|
||||||
return ipcRenderer;
|
return ipcRenderer;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -200,6 +236,7 @@ export const ipcRenderer = {
|
||||||
ipcRenderer.removeListener(channel, wrapped);
|
ipcRenderer.removeListener(channel, wrapped);
|
||||||
listener(...args);
|
listener(...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
return ipcRenderer.on(channel, wrapped);
|
return ipcRenderer.on(channel, wrapped);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -207,8 +244,12 @@ export const ipcRenderer = {
|
||||||
const arr = listeners.get(channel);
|
const arr = listeners.get(channel);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
const idx = arr.indexOf(listener);
|
const idx = arr.indexOf(listener);
|
||||||
if (idx >= 0) arr.splice(idx, 1);
|
|
||||||
|
if (idx >= 0) {
|
||||||
|
arr.splice(idx, 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ipcRenderer;
|
return ipcRenderer;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -218,11 +259,13 @@ export const ipcRenderer = {
|
||||||
} else {
|
} else {
|
||||||
listeners.clear();
|
listeners.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ipcRenderer;
|
return ipcRenderer;
|
||||||
},
|
},
|
||||||
|
|
||||||
_emit(channel, ...args) {
|
_emit(channel, ...args) {
|
||||||
const arr = listeners.get(channel);
|
const arr = listeners.get(channel);
|
||||||
|
|
||||||
if (arr) {
|
if (arr) {
|
||||||
for (const fn of arr) {
|
for (const fn of arr) {
|
||||||
fn({}, ...args);
|
fn({}, ...args);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
// stub
|
||||||
export const clipboardShim = {
|
export const clipboardShim = {
|
||||||
readText() {
|
readText() {
|
||||||
// TODO: maintain a local mirror updated via async reads
|
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,12 @@ export const dialogShim = {
|
||||||
return { canceled: false, filePath: "/downloads/" + name };
|
return { canceled: false, filePath: "/downloads/" + name };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: replace alert() with a styled modal (matching vault manager style)
|
||||||
async showMessageBox(browserWindow, options) {
|
async showMessageBox(browserWindow, options) {
|
||||||
if (typeof browserWindow === "object" && !options) {
|
if (typeof browserWindow === "object" && !options) {
|
||||||
options = browserWindow;
|
options = browserWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[shim:dialog] showMessageBox:", options);
|
console.log("[shim:dialog] showMessageBox:", options);
|
||||||
|
|
||||||
const message = options.message || "";
|
const message = options.message || "";
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export const remoteShim = {
|
||||||
screen: screenShim,
|
screen: screenShim,
|
||||||
nativeImage: nativeImageShim,
|
nativeImage: nativeImageShim,
|
||||||
Notification: notificationShim,
|
Notification: notificationShim,
|
||||||
|
|
||||||
safeStorage: {
|
safeStorage: {
|
||||||
isEncryptionAvailable() {
|
isEncryptionAvailable() {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export class menuShim {
|
||||||
static buildFromTemplate(template) {
|
static buildFromTemplate(template) {
|
||||||
const menu = new menuShim();
|
const menu = new menuShim();
|
||||||
menu.items = (template || []).map((item) => new menuItemShim(item));
|
menu.items = (template || []).map((item) => new menuItemShim(item));
|
||||||
|
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,7 +19,6 @@ export class menuShim {
|
||||||
}
|
}
|
||||||
|
|
||||||
popup(options) {
|
popup(options) {
|
||||||
// TODO: render custom HTML context menu at mouse position
|
|
||||||
console.log("[shim:Menu] popup (stub)", options);
|
console.log("[shim:Menu] popup (stub)", options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,9 +30,7 @@ export class menuShim {
|
||||||
this.items.splice(pos, 0, menuItem);
|
this.items.splice(pos, 0, menuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
closePopup() {
|
closePopup() {}
|
||||||
// TODO: hide custom context menu
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class menuItemShim {
|
export class menuItemShim {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,10 @@ export const themeShim = {
|
||||||
if (event === "updated") {
|
if (event === "updated") {
|
||||||
const wrapped = () => {
|
const wrapped = () => {
|
||||||
const idx = listeners.indexOf(wrapped);
|
const idx = listeners.indexOf(wrapped);
|
||||||
if (idx >= 0) listeners.splice(idx, 1);
|
if (idx >= 0) {
|
||||||
|
listeners.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
};
|
};
|
||||||
listeners.push(wrapped);
|
listeners.push(wrapped);
|
||||||
|
|
@ -47,7 +50,10 @@ export const themeShim = {
|
||||||
|
|
||||||
removeListener(event, callback) {
|
removeListener(event, callback) {
|
||||||
const idx = listeners.indexOf(callback);
|
const idx = listeners.indexOf(callback);
|
||||||
if (idx >= 0) listeners.splice(idx, 1);
|
if (idx >= 0) {
|
||||||
|
listeners.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
return themeShim;
|
return themeShim;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,15 +107,21 @@ const currentWindow = {
|
||||||
},
|
},
|
||||||
|
|
||||||
on(event, handler) {
|
on(event, handler) {
|
||||||
if (event === "focus") window.addEventListener("focus", handler);
|
if (event === "focus") {
|
||||||
else if (event === "blur") window.addEventListener("blur", handler);
|
window.addEventListener("focus", handler);
|
||||||
else if (event === "resize") window.addEventListener("resize", handler);
|
} else if (event === "blur") {
|
||||||
|
window.addEventListener("blur", handler);
|
||||||
|
} else if (event === "resize") {
|
||||||
|
window.addEventListener("resize", handler);
|
||||||
|
}
|
||||||
|
|
||||||
return currentWindow;
|
return currentWindow;
|
||||||
},
|
},
|
||||||
|
|
||||||
once(event, handler) {
|
once(event, handler) {
|
||||||
if (event === "focus")
|
if (event === "focus") {
|
||||||
window.addEventListener("focus", handler, { once: true });
|
window.addEventListener("focus", handler, { once: true });
|
||||||
|
}
|
||||||
return currentWindow;
|
return currentWindow;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -309,19 +315,34 @@ export const windowShim = {
|
||||||
|
|
||||||
getAllWindows() {
|
getAllWindows() {
|
||||||
const wins = [currentWindow];
|
const wins = [currentWindow];
|
||||||
if (_popupWindow) wins.push(_popupWindow);
|
if (_popupWindow) {
|
||||||
|
wins.push(_popupWindow);
|
||||||
|
}
|
||||||
|
|
||||||
return wins;
|
return wins;
|
||||||
},
|
},
|
||||||
|
|
||||||
fromId(id) {
|
fromId(id) {
|
||||||
if (id === currentWindow.id) return currentWindow;
|
if (id === currentWindow.id) {
|
||||||
if (_popupWindow && id === _popupWindow.id) return _popupWindow;
|
return currentWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_popupWindow && id === _popupWindow.id) {
|
||||||
|
return _popupWindow;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
fromWebContents(wc) {
|
fromWebContents(wc) {
|
||||||
if (wc === currentWebContents) return currentWindow;
|
if (wc === currentWebContents) {
|
||||||
if (_popupWebContents && wc === _popupWebContents) return _popupWindow;
|
return currentWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_popupWebContents && wc === _popupWebContents) {
|
||||||
|
return _popupWindow;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -329,14 +350,22 @@ export const windowShim = {
|
||||||
export const webContentsShim = {
|
export const webContentsShim = {
|
||||||
_current: () => currentWebContents,
|
_current: () => currentWebContents,
|
||||||
fromId(id) {
|
fromId(id) {
|
||||||
if (id === currentWebContents.id) return currentWebContents;
|
if (id === currentWebContents.id) {
|
||||||
if (_popupWebContents && id === _popupWebContents.id)
|
return currentWebContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_popupWebContents && id === _popupWebContents.id) {
|
||||||
return _popupWebContents;
|
return _popupWebContents;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
getAllWebContents() {
|
getAllWebContents() {
|
||||||
const wcs = [currentWebContents];
|
const wcs = [currentWebContents];
|
||||||
if (_popupWebContents) wcs.push(_popupWebContents);
|
if (_popupWebContents) {
|
||||||
|
wcs.push(_popupWebContents);
|
||||||
|
}
|
||||||
|
|
||||||
return wcs;
|
return wcs;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export class ContentCache {
|
||||||
entry.accessedAt = Date.now();
|
entry.accessedAt = Date.now();
|
||||||
return entry.data;
|
return entry.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,6 +45,7 @@ export class ContentCache {
|
||||||
delete(path) {
|
delete(path) {
|
||||||
const norm = this._normalize(path);
|
const norm = this._normalize(path);
|
||||||
const entry = this._cache.get(norm);
|
const entry = this._cache.get(norm);
|
||||||
|
|
||||||
if (entry) {
|
if (entry) {
|
||||||
this._currentSize -= entry.size;
|
this._currentSize -= entry.size;
|
||||||
this._cache.delete(norm);
|
this._cache.delete(norm);
|
||||||
|
|
@ -71,12 +73,14 @@ export class ContentCache {
|
||||||
_evictOne() {
|
_evictOne() {
|
||||||
let oldest = null;
|
let oldest = null;
|
||||||
let oldestTime = Infinity;
|
let oldestTime = Infinity;
|
||||||
|
|
||||||
for (const [key, entry] of this._cache) {
|
for (const [key, entry] of this._cache) {
|
||||||
if (entry.accessedAt < oldestTime) {
|
if (entry.accessedAt < oldestTime) {
|
||||||
oldest = key;
|
oldest = key;
|
||||||
oldestTime = entry.accessedAt;
|
oldestTime = entry.accessedAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldest) {
|
if (oldest) {
|
||||||
this.delete(oldest);
|
this.delete(oldest);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// In-memory metadata cache
|
// In-memory metadata cache
|
||||||
// Populated from /api/fs/tree on startup, kept in sync via transport events.
|
// Populated from /api/fs/tree on startup, kept in sync via transport events.
|
||||||
// All stat/exists/readdir calls are served from this cache (zero latency).
|
// All stat/exists/readdir calls are served from this cache.
|
||||||
|
|
||||||
export class MetadataCache {
|
export class MetadataCache {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -38,10 +38,12 @@ export class MetadataCache {
|
||||||
const oldNorm = this._normalize(oldPath);
|
const oldNorm = this._normalize(oldPath);
|
||||||
const newNorm = this._normalize(newPath);
|
const newNorm = this._normalize(newPath);
|
||||||
const meta = this._entries.get(oldNorm);
|
const meta = this._entries.get(oldNorm);
|
||||||
|
|
||||||
if (meta) {
|
if (meta) {
|
||||||
this._entries.delete(oldNorm);
|
this._entries.delete(oldNorm);
|
||||||
this._entries.set(newNorm, meta);
|
this._entries.set(newNorm, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move children
|
// Move children
|
||||||
const prefix = oldNorm + "/";
|
const prefix = oldNorm + "/";
|
||||||
for (const [key, val] of this._entries) {
|
for (const [key, val] of this._entries) {
|
||||||
|
|
@ -65,9 +67,11 @@ export class MetadataCache {
|
||||||
const rest = key.slice(prefix.length);
|
const rest = key.slice(prefix.length);
|
||||||
const slashIdx = rest.indexOf("/");
|
const slashIdx = rest.indexOf("/");
|
||||||
const childName = slashIdx >= 0 ? rest.slice(0, slashIdx) : rest;
|
const childName = slashIdx >= 0 ? rest.slice(0, slashIdx) : rest;
|
||||||
|
|
||||||
if (childName && !seen.has(childName)) {
|
if (childName && !seen.has(childName)) {
|
||||||
seen.add(childName);
|
seen.add(childName);
|
||||||
const childMeta = this._entries.get(prefix + childName);
|
const childMeta = this._entries.get(prefix + childName);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
name: childName,
|
name: childName,
|
||||||
type: childMeta?.type || (slashIdx >= 0 ? "directory" : "file"),
|
type: childMeta?.type || (slashIdx >= 0 ? "directory" : "file"),
|
||||||
|
|
@ -82,10 +86,13 @@ export class MetadataCache {
|
||||||
return this._entries.size;
|
return this._entries.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a stat-like object from metadata
|
|
||||||
toStat(path) {
|
toStat(path) {
|
||||||
const meta = this.get(path);
|
const meta = this.get(path);
|
||||||
if (!meta) return null;
|
|
||||||
|
if (!meta) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
size: meta.size || 0,
|
size: meta.size || 0,
|
||||||
mtimeMs: meta.mtime || 0,
|
mtimeMs: meta.mtime || 0,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
return {
|
return {
|
||||||
async stat(path) {
|
async stat(path) {
|
||||||
const cached = metadataCache.toStat(path);
|
const cached = metadataCache.toStat(path);
|
||||||
if (cached) return cached;
|
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
const meta = await transport.stat(path);
|
const meta = await transport.stat(path);
|
||||||
metadataCache.set(path, meta);
|
metadataCache.set(path, meta);
|
||||||
|
|
@ -16,13 +19,16 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
|
|
||||||
async readdir(path) {
|
async readdir(path) {
|
||||||
const meta = metadataCache.get(path);
|
const meta = metadataCache.get(path);
|
||||||
|
|
||||||
if (meta && meta.type === "file") {
|
if (meta && meta.type === "file") {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!meta && path && path !== "/" && path !== ".") {
|
if (!meta && path && path !== "/" && path !== ".") {
|
||||||
const e = new Error(
|
const e = new Error(
|
||||||
`ENOENT: no such file or directory, scandir '${path}'`,
|
`ENOENT: no such file or directory, scandir '${path}'`,
|
||||||
);
|
);
|
||||||
|
|
||||||
e.code = "ENOENT";
|
e.code = "ENOENT";
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +37,10 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
},
|
},
|
||||||
|
|
||||||
async readFile(path, encoding) {
|
async readFile(path, encoding) {
|
||||||
if (typeof encoding === "object") encoding = encoding?.encoding;
|
if (typeof encoding === "object") {
|
||||||
|
encoding = encoding?.encoding;
|
||||||
|
}
|
||||||
|
|
||||||
const wantText = encoding === "utf8" || encoding === "utf-8";
|
const wantText = encoding === "utf8" || encoding === "utf-8";
|
||||||
|
|
||||||
const meta = metadataCache.get(path);
|
const meta = metadataCache.get(path);
|
||||||
|
|
@ -40,6 +49,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
e.code = "EISDIR";
|
e.code = "EISDIR";
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!meta && path) {
|
if (!meta && path) {
|
||||||
const e = new Error(
|
const e = new Error(
|
||||||
`ENOENT: no such file or directory, open '${path}'`,
|
`ENOENT: no such file or directory, open '${path}'`,
|
||||||
|
|
@ -49,16 +59,19 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = contentCache.get(path);
|
const cached = contentCache.get(path);
|
||||||
|
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
if (wantText) {
|
if (wantText) {
|
||||||
return typeof cached === "string"
|
return typeof cached === "string"
|
||||||
? cached
|
? cached
|
||||||
: new TextDecoder().decode(cached);
|
: new TextDecoder().decode(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
// binary. ensure we return a proper Uint8Array with .buffer
|
// binary. ensure we return a proper Uint8Array with .buffer
|
||||||
if (typeof cached === "string") {
|
if (typeof cached === "string") {
|
||||||
return new TextEncoder().encode(cached);
|
return new TextEncoder().encode(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,11 +81,15 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
},
|
},
|
||||||
|
|
||||||
async writeFile(path, data, encoding) {
|
async writeFile(path, data, encoding) {
|
||||||
if (typeof encoding === "object") encoding = encoding?.encoding;
|
if (typeof encoding === "object") {
|
||||||
|
encoding = encoding?.encoding;
|
||||||
|
}
|
||||||
|
|
||||||
contentCache.set(path, data);
|
contentCache.set(path, data);
|
||||||
|
|
||||||
const size =
|
const size =
|
||||||
typeof data === "string" ? data.length : data.byteLength || 0;
|
typeof data === "string" ? data.length : data.byteLength || 0;
|
||||||
|
|
||||||
metadataCache.set(path, {
|
metadataCache.set(path, {
|
||||||
type: "file",
|
type: "file",
|
||||||
size,
|
size,
|
||||||
|
|
@ -81,6 +98,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await transport.writeFile(path, data, encoding);
|
const result = await transport.writeFile(path, data, encoding);
|
||||||
|
|
||||||
if (result.mtime) {
|
if (result.mtime) {
|
||||||
metadataCache.set(path, {
|
metadataCache.set(path, {
|
||||||
type: "file",
|
type: "file",
|
||||||
|
|
@ -93,6 +111,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
|
|
||||||
async appendFile(path, data, encoding) {
|
async appendFile(path, data, encoding) {
|
||||||
contentCache.invalidate(path);
|
contentCache.invalidate(path);
|
||||||
|
|
||||||
await transport.appendFile(path, data);
|
await transport.appendFile(path, data);
|
||||||
|
|
||||||
const meta = await transport.stat(path);
|
const meta = await transport.stat(path);
|
||||||
|
|
@ -102,15 +121,18 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
async unlink(path) {
|
async unlink(path) {
|
||||||
contentCache.delete(path);
|
contentCache.delete(path);
|
||||||
metadataCache.delete(path);
|
metadataCache.delete(path);
|
||||||
|
|
||||||
await transport.unlink(path);
|
await transport.unlink(path);
|
||||||
},
|
},
|
||||||
|
|
||||||
async rename(oldPath, newPath) {
|
async rename(oldPath, newPath) {
|
||||||
const content = contentCache.get(oldPath);
|
const content = contentCache.get(oldPath);
|
||||||
|
|
||||||
if (content !== null) {
|
if (content !== null) {
|
||||||
contentCache.set(newPath, content);
|
contentCache.set(newPath, content);
|
||||||
contentCache.delete(oldPath);
|
contentCache.delete(oldPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataCache.rename(oldPath, newPath);
|
metadataCache.rename(oldPath, newPath);
|
||||||
|
|
||||||
await transport.rename(oldPath, newPath);
|
await transport.rename(oldPath, newPath);
|
||||||
|
|
@ -119,7 +141,9 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
async mkdir(path, options) {
|
async mkdir(path, options) {
|
||||||
const recursive =
|
const recursive =
|
||||||
typeof options === "object" ? !!options.recursive : !!options;
|
typeof options === "object" ? !!options.recursive : !!options;
|
||||||
|
|
||||||
metadataCache.set(path, { type: "directory" });
|
metadataCache.set(path, { type: "directory" });
|
||||||
|
|
||||||
await transport.mkdir(path, recursive);
|
await transport.mkdir(path, recursive);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -131,19 +155,25 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
async rm(path, options) {
|
async rm(path, options) {
|
||||||
const recursive =
|
const recursive =
|
||||||
typeof options === "object" ? !!options.recursive : false;
|
typeof options === "object" ? !!options.recursive : false;
|
||||||
|
|
||||||
metadataCache.delete(path);
|
metadataCache.delete(path);
|
||||||
contentCache.delete(path);
|
contentCache.delete(path);
|
||||||
|
|
||||||
await transport.rm(path, recursive);
|
await transport.rm(path, recursive);
|
||||||
},
|
},
|
||||||
|
|
||||||
async copyFile(src, dest) {
|
async copyFile(src, dest) {
|
||||||
await transport.copyFile(src, dest);
|
await transport.copyFile(src, dest);
|
||||||
|
|
||||||
const meta = await transport.stat(dest);
|
const meta = await transport.stat(dest);
|
||||||
metadataCache.set(dest, meta);
|
metadataCache.set(dest, meta);
|
||||||
},
|
},
|
||||||
|
|
||||||
async access(path) {
|
async access(path) {
|
||||||
if (metadataCache.has(path)) return;
|
if (metadataCache.has(path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const e = new Error(
|
const e = new Error(
|
||||||
`ENOENT: no such file or directory, access '${path}'`,
|
`ENOENT: no such file or directory, access '${path}'`,
|
||||||
);
|
);
|
||||||
|
|
@ -152,7 +182,10 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
},
|
},
|
||||||
|
|
||||||
async realpath(path) {
|
async realpath(path) {
|
||||||
if (!path || path === "/" || path === ".") return "/";
|
if (!path || path === "/" || path === ".") {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
return transport.realpath(path);
|
return transport.realpath(path);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
|
|
||||||
statSync(path) {
|
statSync(path) {
|
||||||
const stat = metadataCache.toStat(path);
|
const stat = metadataCache.toStat(path);
|
||||||
|
|
||||||
if (!stat) {
|
if (!stat) {
|
||||||
const err = new Error(
|
const err = new Error(
|
||||||
`ENOENT: no such file or directory, stat '${path}'`,
|
`ENOENT: no such file or directory, stat '${path}'`,
|
||||||
|
|
@ -13,6 +14,7 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
err.code = "ENOENT";
|
err.code = "ENOENT";
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stat;
|
return stat;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -27,7 +29,9 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
},
|
},
|
||||||
|
|
||||||
readFileSync(path, encoding) {
|
readFileSync(path, encoding) {
|
||||||
if (typeof encoding === "object") encoding = encoding?.encoding;
|
if (typeof encoding === "object") {
|
||||||
|
encoding = encoding?.encoding;
|
||||||
|
}
|
||||||
|
|
||||||
const meta = metadataCache.get(path);
|
const meta = metadataCache.get(path);
|
||||||
if (meta && meta.type === "directory") {
|
if (meta && meta.type === "directory") {
|
||||||
|
|
@ -43,21 +47,28 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
? cached
|
? cached
|
||||||
: new TextDecoder().decode(cached);
|
: new TextDecoder().decode(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn("[shim:fs] readFileSync cache miss, using sync XHR:", path);
|
console.warn("[shim:fs] readFileSync cache miss, using sync XHR:", path);
|
||||||
|
|
||||||
const data = transport.readFileSync(path, encoding);
|
const data = transport.readFileSync(path, encoding);
|
||||||
contentCache.set(path, data);
|
contentCache.set(path, data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
writeFileSync(path, data, encoding) {
|
writeFileSync(path, data, encoding) {
|
||||||
if (typeof encoding === "object") encoding = encoding?.encoding;
|
if (typeof encoding === "object") {
|
||||||
|
encoding = encoding?.encoding;
|
||||||
|
}
|
||||||
|
|
||||||
contentCache.set(path, data);
|
contentCache.set(path, data);
|
||||||
|
|
||||||
const size =
|
const size =
|
||||||
typeof data === "string" ? data.length : data.byteLength || 0;
|
typeof data === "string" ? data.length : data.byteLength || 0;
|
||||||
|
|
||||||
metadataCache.set(path, {
|
metadataCache.set(path, {
|
||||||
type: "file",
|
type: "file",
|
||||||
size,
|
size,
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ function normPath(p) {
|
||||||
function uint8ToBase64(bytes) {
|
function uint8ToBase64(bytes) {
|
||||||
let binary = "";
|
let binary = "";
|
||||||
const chunk = 8192;
|
const chunk = 8192;
|
||||||
|
|
||||||
for (let i = 0; i < bytes.length; i += chunk) {
|
for (let i = 0; i < bytes.length; i += chunk) {
|
||||||
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
||||||
}
|
}
|
||||||
|
|
||||||
return btoa(binary);
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,7 +25,10 @@ async function request(method, endpoint, params = {}) {
|
||||||
const options = { method };
|
const options = { method };
|
||||||
|
|
||||||
if (method === "GET" || method === "DELETE") {
|
if (method === "GET" || method === "DELETE") {
|
||||||
if (vaultId()) url.searchParams.set("vault", vaultId());
|
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);
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +46,7 @@ async function request(method, endpoint, params = {}) {
|
||||||
e.code = err.code || "UNKNOWN";
|
e.code = err.code || "UNKNOWN";
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +59,10 @@ 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" || method === "DELETE") {
|
if (method === "GET" || method === "DELETE") {
|
||||||
if (vaultId()) url.searchParams.set("vault", vaultId());
|
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);
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +80,7 @@ function requestSync(method, endpoint, params = {}) {
|
||||||
|
|
||||||
if (xhr.status >= 400) {
|
if (xhr.status >= 400) {
|
||||||
let err;
|
let err;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = JSON.parse(xhr.responseText);
|
const body = JSON.parse(xhr.responseText);
|
||||||
err = new Error(body.error || "Request failed");
|
err = new Error(body.error || "Request failed");
|
||||||
|
|
@ -79,6 +89,7 @@ function requestSync(method, endpoint, params = {}) {
|
||||||
err = new Error("Request failed: " + xhr.status);
|
err = new Error("Request failed: " + xhr.status);
|
||||||
err.code = "UNKNOWN";
|
err.code = "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,9 +114,11 @@ export const transport = {
|
||||||
path: normPath(path),
|
path: normPath(path),
|
||||||
encoding: encoding || "",
|
encoding: encoding || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (encoding === "utf8" || encoding === "utf-8") {
|
if (encoding === "utf8" || encoding === "utf-8") {
|
||||||
return res.text();
|
return res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
const buf = await res.arrayBuffer();
|
const buf = await res.arrayBuffer();
|
||||||
return new Uint8Array(buf);
|
return new Uint8Array(buf);
|
||||||
},
|
},
|
||||||
|
|
@ -184,14 +197,18 @@ export const transport = {
|
||||||
path: normPath(path),
|
path: normPath(path),
|
||||||
encoding: encoding || "",
|
encoding: encoding || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (encoding === "utf8" || encoding === "utf-8") {
|
if (encoding === "utf8" || encoding === "utf-8") {
|
||||||
return xhr.responseText;
|
return xhr.responseText;
|
||||||
}
|
}
|
||||||
|
|
||||||
const binary = xhr.responseText;
|
const binary = xhr.responseText;
|
||||||
const bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
|
||||||
for (let i = 0; i < binary.length; i++) {
|
for (let i = 0; i < binary.length; i++) {
|
||||||
bytes[i] = binary.charCodeAt(i);
|
bytes[i] = binary.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@ const DEBUG = true;
|
||||||
const _accessLog = new Map(); // "module.property" -> count
|
const _accessLog = new Map(); // "module.property" -> count
|
||||||
|
|
||||||
function wrapWithProxy(obj, name) {
|
function wrapWithProxy(obj, name) {
|
||||||
if (!DEBUG || !obj || typeof obj !== "object") return obj;
|
if (!DEBUG || !obj || typeof obj !== "object") {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
return new Proxy(obj, {
|
return new Proxy(obj, {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
if (
|
if (
|
||||||
|
|
@ -31,10 +34,12 @@ function wrapWithProxy(obj, name) {
|
||||||
) {
|
) {
|
||||||
const key = `${name}.${prop}`;
|
const key = `${name}.${prop}`;
|
||||||
_accessLog.set(key, (_accessLog.get(key) || 0) + 1);
|
_accessLog.set(key, (_accessLog.get(key) || 0) + 1);
|
||||||
|
|
||||||
if (!(prop in target)) {
|
if (!(prop in target)) {
|
||||||
console.warn(`[shim:MISS] ${key} - property not found on shim`);
|
console.warn(`[shim:MISS] ${key} - property not found on shim`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return target[prop];
|
return target[prop];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -44,6 +49,7 @@ window.__shimLog = function () {
|
||||||
const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]);
|
const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]);
|
||||||
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
|
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
|
||||||
};
|
};
|
||||||
|
|
||||||
window.__shimMisses = function () {
|
window.__shimMisses = function () {
|
||||||
const sorted = [..._accessLog.entries()]
|
const sorted = [..._accessLog.entries()]
|
||||||
.filter(([k]) => {
|
.filter(([k]) => {
|
||||||
|
|
@ -52,6 +58,7 @@ window.__shimMisses = function () {
|
||||||
return shim && !(prop in shim);
|
return shim && !(prop in shim);
|
||||||
})
|
})
|
||||||
.sort((a, b) => b[1] - a[1]);
|
.sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
|
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,9 +89,11 @@ window.require = function (moduleName) {
|
||||||
if (throwOnRequire.has(moduleName)) {
|
if (throwOnRequire.has(moduleName)) {
|
||||||
throw new Error(`Cannot find module '${moduleName}'`);
|
throw new Error(`Cannot find module '${moduleName}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shimRegistry[moduleName]) {
|
if (shimRegistry[moduleName]) {
|
||||||
return shimRegistry[moduleName];
|
return shimRegistry[moduleName];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn("[ignis] Unshimmed require:", moduleName);
|
console.warn("[ignis] Unshimmed require:", moduleName);
|
||||||
return wrapWithProxy({}, `UNKNOWN(${moduleName})`);
|
return wrapWithProxy({}, `UNKNOWN(${moduleName})`);
|
||||||
};
|
};
|
||||||
|
|
@ -97,19 +106,23 @@ if (typeof window.Buffer === "undefined") {
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
return new TextEncoder().encode(data);
|
return new TextEncoder().encode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data instanceof ArrayBuffer) {
|
if (data instanceof ArrayBuffer) {
|
||||||
return new Uint8Array(data);
|
return new Uint8Array(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Uint8Array(data);
|
return new Uint8Array(data);
|
||||||
},
|
},
|
||||||
concat: function (arrays) {
|
concat: function (arrays) {
|
||||||
const total = arrays.reduce((sum, a) => sum + a.length, 0);
|
const total = arrays.reduce((sum, a) => sum + a.length, 0);
|
||||||
const result = new Uint8Array(total);
|
const result = new Uint8Array(total);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
||||||
for (const arr of arrays) {
|
for (const arr of arrays) {
|
||||||
result.set(arr, offset);
|
result.set(arr, offset);
|
||||||
offset += arr.length;
|
offset += arr.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
isBuffer: function (obj) {
|
isBuffer: function (obj) {
|
||||||
|
|
@ -127,29 +140,37 @@ const _originalOpen = window.open;
|
||||||
window.open = function (url, target, features) {
|
window.open = function (url, target, features) {
|
||||||
if (url === "about:blank" || (features && features.includes("popup"))) {
|
if (url === "about:blank" || (features && features.includes("popup"))) {
|
||||||
console.log("[ignis] intercepted popup:", url, features);
|
console.log("[ignis] intercepted popup:", url, features);
|
||||||
|
|
||||||
registerPopupWindow();
|
registerPopupWindow();
|
||||||
|
|
||||||
const iframe = document.createElement("iframe");
|
const iframe = document.createElement("iframe");
|
||||||
iframe.style.cssText =
|
iframe.style.cssText =
|
||||||
"position:fixed;left:-9999px;width:0;height:0;border:none;";
|
"position:fixed;left:-9999px;width:0;height:0;border:none;";
|
||||||
|
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
window.__popupIframe = iframe;
|
window.__popupIframe = iframe;
|
||||||
|
|
||||||
const iframeWin = iframe.contentWindow;
|
const iframeWin = iframe.contentWindow;
|
||||||
|
|
||||||
iframeWin.require = window.require;
|
iframeWin.require = window.require;
|
||||||
iframeWin.module = window.module;
|
iframeWin.module = window.module;
|
||||||
iframeWin.Buffer = window.Buffer;
|
iframeWin.Buffer = window.Buffer;
|
||||||
iframeWin.process = window.process;
|
iframeWin.process = window.process;
|
||||||
iframeWin.global = iframeWin;
|
iframeWin.global = iframeWin;
|
||||||
iframeWin.globalEnhance = window.globalEnhance;
|
iframeWin.globalEnhance = window.globalEnhance;
|
||||||
|
|
||||||
iframeWin.close = function () {
|
iframeWin.close = function () {
|
||||||
unregisterPopupWindow();
|
unregisterPopupWindow();
|
||||||
iframe.remove();
|
iframe.remove();
|
||||||
window.__popupIframe = null;
|
window.__popupIframe = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return iframeWin;
|
return iframeWin;
|
||||||
}
|
}
|
||||||
return _originalOpen.call(window, url, target, features);
|
return _originalOpen.call(window, url, target, features);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"contextmenu",
|
"contextmenu",
|
||||||
(e) => {
|
(e) => {
|
||||||
|
|
@ -167,17 +188,23 @@ window.__currentVaultId = _urlParams.get("vault") || "";
|
||||||
const vaultParam = window.__currentVaultId
|
const vaultParam = window.__currentVaultId
|
||||||
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
xhr.open("GET", "/api/vault/info" + vaultParam, false);
|
xhr.open("GET", "/api/vault/info" + vaultParam, false);
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
const info = JSON.parse(xhr.responseText);
|
const info = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
window.__currentVaultId = info.id;
|
window.__currentVaultId = info.id;
|
||||||
window.__obsidianVersion = info.version || "0.0.0";
|
window.__obsidianVersion = info.version || "0.0.0";
|
||||||
|
|
||||||
window.__vaultConfig = {
|
window.__vaultConfig = {
|
||||||
id: info.id,
|
id: info.id,
|
||||||
path: "/",
|
path: "/",
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[ignis] Vault:", window.__vaultConfig);
|
console.log("[ignis] Vault:", window.__vaultConfig);
|
||||||
console.log("[ignis] Obsidian version:", window.__obsidianVersion);
|
console.log("[ignis] Obsidian version:", window.__obsidianVersion);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -191,8 +218,10 @@ window.__currentVaultId = _urlParams.get("vault") || "";
|
||||||
(function initVaultList() {
|
(function initVaultList() {
|
||||||
try {
|
try {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
xhr.open("GET", "/api/vault/list", false);
|
xhr.open("GET", "/api/vault/list", false);
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
window.__vaultList = JSON.parse(xhr.responseText);
|
window.__vaultList = JSON.parse(xhr.responseText);
|
||||||
}
|
}
|
||||||
|
|
@ -206,14 +235,19 @@ window.__currentVaultId = _urlParams.get("vault") || "";
|
||||||
const vaultParam = window.__currentVaultId
|
const vaultParam = window.__currentVaultId
|
||||||
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
xhr.open("GET", "/api/fs/tree" + vaultParam, false);
|
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);
|
||||||
fsShim._metadataCache.set("", { type: "directory" });
|
fsShim._metadataCache.set("", { type: "directory" });
|
||||||
fsShim._metadataCache.set("/", { type: "directory" });
|
fsShim._metadataCache.set("/", { type: "directory" });
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[ignis] Metadata cache populated:",
|
"[ignis] Metadata cache populated:",
|
||||||
fsShim._metadataCache.size,
|
fsShim._metadataCache.size,
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@ export class EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
on(event, listener) {
|
on(event, listener) {
|
||||||
if (!this._events[event]) this._events[event] = [];
|
if (!this._events[event]) {
|
||||||
|
this._events[event] = [];
|
||||||
|
}
|
||||||
|
|
||||||
this._events[event].push(listener);
|
this._events[event].push(listener);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,24 +18,39 @@ export class EventEmitter {
|
||||||
this.removeListener(event, wrapped);
|
this.removeListener(event, wrapped);
|
||||||
listener.apply(this, args);
|
listener.apply(this, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
wrapped._original = listener;
|
wrapped._original = listener;
|
||||||
return this.on(event, wrapped);
|
return this.on(event, wrapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(event, ...args) {
|
emit(event, ...args) {
|
||||||
const listeners = this._events[event];
|
const listeners = this._events[event];
|
||||||
if (!listeners || listeners.length === 0) return false;
|
|
||||||
|
if (!listeners || listeners.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (const fn of [...listeners]) {
|
for (const fn of [...listeners]) {
|
||||||
fn.apply(this, args);
|
fn.apply(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListener(event, listener) {
|
removeListener(event, listener) {
|
||||||
const arr = this._events[event];
|
const arr = this._events[event];
|
||||||
if (!arr) return this;
|
if (!arr) {
|
||||||
const idx = arr.findIndex((fn) => fn === listener || fn._original === listener);
|
return this;
|
||||||
if (idx >= 0) arr.splice(idx, 1);
|
}
|
||||||
|
|
||||||
|
const idx = arr.findIndex(
|
||||||
|
(fn) => fn === listener || fn._original === listener,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (idx >= 0) {
|
||||||
|
arr.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,6 +64,7 @@ export class EventEmitter {
|
||||||
} else {
|
} else {
|
||||||
this._events = {};
|
this._events = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,8 +81,12 @@ export class EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
prependListener(event, listener) {
|
prependListener(event, listener) {
|
||||||
if (!this._events[event]) this._events[event] = [];
|
if (!this._events[event]) {
|
||||||
|
this._events[event] = [];
|
||||||
|
}
|
||||||
|
|
||||||
this._events[event].unshift(listener);
|
this._events[event].unshift(listener);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export class ClientRequest extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {}
|
end() {}
|
||||||
write() {}
|
write() {}
|
||||||
abort() {}
|
abort() {}
|
||||||
|
|
@ -26,6 +27,7 @@ export function request(options, callback) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
req.once("response", callback);
|
req.once("response", callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Immediately error. real HTTP requests need fetch or the proxy
|
// Immediately error. real HTTP requests need fetch or the proxy
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
req.emit(
|
req.emit(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export const pathShim = {
|
||||||
if (p === "/" && window.__currentVaultId) {
|
if (p === "/" && window.__currentVaultId) {
|
||||||
return window.__currentVaultId;
|
return window.__currentVaultId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _origBasename(p, ext);
|
return _origBasename(p, ext);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
// Override window.requestUrl to proxy external requests through our server,
|
// Override window.requestUrl to proxy external requests through our server, bypassing CORS.
|
||||||
// bypassing browser CORS restrictions. Obsidian sets window.requestUrl = UA
|
// Obsidian sets window.requestUrl in app.js, so we override it after app.js loads.
|
||||||
// in app.js, so we override it after app.js loads.
|
|
||||||
|
|
||||||
function base64ToArrayBuffer(base64) {
|
function base64ToArrayBuffer(base64) {
|
||||||
const binary = atob(base64);
|
const binary = atob(base64);
|
||||||
const bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
|
||||||
for (let i = 0; i < binary.length; i++) {
|
for (let i = 0; i < binary.length; i++) {
|
||||||
bytes[i] = binary.charCodeAt(i);
|
bytes[i] = binary.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes.buffer;
|
return bytes.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15,9 +16,11 @@ function arrayBufferToBase64(buf) {
|
||||||
const bytes = new Uint8Array(buf);
|
const bytes = new Uint8Array(buf);
|
||||||
let binary = "";
|
let binary = "";
|
||||||
const chunk = 8192;
|
const chunk = 8192;
|
||||||
|
|
||||||
for (let i = 0; i < bytes.length; i += chunk) {
|
for (let i = 0; i < bytes.length; i += chunk) {
|
||||||
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
||||||
}
|
}
|
||||||
|
|
||||||
return btoa(binary);
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +40,9 @@ async function proxyRequestUrl(request) {
|
||||||
headers: request.headers || {},
|
headers: request.headers || {},
|
||||||
body: request.body,
|
body: request.body,
|
||||||
});
|
});
|
||||||
|
|
||||||
const arrayBuf = await res.arrayBuffer();
|
const arrayBuf = await res.arrayBuffer();
|
||||||
|
|
||||||
return makeResponse(
|
return makeResponse(
|
||||||
request,
|
request,
|
||||||
res.status,
|
res.status,
|
||||||
|
|
@ -49,6 +54,7 @@ async function proxyRequestUrl(request) {
|
||||||
// Cross-origin: route through server proxy
|
// Cross-origin: route through server proxy
|
||||||
let body = request.body;
|
let body = request.body;
|
||||||
let binary = false;
|
let binary = false;
|
||||||
|
|
||||||
if (body instanceof ArrayBuffer) {
|
if (body instanceof ArrayBuffer) {
|
||||||
body = arrayBufferToBase64(body);
|
body = arrayBufferToBase64(body);
|
||||||
binary = true;
|
binary = true;
|
||||||
|
|
@ -75,6 +81,7 @@ async function proxyRequestUrl(request) {
|
||||||
|
|
||||||
const proxyResult = await res.json();
|
const proxyResult = await res.json();
|
||||||
const arrayBuf = base64ToArrayBuffer(proxyResult.body);
|
const arrayBuf = base64ToArrayBuffer(proxyResult.body);
|
||||||
|
|
||||||
return makeResponse(
|
return makeResponse(
|
||||||
request,
|
request,
|
||||||
proxyResult.status,
|
proxyResult.status,
|
||||||
|
|
@ -86,11 +93,13 @@ async function proxyRequestUrl(request) {
|
||||||
function makeResponse(request, status, headers, arrayBuf) {
|
function makeResponse(request, status, headers, arrayBuf) {
|
||||||
const text = new TextDecoder().decode(arrayBuf);
|
const text = new TextDecoder().decode(arrayBuf);
|
||||||
let json;
|
let json;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json = JSON.parse(text);
|
json = JSON.parse(text);
|
||||||
} catch {
|
} catch {
|
||||||
json = null;
|
json = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status, headers, arrayBuffer: arrayBuf, text, json };
|
return { status, headers, arrayBuffer: arrayBuf, text, json };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// Custom vault manager modal. will migrate to Svelte later
|
// Custom vault manager modal. will migrate to Svelte later
|
||||||
// Shows list of vaults, create new, delete, switch.
|
|
||||||
|
|
||||||
export function showVaultManager() {
|
export function showVaultManager() {
|
||||||
if (!document.querySelector(".workspace")) return;
|
if (!document.querySelector(".workspace")) return;
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,19 @@ export const urlShim = {
|
||||||
// Return an object with .href matching Node's url.pathToFileURL behavior
|
// Return an object with .href matching Node's url.pathToFileURL behavior
|
||||||
const encoded = encodeURI(p.replace(/\\/g, "/"));
|
const encoded = encodeURI(p.replace(/\\/g, "/"));
|
||||||
const href = "file:///" + encoded.replace(/^\/+/, "");
|
const href = "file:///" + encoded.replace(/^\/+/, "");
|
||||||
|
|
||||||
return { href, toString: () => href };
|
return { href, toString: () => href };
|
||||||
},
|
},
|
||||||
|
|
||||||
fileURLToPath(url) {
|
fileURLToPath(url) {
|
||||||
let str = typeof url === "string" ? url : url.href || url.toString();
|
let str = typeof url === "string" ? url : url.href || url.toString();
|
||||||
|
|
||||||
if (str.startsWith("file:///")) {
|
if (str.startsWith("file:///")) {
|
||||||
str = str.slice(8);
|
str = str.slice(8);
|
||||||
} else if (str.startsWith("file://")) {
|
} else if (str.startsWith("file://")) {
|
||||||
str = str.slice(7);
|
str = str.slice(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
return decodeURI(str);
|
return decodeURI(str);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue