diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index 3ae4d60b5..2c4384eb1 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -3,6 +3,8 @@ import protectedSessionService from "../../services/protected_session.js"; import log from "../../services/log.js"; import sql from "../../services/sql.js"; +import optionService from "../../services/options.js"; +import eraseService from "../../services/erase.js"; import utils from "../../services/utils.js"; import dateUtils from "../../services/date_utils.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js"; @@ -68,7 +70,7 @@ class BNote extends AbstractBeccaEntity { /** set during the deletion operation, before it is completed (removed from becca completely). */ isBeingDeleted!: boolean; isDecrypted!: boolean; - + ownedAttributes!: BAttribute[]; parentBranches!: BBranch[]; parents!: BNote[]; @@ -455,8 +457,8 @@ class BNote extends AbstractBeccaEntity { return this.getAttributes().find( attr => attr.name.toLowerCase() === name - && (!value || attr.value.toLowerCase() === value) - && attr.type === type); + && (!value || attr.value.toLowerCase() === value) + && attr.type === type); } getRelationTarget(name: string) { @@ -1612,10 +1614,27 @@ class BNote extends AbstractBeccaEntity { revision.setContent(noteContent); + this.eraseExcessRevisions() return revision; }); } + // Limit the number of Snapshots to revisionSnapshotNumberLimit + // Delete older Snapshots that exceed the limit + eraseExcessRevisions() { + const revisionSnapshotNumberLimit = parseInt(optionService.getOption('revisionSnapshotNumberLimit')); + if (revisionSnapshotNumberLimit >= 0) { + const revisions = this.getRevisions(); + if (revisions.length - revisionSnapshotNumberLimit > 0) { + const revisionIds = revisions + .slice(0, revisions.length - revisionSnapshotNumberLimit) + .map(revision => revision.revisionId) + .filter((id): id is string => id !== undefined); + eraseService.eraseRevisions(revisionIds); + } + } + } + /** * @param matchBy - choose by which property we detect if to update an existing attachment. * Supported values are either 'attachmentId' (default) or 'title' diff --git a/src/public/app/widgets/type_widgets/content_widget.js b/src/public/app/widgets/type_widgets/content_widget.js index 6c2ddc0cb..e4eced3ef 100644 --- a/src/public/app/widgets/type_widgets/content_widget.js +++ b/src/public/app/widgets/type_widgets/content_widget.js @@ -23,6 +23,7 @@ import SearchEngineOptions from "./options/other/search_engine.js"; import TrayOptions from "./options/other/tray.js"; import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.js"; import RevisionsSnapshotIntervalOptions from "./options/other/revisions_snapshot_interval.js"; +import RevisionsSnapshotLimitOptions from "./options/other/revisions_snapshot_limit.js"; import NetworkConnectionsOptions from "./options/other/network_connections.js"; import AdvancedSyncOptions from "./options/advanced/sync.js"; import DatabaseIntegrityCheckOptions from "./options/advanced/database_integrity_check.js"; @@ -88,6 +89,7 @@ const CONTENT_WIDGETS = { NoteErasureTimeoutOptions, AttachmentErasureTimeoutOptions, RevisionsSnapshotIntervalOptions, + RevisionsSnapshotLimitOptions, NetworkConnectionsOptions ], _optionsAdvanced: [ diff --git a/src/public/app/widgets/type_widgets/options/other/revisions_snapshot_limit.js b/src/public/app/widgets/type_widgets/options/other/revisions_snapshot_limit.js new file mode 100644 index 000000000..5d25c99a9 --- /dev/null +++ b/src/public/app/widgets/type_widgets/options/other/revisions_snapshot_limit.js @@ -0,0 +1,42 @@ +import OptionsWidget from "../options_widget.js"; +import { t } from "../../../../services/i18n.js"; +import server from "../../../../services/server.js"; +import toastService from "../../../../services/toast.js"; + +const TPL = ` +
+

${t("revisions_snapshot_limit.note_revisions_snapshot_limit_title")}

+ +

${t("revisions_snapshot_limit.note_revisions_snapshot_limit_description")}

