omnispace 0.4.0 → 0.5.0
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/README.md +1 -1
- package/dist/include.js +38 -2
- package/dist/journal.js +26 -6
- package/dist/pull.js +17 -5
- package/dist/report.js +9 -0
- package/dist/sync.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ No Windows, se o caminho tiver espaço ou apóstrofo, entre na pasta com aspas d
|
|
|
50
50
|
|
|
51
51
|
## O que o Omni lê e envia
|
|
52
52
|
|
|
53
|
-
- A pasta é lida para montar a lista de arquivos. Sobem só arquivos de texto (Markdown, código, JSON e afins), até 25 MB por arquivo e 100 MB por espaço.
|
|
53
|
+
- A pasta é lida para montar a lista de arquivos. Sobem só arquivos de texto (Markdown, código, JSON e afins), até 25 MB por arquivo e 100 MB por espaço. Nome de arquivo que o Windows não aceita (como `CON`, `a:b.md` ou terminado em ponto) fica de fora, para o espaço abrir em qualquer sistema.
|
|
54
54
|
- Nunca sobem: `.env` e variações, chaves (`.pem`, `.key`, `id_rsa`), pastas `.git`, `node_modules` e de build, imagens e mídia.
|
|
55
55
|
- Arquivos `.env` são lidos só para listar os nomes das chaves; nenhum valor é guardado ou enviado.
|
|
56
56
|
- Antes de enviar, a CLI mostra o que sobe e o que fica de fora e pede confirmação.
|
package/dist/include.js
CHANGED
|
@@ -37,6 +37,19 @@ export const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
|
37
37
|
export const MAX_SPACE_BYTES = 100 * 1024 * 1024;
|
|
38
38
|
const MAX_FILES = 20000;
|
|
39
39
|
const MAX_DEPTH = 12;
|
|
40
|
+
const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
41
|
+
/**
|
|
42
|
+
* A file or folder name every OS can create (fatia 6). Windows refuses reserved device names,
|
|
43
|
+
* <>:"|?* and control characters, and names ending in a dot or space; NTFS stops at 255. A name
|
|
44
|
+
* like that made on a Mac would break the pull on Windows, so it never goes up (the server checks
|
|
45
|
+
* the same rule, apps/omni/src/lib/sync-manifest.ts).
|
|
46
|
+
*/
|
|
47
|
+
export function isPortableName(name) {
|
|
48
|
+
return (name.length <= 255 &&
|
|
49
|
+
!/[<>:"|?*\\\x00-\x1f]/.test(name) &&
|
|
50
|
+
!/[. ]$/.test(name) &&
|
|
51
|
+
!WINDOWS_RESERVED.test(name));
|
|
52
|
+
}
|
|
40
53
|
/** Files that never leave the machine, with no option to turn them on (ADR 003 item 3, ADR 004). */
|
|
41
54
|
export function isSecret(name) {
|
|
42
55
|
const lower = name.toLowerCase();
|
|
@@ -85,6 +98,7 @@ export async function buildManifest(root) {
|
|
|
85
98
|
const files = [];
|
|
86
99
|
const excluded = [];
|
|
87
100
|
const repos = [];
|
|
101
|
+
const unreadable = [];
|
|
88
102
|
let totalBytes = 0;
|
|
89
103
|
let seen = 0;
|
|
90
104
|
let truncated = false;
|
|
@@ -95,7 +109,14 @@ export async function buildManifest(root) {
|
|
|
95
109
|
truncated = true;
|
|
96
110
|
return;
|
|
97
111
|
}
|
|
98
|
-
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
unreadable.push(normalizePath(parts) || ".");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
99
120
|
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
100
121
|
for (const entry of entries) {
|
|
101
122
|
if (entry.isSymbolicLink())
|
|
@@ -133,6 +154,10 @@ export async function buildManifest(root) {
|
|
|
133
154
|
excluded.push({ path, reason: "binário ou mídia" });
|
|
134
155
|
continue;
|
|
135
156
|
}
|
|
157
|
+
if (!relParts.every(isPortableName)) {
|
|
158
|
+
excluded.push({ path, reason: "nome que o Windows não aceita" });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
136
161
|
let size;
|
|
137
162
|
try {
|
|
138
163
|
size = (await stat(abs)).size;
|
|
@@ -144,7 +169,17 @@ export async function buildManifest(root) {
|
|
|
144
169
|
excluded.push({ path, reason: "maior que 25 MB" });
|
|
145
170
|
continue;
|
|
146
171
|
}
|
|
147
|
-
|
|
172
|
+
let hash;
|
|
173
|
+
try {
|
|
174
|
+
hash = await hashFile(abs);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
if (err.code === "ENOENT")
|
|
178
|
+
continue; // vanished meanwhile
|
|
179
|
+
unreadable.push(path);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
files.push({ path, hash, size, abs });
|
|
148
183
|
totalBytes += size;
|
|
149
184
|
}
|
|
150
185
|
}
|
|
@@ -155,6 +190,7 @@ export async function buildManifest(root) {
|
|
|
155
190
|
repos,
|
|
156
191
|
totalBytes,
|
|
157
192
|
collisions: findCaseCollisions(files.map((f) => f.path)),
|
|
193
|
+
unreadable,
|
|
158
194
|
truncated,
|
|
159
195
|
};
|
|
160
196
|
}
|
package/dist/journal.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { copyFile, mkdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { LOCAL_TMP_DIR } from "./include.js";
|
|
4
|
+
// The apply step of a pull, written down before the first file is touched (fatia 5). Every
|
|
5
|
+
// decision (merge, conflict choice, backup) is already made and every final content already sits in
|
|
6
|
+
// .omni/tmp by hash, so the journal is just "put this content at this path" or "remove this path".
|
|
7
|
+
// Replaying it is idempotent: a pull killed halfway through writing finishes on the next run,
|
|
8
|
+
// without asking again and without merging a file that already holds the merge.
|
|
9
|
+
/** A write or removal that failed, with the space path (not the long absolute one) and the cause. */
|
|
10
|
+
export class ApplyError extends Error {
|
|
11
|
+
path;
|
|
12
|
+
code;
|
|
13
|
+
constructor(path, code) {
|
|
14
|
+
super(`${path}: ${code ?? "erro"}`);
|
|
15
|
+
this.path = path;
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
4
19
|
function journalPath(root) {
|
|
5
20
|
return join(root, ".omni", "pull-pending.json");
|
|
6
21
|
}
|
|
@@ -48,13 +63,18 @@ export async function applyJournal(root, journal, afterOp) {
|
|
|
48
63
|
for (let i = 0; i < journal.ops.length; i++) {
|
|
49
64
|
const op = journal.ops[i];
|
|
50
65
|
const abs = toAbs(root, op.path);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
try {
|
|
67
|
+
if ("remove" in op) {
|
|
68
|
+
await rm(abs, { force: true });
|
|
69
|
+
await pruneEmptyDirs(root, op.path);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
73
|
+
await copyFile(join(tmpDir, op.hash), abs);
|
|
74
|
+
}
|
|
54
75
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await copyFile(join(tmpDir, op.hash), abs);
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw new ApplyError(op.path, err.code);
|
|
58
78
|
}
|
|
59
79
|
afterOp?.(i);
|
|
60
80
|
}
|
package/dist/pull.js
CHANGED
|
@@ -7,9 +7,10 @@ import { callApi } from "./api.js";
|
|
|
7
7
|
import { buildManifest, hashFile, LOCAL_BACKUPS_DIR, LOCAL_TMP_DIR } from "./include.js";
|
|
8
8
|
import { mergeText } from "./merge.js";
|
|
9
9
|
import { addConflicts } from "./conflicts.js";
|
|
10
|
-
import { applyJournal, clearJournal, readJournal, writeJournal } from "./journal.js";
|
|
10
|
+
import { ApplyError, applyJournal, clearJournal, readJournal, writeJournal } from "./journal.js";
|
|
11
11
|
import { withLock } from "./lock.js";
|
|
12
12
|
import { pushSpace } from "./sync.js";
|
|
13
|
+
import { printUnreadable } from "./report.js";
|
|
13
14
|
import { defaultApiUrl, loadToken, machineHostname, readSpaceConfig, saveToken, writeSpaceConfig, } from "./config.js";
|
|
14
15
|
/**
|
|
15
16
|
* Three-way comparison per path (ADR 002): local, base (the version this folder last synced) and
|
|
@@ -257,8 +258,11 @@ async function finishPull(root, config, token, journal, opts, asker) {
|
|
|
257
258
|
});
|
|
258
259
|
}
|
|
259
260
|
catch (err) {
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
const busy = err instanceof ApplyError && ["EBUSY", "EPERM", "EACCES"].includes(err.code ?? "");
|
|
262
|
+
const what = err instanceof ApplyError ? err.path : "um arquivo";
|
|
263
|
+
const why = busy ? "está aberto em outro programa ou sem permissão" : err.message;
|
|
264
|
+
console.error(`\nNão deu para gravar ${what}: ${why}.`);
|
|
265
|
+
console.error("Feche o que estiver usando o arquivo e rode de novo: o pull continua de onde parou.");
|
|
262
266
|
return 1;
|
|
263
267
|
}
|
|
264
268
|
// Again after the writes: a push run during the interruption may have dropped a path from the
|
|
@@ -314,6 +318,10 @@ async function pullWith(root, config, token, opts, asker) {
|
|
|
314
318
|
console.error("Pasta grande ou funda demais para sincronizar (limite de 20.000 arquivos e 12 níveis).");
|
|
315
319
|
return 1;
|
|
316
320
|
}
|
|
321
|
+
if (manifest.unreadable.length > 0) {
|
|
322
|
+
printUnreadable(manifest.unreadable, "Nada foi alterado na pasta.");
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
317
325
|
const tmpDir = toAbs(root, LOCAL_TMP_DIR);
|
|
318
326
|
const cached = await cachedObjects(tmpDir);
|
|
319
327
|
const have = [...new Set([...manifest.files.map((f) => f.hash), ...cached])];
|
|
@@ -443,7 +451,7 @@ async function pullWith(root, config, token, opts, asker) {
|
|
|
443
451
|
await addConflicts(root, journal.marked);
|
|
444
452
|
return finishPull(root, config, token, journal, opts, asker);
|
|
445
453
|
}
|
|
446
|
-
async function buildJournal(root, head, plan, resolutions, local, autoMerged) {
|
|
454
|
+
export async function buildJournal(root, head, plan, resolutions, local, autoMerged) {
|
|
447
455
|
const tmpDir = toAbs(root, LOCAL_TMP_DIR);
|
|
448
456
|
const ops = [...plan.writes, ...plan.deletes.map((path) => ({ path, remove: true }))];
|
|
449
457
|
const stamp = backupStamp();
|
|
@@ -480,9 +488,13 @@ async function buildJournal(root, head, plan, resolutions, local, autoMerged) {
|
|
|
480
488
|
const pendingPush = [...plan.localOnly, ...resolutions.filter((r) => r.pendingPush && !r.markers).map((r) => r.path)]
|
|
481
489
|
.sort()
|
|
482
490
|
.map((path) => ({ path, deletedHere: !afterPull.has(path) }));
|
|
491
|
+
// Removals first. A rename that only changes letter case (Readme.md to README.md) arrives as
|
|
492
|
+
// "write README.md" + "remove Readme.md"; on Windows both are the same file, so writing first
|
|
493
|
+
// and removing after would delete the file (fatia 6). Removing first also lets the new name win.
|
|
494
|
+
const ordered = [...ops.filter((op) => "remove" in op), ...ops.filter((op) => !("remove" in op))];
|
|
483
495
|
return {
|
|
484
496
|
head,
|
|
485
|
-
ops,
|
|
497
|
+
ops: ordered,
|
|
486
498
|
written: ops.filter((op) => !("remove" in op)).length,
|
|
487
499
|
deleted: ops.filter((op) => "remove" in op).length,
|
|
488
500
|
autoMerged,
|
package/dist/report.js
CHANGED
|
@@ -19,6 +19,15 @@ export function humanSize(bytes) {
|
|
|
19
19
|
* What `omni sync --push` would send (ADR 003 item 6: show what goes up and what stays out, with
|
|
20
20
|
* the reason, before anything leaves the machine). Used by the scan and by the push itself.
|
|
21
21
|
*/
|
|
22
|
+
/** Files the manifest could not read. Push and pull stop on them: unread is not the same as deleted. */
|
|
23
|
+
export function printUnreadable(paths, outcome) {
|
|
24
|
+
console.error("Não deu para ler estes arquivos (abertos em outro programa ou sem permissão):");
|
|
25
|
+
for (const path of paths.slice(0, 20))
|
|
26
|
+
console.error(` ${path}`);
|
|
27
|
+
if (paths.length > 20)
|
|
28
|
+
console.error(` ... e mais ${paths.length - 20}`);
|
|
29
|
+
console.error(`Feche o programa que está com eles e rode de novo. ${outcome}`);
|
|
30
|
+
}
|
|
22
31
|
export function printSyncPreview(manifest) {
|
|
23
32
|
console.log(` Sobem: ${manifest.files.length} arquivos de texto · ${humanSize(manifest.totalBytes)}`);
|
|
24
33
|
if (manifest.totalBytes > MAX_SPACE_BYTES) {
|
package/dist/sync.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { buildManifest, MAX_SPACE_BYTES } from "./include.js";
|
|
5
|
-
import { humanSize, printSyncPreview } from "./report.js";
|
|
5
|
+
import { humanSize, printSyncPreview, printUnreadable } from "./report.js";
|
|
6
6
|
import { loadToken, readSpaceConfig, writeSpaceConfig } from "./config.js";
|
|
7
7
|
import { callApi } from "./api.js";
|
|
8
8
|
import { unresolvedConflicts } from "./conflicts.js";
|
|
@@ -67,6 +67,10 @@ async function pushLocked(root, opts) {
|
|
|
67
67
|
console.error("Pasta grande ou funda demais para sincronizar (limite de 20.000 arquivos e 12 níveis).");
|
|
68
68
|
return 1;
|
|
69
69
|
}
|
|
70
|
+
if (manifest.unreadable.length > 0) {
|
|
71
|
+
printUnreadable(manifest.unreadable, "Nada foi enviado.");
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
70
74
|
if (manifest.collisions.length > 0) {
|
|
71
75
|
console.error("Há arquivos cujo nome só difere por maiúsculas/minúsculas; no Windows eles colidem:");
|
|
72
76
|
for (const group of manifest.collisions)
|