Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
set -eu

exec java -Djava.awt.headless=true ${JAVA_OPTS:-} -jar /app/app.jar
exec java -Djava.awt.headless=true --enable-native-access=ALL-UNNAMED ${JAVA_OPTS:-} -jar /app/app.jar
59 changes: 44 additions & 15 deletions obsidian-plugin/src/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { App, PluginManifest } from "obsidian";
import { TFile } from "obsidian";
import { describe, expect, test, vi } from "vitest";
import { DEFAULT_SETTINGS } from "./domain/types";
import { DEFAULT_SETTINGS, type MdbrainSettings } from "./domain/types";
import MdbrainPlugin from "./main";

const createPlugin = (appOverrides?: Partial<App>) => {
const createPlugin = (
appOverrides?: Partial<App>,
settingsOverrides?: Partial<MdbrainSettings>,
) => {
const app = {
vault: { read: async () => "" },
metadataCache: { getFileCache: () => null },
Expand All @@ -21,7 +24,7 @@ const createPlugin = (appOverrides?: Partial<App>) => {
isDesktopOnly: false,
};
const plugin = new MdbrainPlugin(app, manifest);
plugin.settings = { ...DEFAULT_SETTINGS };
plugin.settings = { ...DEFAULT_SETTINGS, ...settingsOverrides };
return plugin;
};

Expand Down Expand Up @@ -63,7 +66,7 @@ describe("MdbrainPlugin.handleAssetChange", () => {
});

test("does not skip assets when reference index is not ready", async () => {
const plugin = createPlugin();
const plugin = createPlugin(undefined, { publishKey: "test-key" });
const file = new TFile("assets/image.png", "image", "png");
const syncAssetFile = vi.fn().mockResolvedValue(true);

Expand All @@ -82,7 +85,7 @@ describe("MdbrainPlugin.handleAssetChange", () => {
});

test("syncs when asset is referenced", async () => {
const plugin = createPlugin();
const plugin = createPlugin(undefined, { publishKey: "test-key" });
const file = new TFile("assets/image.png", "image", "png");
const syncAssetFile = vi.fn().mockResolvedValue(true);

Expand Down Expand Up @@ -163,10 +166,13 @@ describe("MdbrainPlugin.collectReferencedAssetFiles", () => {

describe("MdbrainPlugin.handleFileRename", () => {
test("renames reference index and syncs note", async () => {
const plugin = createPlugin({
metadataCache: { getFileCache: () => null } as never,
vault: { read: async () => "" } as never,
});
const plugin = createPlugin(
{
metadataCache: { getFileCache: () => null } as never,
vault: { read: async () => "" } as never,
},
{ publishKey: "test-key" },
);
const file = new TFile("notes/new.md");
const syncNoteFile = vi.fn().mockResolvedValue({
success: true,
Expand Down Expand Up @@ -233,7 +239,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => {
},
};

const plugin = createPlugin({ vault, metadataCache });
const plugin = createPlugin({ vault, metadataCache }, { publishKey: "test-key" });

const syncNote = vi.fn().mockResolvedValue({
success: true,
Expand Down Expand Up @@ -287,7 +293,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => {
});

test("uploads referenced assets after note sync", async () => {
const plugin = createPlugin();
const plugin = createPlugin(undefined, { publishKey: "test-key" });
const note = new TFile("notes/a.md");
const assetA = new TFile("assets/a.png", "a", "png");
const assetB = new TFile("assets/b.png", "b", "png");
Expand Down Expand Up @@ -350,7 +356,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => {

test("debounces multiple rapid cache changes and uses the latest content", async () => {
vi.useFakeTimers();
const plugin = createPlugin();
const plugin = createPlugin(undefined, { publishKey: "test-key" });
const note = new TFile("notes/a.md");

const syncNoteFromCache = vi.fn().mockResolvedValue({
Expand Down Expand Up @@ -397,7 +403,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => {
});

test("uploads linked notes after note sync", async () => {
const plugin = createPlugin();
const plugin = createPlugin(undefined, { publishKey: "test-key" });
const note = new TFile("notes/a.md");
const linked = new TFile("notes/b.md");
const linkedMap = new Map<string, TFile>([["note-b", linked]]);
Expand Down Expand Up @@ -467,7 +473,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => {

test("sync does not write to file (no self-write loop)", async () => {
vi.useFakeTimers();
const plugin = createPlugin();
const plugin = createPlugin(undefined, { publishKey: "test-key" });
const note = new TFile("notes/a.md");
const originalContent = "# Test content";

Expand Down Expand Up @@ -515,7 +521,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => {

test("skips sync when note has no client ID", async () => {
vi.useFakeTimers();
const plugin = createPlugin();
const plugin = createPlugin(undefined, { publishKey: "test-key" });
const note = new TFile("notes/a.md");

const getClientId = vi.fn().mockResolvedValue(null);
Expand All @@ -539,4 +545,27 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => {
expect(syncNoteFromCache).not.toHaveBeenCalled();
vi.useRealTimers();
});

test("skips sync when publish config is missing", async () => {
vi.useFakeTimers();
const plugin = createPlugin();
const note = new TFile("notes/a.md");

const syncNoteFromCache = vi.fn();

const pluginAccess = plugin as unknown as {
syncNoteFromCache: (file: TFile, data: string, cache: unknown) => Promise<unknown>;
settings: { autoSync: boolean };
referenceIndex: { updateNote: (notePath: string) => void };
};
pluginAccess.syncNoteFromCache = syncNoteFromCache;
pluginAccess.settings.autoSync = true;
pluginAccess.referenceIndex = { updateNote: vi.fn() };

plugin.handleMarkdownCacheChanged(note, "content", null);
await vi.advanceTimersByTimeAsync(1200);

expect(syncNoteFromCache).not.toHaveBeenCalled();
vi.useRealTimers();
});
});
38 changes: 38 additions & 0 deletions obsidian-plugin/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ export default class MdbrainPlugin extends Plugin {
this.referenceIndexReady = false;
}

private isSyncConfigured(): boolean {
const serverUrl = this.settings.serverUrl?.trim();
const publishKey = this.settings.publishKey?.trim();
return Boolean(serverUrl) && Boolean(publishKey);
}

private ensureSyncConfigured(notify = false): boolean {
if (this.isSyncConfigured()) return true;
if (notify) {
new Notice("Publish is not configured. Set Publish URL and Key in Mdbrain settings.");
}
return false;
}

async onload() {
console.log("[Mdbrain] Plugin loading (snapshot publish)...");
await this.loadSettings();
Expand Down Expand Up @@ -163,6 +177,9 @@ export default class MdbrainPlugin extends Plugin {
}

private async syncCurrentFile(file: TFile): Promise<void> {
if (!this.ensureSyncConfigured(true)) {
return;
}
const result = await this.syncNoteFile(file);
if (!result.success) {
new Notice("Publish failed: note upload failed");
Expand All @@ -177,6 +194,8 @@ export default class MdbrainPlugin extends Plugin {

this.referenceIndex.updateNote(file.path, this.extractLinkpaths(data, cache));

if (!this.isSyncConfigured()) return;

this.debounceService.debounce(
file.path,
async () => {
Expand All @@ -201,6 +220,7 @@ export default class MdbrainPlugin extends Plugin {
async handleFileDelete(_file: TFile) {
if (!this.settings.autoSync) return;
this.referenceIndex.removeNote(_file.path);
if (!this.isSyncConfigured()) return;
await this.fullSync();
}

Expand All @@ -211,6 +231,8 @@ export default class MdbrainPlugin extends Plugin {
const content = cache ? "" : await this.app.vault.read(file);
this.referenceIndex.updateNote(file.path, this.extractLinkpaths(content, cache));

if (!this.isSyncConfigured()) return;

const result = await this.syncNoteFile(file);
if (!result.success) {
new Notice("Publish failed: note rename failed");
Expand All @@ -222,6 +244,7 @@ export default class MdbrainPlugin extends Plugin {

async handleAssetChange(file: TFile) {
if (!this.settings.autoSync) return;
if (!this.isSyncConfigured()) return;
if (this.referenceIndexReady && !this.referenceIndex.isAssetReferenced(file.path)) {
return;
}
Expand All @@ -246,6 +269,15 @@ export default class MdbrainPlugin extends Plugin {
needUploadNotes: Array<{ id: string; hash: string }>;
linkedNotesById: Map<string, TFile>;
}> {
if (!this.isSyncConfigured()) {
return {
success: false,
needUploadAssets: [],
assetsById: new Map(),
needUploadNotes: [],
linkedNotesById: new Map(),
};
}
const clientId = await getClientId(file, this.app);
if (!clientId) {
return {
Expand Down Expand Up @@ -293,6 +325,9 @@ export default class MdbrainPlugin extends Plugin {
}

private async syncAssetFile(file: TFile): Promise<boolean> {
if (!this.isSyncConfigured()) {
return false;
}
const buffer = await this.app.vault.readBinary(file);
const assetId = await hashString(file.path);
const hash = await md5Hash(buffer);
Expand All @@ -310,6 +345,9 @@ export default class MdbrainPlugin extends Plugin {
}

async fullSync(): Promise<void> {
if (!this.ensureSyncConfigured(true)) {
return;
}
const notes = await this.buildNoteSnapshot();
const referencedAssets = await this.collectReferencedAssetFiles();
const assets = await this.buildAssetSnapshot(referencedAssets);
Expand Down
17 changes: 7 additions & 10 deletions obsidian-plugin/src/plugin/settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export class MdbrainSettingTab extends PluginSettingTab {

new Setting(containerEl)
.setName("Publish URL")
.setDesc("Your Mdbrain publish URL (must route /obsidian/* to Console)")
.setDesc("Your Mdbrain publish URL")
.addText((text) =>
text
.setPlaceholder("https://notes.example.com")
.setValue(this.plugin.settings.serverUrl)
.setPlaceholder("https://console.example.com")
.setValue(this.plugin.settings.serverUrl ?? "")
.onChange(async (value) => {
this.plugin.settings.serverUrl = value;
await this.plugin.saveSettings();
Expand All @@ -32,13 +32,10 @@ export class MdbrainSettingTab extends PluginSettingTab {
.setName("Publish Key")
.setDesc("Publish Key from Mdbrain Console")
.addText((text) =>
text
.setPlaceholder("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
.setValue(this.plugin.settings.publishKey)
.onChange(async (value) => {
this.plugin.settings.publishKey = value;
await this.plugin.saveSettings();
}),
text.setPlaceholder("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").onChange(async (value) => {
this.plugin.settings.publishKey = value;
await this.plugin.saveSettings();
}),
);

new Setting(containerEl)
Expand Down
2 changes: 1 addition & 1 deletion selfhosted/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Docker image
# Pin to a version (recommended) for reproducible deployments.
mdbrain_IMAGE=ghcr.io/blackstorm/mdbrain:latest
MDBRAIN_IMAGE=ghcr.io/blackstorm/mdbrain:latest

## Data path (optional)
# Base directory inside the container where mdbrain stores DB, secrets, and local storage.
Expand Down
12 changes: 12 additions & 0 deletions server/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ body {
background-color: #fafafc;
}

/* Keep prose in control of code block layout; highlight.js only adds colors. */
.prose {
--tw-prose-pre-bg: #0d1117;
--tw-prose-pre-code: #c9d1d9;
}

/* Align highlight.js with typography pre styles. */
.prose pre code.hljs {
background: transparent;
padding: 0;
}

.navbar {
position: fixed;
top: 0;
Expand Down
7 changes: 5 additions & 2 deletions server/deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@
selmer/selmer {:mvn/version "1.12.69"}

;; Markdown 解析
org.commonmark/commonmark {:mvn/version "0.27.0"}
org.commonmark/commonmark-ext-yaml-front-matter {:mvn/version "0.27.0"}
org.commonmark/commonmark {:mvn/version "0.27.1"}
org.commonmark/commonmark-ext-yaml-front-matter {:mvn/version "0.27.1"}
org.commonmark/commonmark-ext-gfm-tables {:mvn/version "0.27.1"}
org.commonmark/commonmark-ext-task-list-items {:mvn/version "0.27.1"}
org.commonmark/commonmark-ext-gfm-strikethrough {:mvn/version "0.27.1"}

;; 日志
org.clojure/tools.logging {:mvn/version "1.3.0"}
Expand Down
8 changes: 8 additions & 0 deletions server/resources/publics/app/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,14 @@ body {
padding-top: var(--navbar-height);
background-color: #fafafc;
}
.prose {
--tw-prose-pre-bg: #0d1117;
--tw-prose-pre-code: #c9d1d9;
}
.prose pre code.hljs {
background: transparent;
padding: 0;
}
.navbar {
position: fixed;
top: 0;
Expand Down
Loading
Loading