domma-cms 1.8.25 → 1.8.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.26] - 2026-10-18
4
+
5
+ - **Updates clear out their old backups.** Every `npx domma-cms update` copies the engine and plugins into `.domma-backups/` first, and nothing ever removed those copies - some sites held 250+ of up to 490 MB each. After a successful update it now keeps the newest 7, plus one about a week, a month, three, six and twelve months old (at most 16), and nothing older than a year. Backups you put in that folder yourself, under any other name, are never touched.
6
+
3
7
  ## [1.8.25] - 2026-10-18
4
8
 
5
9
  - **Tutorial: The Editor.** A walk through a working session in the editor - the list, the tabs, coloured text, the live preview, saving and drafts, and the settings (font, wrapping, smooth typing, the admin menu folding away). Under Documentation › Tutorials, first in the list on every site.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Update backups, for the engine updater.
3
+ *
4
+ * Every update copies the engine's code, structural configs and plugins into
5
+ * .domma-backups/<timestamp>/ (about 500 MB on a site with plugins) and nothing
6
+ * ever removed them: a site that rolled a few times a day had 250+ of them. The
7
+ * updater now keeps the newest few plus one backup per age band - about a week,
8
+ * a month, three months, six months and a year old - and nothing older than a
9
+ * year. Only the names the updater writes are counted: anything else in the
10
+ * folder was put there by hand and is left alone.
11
+ *
12
+ * Each band keeps its OLDEST backup - that is what lets one move up: it ages out
13
+ * of its band into the next. Past the first week a band also keeps its NEWEST,
14
+ * the next in line, so when the oldest moves on the band is never left empty
15
+ * (with only the oldest kept, the year band stood empty 44% of the time at three
16
+ * updates a day). So at most 7 + 1 + 2 x 4 = 16 are kept.
17
+ *
18
+ * @module bin/lib/backup-retention
19
+ */
20
+
21
+ import {existsSync, readdirSync, rmSync} from 'node:fs';
22
+ import path from 'node:path';
23
+
24
+ /** How many of the newest update backups a site keeps outright. */
25
+ export const KEEP_UPDATE_BACKUPS = 7;
26
+
27
+ const DAY = 24 * 60 * 60 * 1000;
28
+
29
+ /** Age bands, in days: [from, to). The first starts at 0 so a backup can survive long enough to reach a week. */
30
+ export const BACKUP_BANDS = [[0, 7], [7, 30], [30, 90], [90, 180], [180, 365]];
31
+
32
+ /** `new Date().toISOString()` with `:` and `.` made `-` - the updater's own names. */
33
+ const UPDATE_BACKUP_NAME = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/;
34
+
35
+ /** When an update backup was taken, from its name; null for a name the updater did not write. */
36
+ export function backupTime(name) {
37
+ const m = UPDATE_BACKUP_NAME.exec(name);
38
+ if (!m) return null;
39
+ const t = Date.parse(`${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`);
40
+ return Number.isNaN(t) ? null : t;
41
+ }
42
+
43
+ /** The update backups to remove from a listing, oldest first. Pure: `now` is a timestamp in ms. */
44
+ export function backupsToPrune(names, now = Date.now(), keep = KEEP_UPDATE_BACKUPS) {
45
+ const ours = names
46
+ .map(name => ({name, at: backupTime(name)}))
47
+ .filter(b => b.at !== null)
48
+ .sort((a, b) => a.at - b.at);
49
+ const kept = new Set(ours.slice(-keep).map(b => b.name));
50
+ BACKUP_BANDS.forEach(([from, to], i) => {
51
+ const inBand = ours.filter(b => now - b.at >= from * DAY && now - b.at < to * DAY);
52
+ if (!inBand.length) return;
53
+ kept.add(inBand[0].name);
54
+ if (i > 0) kept.add(inBand.at(-1).name);
55
+ });
56
+ return ours.filter(b => !kept.has(b.name)).map(b => b.name);
57
+ }
58
+
59
+ /** Apply the retention rule to `dir`. Returns the names removed. */
60
+ export function pruneUpdateBackups(dir, now = Date.now()) {
61
+ if (!existsSync(dir)) return [];
62
+ const names = readdirSync(dir, {withFileTypes: true}).filter(e => e.isDirectory()).map(e => e.name);
63
+ const doomed = backupsToPrune(names, now);
64
+ for (const name of doomed) rmSync(path.join(dir, name), {recursive: true, force: true});
65
+ return doomed;
66
+ }
package/bin/update.js CHANGED
@@ -15,6 +15,7 @@ import {createInterface} from 'node:readline';
15
15
  import {fileURLToPath} from 'node:url';
