mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	Merge pull request #2014 from FliegendeWurst/demo-mode
feat(server): add option to mount database read-only
This commit is contained in:
		
						commit
						79422da733
					
				| @ -159,6 +159,9 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     saveToRecentNotes(resolvedNotePath: string) { |     saveToRecentNotes(resolvedNotePath: string) { | ||||||
|  |         if (options.is("databaseReadonly")) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         setTimeout(async () => { |         setTimeout(async () => { | ||||||
|             // we include the note in the recent list only if the user stayed on the note at least 5 seconds
 |             // we include the note in the recent list only if the user stayed on the note at least 5 seconds
 | ||||||
|             if (resolvedNotePath && resolvedNotePath === this.notePath) { |             if (resolvedNotePath && resolvedNotePath === this.notePath) { | ||||||
| @ -254,6 +257,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> | |||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (options.is("databaseReadonly")) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (this.note.isLabelTruthy("readOnly")) { |         if (this.note.isLabelTruthy("readOnly")) { | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -44,6 +44,9 @@ export default class TabManager extends Component { | |||||||
|             if (!appContext.isMainWindow) { |             if (!appContext.isMainWindow) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |             if (options.is("databaseReadonly")) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             const openNoteContexts = this.noteContexts |             const openNoteContexts = this.noteContexts | ||||||
|                 .map((nc) => nc.getPojoState()) |                 .map((nc) => nc.getPojoState()) | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import { t } from "../../services/i18n.js"; | |||||||
| import LoadResults from "../../services/load_results.js"; | import LoadResults from "../../services/load_results.js"; | ||||||
| import type { AttributeRow } from "../../services/load_results.js"; | import type { AttributeRow } from "../../services/load_results.js"; | ||||||
| import FNote from "../../entities/fnote.js"; | import FNote from "../../entities/fnote.js"; | ||||||
|  | import options from "../../services/options.js"; | ||||||
| 
 | 
 | ||||||
| export default class EditButton extends OnClickButtonWidget { | export default class EditButton extends OnClickButtonWidget { | ||||||
|     isEnabled(): boolean { |     isEnabled(): boolean { | ||||||
| @ -27,6 +28,10 @@ export default class EditButton extends OnClickButtonWidget { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshWithNote(note: FNote): Promise<void> { |     async refreshWithNote(note: FNote): Promise<void> { | ||||||
|  |         if (options.is("databaseReadonly")) { | ||||||
|  |             this.toggleInt(false); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { |         if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { | ||||||
|             this.toggleInt(false); |             this.toggleInt(false); | ||||||
|         } else { |         } else { | ||||||
|  | |||||||
| @ -1172,16 +1172,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|             let noneCollapsedYet = true; |             let noneCollapsedYet = true; | ||||||
| 
 | 
 | ||||||
|             this.tree.getRootNode().visit((node) => { |             if (!options.is("databaseReadonly")) { | ||||||
|                 if (node.isExpanded() && !noteIdsToKeepExpanded.has(node.data.noteId)) { |                 // can't change expanded notes when database is readonly
 | ||||||
|                     node.setExpanded(false); |                 this.tree.getRootNode().visit((node) => { | ||||||
|  |                     if (node.isExpanded() && !noteIdsToKeepExpanded.has(node.data.noteId)) { | ||||||
|  |                         node.setExpanded(false); | ||||||
| 
 | 
 | ||||||
|                     if (noneCollapsedYet) { |                         if (noneCollapsedYet) { | ||||||
|                         toastService.showMessage(t("note_tree.auto-collapsing-notes-after-inactivity")); |                             toastService.showMessage(t("note_tree.auto-collapsing-notes-after-inactivity")); | ||||||
|                         noneCollapsedYet = false; |                             noneCollapsedYet = false; | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 }, false); | ||||||
|             }, false); |             } | ||||||
| 
 | 
 | ||||||
|             this.filterHoistedBranch(true); |             this.filterHoistedBranch(true); | ||||||
|         }, 600 * 1000); |         }, 600 * 1000); | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import utils from "../../services/utils.js"; | |||||||
| import linkService from "../../services/link.js"; | import linkService from "../../services/link.js"; | ||||||
| import server from "../../services/server.js"; | import server from "../../services/server.js"; | ||||||
| import type FNote from "../../entities/fnote.js"; | import type FNote from "../../entities/fnote.js"; | ||||||
|  | import options from "../../services/options.js"; | ||||||
| import type { ExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types"; | import type { ExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types"; | ||||||
| import type { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem, SceneData } from "@excalidraw/excalidraw/types"; | import type { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem, SceneData } from "@excalidraw/excalidraw/types"; | ||||||
| import type { JSX } from "react"; | import type { JSX } from "react"; | ||||||
| @ -447,6 +448,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     onChangeHandler() { |     onChangeHandler() { | ||||||
|  |         if (options.is("databaseReadonly")) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         // changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc.
 |         // changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc.
 | ||||||
|         // make sure only when a new element is added, we actually save something.
 |         // make sure only when a new element is added, we actually save something.
 | ||||||
|         const isNewSceneVersion = this.isNewSceneVersion(); |         const isNewSceneVersion = this.isNewSceneVersion(); | ||||||
| @ -540,7 +544,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|                         this.saveData(); |                         this.saveData(); | ||||||
|                     }, |                     }, | ||||||
|                     onChange: () => this.onChangeHandler(), |                     onChange: () => this.onChangeHandler(), | ||||||
|                     viewModeEnabled: false, |                     viewModeEnabled: options.is("databaseReadonly"), | ||||||
|                     zenModeEnabled: false, |                     zenModeEnabled: false, | ||||||
|                     gridModeEnabled: false, |                     gridModeEnabled: false, | ||||||
|                     isCollaborating: false, |                     isCollaborating: false, | ||||||
| @ -567,6 +571,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget { | |||||||
|      * info: sceneVersions are not incrementing. it seems to be a pseudo-random number |      * info: sceneVersions are not incrementing. it seems to be a pseudo-random number | ||||||
|      */ |      */ | ||||||
|     isNewSceneVersion() { |     isNewSceneVersion() { | ||||||
|  |         if (options.is("databaseReadonly")) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         const sceneVersion = this.getSceneVersion(); |         const sceneVersion = this.getSceneVersion(); | ||||||
| 
 | 
 | ||||||
|         return ( |         return ( | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ import ValidationError from "../../errors/validation_error.js"; | |||||||
| import type { Request } from "express"; | import type { Request } from "express"; | ||||||
| import { changeLanguage, getLocales } from "../../services/i18n.js"; | import { changeLanguage, getLocales } from "../../services/i18n.js"; | ||||||
| import type { OptionNames } from "@triliumnext/commons"; | import type { OptionNames } from "@triliumnext/commons"; | ||||||
|  | import config from "../../services/config.js"; | ||||||
| 
 | 
 | ||||||
| // options allowed to be updated directly in the Options dialog
 | // options allowed to be updated directly in the Options dialog
 | ||||||
| const ALLOWED_OPTIONS = new Set<OptionNames>([ | const ALLOWED_OPTIONS = new Set<OptionNames>([ | ||||||
| @ -127,6 +128,12 @@ function getOptions() { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     resultMap["isPasswordSet"] = optionMap["passwordVerificationHash"] ? "true" : "false"; |     resultMap["isPasswordSet"] = optionMap["passwordVerificationHash"] ? "true" : "false"; | ||||||
|  |     // if database is read-only, disable editing in UI by setting 0 here
 | ||||||
|  |     if (config.General.readOnly) { | ||||||
|  |         resultMap["autoReadonlySizeText"] = "0"; | ||||||
|  |         resultMap["autoReadonlySizeCode"] = "0"; | ||||||
|  |         resultMap["databaseReadonly"] = "true"; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return resultMap; |     return resultMap; | ||||||
| } | } | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ export interface TriliumConfig { | |||||||
|         noAuthentication: boolean; |         noAuthentication: boolean; | ||||||
|         noBackup: boolean; |         noBackup: boolean; | ||||||
|         noDesktopIcon: boolean; |         noDesktopIcon: boolean; | ||||||
|  |         readOnly: boolean; | ||||||
|     }; |     }; | ||||||
|     Network: { |     Network: { | ||||||
|         host: string; |         host: string; | ||||||
| @ -62,7 +63,10 @@ const config: TriliumConfig = { | |||||||
|             envToBoolean(process.env.TRILIUM_GENERAL_NOBACKUP) || iniConfig.General.noBackup || false, |             envToBoolean(process.env.TRILIUM_GENERAL_NOBACKUP) || iniConfig.General.noBackup || false, | ||||||
| 
 | 
 | ||||||
|         noDesktopIcon: |         noDesktopIcon: | ||||||
|             envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false |             envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false, | ||||||
|  | 
 | ||||||
|  |         readOnly: | ||||||
|  |             envToBoolean(process.env.TRILIUM_GENERAL_READONLY) || iniConfig.General.readOnly || false | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     Network: { |     Network: { | ||||||
|  | |||||||
| @ -13,18 +13,20 @@ import Database from "better-sqlite3"; | |||||||
| import ws from "./ws.js"; | import ws from "./ws.js"; | ||||||
| import becca_loader from "../becca/becca_loader.js"; | import becca_loader from "../becca/becca_loader.js"; | ||||||
| import entity_changes from "./entity_changes.js"; | import entity_changes from "./entity_changes.js"; | ||||||
|  | import config from "./config.js"; | ||||||
| 
 | 
 | ||||||
| let dbConnection: DatabaseType = buildDatabase(); | let dbConnection: DatabaseType = buildDatabase(); | ||||||
| let statementCache: Record<string, Statement> = {}; | let statementCache: Record<string, Statement> = {}; | ||||||
| 
 | 
 | ||||||
| function buildDatabase() { | function buildDatabase() { | ||||||
|  |     // for integration tests, ignore the config's readOnly setting
 | ||||||
|     if (process.env.TRILIUM_INTEGRATION_TEST === "memory") { |     if (process.env.TRILIUM_INTEGRATION_TEST === "memory") { | ||||||
|         return buildIntegrationTestDatabase(); |         return buildIntegrationTestDatabase(); | ||||||
|     } else if (process.env.TRILIUM_INTEGRATION_TEST === "memory-no-store") { |     } else if (process.env.TRILIUM_INTEGRATION_TEST === "memory-no-store") { | ||||||
|         return new Database(":memory:"); |         return new Database(":memory:"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return new Database(dataDir.DOCUMENT_PATH); |     return new Database(dataDir.DOCUMENT_PATH, { readonly: config.General.readOnly }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function buildIntegrationTestDatabase(dbPath?: string) { | function buildIntegrationTestDatabase(dbPath?: string) { | ||||||
| @ -208,6 +210,13 @@ function getColumn<T>(query: string, params: Params = []): T[] { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function execute(query: string, params: Params = []): RunResult { | function execute(query: string, params: Params = []): RunResult { | ||||||
|  |     if (config.General.readOnly && (query.startsWith("UPDATE") || query.startsWith("INSERT") || query.startsWith("DELETE"))) { | ||||||
|  |         log.error(`read-only DB ignored: ${query} with parameters ${JSON.stringify(params)}`); | ||||||
|  |         return { | ||||||
|  |             changes: 0, | ||||||
|  |             lastInsertRowid: 0 | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|     return wrap(query, (s) => s.run(params)) as RunResult; |     return wrap(query, (s) => s.run(params)) as RunResult; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -186,6 +186,9 @@ function setDbAsInitialized() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function optimize() { | function optimize() { | ||||||
|  |     if (config.General.readOnly) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|     log.info("Optimizing database"); |     log.info("Optimizing database"); | ||||||
|     const start = Date.now(); |     const start = Date.now(); | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran