@seedvault/cli 0.3.0 → 0.4.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/dist/sv.js +105 -46
- package/package.json +1 -1
package/dist/sv.js
CHANGED
|
@@ -176,6 +176,10 @@ function createClient(serverUrl, token) {
|
|
|
176
176
|
return res;
|
|
177
177
|
}
|
|
178
178
|
return {
|
|
179
|
+
async me() {
|
|
180
|
+
const res = await request("GET", "/v1/me");
|
|
181
|
+
return res.json();
|
|
182
|
+
},
|
|
179
183
|
async signup(name, invite) {
|
|
180
184
|
const body = { name };
|
|
181
185
|
if (invite)
|
|
@@ -231,15 +235,12 @@ async function init(args) {
|
|
|
231
235
|
}
|
|
232
236
|
if (flags.server && flags.token) {
|
|
233
237
|
const client = createClient(flags.server, flags.token);
|
|
238
|
+
let username;
|
|
234
239
|
try {
|
|
235
|
-
await client.
|
|
240
|
+
const me = await client.me();
|
|
241
|
+
username = me.username;
|
|
236
242
|
} catch {
|
|
237
|
-
console.error(`Could not
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
240
|
-
const username = flags["username"] || "";
|
|
241
|
-
if (!username) {
|
|
242
|
-
console.error("When using --token, also pass --username");
|
|
243
|
+
console.error(`Could not authenticate with server at ${flags.server}`);
|
|
243
244
|
process.exit(1);
|
|
244
245
|
}
|
|
245
246
|
const config = {
|
|
@@ -303,12 +304,20 @@ async function init(args) {
|
|
|
303
304
|
}
|
|
304
305
|
const hasToken = await rl.question("Do you already have a token? (y/N): ");
|
|
305
306
|
if (hasToken.toLowerCase() === "y") {
|
|
306
|
-
const token = await rl.question("Token: ");
|
|
307
|
-
const
|
|
308
|
-
|
|
307
|
+
const token = (await rl.question("Token: ")).trim();
|
|
308
|
+
const authedClient = createClient(server, token);
|
|
309
|
+
let username;
|
|
310
|
+
try {
|
|
311
|
+
const me = await authedClient.me();
|
|
312
|
+
username = me.username;
|
|
313
|
+
} catch {
|
|
314
|
+
console.error(" Token is invalid or server rejected it.");
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
const config = { server, token, username, collections: [] };
|
|
309
318
|
saveConfig(config);
|
|
310
319
|
console.log(`
|
|
311
|
-
Seedvault configured
|
|
320
|
+
Seedvault configured as '${username}'`);
|
|
312
321
|
} else {
|
|
313
322
|
const name = await rl.question("Username (e.g. your-name-notes): ");
|
|
314
323
|
const invite = await rl.question("Invite code (leave blank if first user): ");
|
|
@@ -333,6 +342,7 @@ Next steps:`);
|
|
|
333
342
|
} finally {
|
|
334
343
|
rl.close();
|
|
335
344
|
}
|
|
345
|
+
process.exit(0);
|
|
336
346
|
}
|
|
337
347
|
function parseFlags(args) {
|
|
338
348
|
const flags = {};
|
|
@@ -2126,6 +2136,8 @@ class RetryQueue {
|
|
|
2126
2136
|
}
|
|
2127
2137
|
|
|
2128
2138
|
// src/daemon/syncer.ts
|
|
2139
|
+
var SYNC_CONCURRENCY = 10;
|
|
2140
|
+
|
|
2129
2141
|
class Syncer {
|
|
2130
2142
|
client;
|
|
2131
2143
|
username;
|
|
@@ -2167,6 +2179,7 @@ class Syncer {
|
|
|
2167
2179
|
}
|
|
2168
2180
|
const localFiles = await walkMd(collection.path);
|
|
2169
2181
|
const localServerPaths = new Set;
|
|
2182
|
+
const toUpload = [];
|
|
2170
2183
|
for (const localFile of localFiles) {
|
|
2171
2184
|
const relPath = toPosixPath(relative5(collection.path, localFile.path));
|
|
2172
2185
|
const serverPath = `${collection.name}/${relPath}`;
|
|
@@ -2181,22 +2194,24 @@ class Syncer {
|
|
|
2181
2194
|
}
|
|
2182
2195
|
}
|
|
2183
2196
|
const content = await readFile(localFile.path, "utf-8");
|
|
2197
|
+
toUpload.push({ serverPath, content });
|
|
2198
|
+
}
|
|
2199
|
+
await pooled(toUpload, SYNC_CONCURRENCY, async (item) => {
|
|
2184
2200
|
try {
|
|
2185
|
-
await this.client.putFile(this.username, serverPath, content);
|
|
2201
|
+
await this.client.putFile(this.username, item.serverPath, item.content);
|
|
2186
2202
|
uploaded++;
|
|
2187
2203
|
} catch {
|
|
2188
2204
|
this.queue.enqueue({
|
|
2189
2205
|
type: "put",
|
|
2190
2206
|
username: this.username,
|
|
2191
|
-
serverPath,
|
|
2192
|
-
content,
|
|
2207
|
+
serverPath: item.serverPath,
|
|
2208
|
+
content: item.content,
|
|
2193
2209
|
queuedAt: new Date().toISOString()
|
|
2194
2210
|
});
|
|
2195
2211
|
}
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
continue;
|
|
2212
|
+
});
|
|
2213
|
+
const toDelete = serverFiles.filter((f) => !localServerPaths.has(f.path));
|
|
2214
|
+
await pooled(toDelete, SYNC_CONCURRENCY, async (f) => {
|
|
2200
2215
|
try {
|
|
2201
2216
|
await this.client.deleteFile(this.username, f.path);
|
|
2202
2217
|
deleted++;
|
|
@@ -2209,7 +2224,7 @@ class Syncer {
|
|
|
2209
2224
|
queuedAt: new Date().toISOString()
|
|
2210
2225
|
});
|
|
2211
2226
|
}
|
|
2212
|
-
}
|
|
2227
|
+
});
|
|
2213
2228
|
this.log(` '${collection.name}': ${uploaded} uploaded, ${skipped} up-to-date, ${deleted} deleted`);
|
|
2214
2229
|
} catch (e) {
|
|
2215
2230
|
this.log(` '${collection.name}': sync failed (${e.message})`);
|
|
@@ -2222,7 +2237,7 @@ class Syncer {
|
|
|
2222
2237
|
this.log(`Removing '${collection.name}' files from server...`);
|
|
2223
2238
|
try {
|
|
2224
2239
|
const { files: serverFiles } = await this.client.listFiles(this.username, collection.name + "/");
|
|
2225
|
-
|
|
2240
|
+
await pooled(serverFiles, SYNC_CONCURRENCY, async (f) => {
|
|
2226
2241
|
try {
|
|
2227
2242
|
await this.client.deleteFile(this.username, f.path);
|
|
2228
2243
|
deleted++;
|
|
@@ -2236,7 +2251,7 @@ class Syncer {
|
|
|
2236
2251
|
});
|
|
2237
2252
|
queued++;
|
|
2238
2253
|
}
|
|
2239
|
-
}
|
|
2254
|
+
});
|
|
2240
2255
|
this.log(` '${collection.name}': ${deleted} deleted, ${queued} queued`);
|
|
2241
2256
|
} catch (e) {
|
|
2242
2257
|
this.log(` '${collection.name}': remove failed (${e.message})`);
|
|
@@ -2280,6 +2295,16 @@ async function walkMd(dir) {
|
|
|
2280
2295
|
await walkDirRecursive(dir, results);
|
|
2281
2296
|
return results;
|
|
2282
2297
|
}
|
|
2298
|
+
async function pooled(items, concurrency, fn) {
|
|
2299
|
+
let i = 0;
|
|
2300
|
+
async function worker() {
|
|
2301
|
+
while (i < items.length) {
|
|
2302
|
+
const idx = i++;
|
|
2303
|
+
await fn(items[idx]);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
2307
|
+
}
|
|
2283
2308
|
async function walkDirRecursive(dir, results) {
|
|
2284
2309
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2285
2310
|
for (const entry of entries) {
|
|
@@ -2646,7 +2671,7 @@ async function startForeground() {
|
|
|
2646
2671
|
if (removedOverlappingCollections.length > 0) {
|
|
2647
2672
|
config = normalizedConfig;
|
|
2648
2673
|
}
|
|
2649
|
-
|
|
2674
|
+
let client = createClient(config.server, config.token);
|
|
2650
2675
|
try {
|
|
2651
2676
|
await client.health();
|
|
2652
2677
|
} catch {
|
|
@@ -2680,7 +2705,7 @@ async function startForeground() {
|
|
|
2680
2705
|
log(` Collections: ${config.collections.map((f) => f.name).join(", ")}`);
|
|
2681
2706
|
}
|
|
2682
2707
|
writeFileSync2(getPidPath(), String(process.pid));
|
|
2683
|
-
|
|
2708
|
+
let syncer = new Syncer({
|
|
2684
2709
|
client,
|
|
2685
2710
|
username: config.username,
|
|
2686
2711
|
collections: config.collections,
|
|
@@ -2696,11 +2721,6 @@ async function startForeground() {
|
|
|
2696
2721
|
log("Will continue watching for changes...");
|
|
2697
2722
|
}
|
|
2698
2723
|
}
|
|
2699
|
-
const onWatcherEvent = (event) => {
|
|
2700
|
-
syncer.handleEvent(event).catch((e) => {
|
|
2701
|
-
log(`Error handling ${event.type} for ${event.serverPath}: ${e.message}`);
|
|
2702
|
-
});
|
|
2703
|
-
};
|
|
2704
2724
|
let watcher = null;
|
|
2705
2725
|
const rebuildWatcher = async (collections2) => {
|
|
2706
2726
|
if (watcher) {
|
|
@@ -2711,13 +2731,17 @@ async function startForeground() {
|
|
|
2711
2731
|
log("No collections configured. Daemon idle.");
|
|
2712
2732
|
return;
|
|
2713
2733
|
}
|
|
2714
|
-
watcher = createWatcher(collections2,
|
|
2734
|
+
watcher = createWatcher(collections2, (event) => {
|
|
2735
|
+
syncer.handleEvent(event).catch((e) => {
|
|
2736
|
+
log(`Error handling ${event.type} for ${event.serverPath}: ${e.message}`);
|
|
2737
|
+
});
|
|
2738
|
+
});
|
|
2715
2739
|
log(`Watching ${collections2.length} collection(s): ${collections2.map((f) => f.name).join(", ")}`);
|
|
2716
2740
|
};
|
|
2717
2741
|
await rebuildWatcher(config.collections);
|
|
2718
|
-
let
|
|
2742
|
+
let reloading = false;
|
|
2719
2743
|
const pollTimer = setInterval(() => {
|
|
2720
|
-
if (
|
|
2744
|
+
if (reloading)
|
|
2721
2745
|
return;
|
|
2722
2746
|
let nextConfig;
|
|
2723
2747
|
try {
|
|
@@ -2728,26 +2752,61 @@ async function startForeground() {
|
|
|
2728
2752
|
}
|
|
2729
2753
|
({ config: normalizedConfig, removedOverlappingCollections } = normalizeConfigCollections(nextConfig));
|
|
2730
2754
|
maybeLogOverlapWarning(removedOverlappingCollections);
|
|
2731
|
-
|
|
2755
|
+
const coreChanged = normalizedConfig.server !== config.server || normalizedConfig.token !== config.token || normalizedConfig.username !== config.username;
|
|
2756
|
+
if (!coreChanged) {
|
|
2757
|
+
const { nextConfig: reconciledConfig, added, removed } = reconcileCollections(config, normalizedConfig);
|
|
2758
|
+
if (added.length === 0 && removed.length === 0)
|
|
2759
|
+
return;
|
|
2760
|
+
reloading = true;
|
|
2761
|
+
(async () => {
|
|
2762
|
+
try {
|
|
2763
|
+
log(`Collections changed: +${added.map((c) => c.name).join(", ") || "none"}, -${removed.map((c) => c.name).join(", ") || "none"}`);
|
|
2764
|
+
config = reconciledConfig;
|
|
2765
|
+
syncer.setCollections(reconciledConfig.collections);
|
|
2766
|
+
await rebuildWatcher(reconciledConfig.collections);
|
|
2767
|
+
for (const collection of removed) {
|
|
2768
|
+
await syncer.purgeCollection(collection);
|
|
2769
|
+
}
|
|
2770
|
+
for (const collection of added) {
|
|
2771
|
+
await syncer.syncCollection(collection);
|
|
2772
|
+
}
|
|
2773
|
+
} catch (e) {
|
|
2774
|
+
log(`Failed to reload collections: ${e.message}`);
|
|
2775
|
+
} finally {
|
|
2776
|
+
reloading = false;
|
|
2777
|
+
}
|
|
2778
|
+
})();
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
reloading = true;
|
|
2732
2782
|
(async () => {
|
|
2733
2783
|
try {
|
|
2734
|
-
|
|
2735
|
-
if (
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2784
|
+
log("Config changed, reinitializing...");
|
|
2785
|
+
if (config.server !== normalizedConfig.server)
|
|
2786
|
+
log(` Server: ${config.server} -> ${normalizedConfig.server}`);
|
|
2787
|
+
if (config.username !== normalizedConfig.username)
|
|
2788
|
+
log(` Username: ${config.username} -> ${normalizedConfig.username}`);
|
|
2789
|
+
if (config.token !== normalizedConfig.token)
|
|
2790
|
+
log(` Token: updated`);
|
|
2791
|
+
syncer.stop();
|
|
2792
|
+
client = createClient(normalizedConfig.server, normalizedConfig.token);
|
|
2793
|
+
config = normalizedConfig;
|
|
2794
|
+
syncer = new Syncer({
|
|
2795
|
+
client,
|
|
2796
|
+
username: config.username,
|
|
2797
|
+
collections: config.collections,
|
|
2798
|
+
onLog: log
|
|
2799
|
+
});
|
|
2800
|
+
await rebuildWatcher(config.collections);
|
|
2801
|
+
if (config.collections.length > 0) {
|
|
2802
|
+
log("Running sync after reinitialize...");
|
|
2803
|
+
const { uploaded, skipped, deleted } = await syncer.initialSync();
|
|
2804
|
+
log(`Sync complete: ${uploaded} uploaded, ${skipped} skipped, ${deleted} deleted`);
|
|
2746
2805
|
}
|
|
2747
2806
|
} catch (e) {
|
|
2748
|
-
log(`Failed to
|
|
2807
|
+
log(`Failed to reinitialize: ${e.message}`);
|
|
2749
2808
|
} finally {
|
|
2750
|
-
|
|
2809
|
+
reloading = false;
|
|
2751
2810
|
}
|
|
2752
2811
|
})();
|
|
2753
2812
|
}, 1500);
|