mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
headless-sync-server-plugin
This commit is contained in:
127
server/plugins/headless-sync/auth.js
Normal file
127
server/plugins/headless-sync/auth.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
|
||||
function getObAuthFile() {
|
||||
return path.join(
|
||||
os.homedir(),
|
||||
".config",
|
||||
"obsidian-headless",
|
||||
"auth_token",
|
||||
);
|
||||
}
|
||||
|
||||
function getInternalTokenFile(dataDir) {
|
||||
return path.join(dataDir, "auth-token.json");
|
||||
}
|
||||
|
||||
function loadToken(dataDir) {
|
||||
const internalFile = getInternalTokenFile(dataDir);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(internalFile)) {
|
||||
const data = JSON.parse(fs.readFileSync(internalFile, "utf-8"));
|
||||
|
||||
if (data && data.token) {
|
||||
syncToObCli(data.token);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Fall back to ob CLI's own auth file
|
||||
const obAuthFile = getObAuthFile();
|
||||
|
||||
try {
|
||||
if (fs.existsSync(obAuthFile)) {
|
||||
const token = fs.readFileSync(obAuthFile, "utf-8").trim();
|
||||
|
||||
if (token) {
|
||||
const data = { token };
|
||||
saveInternal(dataDir, data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveToken(dataDir, tokenData) {
|
||||
saveInternal(dataDir, tokenData);
|
||||
syncToObCli(tokenData.token);
|
||||
}
|
||||
|
||||
function clearToken(dataDir) {
|
||||
const internalFile = getInternalTokenFile(dataDir);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(internalFile)) {
|
||||
fs.unlinkSync(internalFile);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const obAuthFile = getObAuthFile();
|
||||
|
||||
try {
|
||||
if (fs.existsSync(obAuthFile)) {
|
||||
fs.unlinkSync(obAuthFile);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function isAuthenticated(dataDir) {
|
||||
const internalFile = getInternalTokenFile(dataDir);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(internalFile)) {
|
||||
const data = JSON.parse(fs.readFileSync(internalFile, "utf-8"));
|
||||
return !!(data && data.token);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function saveInternal(dataDir, tokenData) {
|
||||
const internalFile = getInternalTokenFile(dataDir);
|
||||
const dir = path.dirname(internalFile);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(internalFile, JSON.stringify(tokenData, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
function syncToObCli(token) {
|
||||
const obAuthFile = getObAuthFile();
|
||||
|
||||
try {
|
||||
const dir = path.dirname(obAuthFile);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(obAuthFile, token, "utf-8");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getTokenInfo(dataDir) {
|
||||
const internalFile = getInternalTokenFile(dataDir);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(internalFile)) {
|
||||
const data = JSON.parse(fs.readFileSync(internalFile, "utf-8"));
|
||||
|
||||
if (data && data.token) {
|
||||
return { email: data.email || null, name: data.name || null };
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { loadToken, saveToken, clearToken, isAuthenticated, getTokenInfo };
|
||||
59
server/plugins/headless-sync/index.js
Normal file
59
server/plugins/headless-sync/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const path = require("path");
|
||||
const obCli = require("./ob-cli");
|
||||
const auth = require("./auth");
|
||||
|
||||
module.exports = {
|
||||
id: "headless-sync",
|
||||
name: "Headless Sync",
|
||||
description: "Server-side vault sync via obsidian-headless CLI",
|
||||
|
||||
obsidianPlugin: path.join(__dirname, "plugin"),
|
||||
|
||||
_ctx: null,
|
||||
_obStatus: null,
|
||||
|
||||
async register(ctx) {
|
||||
this._ctx = ctx;
|
||||
|
||||
this._obStatus = obCli.checkInstalled();
|
||||
|
||||
if (this._obStatus.installed) {
|
||||
ctx.log(`ob CLI available (${this._obStatus.version})`);
|
||||
} else {
|
||||
ctx.log("ob CLI not found. Install obsidian-headless to enable sync.");
|
||||
}
|
||||
|
||||
const token = auth.loadToken(ctx.dataDir);
|
||||
|
||||
if (token) {
|
||||
ctx.log("Auth token loaded");
|
||||
}
|
||||
|
||||
const { mountRoutes } = require("./routes");
|
||||
mountRoutes(ctx.router, this);
|
||||
},
|
||||
|
||||
async shutdown() {
|
||||
this._ctx = null;
|
||||
},
|
||||
|
||||
async onVaultEnabled(vaultId, vaultPath) {
|
||||
if (this._ctx) {
|
||||
this._ctx.log(`Vault enabled: ${vaultId}`);
|
||||
}
|
||||
},
|
||||
|
||||
async onVaultDisabled(vaultId, vaultPath) {
|
||||
if (this._ctx) {
|
||||
this._ctx.log(`Vault disabled: ${vaultId}`);
|
||||
}
|
||||
},
|
||||
|
||||
getObStatus() {
|
||||
return this._obStatus;
|
||||
},
|
||||
|
||||
getCtx() {
|
||||
return this._ctx;
|
||||
},
|
||||
};
|
||||
52
server/plugins/headless-sync/ob-cli.js
Normal file
52
server/plugins/headless-sync/ob-cli.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { spawn, execSync } = require("child_process");
|
||||
const os = require("os");
|
||||
|
||||
function checkInstalled() {
|
||||
try {
|
||||
const output = execSync("ob --version", { stdio: "pipe" }).toString().trim();
|
||||
return { installed: true, version: output || "unknown" };
|
||||
} catch {
|
||||
return { installed: false, version: null };
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(args, opts = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnOpts = {
|
||||
env: { ...process.env, HOME: os.homedir() },
|
||||
};
|
||||
|
||||
if (opts.cwd) {
|
||||
spawnOpts.cwd = opts.cwd;
|
||||
}
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
const proc = spawn("ob", args, spawnOpts);
|
||||
|
||||
proc.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
proc.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(
|
||||
new Error(`ob ${args[0]} failed (code ${code}): ${stderr || stdout}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
proc.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { checkInstalled, runCommand };
|
||||
14
server/plugins/headless-sync/plugin/main.js
Normal file
14
server/plugins/headless-sync/plugin/main.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// Stub - will be replaced with real implementation in Phase 4
|
||||
const { Plugin } = require("obsidian");
|
||||
|
||||
class IgnisHeadlessSyncPlugin extends Plugin {
|
||||
async onload() {
|
||||
console.log("[ignis-headless-sync] Loaded (stub)");
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log("[ignis-headless-sync] Unloaded (stub)");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IgnisHeadlessSyncPlugin;
|
||||
9
server/plugins/headless-sync/plugin/manifest.json
Normal file
9
server/plugins/headless-sync/plugin/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "ignis-headless-sync",
|
||||
"name": "Ignis Headless Sync",
|
||||
"version": "0.6.4",
|
||||
"minAppVersion": "1.0.0",
|
||||
"description": "Client-side companion for server-side Obsidian Sync",
|
||||
"author": "Ignis",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
89
server/plugins/headless-sync/routes.js
Normal file
89
server/plugins/headless-sync/routes.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const auth = require("./auth");
|
||||
const obCli = require("./ob-cli");
|
||||
|
||||
function mountRoutes(router, plugin) {
|
||||
router.get("/status", (req, res) => {
|
||||
const ctx = plugin.getCtx();
|
||||
const obStatus = plugin.getObStatus();
|
||||
|
||||
const tokenInfo = auth.getTokenInfo(ctx.dataDir);
|
||||
|
||||
res.json({
|
||||
installed: obStatus?.installed || false,
|
||||
version: obStatus?.version || null,
|
||||
authenticated: auth.isAuthenticated(ctx.dataDir),
|
||||
email: tokenInfo?.email || null,
|
||||
name: tokenInfo?.name || null,
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/login", (req, res) => {
|
||||
const ctx = plugin.getCtx();
|
||||
const { token, email, name } = req.body;
|
||||
|
||||
if (!token) {
|
||||
return res.status(400).json({ error: "Token is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
auth.saveToken(ctx.dataDir, { token, email: email || null, name: name || null });
|
||||
ctx.log(`Auth token saved${email ? ` for ${email}` : ""}`);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/logout", (req, res) => {
|
||||
const ctx = plugin.getCtx();
|
||||
|
||||
try {
|
||||
auth.clearToken(ctx.dataDir);
|
||||
ctx.log("Auth token cleared");
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/remote-vaults", async (req, res) => {
|
||||
const ctx = plugin.getCtx();
|
||||
|
||||
if (!auth.isAuthenticated(ctx.dataDir)) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await obCli.runCommand(["sync-list-remote"]);
|
||||
const vaults = parseRemoteVaults(result.stdout);
|
||||
res.json({ vaults });
|
||||
} catch (e) {
|
||||
ctx.log(`Failed to list remote vaults: ${e.message}`);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseRemoteVaults(stdout) {
|
||||
const lines = stdout.trim().split("\n");
|
||||
const vaults = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (!trimmed || trimmed.startsWith("Available")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Format: [vaultId] "[vaultName]" ([region])
|
||||
const match = trimmed.match(/^([a-f0-9]+)\s+"([^"]+)"/);
|
||||
|
||||
if (match) {
|
||||
vaults.push({ id: match[1], name: match[2] });
|
||||
}
|
||||
}
|
||||
|
||||
return vaults;
|
||||
}
|
||||
|
||||
module.exports = { mountRoutes };
|
||||
Reference in New Issue
Block a user