mirror of
https://github.com/zadam/trilium.git
synced 2026-03-24 12:50:10 +01:00
Compare commits
22 Commits
standalone
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc575c508 | ||
|
|
62d6ce08a0 | ||
|
|
b50127b0d3 | ||
|
|
669a58cc0e | ||
|
|
bf4b5dad5a | ||
|
|
39972a9bd7 | ||
|
|
44f519c1d6 | ||
|
|
dd6c5bbf12 | ||
|
|
20d4db2608 | ||
|
|
3151e86665 | ||
|
|
96a0d483f5 | ||
|
|
3faefdbc85 | ||
|
|
12347d5c4a | ||
|
|
4dbaadf9cc | ||
|
|
2a1c165a54 | ||
|
|
939f931809 | ||
|
|
4fd09bf1f8 | ||
|
|
3231db3c3f | ||
|
|
c07ea1bfa7 | ||
|
|
79db638bf4 | ||
|
|
794dab2894 | ||
|
|
97b303aea6 |
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { BootstrapDefinition } from '@triliumnext/commons';
|
||||
import { entity_changes, getContext, getSharedBootstrapItems, getSql, routes } from '@triliumnext/core';
|
||||
import { entity_changes, getContext, getSharedBootstrapItems, getSql, routes, sql_init } from '@triliumnext/core';
|
||||
import { getIconConfig } from '@triliumnext/core/src/services/bootstrap_utils';
|
||||
|
||||
import packageJson from '../../package.json' with { type: 'json' };
|
||||
import { type BrowserRequest, BrowserRouter } from './browser_router';
|
||||
@@ -178,10 +179,23 @@ function apiResultHandler(_req: any, res: ResultHandlerResponse, result: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op auth middleware for standalone — there's no authentication.
|
||||
* No-op middleware stubs for standalone mode.
|
||||
*
|
||||
* In a browser context there is no network authentication, rate limiting,
|
||||
* or multi-user access, so all auth/rate-limit middleware is a no-op.
|
||||
*
|
||||
* `checkAppNotInitialized` still guards setup routes: if the database is
|
||||
* already initialised the middleware throws so the route handler is never
|
||||
* reached (mirrors the server behaviour).
|
||||
*/
|
||||
function checkApiAuth() {
|
||||
// No authentication in standalone mode.
|
||||
function noopMiddleware() {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
function checkAppNotInitialized() {
|
||||
if (sql_init.isDbInitialized()) {
|
||||
throw new Error("App already initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,11 +220,15 @@ export function registerRoutes(router: BrowserRouter): void {
|
||||
const apiRoute = createApiRoute(router, true);
|
||||
routes.buildSharedApiRoutes({
|
||||
route: createRoute(router),
|
||||
asyncRoute: createRoute(router),
|
||||
apiRoute,
|
||||
asyncApiRoute: createApiRoute(router, false),
|
||||
apiResultHandler,
|
||||
checkApiAuth,
|
||||
checkApiAuthOrElectron: checkApiAuth
|
||||
checkApiAuth: noopMiddleware,
|
||||
checkApiAuthOrElectron: noopMiddleware,
|
||||
checkAppNotInitialized,
|
||||
checkCredentials: noopMiddleware,
|
||||
loginRateLimiter: noopMiddleware
|
||||
});
|
||||
apiRoute('get', '/bootstrap', bootstrapRoute);
|
||||
|
||||
@@ -220,10 +238,22 @@ export function registerRoutes(router: BrowserRouter): void {
|
||||
apiRoute("get", "/api/system-checks", () => ({ isCpuArchMismatch: false }));
|
||||
}
|
||||
|
||||
function bootstrapRoute() {
|
||||
function bootstrapRoute(): BootstrapDefinition {
|
||||
const assetPath = ".";
|
||||
|
||||
if (!sql_init.isDbInitialized()) {
|
||||
return {
|
||||
dbInitialized: false,
|
||||
baseApiUrl: "../api/",
|
||||
assetPath,
|
||||
themeCssUrl: false,
|
||||
themeUseNextAsBase: "next",
|
||||
...getIconConfig(assetPath)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
dbInitialized: true,
|
||||
...getSharedBootstrapItems(assetPath),
|
||||
appPath: assetPath,
|
||||
device: false, // Let the client detect device type.
|
||||
@@ -248,7 +278,7 @@ function bootstrapRoute() {
|
||||
instanceName: null,
|
||||
appCssNoteIds: [],
|
||||
TRILIUM_SAFE_MODE: false
|
||||
} satisfies BootstrapDefinition;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,6 @@
|
||||
import { type BindableValue, default as sqlite3InitModule } from "@sqlite.org/sqlite-wasm";
|
||||
import type { DatabaseProvider, RunResult, Statement, Transaction } from "@triliumnext/core";
|
||||
|
||||
import demoDbSql from "./db.sql?raw";
|
||||
|
||||
// Type definitions for SQLite WASM (the library doesn't export these directly)
|
||||
type Sqlite3Module = Awaited<ReturnType<typeof sqlite3InitModule>>;
|
||||
type Sqlite3Database = InstanceType<Sqlite3Module["oo1"]["DB"]>;
|
||||
@@ -431,32 +429,10 @@ export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
this.opfsDbPath = undefined; // Not using OPFS
|
||||
this.db.exec("PRAGMA journal_mode = WAL");
|
||||
|
||||
// Initialize with demo data for in-memory databases
|
||||
// (since they won't persist anyway)
|
||||
this.initializeDemoDatabase();
|
||||
|
||||
const loadTime = performance.now() - startTime;
|
||||
console.log(`[BrowserSqlProvider] In-memory database created in ${loadTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database with demo/starter data.
|
||||
* This should only be called once when creating a new database.
|
||||
*
|
||||
* For OPFS databases, this is called automatically only if the database
|
||||
* doesn't already exist.
|
||||
*/
|
||||
initializeDemoDatabase(): void {
|
||||
this.ensureDb();
|
||||
console.log("[BrowserSqlProvider] Initializing database with demo data...");
|
||||
const startTime = performance.now();
|
||||
|
||||
this.db!.exec(demoDbSql);
|
||||
|
||||
const loadTime = performance.now() - startTime;
|
||||
console.log(`[BrowserSqlProvider] Demo data loaded in ${loadTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
loadFromBuffer(buffer: Uint8Array): void {
|
||||
this.ensureSqlite3();
|
||||
// SQLite WASM can deserialize a database from a byte array
|
||||
|
||||
@@ -132,15 +132,6 @@ async function initialize(): Promise<void> {
|
||||
if (sqlProvider!.isOpfsAvailable()) {
|
||||
console.log("[Worker] OPFS available, loading persistent database...");
|
||||
sqlProvider!.loadFromOpfs("/trilium.db");
|
||||
|
||||
// Check if database is initialized (schema exists)
|
||||
if (!sqlProvider!.isDbInitialized()) {
|
||||
console.log("[Worker] Database not initialized, loading demo data...");
|
||||
sqlProvider!.initializeDemoDatabase();
|
||||
console.log("[Worker] Demo data loaded");
|
||||
} else {
|
||||
console.log("[Worker] Existing initialized database loaded");
|
||||
}
|
||||
} else {
|
||||
// Fall back to in-memory database (non-persistent)
|
||||
console.warn("[Worker] OPFS not available, using in-memory database (data will not persist)");
|
||||
@@ -177,8 +168,16 @@ async function initialize(): Promise<void> {
|
||||
router = createConfiguredRouter();
|
||||
console.log("[Worker] Router configured");
|
||||
|
||||
console.log("[Worker] Initializing becca...");
|
||||
await coreModule.becca_loader.beccaLoaded;
|
||||
// initializeDb runs initDbConnection inside an execution context,
|
||||
// which resolves dbReady — required before beccaLoaded can settle.
|
||||
coreModule.sql_init.initializeDb();
|
||||
|
||||
if (coreModule.sql_init.isDbInitialized()) {
|
||||
console.log("[Worker] Database already initialized, loading becca...");
|
||||
await coreModule.becca_loader.beccaLoaded;
|
||||
} else {
|
||||
console.log("[Worker] Database not initialized, skipping becca load (will be loaded during DB initialization)");
|
||||
}
|
||||
|
||||
console.log("[Worker] Initialization complete");
|
||||
} catch (error) {
|
||||
|
||||
@@ -112,6 +112,8 @@ function loadIcons() {
|
||||
}
|
||||
|
||||
function setBodyAttributes() {
|
||||
if (!glob.dbInitialized) return;
|
||||
|
||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
|
||||
const classesToSet = [
|
||||
device,
|
||||
@@ -132,6 +134,11 @@ function setBodyAttributes() {
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
if (!glob.dbInitialized) {
|
||||
await import("./setup.js");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (glob.device) {
|
||||
case "mobile":
|
||||
await import("./mobile.js");
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import appContext from "../components/app_context.js";
|
||||
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
|
||||
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
|
||||
import FBlob, { type FBlobRow } from "../entities/fblob.js";
|
||||
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
|
||||
import FNote, { type FNoteRow } from "../entities/fnote.js";
|
||||
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
|
||||
import server from "./server.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import FBlob, { type FBlobRow } from "../entities/fblob.js";
|
||||
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
|
||||
import type { Froca } from "./froca-interface.js";
|
||||
import server from "./server.js";
|
||||
|
||||
interface SubtreeResponse {
|
||||
notes: FNoteRow[];
|
||||
@@ -44,8 +44,9 @@ class FrocaImpl implements Froca {
|
||||
}
|
||||
|
||||
async loadInitialTree() {
|
||||
const resp = await server.get<SubtreeResponse>("tree");
|
||||
if (!glob.dbInitialized) return;
|
||||
|
||||
const resp = await server.get<SubtreeResponse>("tree");
|
||||
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
|
||||
this.#clear();
|
||||
this.addResp(resp);
|
||||
@@ -77,7 +78,7 @@ class FrocaImpl implements Froca {
|
||||
for (const noteRow of noteRows) {
|
||||
const { noteId } = noteRow;
|
||||
|
||||
let note = this.notes[noteId];
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (note) {
|
||||
note.update(noteRow);
|
||||
@@ -240,9 +241,8 @@ class FrocaImpl implements Froca {
|
||||
console.trace(`Can't find note '${noteId}'`);
|
||||
|
||||
return null;
|
||||
} else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
return this.notes[noteId];
|
||||
})
|
||||
.filter((note) => !!note) as FNote[];
|
||||
}
|
||||
@@ -263,9 +263,8 @@ class FrocaImpl implements Froca {
|
||||
console.trace(`Can't find note '${noteId}'`);
|
||||
|
||||
return null;
|
||||
} else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
return this.notes[noteId];
|
||||
})
|
||||
.filter((note) => !!note) as FNote[];
|
||||
}
|
||||
@@ -338,11 +337,10 @@ class FrocaImpl implements Froca {
|
||||
attachmentRows = await server.getWithSilentNotFound<FAttachmentRow[]>(`attachments/${attachmentId}/all`);
|
||||
} catch (e: any) {
|
||||
if (silentNotFoundError) {
|
||||
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ` + e.message);
|
||||
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ${ e.message}`);
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const attachments = this.processAttachmentRows(attachmentRows);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import options from "./options.js";
|
||||
import { type Locale, LOCALE_IDS, setDayjsLocale } from "@triliumnext/commons";
|
||||
import i18next from "i18next";
|
||||
import i18nextHttpBackend from "i18next-http-backend";
|
||||
import server from "./server.js";
|
||||
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import options from "./options.js";
|
||||
import server from "./server.js";
|
||||
|
||||
let locales: Locale[] | null;
|
||||
|
||||
/**
|
||||
* A deferred promise that resolves when translations are initialized.
|
||||
*/
|
||||
export let translationsInitializedPromise = $.Deferred();
|
||||
export const translationsInitializedPromise = $.Deferred();
|
||||
|
||||
export async function initLocale() {
|
||||
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
|
||||
@@ -34,7 +35,7 @@ export async function initLocale() {
|
||||
|
||||
export function getAvailableLocales() {
|
||||
if (!locales) {
|
||||
throw new Error("Tried to load list of locales, but localization is not yet initialized.")
|
||||
throw new Error("Tried to load list of locales, but localization is not yet initialized.");
|
||||
}
|
||||
|
||||
return locales;
|
||||
|
||||
111
apps/client/src/setup.css
Normal file
111
apps/client/src/setup.css
Normal file
@@ -0,0 +1,111 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.setup {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&>div {
|
||||
background: var(--left-pane-background-color);
|
||||
padding: 2em;
|
||||
width: 600px;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.setup-container {
|
||||
background-color: var(--main-background-color);
|
||||
border-radius: 16px;
|
||||
padding: 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
height: 550px;
|
||||
width: 700px;
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||
|
||||
.setup-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.setup-option-card {
|
||||
padding: 1.5em;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--card-background-hover-color);
|
||||
filter: contrast(105%);
|
||||
transition: background-color .2s ease-out;
|
||||
}
|
||||
|
||||
.tn-icon {
|
||||
font-size: 2.5em;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
>main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
>footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
gap: 1rem;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.form-item-with-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.tn-icon {
|
||||
font-size: 1.5em;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
apps/client/src/setup.tsx
Normal file
129
apps/client/src/setup.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import "./setup.css";
|
||||
|
||||
import { ComponentChildren, render } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { initLocale, t } from "./services/i18n";
|
||||
import server from "./services/server";
|
||||
import Button from "./widgets/react/Button";
|
||||
import { CardFrame } from "./widgets/react/Card";
|
||||
import FormTextBox from "./widgets/react/FormTextBox";
|
||||
import Icon from "./widgets/react/Icon";
|
||||
|
||||
async function main() {
|
||||
await initLocale();
|
||||
|
||||
const bodyWrapper = document.createElement("div");
|
||||
document.body.classList.add("setup");
|
||||
render(<App />, bodyWrapper);
|
||||
document.body.replaceChildren(bodyWrapper);
|
||||
}
|
||||
|
||||
type State = "firstOptions" | "syncFromDesktop" | "syncFromServer";
|
||||
|
||||
function App() {
|
||||
const [ state, setState ] = useState<State>("syncFromServer");
|
||||
|
||||
return (
|
||||
<div class="setup-container">
|
||||
{state === "firstOptions" && <SetupOptions setState={setState} />}
|
||||
{state === "syncFromServer" && <SyncFromServer setState={setState} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupOptions({ setState }: { setState: (state: State) => void }) {
|
||||
return (
|
||||
<div class="page setup-options-container">
|
||||
<h1>{t("setup.heading")}</h1>
|
||||
|
||||
<div class="setup-options">
|
||||
<SetupOptionCard
|
||||
icon="bx bx-file-blank"
|
||||
title={t("setup.new-document")}
|
||||
description={t("setup.new-document-description")}
|
||||
onClick={async () => {
|
||||
await server.post("setup/new-document");
|
||||
|
||||
// After creating a new document, we can just reload the page to load it.
|
||||
location.reload();
|
||||
}}
|
||||
/>
|
||||
|
||||
<SetupOptionCard
|
||||
icon="bx bx-server"
|
||||
title={t("setup.sync-from-server")}
|
||||
description={t("setup.sync-from-server-description")}
|
||||
onClick={() => setState("syncFromServer")}
|
||||
/>
|
||||
|
||||
<SetupOptionCard
|
||||
icon="bx bx-desktop"
|
||||
title={t("setup.sync-from-desktop")}
|
||||
description={t("setup.sync-from-desktop-description")}
|
||||
onClick={() => setState("syncFromDesktop")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SyncFromServer({ setState }: { setState: (state: State) => void }) {
|
||||
const [serverUrl, setServerUrl] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
function handleFinishSetup() {
|
||||
server.post("setup/sync-from-server", {
|
||||
syncServerHost: serverUrl,
|
||||
password
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="page sync-from-server">
|
||||
<h1>{t("setup.sync-from-server-page-title")}</h1>
|
||||
<p>{t("setup.sync-from-server-page-description")}</p>
|
||||
|
||||
<main>
|
||||
<form>
|
||||
<FormItemWithIcon icon="bx bx-server">
|
||||
<FormTextBox placeholder="https://example.com" currentValue={serverUrl} onChange={setServerUrl} />
|
||||
</FormItemWithIcon>
|
||||
|
||||
<FormItemWithIcon icon="bx bx-lock">
|
||||
<FormTextBox placeholder={t("setup.password-placeholder")} type="password" currentValue={password} onChange={setPassword} />
|
||||
</FormItemWithIcon>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<Button text={t("setup.button-back")} onClick={() => setState("firstOptions")} kind="lowProfile" />
|
||||
<Button text={t("setup.button-finish-setup")} kind="primary" onClick={handleFinishSetup} />
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormItemWithIcon({ icon, children }: { icon: string; children: ComponentChildren }) {
|
||||
return (
|
||||
<div class="form-item-with-icon">
|
||||
<Icon icon={icon} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupOptionCard({ title, description, icon, onClick }: { title: string; description: string, icon: string, onClick?: () => void }) {
|
||||
return (
|
||||
<CardFrame className="setup-option-card" onClick={onClick}>
|
||||
<Icon icon={icon} />
|
||||
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -2230,5 +2230,23 @@
|
||||
"sample_xy": "XY",
|
||||
"sample_venn": "Venn",
|
||||
"sample_ishikawa": "Ishikawa"
|
||||
},
|
||||
"setup": {
|
||||
"heading": "Trilium Notes setup",
|
||||
"new-document": "New document",
|
||||
"new-document-description": "Start with a clean workspace and begin immediately.",
|
||||
"sync-from-desktop": "Sync from desktop",
|
||||
"sync-from-desktop-description": "“Import data from your desktop app on this network.",
|
||||
"sync-from-server": "Sync from server",
|
||||
"sync-from-server-description": "Connect to an existing sync server with your credentials.",
|
||||
"next": "Next",
|
||||
"init-in-progress": "Document initialization in progress",
|
||||
"redirecting": "You will be shortly redirected to the application.",
|
||||
"title": "Setup",
|
||||
"sync-from-server-page-title": "Connect to sync server",
|
||||
"sync-from-server-page-description": "Enter your server details below to connect your existing workspace.",
|
||||
"password-placeholder": "Password",
|
||||
"button-back": "Back",
|
||||
"button-finish-setup": "Finish setup"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import passwordApiRoute from "./api/password.js";
|
||||
import recoveryCodes from './api/recovery_codes.js';
|
||||
import scriptRoute from "./api/script.js";
|
||||
import senderRoute from "./api/sender.js";
|
||||
import setupApiRoute from "./api/setup.js";
|
||||
import systemInfoRoute from "./api/system_info.js";
|
||||
import totp from './api/totp.js';
|
||||
// API routes
|
||||
@@ -83,11 +82,15 @@ function register(app: express.Application) {
|
||||
|
||||
routes.buildSharedApiRoutes({
|
||||
route,
|
||||
asyncRoute,
|
||||
apiRoute,
|
||||
asyncApiRoute,
|
||||
apiResultHandler,
|
||||
checkApiAuth: auth.checkApiAuth,
|
||||
checkApiAuthOrElectron: auth.checkApiAuthOrElectron
|
||||
checkApiAuthOrElectron: auth.checkApiAuthOrElectron,
|
||||
checkAppNotInitialized: auth.checkAppNotInitialized,
|
||||
checkCredentials: auth.checkCredentials,
|
||||
loginRateLimiter
|
||||
});
|
||||
|
||||
route(PUT, "/api/notes/:noteId/file", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], filesRoute.updateFile, apiResultHandler);
|
||||
@@ -149,13 +152,6 @@ function register(app: express.Application) {
|
||||
// docker health check
|
||||
route(GET, "/api/health-check", [], () => ({ status: "ok" }), apiResultHandler);
|
||||
|
||||
// group of the services below are meant to be executed from the outside
|
||||
route(GET, "/api/setup/status", [], setupApiRoute.getStatus, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/new-document", [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-from-server", [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
|
||||
route(GET, "/api/setup/sync-seed", [loginRateLimiter, auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-seed", [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
|
||||
|
||||
route(PST, "/api/login/sync", [loginRateLimiter], loginApiRoute.loginSync, apiResultHandler);
|
||||
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
||||
apiRoute(PST, "/api/login/protected", loginApiRoute.loginToProtectedSession);
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import syncService from "./sync.js";
|
||||
import log from "./log.js";
|
||||
import sqlInit from "./sql_init.js";
|
||||
import optionService from "./options.js";
|
||||
import syncOptions from "./sync_options.js";
|
||||
import { request } from "@triliumnext/core";
|
||||
import appInfo from "./app_info.js";
|
||||
import { timeLimit } from "./utils.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import type { SetupStatusResponse, SetupSyncSeedResponse } from "./api-interface.js";
|
||||
|
||||
async function hasSyncServerSchemaAndSeed() {
|
||||
const response = await requestToSyncServer<SetupStatusResponse>("GET", "/api/setup/status");
|
||||
|
||||
if (response.syncVersion !== appInfo.syncVersion) {
|
||||
throw new Error(
|
||||
`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`
|
||||
);
|
||||
}
|
||||
|
||||
return response.schemaExists;
|
||||
}
|
||||
|
||||
function triggerSync() {
|
||||
log.info("Triggering sync.");
|
||||
|
||||
// it's ok to not wait for it here
|
||||
syncService.sync().then((res) => {
|
||||
if (res.success) {
|
||||
sqlInit.setDbAsInitialized();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function sendSeedToSyncServer() {
|
||||
log.info("Initiating sync to server");
|
||||
|
||||
await requestToSyncServer<void>("POST", "/api/setup/sync-seed", {
|
||||
options: getSyncSeedOptions(),
|
||||
syncVersion: appInfo.syncVersion
|
||||
});
|
||||
|
||||
// this is a completely new sync, need to reset counters. If this was not a new sync,
|
||||
// the previous request would have failed.
|
||||
optionService.setOption("lastSyncedPush", 0);
|
||||
optionService.setOption("lastSyncedPull", 0);
|
||||
}
|
||||
|
||||
async function requestToSyncServer<T>(method: string, path: string, body?: string | {}): Promise<T> {
|
||||
const timeout = syncOptions.getSyncTimeout();
|
||||
|
||||
return (await timeLimit(
|
||||
request.exec({
|
||||
method,
|
||||
url: syncOptions.getSyncServerHost() + path,
|
||||
body,
|
||||
proxy: syncOptions.getSyncProxy(),
|
||||
timeout: timeout
|
||||
}),
|
||||
timeout
|
||||
)) as T;
|
||||
}
|
||||
|
||||
async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string) {
|
||||
if (sqlInit.isDbInitialized()) {
|
||||
return {
|
||||
result: "failure",
|
||||
error: "DB is already initialized."
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Getting document options FROM sync server.");
|
||||
|
||||
// the response is expected to contain documentId and documentSecret options
|
||||
const resp = await request.exec<SetupSyncSeedResponse>({
|
||||
method: "get",
|
||||
url: `${syncServerHost}/api/setup/sync-seed`,
|
||||
auth: { password },
|
||||
proxy: syncProxy,
|
||||
timeout: 30000 // seed request should not take long
|
||||
});
|
||||
|
||||
if (resp.syncVersion !== appInfo.syncVersion) {
|
||||
const message = `Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${resp.syncVersion}. To fix this issue, use same Trilium version on all instances.`;
|
||||
|
||||
log.error(message);
|
||||
|
||||
return {
|
||||
result: "failure",
|
||||
error: message
|
||||
};
|
||||
}
|
||||
|
||||
await sqlInit.createDatabaseForSync(resp.options, syncServerHost, syncProxy);
|
||||
|
||||
triggerSync();
|
||||
|
||||
return { result: "success" };
|
||||
} catch (e: any) {
|
||||
log.error(`Sync failed: '${e.message}', stack: ${e.stack}`);
|
||||
|
||||
return {
|
||||
result: "failure",
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getSyncSeedOptions() {
|
||||
return [becca.getOption("documentId"), becca.getOption("documentSecret")];
|
||||
}
|
||||
|
||||
export default {
|
||||
hasSyncServerSchemaAndSeed,
|
||||
triggerSync,
|
||||
sendSeedToSyncServer,
|
||||
setupSyncFromSyncServer,
|
||||
getSyncSeedOptions
|
||||
};
|
||||
@@ -1,164 +1,27 @@
|
||||
import { deferred, type OptionRow } from "@triliumnext/commons";
|
||||
import { events as eventService } from "@triliumnext/core";
|
||||
import { type OptionRow } from "@triliumnext/commons";
|
||||
import { sql_init as coreSqlInit } from "@triliumnext/core";
|
||||
import fs from "fs";
|
||||
import { t } from "i18next";
|
||||
|
||||
import BBranch from "../becca/entities/bbranch.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import BOption from "../becca/entities/boption.js";
|
||||
import backup from "./backup.js";
|
||||
import cls from "./cls.js";
|
||||
import config from "./config.js";
|
||||
import password from "./encryption/password.js";
|
||||
import hidden_subtree from "./hidden_subtree.js";
|
||||
import zipImportService from "./import/zip.js";
|
||||
import log from "./log.js";
|
||||
import migrationService from "./migration.js";
|
||||
import optionService from "./options.js";
|
||||
import port from "./port.js";
|
||||
import resourceDir from "./resource_dir.js";
|
||||
import sql from "./sql.js";
|
||||
import TaskContext from "./task_context.js";
|
||||
import { isElectron } from "./utils.js";
|
||||
|
||||
export const dbReady = deferred<void>();
|
||||
|
||||
function schemaExists() {
|
||||
return !!sql.getValue(/*sql*/`SELECT name FROM sqlite_master
|
||||
WHERE type = 'table' AND name = 'options'`);
|
||||
}
|
||||
|
||||
function isDbInitialized() {
|
||||
if (!schemaExists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const initialized = sql.getValue("SELECT value FROM options WHERE name = 'initialized'");
|
||||
|
||||
return initialized === "true";
|
||||
}
|
||||
|
||||
async function initDbConnection() {
|
||||
if (!isDbInitialized()) {
|
||||
if (isElectron) {
|
||||
log.info(t("sql_init.db_not_initialized_desktop"));
|
||||
} else {
|
||||
log.info(t("sql_init.db_not_initialized_server", { port }));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await migrationService.migrateIfNecessary();
|
||||
|
||||
sql.execute('CREATE TEMP TABLE IF NOT EXISTS "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)');
|
||||
|
||||
sql.execute(`
|
||||
CREATE TABLE IF NOT EXISTS "user_data"
|
||||
(
|
||||
tmpID INT,
|
||||
username TEXT,
|
||||
email TEXT,
|
||||
userIDEncryptedDataKey TEXT,
|
||||
userIDVerificationHash TEXT,
|
||||
salt TEXT,
|
||||
derivedKey TEXT,
|
||||
isSetup TEXT DEFAULT "false",
|
||||
UNIQUE (tmpID),
|
||||
PRIMARY KEY (tmpID)
|
||||
);`);
|
||||
|
||||
dbReady.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the database schema, creating the necessary tables and importing the demo content.
|
||||
*
|
||||
* @param skipDemoDb if set to `true`, then the demo database will not be imported, resulting in an empty root note.
|
||||
* @throws {Error} if the database is already initialized.
|
||||
*/
|
||||
async function createInitialDatabase(skipDemoDb?: boolean) {
|
||||
if (isDbInitialized()) {
|
||||
throw new Error("DB is already initialized");
|
||||
}
|
||||
|
||||
const schema = fs.readFileSync(`${resourceDir.DB_INIT_DIR}/schema.sql`, "utf-8");
|
||||
const demoFile = (!skipDemoDb ? fs.readFileSync(`${resourceDir.DB_INIT_DIR}/demo.zip`) : null);
|
||||
|
||||
let rootNote!: BNote;
|
||||
|
||||
// We have to import async since options init requires keyboard actions which require translations.
|
||||
const optionsInitService = (await import("./options_init.js")).default;
|
||||
const becca_loader = (await import("@triliumnext/core")).becca_loader;
|
||||
|
||||
sql.transactional(() => {
|
||||
log.info("Creating database schema ...");
|
||||
|
||||
sql.executeScript(schema);
|
||||
|
||||
becca_loader.load();
|
||||
|
||||
log.info("Creating root note ...");
|
||||
|
||||
rootNote = new BNote({
|
||||
noteId: "root",
|
||||
title: "root",
|
||||
type: "text",
|
||||
mime: "text/html"
|
||||
}).save();
|
||||
|
||||
rootNote.setContent("");
|
||||
|
||||
new BBranch({
|
||||
noteId: "root",
|
||||
parentNoteId: "none",
|
||||
isExpanded: true,
|
||||
notePosition: 10
|
||||
}).save();
|
||||
|
||||
optionsInitService.initDocumentOptions();
|
||||
optionsInitService.initNotSyncedOptions(true, {});
|
||||
optionsInitService.initStartupOptions();
|
||||
password.resetPassword();
|
||||
});
|
||||
|
||||
// Check hidden subtree.
|
||||
// This ensures the existence of system templates, for the demo content.
|
||||
console.log("Checking hidden subtree at first start.");
|
||||
cls.init(() => hidden_subtree.checkHiddenSubtree());
|
||||
|
||||
// Import demo content.
|
||||
log.info("Importing demo content...");
|
||||
|
||||
const dummyTaskContext = new TaskContext("no-progress-reporting", "importNotes", null);
|
||||
|
||||
if (demoFile) {
|
||||
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
|
||||
}
|
||||
|
||||
// Post-demo.
|
||||
sql.transactional(() => {
|
||||
// this needs to happen after ZIP import,
|
||||
// the previous solution was to move option initialization here, but then the important parts of initialization
|
||||
// are not all in one transaction (because ZIP import is async and thus not transactional)
|
||||
|
||||
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
|
||||
|
||||
optionService.setOption(
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
log.info("Schema and initial content generated.");
|
||||
|
||||
initDbConnection();
|
||||
}
|
||||
const schemaExists = coreSqlInit.schemaExists;
|
||||
const isDbInitialized = coreSqlInit.isDbInitialized;
|
||||
const dbReady = coreSqlInit.dbReady;
|
||||
const setDbAsInitialized = coreSqlInit.setDbAsInitialized;
|
||||
const initDbConnection = coreSqlInit.initDbConnection;
|
||||
const initializeDb = coreSqlInit.initializeDb;
|
||||
export const getDbSize = coreSqlInit.getDbSize;
|
||||
|
||||
async function createDatabaseForSync(options: OptionRow[], syncServerHost = "", syncProxy = "") {
|
||||
log.info("Creating database for sync");
|
||||
@@ -186,57 +49,6 @@ async function createDatabaseForSync(options: OptionRow[], syncServerHost = "",
|
||||
log.info("Schema and not synced options generated.");
|
||||
}
|
||||
|
||||
function setDbAsInitialized() {
|
||||
if (!isDbInitialized()) {
|
||||
optionService.setOption("initialized", "true");
|
||||
|
||||
initDbConnection();
|
||||
|
||||
// Emit an event to notify that the database is now initialized
|
||||
eventService.emit(eventService.DB_INITIALIZED);
|
||||
|
||||
log.info("Database initialization completed, emitted DB_INITIALIZED event");
|
||||
}
|
||||
}
|
||||
|
||||
function optimize() {
|
||||
if (config.General.readOnly) {
|
||||
return;
|
||||
}
|
||||
log.info("Optimizing database");
|
||||
const start = Date.now();
|
||||
|
||||
sql.execute("PRAGMA optimize");
|
||||
|
||||
log.info(`Optimization finished in ${Date.now() - start}ms.`);
|
||||
}
|
||||
|
||||
export function getDbSize() {
|
||||
return sql.getValue<number>("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()");
|
||||
}
|
||||
|
||||
function initializeDb() {
|
||||
cls.init(initDbConnection);
|
||||
|
||||
dbReady.then(() => {
|
||||
if (config.General && config.General.noBackup === true) {
|
||||
log.info("Disabling scheduled backups.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setInterval(() => backup.regularBackup(), 4 * 60 * 60 * 1000);
|
||||
|
||||
// kickoff first backup soon after start up
|
||||
setTimeout(() => backup.regularBackup(), 5 * 60 * 1000);
|
||||
|
||||
// optimize is usually inexpensive no-op, so running it semi-frequently is not a big deal
|
||||
setTimeout(() => optimize(), 60 * 60 * 1000);
|
||||
|
||||
setInterval(() => optimize(), 10 * 60 * 60 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
dbReady,
|
||||
schemaExists,
|
||||
|
||||
@@ -312,11 +312,25 @@ export interface DefinitionObject {
|
||||
inverseRelation?: string;
|
||||
}
|
||||
|
||||
export interface BootstrapDefinition {
|
||||
device: "mobile" | "desktop" | "print" | false;
|
||||
csrfToken: string;
|
||||
/**
|
||||
* Subset of bootstrap items that are available both in the main client and in the setup page.
|
||||
*/
|
||||
interface BootstrapCommonItems {
|
||||
baseApiUrl: string;
|
||||
assetPath: string;
|
||||
themeCssUrl: string | false;
|
||||
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
|
||||
iconPackCss: string;
|
||||
iconRegistry: IconRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap items that the client needs to start up. These are sent by the server in the HTML and made available as `window.glob`.
|
||||
*/
|
||||
export type BootstrapDefinition = BootstrapCommonItems & ({
|
||||
dbInitialized: true;
|
||||
device: "mobile" | "desktop" | "print" | false;
|
||||
csrfToken: string;
|
||||
headingStyle: "plain" | "underline" | "markdown";
|
||||
layoutOrientation: "vertical" | "horizontal";
|
||||
platform?: typeof process.platform | "web";
|
||||
@@ -332,15 +346,13 @@ export interface BootstrapDefinition {
|
||||
isMainWindow: boolean;
|
||||
isProtectedSessionAvailable: boolean;
|
||||
triliumVersion: string;
|
||||
assetPath: string;
|
||||
appPath: string;
|
||||
baseApiUrl: string;
|
||||
currentLocale: Locale;
|
||||
isRtl: boolean;
|
||||
iconPackCss: string;
|
||||
iconRegistry: IconRegistry;
|
||||
TRILIUM_SAFE_MODE: boolean;
|
||||
}
|
||||
} | {
|
||||
dbInitialized: false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Response for /api/setup/status.
|
||||
|
||||
@@ -10,6 +10,7 @@ import appInfo from "./services/app_info";
|
||||
|
||||
export type * from "./services/sql/types";
|
||||
export * from "./services/sql/index";
|
||||
export { default as sql_init } from "./services/sql_init";
|
||||
export * as protected_session from "./services/protected_session";
|
||||
export { default as data_encryption } from "./services/encryption/data_encryption"
|
||||
export * as binary_utils from "./services/utils/binary";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
import sqlInit from "../../services/sql_init.js";
|
||||
import setupService from "../../services/setup.js";
|
||||
import log from "../../services/log.js";
|
||||
import { getLog } from "../../services/log.js";
|
||||
import appInfo from "../../services/app_info.js";
|
||||
import type { Request } from "express";
|
||||
|
||||
@@ -27,6 +25,7 @@ function setupSyncFromServer(req: Request) {
|
||||
function saveSyncSeed(req: Request) {
|
||||
const { options, syncVersion } = req.body;
|
||||
|
||||
const log = getLog();
|
||||
if (appInfo.syncVersion !== syncVersion) {
|
||||
const message = `Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${syncVersion}. To fix this issue, use same Trilium version on all instances.`;
|
||||
|
||||
@@ -42,7 +41,7 @@ function saveSyncSeed(req: Request) {
|
||||
|
||||
log.info("Saved sync seed.");
|
||||
|
||||
sqlInit.createDatabaseForSync(options);
|
||||
// sqlInit.createDatabaseForSync(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +73,7 @@ function saveSyncSeed(req: Request) {
|
||||
* - user-password: []
|
||||
*/
|
||||
function getSyncSeed() {
|
||||
log.info("Serving sync seed.");
|
||||
getLog().info("Serving sync seed.");
|
||||
|
||||
return {
|
||||
options: setupService.getSyncSeedOptions(),
|
||||
@@ -23,6 +23,7 @@ import syncApiRoute from "./api/sync";
|
||||
import autocompleteApiRoute from "./api/autocomplete";
|
||||
import similarNotesRoute from "./api/similar_notes";
|
||||
import imageRoute from "./api/image";
|
||||
import setupApiRoute from "./api/setup";
|
||||
|
||||
// TODO: Deduplicate with routes.ts
|
||||
const GET = "get",
|
||||
@@ -33,14 +34,18 @@ const GET = "get",
|
||||
|
||||
interface SharedApiRoutesContext {
|
||||
route: any;
|
||||
asyncRoute: any;
|
||||
apiRoute: any;
|
||||
asyncApiRoute: any;
|
||||
checkApiAuth: any;
|
||||
apiResultHandler: any;
|
||||
checkApiAuthOrElectron: any;
|
||||
checkAppNotInitialized: any;
|
||||
loginRateLimiter: any;
|
||||
checkCredentials: any;
|
||||
}
|
||||
|
||||
export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron }: SharedApiRoutesContext) {
|
||||
export function buildSharedApiRoutes({ route, asyncRoute, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron, checkAppNotInitialized, checkCredentials, loginRateLimiter }: SharedApiRoutesContext) {
|
||||
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
|
||||
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
|
||||
|
||||
@@ -111,6 +116,13 @@ export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiA
|
||||
route(GET, "/api/attachments/:attachmentId/image/:filename", [checkApiAuthOrElectron], imageRoute.returnAttachedImage);
|
||||
route(GET, "/api/images/:noteId/:filename", [checkApiAuthOrElectron], imageRoute.returnImageFromNote);
|
||||
|
||||
// group of the services below are meant to be executed from the outside
|
||||
route(GET, "/api/setup/status", [], setupApiRoute.getStatus, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/new-document", [checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-from-server", [checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
|
||||
route(GET, "/api/setup/sync-seed", [loginRateLimiter, checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-seed", [checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
|
||||
|
||||
asyncApiRoute(PST, "/api/sync/test", syncApiRoute.testSync);
|
||||
asyncApiRoute(PST, "/api/sync/now", syncApiRoute.syncNow);
|
||||
apiRoute(PST, "/api/sync/fill-entity-changes", syncApiRoute.fillEntityChanges);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getCurrentLocale } from "./i18n";
|
||||
|
||||
export default function getSharedBootstrapItems(assetPath: string): Pick<BootstrapDefinition, "assetPath" | "headingStyle" | "layoutOrientation" | "maxEntityChangeIdAtLoad" | "maxEntityChangeSyncIdAtLoad" | "isProtectedSessionAvailable" | "iconRegistry" | "iconPackCss" | "currentLocale" | "isRtl"> {
|
||||
const sql = getSql();
|
||||
const iconPacks = getIconPacks();
|
||||
const currentLocale = getCurrentLocale();
|
||||
|
||||
return {
|
||||
@@ -19,6 +18,14 @@ export default function getSharedBootstrapItems(assetPath: string): Pick<Bootstr
|
||||
isProtectedSessionAvailable: protected_session.isProtectedSessionAvailable(),
|
||||
currentLocale,
|
||||
isRtl: !!currentLocale.rtl,
|
||||
...getIconConfig(assetPath)
|
||||
}
|
||||
}
|
||||
|
||||
export function getIconConfig(assetPath: string): Pick<BootstrapDefinition, "iconRegistry" | "iconPackCss"> {
|
||||
const iconPacks = getIconPacks();
|
||||
|
||||
return {
|
||||
iconRegistry: generateIconRegistry(iconPacks),
|
||||
iconPackCss: iconPacks
|
||||
.map(p => generateCss(p, p.builtin
|
||||
@@ -26,5 +33,5 @@ export default function getSharedBootstrapItems(assetPath: string): Pick<Bootstr
|
||||
: `api/attachments/download/${p.fontAttachmentId}`))
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,73 @@
|
||||
import { deferred } from "@triliumnext/commons";
|
||||
import { getSql } from "./sql";
|
||||
import { getLog } from "./log";
|
||||
import { isElectron } from "./utils";
|
||||
import { t } from "i18next";
|
||||
import optionService from "./options";
|
||||
import eventService from "./events";
|
||||
import { getContext } from "./context";
|
||||
import config from "./config";
|
||||
import BNote from "../becca/entities/bnote";
|
||||
import BBranch from "../becca/entities/bbranch";
|
||||
import schema from "../assets/schema.sql?raw";
|
||||
import hidden_subtree from "./hidden_subtree";
|
||||
import TaskContext from "./task_context";
|
||||
|
||||
export const dbReady = deferred<void>();
|
||||
|
||||
// TODO: Proper impl.
|
||||
setTimeout(() => {
|
||||
dbReady.resolve();
|
||||
}, 850);
|
||||
function schemaExists() {
|
||||
return !!getSql().getValue(/*sql*/`SELECT name FROM sqlite_master
|
||||
WHERE type = 'table' AND name = 'options'`);
|
||||
}
|
||||
|
||||
function isDbInitialized() {
|
||||
return true;
|
||||
try {
|
||||
if (!schemaExists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const initialized = getSql().getValue("SELECT value FROM options WHERE name = 'initialized'");
|
||||
return initialized === "true";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function initDbConnection() {
|
||||
if (!isDbInitialized()) {
|
||||
const log = getLog();
|
||||
if (isElectron) {
|
||||
log.info(t("sql_init.db_not_initialized_desktop"));
|
||||
} else {
|
||||
// TODO: Bring back port.
|
||||
log.info(t("sql_init.db_not_initialized_server", { port: 1234 }));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO: Renable migration
|
||||
//await migrationService.migrateIfNecessary();
|
||||
|
||||
const sql = getSql();
|
||||
sql.execute('CREATE TEMP TABLE IF NOT EXISTS "param_list" (`paramId` TEXT NOT NULL PRIMARY KEY)');
|
||||
|
||||
sql.execute(`
|
||||
CREATE TABLE IF NOT EXISTS "user_data"
|
||||
(
|
||||
tmpID INT,
|
||||
username TEXT,
|
||||
email TEXT,
|
||||
userIDEncryptedDataKey TEXT,
|
||||
userIDVerificationHash TEXT,
|
||||
salt TEXT,
|
||||
derivedKey TEXT,
|
||||
isSetup TEXT DEFAULT "false",
|
||||
UNIQUE (tmpID),
|
||||
PRIMARY KEY (tmpID)
|
||||
);`);
|
||||
|
||||
dbReady.resolve();
|
||||
}
|
||||
|
||||
async function createDatabaseForSync(a: any, b: string, c: any) {
|
||||
@@ -16,15 +75,146 @@ async function createDatabaseForSync(a: any, b: string, c: any) {
|
||||
}
|
||||
|
||||
function setDbAsInitialized() {
|
||||
// Noop.
|
||||
}
|
||||
if (!isDbInitialized()) {
|
||||
optionService.setOption("initialized", "true");
|
||||
|
||||
function schemaExists() {
|
||||
return true;
|
||||
initDbConnection();
|
||||
|
||||
// Emit an event to notify that the database is now initialized
|
||||
eventService.emit(eventService.DB_INITIALIZED);
|
||||
|
||||
getLog().info("Database initialization completed, emitted DB_INITIALIZED event");
|
||||
}
|
||||
}
|
||||
|
||||
function getDbSize() {
|
||||
return 1000;
|
||||
return getSql().getValue<number>("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()");
|
||||
}
|
||||
|
||||
export default { isDbInitialized, createDatabaseForSync, setDbAsInitialized, schemaExists, getDbSize, dbReady };
|
||||
function optimize() {
|
||||
if (config.General.readOnly) {
|
||||
return;
|
||||
}
|
||||
const log = getLog();
|
||||
log.info("Optimizing database");
|
||||
const start = Date.now();
|
||||
|
||||
getSql().execute("PRAGMA optimize");
|
||||
|
||||
log.info(`Optimization finished in ${Date.now() - start}ms.`);
|
||||
}
|
||||
|
||||
function initializeDb() {
|
||||
getContext().init(initDbConnection);
|
||||
|
||||
dbReady.then(() => {
|
||||
// TODO: Re-enable backup.
|
||||
// if (config.General && config.General.noBackup === true) {
|
||||
// log.info("Disabling scheduled backups.");
|
||||
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setInterval(() => backup.regularBackup(), 4 * 60 * 60 * 1000);
|
||||
|
||||
// // kickoff first backup soon after start up
|
||||
// setTimeout(() => backup.regularBackup(), 5 * 60 * 1000);
|
||||
|
||||
// // optimize is usually inexpensive no-op, so running it semi-frequently is not a big deal
|
||||
// setTimeout(() => optimize(), 60 * 60 * 1000);
|
||||
|
||||
// setInterval(() => optimize(), 10 * 60 * 60 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the database schema, creating the necessary tables and importing the demo content.
|
||||
*
|
||||
* @param skipDemoDb if set to `true`, then the demo database will not be imported, resulting in an empty root note.
|
||||
* @throws {Error} if the database is already initialized.
|
||||
*/
|
||||
async function createInitialDatabase(skipDemoDb?: boolean) {
|
||||
if (isDbInitialized()) {
|
||||
throw new Error("DB is already initialized");
|
||||
}
|
||||
|
||||
let rootNote!: BNote;
|
||||
|
||||
// We have to import async since options init requires keyboard actions which require translations.
|
||||
const optionsInitService = (await import("./options_init.js")).default;
|
||||
const becca_loader = (await import("../becca/becca_loader.js")).default;
|
||||
|
||||
const sql = getSql();
|
||||
const log = getLog();
|
||||
sql.transactional(() => {
|
||||
log.info("Creating database schema ...");
|
||||
sql.executeScript(schema);
|
||||
|
||||
becca_loader.load();
|
||||
|
||||
log.info("Creating root note ...");
|
||||
|
||||
rootNote = new BNote({
|
||||
noteId: "root",
|
||||
title: "root",
|
||||
type: "text",
|
||||
mime: "text/html"
|
||||
}).save();
|
||||
|
||||
rootNote.setContent("");
|
||||
|
||||
new BBranch({
|
||||
noteId: "root",
|
||||
parentNoteId: "none",
|
||||
isExpanded: true,
|
||||
notePosition: 10
|
||||
}).save();
|
||||
|
||||
// Bring in option init.
|
||||
optionsInitService.initDocumentOptions();
|
||||
optionsInitService.initNotSyncedOptions(true, {});
|
||||
optionsInitService.initStartupOptions();
|
||||
// password.resetPassword();
|
||||
});
|
||||
|
||||
// Check hidden subtree.
|
||||
// This ensures the existence of system templates, for the demo content.
|
||||
console.log("Checking hidden subtree at first start.");
|
||||
getContext().init(() => {
|
||||
getSql().transactional(() => hidden_subtree.checkHiddenSubtree());
|
||||
});
|
||||
|
||||
// Import demo content.
|
||||
log.info("Importing demo content...");
|
||||
|
||||
const dummyTaskContext = new TaskContext("no-progress-reporting", "importNotes", null);
|
||||
|
||||
// if (demoFile) {
|
||||
// await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
|
||||
// }
|
||||
|
||||
// Post-demo.
|
||||
sql.transactional(() => {
|
||||
// this needs to happen after ZIP import,
|
||||
// the previous solution was to move option initialization here, but then the important parts of initialization
|
||||
// are not all in one transaction (because ZIP import is async and thus not transactional)
|
||||
|
||||
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
|
||||
|
||||
optionService.setOption(
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
log.info("Schema and initial content generated.");
|
||||
|
||||
initDbConnection();
|
||||
}
|
||||
|
||||
export default { isDbInitialized, createDatabaseForSync, setDbAsInitialized, schemaExists, getDbSize, initDbConnection, dbReady, initializeDb, createInitialDatabase };
|
||||
|
||||
Reference in New Issue
Block a user