meta-sovereign 0.49.8 → 0.49.9
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/js/src/sources/archive/media-store.js +2 -1
- package/js/src/sources/auto-import/sign-ins.js +2 -1
- package/js/src/sources/auto-import/telegram-creds.js +2 -1
- package/js/src/sources/telegram-export-run.js +2 -1
- package/js/src/storage/durable.js +43 -3
- package/js/src/storage/lino-store.js +34 -13
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createHash, randomBytes } from 'node:crypto';
|
|
9
9
|
import { createWriteStream, promises as fs } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { renameDurable } from '../../storage/durable.js';
|
|
11
12
|
import { pipeline } from 'node:stream/promises';
|
|
12
13
|
|
|
13
14
|
const extensionOf = (name) => {
|
|
@@ -39,7 +40,7 @@ export const saveMedia = async ({ mediaDir, network, stream, name }) => {
|
|
|
39
40
|
);
|
|
40
41
|
const sha256 = hash.digest('hex');
|
|
41
42
|
const file = `${sha256}${extensionOf(name)}`;
|
|
42
|
-
await
|
|
43
|
+
await renameDurable(temp, path.join(dir, file)); // (R-Z19)
|
|
43
44
|
await fs.chmod(path.join(dir, file), 0o600);
|
|
44
45
|
return { path: `media/${network}/${file}`, sha256, size };
|
|
45
46
|
} catch (error) {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { promises as fs } from 'node:fs';
|
|
29
29
|
import os from 'node:os';
|
|
30
30
|
import path from 'node:path';
|
|
31
|
+
import { renameDurable } from '../../storage/durable.js';
|
|
31
32
|
import { execFile } from 'node:child_process';
|
|
32
33
|
import { autoImportRecipes } from './recipes.js';
|
|
33
34
|
|
|
@@ -229,7 +230,7 @@ export const createSignIns = ({
|
|
|
229
230
|
const file = fileOf(provider);
|
|
230
231
|
const temp = `${file}.${process.pid}.tmp`;
|
|
231
232
|
await fs.writeFile(temp, JSON.stringify(seed), { mode: 0o600 });
|
|
232
|
-
await
|
|
233
|
+
await renameDurable(temp, file); // (R-Z19)
|
|
233
234
|
};
|
|
234
235
|
|
|
235
236
|
const access = () => probeFullDiskAccess({ platform, homeDir, env });
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import { promises as fs } from 'node:fs';
|
|
33
33
|
import os from 'node:os';
|
|
34
34
|
import path from 'node:path';
|
|
35
|
+
import { renameDurable } from '../../storage/durable.js';
|
|
35
36
|
import crypto from 'node:crypto';
|
|
36
37
|
import { timedRequest } from '../http.js';
|
|
37
38
|
|
|
@@ -765,7 +766,7 @@ export const createTelegramCreds = ({
|
|
|
765
766
|
typeof v === 'bigint' ? v.toString() : v
|
|
766
767
|
);
|
|
767
768
|
await fs.writeFile(temp, text, { mode: 0o600 });
|
|
768
|
-
await
|
|
769
|
+
await renameDurable(temp, cacheFile); // (R-Z19)
|
|
769
770
|
};
|
|
770
771
|
|
|
771
772
|
const verifyAll = (records) =>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { promises as fs } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
+
import { renameDurable } from '../storage/durable.js';
|
|
13
14
|
import { rawMessage, floodSeconds } from './telegram-history.js';
|
|
14
15
|
import { bigSize, locationOf, fileLocation } from './telegram-media.js';
|
|
15
16
|
import { failureOf } from './telegram-stickers.js';
|
|
@@ -149,7 +150,7 @@ const download = async (r, info, target) => {
|
|
|
149
150
|
outputFile: part,
|
|
150
151
|
})
|
|
151
152
|
);
|
|
152
|
-
await
|
|
153
|
+
await renameDurable(part, target); // (R-Z19)
|
|
153
154
|
} catch (error) {
|
|
154
155
|
await fs.rm(part, { force: true });
|
|
155
156
|
throw error;
|
|
@@ -45,6 +45,46 @@ export const fsyncDir = async (dir) => {
|
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
const WINDOWS_BUSY = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
49
|
+
const defaultSleep = (ms) =>
|
|
50
|
+
new Promise((resolve) => {
|
|
51
|
+
globalThis.setTimeout(resolve, ms);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Rename `from` over `to`, retrying while another handle holds the target
|
|
56
|
+
* (R-Z19). On Windows a rename fails with EPERM/EBUSY/EACCES while any
|
|
57
|
+
* process (antivirus, the search indexer, our own snapshot worker) has the
|
|
58
|
+
* target open, usually for a few milliseconds: retried up to `attempts`
|
|
59
|
+
* times with a growing pause (about a second in all, like graceful-fs).
|
|
60
|
+
* Elsewhere rename is atomic and those codes are real errors, so only
|
|
61
|
+
* EBUSY is retried. After the last attempt the error is thrown and the
|
|
62
|
+
* target is untouched. `rename`, `platform` and `sleep` are injectable.
|
|
63
|
+
*/
|
|
64
|
+
export const renameDurable = async (
|
|
65
|
+
from,
|
|
66
|
+
to,
|
|
67
|
+
{
|
|
68
|
+
rename = fs.rename,
|
|
69
|
+
platform = process.platform,
|
|
70
|
+
sleep = defaultSleep,
|
|
71
|
+
attempts = 10,
|
|
72
|
+
delayMs = 20,
|
|
73
|
+
} = {}
|
|
74
|
+
) => {
|
|
75
|
+
const codes = platform === 'win32' ? WINDOWS_BUSY : new Set(['EBUSY']);
|
|
76
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
77
|
+
try {
|
|
78
|
+
return await rename(from, to);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (attempt >= attempts || !codes.has(error?.code)) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
await sleep(delayMs * attempt);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
48
88
|
/** Create `dir` (and parents) private to the user. */
|
|
49
89
|
export const ensurePrivateDir = (dir) =>
|
|
50
90
|
fs.mkdir(dir, { recursive: true, mode: DIR_MODE });
|
|
@@ -74,7 +114,7 @@ export const tighten = async (target, mode = FILE_MODE) => {
|
|
|
74
114
|
export const writeFileDurable = async (
|
|
75
115
|
file,
|
|
76
116
|
data,
|
|
77
|
-
{ utime, tag = '' } = {}
|
|
117
|
+
{ utime, tag = '', rename } = {}
|
|
78
118
|
) => {
|
|
79
119
|
const dir = path.dirname(file);
|
|
80
120
|
await ensurePrivateDir(dir);
|
|
@@ -90,7 +130,7 @@ export const writeFileDurable = async (
|
|
|
90
130
|
} finally {
|
|
91
131
|
await handle.close();
|
|
92
132
|
}
|
|
93
|
-
await
|
|
133
|
+
await renameDurable(tmp, file, { rename });
|
|
94
134
|
} catch (error) {
|
|
95
135
|
// A full disk: the half-written temp file goes at once, not at the
|
|
96
136
|
// next start (R-Z14).
|
|
@@ -116,7 +156,7 @@ export const writeStreamDurable = async (file, ...streams) => {
|
|
|
116
156
|
createWriteStream(tmp, { flags: 'w', mode: FILE_MODE })
|
|
117
157
|
);
|
|
118
158
|
await fsyncFile(tmp);
|
|
119
|
-
await
|
|
159
|
+
await renameDurable(tmp, file);
|
|
120
160
|
} catch (error) {
|
|
121
161
|
await fs.unlink(tmp).catch(() => {});
|
|
122
162
|
throw error;
|
|
@@ -28,7 +28,13 @@ import { createInterface } from 'node:readline';
|
|
|
28
28
|
import { decodeLine, encodeLine } from 'lino-objects-codec';
|
|
29
29
|
import { createMemoryStore } from './universal.js';
|
|
30
30
|
import { parseLino } from './lino.js';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
FILE_MODE,
|
|
33
|
+
fsyncDir,
|
|
34
|
+
fsyncFile,
|
|
35
|
+
renameDurable,
|
|
36
|
+
tighten,
|
|
37
|
+
} from './durable.js';
|
|
32
38
|
import { enoughSpace, skippedEvent } from './disk-space.js';
|
|
33
39
|
import { createWriteQueue, storeError, writeFailed } from './write-queue.js';
|
|
34
40
|
|
|
@@ -380,7 +386,7 @@ export const readLinoFile = async (filePath) => {
|
|
|
380
386
|
// Write every link to a temp file in chunks, flush it to disk, swap it
|
|
381
387
|
// in, and flush the directory: after a power cut the file is the old log
|
|
382
388
|
// or the whole new one, never an empty or half-written one (R-Z2).
|
|
383
|
-
const rewriteFile = async (filePath, links) => {
|
|
389
|
+
const rewriteFile = async (filePath, links, rename) => {
|
|
384
390
|
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
385
391
|
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
386
392
|
try {
|
|
@@ -393,7 +399,7 @@ const rewriteFile = async (filePath, links) => {
|
|
|
393
399
|
} finally {
|
|
394
400
|
await handle.close();
|
|
395
401
|
}
|
|
396
|
-
await
|
|
402
|
+
await renameDurable(tmp, filePath, { rename });
|
|
397
403
|
} catch (error) {
|
|
398
404
|
// The half-written copy goes at once: on a full disk it is what
|
|
399
405
|
// keeps the disk full (R-Z14).
|
|
@@ -461,6 +467,9 @@ const SYNC_MS = 200;
|
|
|
461
467
|
|
|
462
468
|
// A compaction skipped for lack of space is asked again after this long (R-Z14).
|
|
463
469
|
const COMPACT_RETRY_MS = 10 * 60_000;
|
|
470
|
+
// A compaction that failed (a busy file on Windows) leaves the old log intact
|
|
471
|
+
// and is asked again after this, not on every batch (R-Z19).
|
|
472
|
+
const COMPACT_FAILED_RETRY_MS = 30_000;
|
|
464
473
|
|
|
465
474
|
// Flushes appends to disk together, `syncMs` after the first (R-Z2).
|
|
466
475
|
const createSyncer = (filePath, syncMs, enqueue) => {
|
|
@@ -506,7 +515,15 @@ const compactionFailed = (onEvent) => (error) =>
|
|
|
506
515
|
// The log rewritten with one line per live link, after the unreadable
|
|
507
516
|
// lines were set aside (R-Z2); skipped when the disk has no room for the
|
|
508
517
|
// copy (R-Z14). `{ skipped }`.
|
|
509
|
-
const compactLog = async ({
|
|
518
|
+
const compactLog = async ({
|
|
519
|
+
filePath,
|
|
520
|
+
bad,
|
|
521
|
+
mem,
|
|
522
|
+
onCorrupt,
|
|
523
|
+
onEvent,
|
|
524
|
+
free,
|
|
525
|
+
rename,
|
|
526
|
+
}) => {
|
|
510
527
|
if (bad.overflow) {
|
|
511
528
|
return { skipped: false }; // more unreadable lines than can be kept
|
|
512
529
|
}
|
|
@@ -522,7 +539,7 @@ const compactLog = async ({ filePath, bad, mem, onCorrupt, onEvent, free }) => {
|
|
|
522
539
|
bad.lines = [];
|
|
523
540
|
bad.count = 0;
|
|
524
541
|
}
|
|
525
|
-
await rewriteFile(filePath, await mem.query());
|
|
542
|
+
await rewriteFile(filePath, await mem.query(), rename);
|
|
526
543
|
return { skipped: false };
|
|
527
544
|
};
|
|
528
545
|
|
|
@@ -588,6 +605,14 @@ const createHold = () => {
|
|
|
588
605
|
};
|
|
589
606
|
};
|
|
590
607
|
|
|
608
|
+
// A compaction skipped for lack of space, as an error for the write queue.
|
|
609
|
+
const throwingWhenSkipped = (compact) => async () => {
|
|
610
|
+
const done = await compact();
|
|
611
|
+
if (done?.skipped) {
|
|
612
|
+
throw storeError('ENOSPC', 'not enough free space to rewrite the log');
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
591
616
|
const readOnlyError = () =>
|
|
592
617
|
Object.assign(new Error('the store was opened read-only'), {
|
|
593
618
|
code: 'STORE_READ_ONLY',
|
|
@@ -607,6 +632,7 @@ export const createLinoTextStore = async (
|
|
|
607
632
|
// (tests inject an append handle that fails).
|
|
608
633
|
queue: queueOptions = {},
|
|
609
634
|
measureFree,
|
|
635
|
+
rename, // injectable fs.rename (R-Z19)
|
|
610
636
|
} = {}
|
|
611
637
|
) => {
|
|
612
638
|
const mem = createMemoryStore();
|
|
@@ -642,6 +668,7 @@ export const createLinoTextStore = async (
|
|
|
642
668
|
onCorrupt,
|
|
643
669
|
onEvent,
|
|
644
670
|
free: measureFree,
|
|
671
|
+
rename,
|
|
645
672
|
});
|
|
646
673
|
if (done.skipped) {
|
|
647
674
|
compactAfterAt = Date.now() + COMPACT_RETRY_MS;
|
|
@@ -676,16 +703,9 @@ export const createLinoTextStore = async (
|
|
|
676
703
|
attempt: () => (rewrite ? compactOrThrow() : wq.write()),
|
|
677
704
|
onRecovered: syncer.soon,
|
|
678
705
|
});
|
|
679
|
-
const compactOrThrow =
|
|
680
|
-
const done = await compact();
|
|
681
|
-
if (done?.skipped) {
|
|
682
|
-
throw storeError('ENOSPC', 'not enough free space to rewrite the log');
|
|
683
|
-
}
|
|
684
|
-
};
|
|
706
|
+
const compactOrThrow = throwingWhenSkipped(compact);
|
|
685
707
|
const hold = createHold();
|
|
686
708
|
const maintain = async () => {
|
|
687
|
-
// The batch is on disk by now; a failed compaction costs nothing but
|
|
688
|
-
// the rewrite, so it never fails the writes (R-Z14).
|
|
689
709
|
try {
|
|
690
710
|
if (
|
|
691
711
|
!hold.active() &&
|
|
@@ -695,6 +715,7 @@ export const createLinoTextStore = async (
|
|
|
695
715
|
await compact();
|
|
696
716
|
}
|
|
697
717
|
} catch (error) {
|
|
718
|
+
compactAfterAt = Date.now() + COMPACT_FAILED_RETRY_MS;
|
|
698
719
|
compactionFailed(onEvent)(error);
|
|
699
720
|
}
|
|
700
721
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meta-sovereign",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.9",
|
|
4
4
|
"description": "Personal meta profile sovereign system - fully local, privacy-respecting personal CRM and unified messenger across services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "js/src/index.js",
|