16
16
  import {deepMergeNewKeys} from './lib/config-merge.js';
17
17
  import {shouldReplacePlugin} from './lib/plugin-version.js';
18
+ import {KEEP_UPDATE_BACKUPS, pruneUpdateBackups} from './lib/backup-retention.js';
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
21
  // Paths
@@ -567,6 +568,13 @@ export default async function update(_positional, flags) {
567
568
  console.log('');
568
569
  }
569
570
 
571
+ // -------------------------------------------------------------------------
572
+ // 13b. Thin the update backups (bin/lib/backup-retention.js) - done last, so a failed update
573
+ // never costs a backup, and only when this run took one.
574
+ // -------------------------------------------------------------------------
575
+
576
+ const prunedBackups = backupPath ? pruneUpdateBackups(path.join(cwd, '.domma-backups')) : [];
577
+
570
578
  // -------------------------------------------------------------------------
571
579
  // 14. Summary
572
580
  // -------------------------------------------------------------------------
@@ -581,6 +589,9 @@ export default async function update(_positional, flags) {
581
589
 
582
590
  if (backupPath) {
583
591
  info(`Backup saved to: .domma-backups/${path.basename(backupPath)}/`);
592
+ if (prunedBackups.length) {
593
+ info(`Removed ${prunedBackups.length} older update backup(s): the newest ${KEEP_UPDATE_BACKUPS} are kept, plus one about a week, a month, 3, 6 and 12 months old.`);
594
+ }
584
595
  }
585
596
 
586
597
  if (newPlugins.length > 0) {
@@ -1280,9 +1280,10 @@ The four Tools below were plugins until 0.67.0 and are now part of the CMS. They
1280
1280
  - Registers new plugins, disabled except the protective ones such as Security.
1281
1281
  - Adds only new keys to `server.json`, `auth.json`, `content.json` and `presets.json`.
1282
1282
  - Swaps the CMS dependencies but keeps dependencies you added, self-heals `.gitignore`, updates the marker and runs `npm install`.
1283
+ - Thins `.domma-backups/` after a successful update: the newest 7 update backups, plus the oldest and newest backup aged 7-30 days, 30-90 days, 90-180 days and 180-365 days (and the oldest under a week), so one about a week, a month, three, six and twelve months old is always there - at most 16. Nothing older than a year is kept, and backups not named by the updater are never touched (unreleased).
1283
1284
 
1284
1285
  Options: `--yes`, `--no-backup`, `--no-install`, `--dry-run` (preview only), `--force` (reinstall the same version).
1285
- Where: `bin/update.js`, `bin/lib/config-merge.js`, `bin/lib/plugin-version.js` - Since: 0.6.5.
1286
+ Where: `bin/update.js`, `bin/lib/config-merge.js`, `bin/lib/plugin-version.js`, `bin/lib/backup-retention.js` - Since: 0.6.5.
1286
1287
  - **`npm run setup`** - The terminal setup wizard:
1287
1288
  1. Generates `JWT_SECRET` and `INSTANCE_SECRET`.
1288
1289
  2. Creates the first admin if there are no users (password of at least 8 characters).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.8.25",
3
+ "version": "1.8.26",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",