+ +
+ + +
+ + +
`; + +export default class RevisionsSnapshotLimitOptions extends OptionsWidget { + doRender() { + this.$widget = $(TPL); + this.$revisionsNumberLimit = this.$widget.find(".revision-snapshot-number-limit"); + this.$revisionsNumberLimit.on('change', () => { + let revisionSnapshotNumberLimit = this.$revisionsNumberLimit.val(); + if (!isNaN(revisionSnapshotNumberLimit) && revisionSnapshotNumberLimit >= -1) { + this.updateOption('revisionSnapshotNumberLimit', revisionSnapshotNumberLimit) + } + }); + this.$eraseExcessRevisionSnapshotsButton = this.$widget.find(".erase-excess-revision-snapshots-now-button"); + this.$eraseExcessRevisionSnapshotsButton.on('click', () => { + server.post('revisions/erase-all-excess-revisions').then(() => { + toastService.showMessage(t("revisions_snapshot_limit.erase_excess_revision_snapshots_prompt")); + }); + }); + } + + async optionsLoaded(options) { + this.$revisionsNumberLimit.val(options.revisionSnapshotNumberLimit); + } +} diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index d92f2fe60..61e7af1f9 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1064,7 +1064,14 @@ "revisions_snapshot_interval": { "note_revisions_snapshot_interval_title": "Note Revisions Snapshot Interval", "note_revisions_snapshot_description": "Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See wiki for more info.", - "snapshot_time_interval_label": "Note revision snapshot time interval (in seconds)" + "snapshot_time_interval_label": "Note revision snapshot time interval (in seconds):" + }, + "revisions_snapshot_limit": { + "note_revisions_snapshot_limit_title": "Note Revisions Snapshot Limit", + "note_revisions_snapshot_limit_description": "The note revision snapshot number limit refers to the maximum number of revisions that can be saved for each note. Where -1 means no limit, 0 means delete all revisions.", + "snapshot_number_limit_label": "Note revision snapshot number limit:", + "erase_excess_revision_snapshots": "Erase excess revision snapshots now", + "erase_excess_revision_snapshots_prompt": "Excess revision snapshots have been erased." }, "search_engine": { "title": "Search Engine", diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 4c7b06d2a..ddba39d4a 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -11,6 +11,7 @@ const ALLOWED_OPTIONS = new Set([ 'eraseEntitiesAfterTimeInSeconds', 'protectedSessionTimeout', 'revisionSnapshotTimeInterval', + 'revisionSnapshotNumberLimit', 'zoomFactor', 'theme', 'syncServerHost', diff --git a/src/routes/api/revisions.ts b/src/routes/api/revisions.ts index 7e98318d5..f791896e4 100644 --- a/src/routes/api/revisions.ts +++ b/src/routes/api/revisions.ts @@ -2,6 +2,7 @@ import beccaService from "../../becca/becca_service.js"; import revisionService from "../../services/revisions.js"; +import optionService from "../../services/options.js"; import utils from "../../services/utils.js"; import sql from "../../services/sql.js"; import cls from "../../services/cls.js"; @@ -112,6 +113,13 @@ function eraseRevision(req: Request) { eraseService.eraseRevisions([req.params.revisionId]); } +function eraseAllExcessRevisions() { + let allNoteIds = sql.getRows('SELECT noteId FROM notes') as { noteId: string }[]; + allNoteIds.forEach(row => { + becca.getNote(row.noteId)?.eraseExcessRevisions() + }); +} + function restoreRevision(req: Request) { const revision = becca.getRevision(req.params.revisionId); @@ -211,6 +219,7 @@ export default { downloadRevision, getEditedNotesOnDate, eraseAllRevisions, + eraseAllExcessRevisions, eraseRevision, restoreRevision }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index fd440804d..f1845eb1f 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -184,6 +184,7 @@ function register(app: express.Application) { apiRoute(GET, '/api/notes/:noteId/revisions', revisionsApiRoute.getRevisions); apiRoute(DEL, '/api/notes/:noteId/revisions', revisionsApiRoute.eraseAllRevisions); + apiRoute(PST, '/api/revisions/erase-all-excess-revisions', revisionsApiRoute.eraseAllExcessRevisions); apiRoute(GET, '/api/revisions/:revisionId', revisionsApiRoute.getRevision); apiRoute(GET, '/api/revisions/:revisionId/blob', revisionsApiRoute.getRevisionBlob); apiRoute(DEL, '/api/revisions/:revisionId', revisionsApiRoute.eraseRevision); diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 2adbadd2b..e32787dc9 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -49,6 +49,7 @@ async function initNotSyncedOptions(initialized: boolean, theme: string, opts: N const defaultOptions: DefaultOption[] = [ { name: 'revisionSnapshotTimeInterval', value: '600', isSynced: true }, + { name: 'revisionSnapshotNumberLimit', value: '-1', isSynced: true }, { name: 'protectedSessionTimeout', value: '600', isSynced: true }, { name: 'zoomFactor', value: process.platform === "win32" ? '0.9' : '1.0', isSynced: false }, { name: 'overrideThemeFonts', value: 'false', isSynced: false },