sftp-push-sync 1.0.11 → 1.0.12
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 +10 -2
- package/bin/sftp-push-sync.mjs +162 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,11 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
66
66
|
"textExtensions": [
|
|
67
67
|
".html", ".xml", ".txt", ".json", ".js", ".css", ".md", ".svg"
|
|
68
68
|
],
|
|
69
|
+
"progress": {
|
|
70
|
+
"scanChunk": 10,
|
|
71
|
+
"analyzeChunk": 1
|
|
72
|
+
},
|
|
73
|
+
"logLevel": "normal", // or: "verbose", "laconic"
|
|
69
74
|
"uploadList": [],
|
|
70
75
|
"downloadList": [
|
|
71
76
|
"download-counter.json"
|
|
@@ -84,7 +89,6 @@ A list of files that are excluded from the sync comparison and can be downloaded
|
|
|
84
89
|
- Relative to remoteRoot "download-counter.json"
|
|
85
90
|
- or e.g. "logs/download-counter.json"
|
|
86
91
|
|
|
87
|
-
|
|
88
92
|
```bash
|
|
89
93
|
# normal synchronisation
|
|
90
94
|
sftp-push-sync staging
|
|
@@ -97,6 +101,10 @@ sftp-push-sync prod --download-list --dry-run # view first
|
|
|
97
101
|
sftp-push-sync prod --download-list # then do
|
|
98
102
|
```
|
|
99
103
|
|
|
104
|
+
### Logging Progress
|
|
105
|
+
|
|
106
|
+
For >100k files, use ANALYZE_CHUNKS = 10 or 50, otherwise the TTY output itself is a relevant factor.
|
|
107
|
+
|
|
100
108
|
## NPM Scripts
|
|
101
109
|
|
|
102
110
|
- Can be conveniently started via the scripts in `package.json`:
|
|
@@ -146,4 +154,4 @@ The first run always takes a while, especially with lots of images – so be pat
|
|
|
146
154
|
## Links
|
|
147
155
|
|
|
148
156
|
- <https://www.npmjs.com/package/sftp-push-sync>
|
|
149
|
-
- <https://github.com/cnichte/sftp-push-sync>
|
|
157
|
+
- <https://github.com/cnichte/sftp-push-sync>
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -40,6 +40,11 @@ import { createHash } from "crypto";
|
|
|
40
40
|
import { Writable } from "stream";
|
|
41
41
|
import pc from "picocolors";
|
|
42
42
|
|
|
43
|
+
// get Versionsnummer
|
|
44
|
+
import { createRequire } from "module";
|
|
45
|
+
const require = createRequire(import.meta.url);
|
|
46
|
+
const pkg = require("../package.json");
|
|
47
|
+
|
|
43
48
|
// Colors for the State (works on dark + light background)
|
|
44
49
|
const ADD = pc.green("+"); // Added
|
|
45
50
|
const CHA = pc.yellow("~"); // Changed
|
|
@@ -53,10 +58,14 @@ const EXC = pc.redBright("-"); // Excluded
|
|
|
53
58
|
const args = process.argv.slice(2);
|
|
54
59
|
const TARGET = args[0];
|
|
55
60
|
const DRY_RUN = args.includes("--dry-run");
|
|
56
|
-
const VERBOSE = args.includes("--verbose") || args.includes("-v");
|
|
57
61
|
const RUN_UPLOAD_LIST = args.includes("--upload-list");
|
|
58
62
|
const RUN_DOWNLOAD_LIST = args.includes("--download-list");
|
|
59
63
|
|
|
64
|
+
// logLevel override via CLI (optional)
|
|
65
|
+
let cliLogLevel = null;
|
|
66
|
+
if (args.includes("--verbose")) cliLogLevel = "verbose";
|
|
67
|
+
if (args.includes("--laconic")) cliLogLevel = "laconic";
|
|
68
|
+
|
|
60
69
|
if (!TARGET) {
|
|
61
70
|
console.error(pc.red("❌ Please specify a connection profile:"));
|
|
62
71
|
console.error(pc.yellow(" sftp-push-sync staging --dry-run"));
|
|
@@ -78,18 +87,18 @@ let CONFIG_RAW;
|
|
|
78
87
|
try {
|
|
79
88
|
CONFIG_RAW = JSON.parse(await fsp.readFile(CONFIG_PATH, "utf8"));
|
|
80
89
|
} catch (err) {
|
|
81
|
-
|
|
90
|
+
console.error(pc.red("❌ Error reading sync.config.json:"), err.message);
|
|
82
91
|
process.exit(1);
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
|
|
86
|
-
|
|
95
|
+
console.error(pc.red("❌ sync.config.json must have a 'connections' field."));
|
|
87
96
|
process.exit(1);
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
|
|
91
100
|
if (!TARGET_CONFIG) {
|
|
92
|
-
|
|
101
|
+
console.error(pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`));
|
|
93
102
|
process.exit(1);
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -103,8 +112,30 @@ const CONNECTION = {
|
|
|
103
112
|
workers: TARGET_CONFIG.worker ?? 2,
|
|
104
113
|
};
|
|
105
114
|
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// LogLevel + Progress aus Config
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
// logLevel: "verbose", "normal", "laconic"
|
|
120
|
+
let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
|
|
121
|
+
|
|
122
|
+
// CLI-Flags überschreiben Config
|
|
123
|
+
if (cliLogLevel) {
|
|
124
|
+
LOG_LEVEL = cliLogLevel;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const IS_VERBOSE = LOG_LEVEL === "verbose";
|
|
128
|
+
const IS_LACONIC = LOG_LEVEL === "laconic";
|
|
129
|
+
|
|
130
|
+
const PROGRESS = CONFIG_RAW.progress ?? {};
|
|
131
|
+
const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
|
|
132
|
+
const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
133
|
+
// Für >100k Files eher 10–50, bei Debug/Fehlersuche 1.
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
106
136
|
// Shared config from JSON
|
|
107
|
-
//
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
108
139
|
const INCLUDE = CONFIG_RAW.include ?? [];
|
|
109
140
|
const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
|
|
110
141
|
|
|
@@ -113,8 +144,7 @@ function normalizeList(list) {
|
|
|
113
144
|
if (!Array.isArray(list)) return [];
|
|
114
145
|
return list.flatMap((item) =>
|
|
115
146
|
typeof item === "string"
|
|
116
|
-
?
|
|
117
|
-
item
|
|
147
|
+
? item
|
|
118
148
|
.split(",")
|
|
119
149
|
.map((s) => s.trim())
|
|
120
150
|
.filter(Boolean)
|
|
@@ -144,6 +174,7 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
|
|
|
144
174
|
".md",
|
|
145
175
|
".svg",
|
|
146
176
|
];
|
|
177
|
+
|
|
147
178
|
// Cache file name per connection
|
|
148
179
|
const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
|
|
149
180
|
const CACHE_PATH = path.resolve(syncCacheName);
|
|
@@ -166,7 +197,10 @@ try {
|
|
|
166
197
|
CACHE.remote = raw.remote ?? {};
|
|
167
198
|
}
|
|
168
199
|
} catch (err) {
|
|
169
|
-
|
|
200
|
+
console.warn(
|
|
201
|
+
pc.yellow("⚠️ Could not load cache, starting without:"),
|
|
202
|
+
err.message
|
|
203
|
+
);
|
|
170
204
|
}
|
|
171
205
|
|
|
172
206
|
function cacheKey(relPath) {
|
|
@@ -217,7 +251,7 @@ function log(...msg) {
|
|
|
217
251
|
}
|
|
218
252
|
|
|
219
253
|
function vlog(...msg) {
|
|
220
|
-
if (!
|
|
254
|
+
if (!IS_VERBOSE) return;
|
|
221
255
|
clearProgressLine();
|
|
222
256
|
console.log(...msg);
|
|
223
257
|
}
|
|
@@ -256,27 +290,67 @@ function isTextFile(relPath) {
|
|
|
256
290
|
return TEXT_EXT.includes(ext);
|
|
257
291
|
}
|
|
258
292
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
function shortenPathForProgress(rel) {
|
|
294
|
+
if (!rel) return "";
|
|
295
|
+
// Nur Dateinamen?
|
|
296
|
+
// return path.basename(rel);
|
|
263
297
|
|
|
298
|
+
// Letzte 2 Segmente des Pfades
|
|
299
|
+
const parts = rel.split("/");
|
|
300
|
+
if (parts.length === 1) {
|
|
301
|
+
return rel; // nur Dateiname
|
|
302
|
+
}
|
|
303
|
+
if (parts.length === 2) {
|
|
304
|
+
return rel; // schon kurz genug
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const last = parts[parts.length - 1];
|
|
308
|
+
const prev = parts[parts.length - 2];
|
|
309
|
+
|
|
310
|
+
// z.B. …/images/foo.jpg
|
|
311
|
+
return `…/${prev}/${last}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Two-line progress bar
|
|
315
|
+
function updateProgress2(prefix, current, total, rel = "") {
|
|
264
316
|
if (!process.stdout.isTTY) {
|
|
265
|
-
// Fallback
|
|
266
|
-
|
|
317
|
+
// Fallback für Pipes / Logs
|
|
318
|
+
if (total && total > 0) {
|
|
319
|
+
const percent = ((current / total) * 100).toFixed(1);
|
|
320
|
+
console.log(`${prefix}${current}/${total} Dateien (${percent}%) – ${rel}`);
|
|
321
|
+
} else {
|
|
322
|
+
console.log(`${prefix}${current} Dateien – ${rel}`);
|
|
323
|
+
}
|
|
267
324
|
return;
|
|
268
325
|
}
|
|
269
326
|
|
|
270
327
|
const width = process.stdout.columns || 80;
|
|
271
|
-
const line = msg.padEnd(width - 1);
|
|
272
328
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
329
|
+
let line1;
|
|
330
|
+
if (total && total > 0) {
|
|
331
|
+
const percent = ((current / total) * 100).toFixed(1);
|
|
332
|
+
line1 = `${prefix}${current}/${total} Dateien (${percent}%)`;
|
|
333
|
+
} else {
|
|
334
|
+
// „unknown total“ / Scanner-Modus
|
|
335
|
+
line1 = `${prefix}${current} Dateien`;
|
|
279
336
|
}
|
|
337
|
+
|
|
338
|
+
// Pfad einkürzen falls nötig (deine bestehende Funktion verwenden)
|
|
339
|
+
const short = rel ? shortenPathForProgress(rel) : "";
|
|
340
|
+
|
|
341
|
+
let line2 = short;
|
|
342
|
+
|
|
343
|
+
if (line1.length > width) line1 = line1.slice(0, width - 1);
|
|
344
|
+
if (line2.length > width) line2 = line2.slice(0, width - 1);
|
|
345
|
+
|
|
346
|
+
// zwei Zeilen überschreiben
|
|
347
|
+
process.stdout.write("\r" + line1.padEnd(width) + "\n");
|
|
348
|
+
process.stdout.write(line2.padEnd(width));
|
|
349
|
+
|
|
350
|
+
// Cursor wieder nach oben (auf die Fortschrittszeile)
|
|
351
|
+
process.stdout.write("\x1b[1A");
|
|
352
|
+
|
|
353
|
+
progressActive = true;
|
|
280
354
|
}
|
|
281
355
|
|
|
282
356
|
// Simple worker pool for parallel tasks
|
|
@@ -301,7 +375,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
301
375
|
}
|
|
302
376
|
done += 1;
|
|
303
377
|
if (done % 10 === 0 || done === total) {
|
|
304
|
-
|
|
378
|
+
updateProgress2(` ${label}: `, done, total);
|
|
305
379
|
}
|
|
306
380
|
}
|
|
307
381
|
}
|
|
@@ -320,6 +394,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
320
394
|
|
|
321
395
|
async function walkLocal(root) {
|
|
322
396
|
const result = new Map();
|
|
397
|
+
let scanned = 0;
|
|
323
398
|
|
|
324
399
|
async function recurse(current) {
|
|
325
400
|
const entries = await fsp.readdir(current, { withFileTypes: true });
|
|
@@ -329,7 +404,9 @@ async function walkLocal(root) {
|
|
|
329
404
|
await recurse(full);
|
|
330
405
|
} else if (entry.isFile()) {
|
|
331
406
|
const rel = toPosix(path.relative(root, full));
|
|
407
|
+
|
|
332
408
|
if (!isIncluded(rel)) continue;
|
|
409
|
+
|
|
333
410
|
const stat = await fsp.stat(full);
|
|
334
411
|
result.set(rel, {
|
|
335
412
|
rel,
|
|
@@ -338,11 +415,26 @@ async function walkLocal(root) {
|
|
|
338
415
|
mtimeMs: stat.mtimeMs,
|
|
339
416
|
isText: isTextFile(rel),
|
|
340
417
|
});
|
|
418
|
+
|
|
419
|
+
scanned += 1;
|
|
420
|
+
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
421
|
+
if (scanned === 1 || scanned % chunk === 0) {
|
|
422
|
+
// total unbekannt → total = 0 → kein automatisches \n
|
|
423
|
+
updateProgress2(" Scan local: ", scanned, 0, rel);
|
|
424
|
+
}
|
|
341
425
|
}
|
|
342
426
|
}
|
|
343
427
|
}
|
|
344
428
|
|
|
345
429
|
await recurse(root);
|
|
430
|
+
|
|
431
|
+
if (scanned > 0) {
|
|
432
|
+
// letzte Zeile + sauberer Abschluss
|
|
433
|
+
updateProgress2(" Scan local: ", scanned, 0, "fertig");
|
|
434
|
+
process.stdout.write("\n");
|
|
435
|
+
progressActive = false;
|
|
436
|
+
}
|
|
437
|
+
|
|
346
438
|
return result;
|
|
347
439
|
}
|
|
348
440
|
|
|
@@ -352,6 +444,7 @@ async function walkLocal(root) {
|
|
|
352
444
|
|
|
353
445
|
async function walkRemote(sftp, remoteRoot) {
|
|
354
446
|
const result = new Map();
|
|
447
|
+
let scanned = 0;
|
|
355
448
|
|
|
356
449
|
async function recurse(remoteDir, prefix) {
|
|
357
450
|
const items = await sftp.list(remoteDir);
|
|
@@ -374,11 +467,24 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
374
467
|
size: Number(item.size),
|
|
375
468
|
modifyTime: item.modifyTime ?? 0,
|
|
376
469
|
});
|
|
470
|
+
|
|
471
|
+
scanned += 1;
|
|
472
|
+
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
473
|
+
if (scanned === 1 || scanned % chunk === 0) {
|
|
474
|
+
updateProgress2(" Scan remote: ", scanned, 0, rel);
|
|
475
|
+
}
|
|
377
476
|
}
|
|
378
477
|
}
|
|
379
478
|
}
|
|
380
479
|
|
|
381
|
-
await recurse(remoteRoot
|
|
480
|
+
await recurse(remoteRoot);
|
|
481
|
+
|
|
482
|
+
if (scanned > 0) {
|
|
483
|
+
updateProgress2(" Scan remote: ", scanned, 0, "fertig");
|
|
484
|
+
process.stdout.write("\n");
|
|
485
|
+
progressActive = false;
|
|
486
|
+
}
|
|
487
|
+
|
|
382
488
|
return result;
|
|
383
489
|
}
|
|
384
490
|
|
|
@@ -463,7 +569,11 @@ async function main() {
|
|
|
463
569
|
const start = Date.now();
|
|
464
570
|
|
|
465
571
|
log("\n\n==================================================================");
|
|
466
|
-
log(
|
|
572
|
+
log(
|
|
573
|
+
pc.bold(
|
|
574
|
+
`🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
|
|
575
|
+
)
|
|
576
|
+
);
|
|
467
577
|
log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
|
|
468
578
|
log(` Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
|
|
469
579
|
log(` Local: ${pc.green(CONNECTION.localRoot)}`);
|
|
@@ -531,28 +641,34 @@ async function main() {
|
|
|
531
641
|
// Analysis: just decide, don't upload/delete anything yet
|
|
532
642
|
for (const rel of localKeys) {
|
|
533
643
|
checkedCount += 1;
|
|
644
|
+
|
|
645
|
+
const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
|
|
534
646
|
if (
|
|
535
647
|
checkedCount === 1 || // sofortige erste Ausgabe
|
|
536
|
-
checkedCount %
|
|
537
|
-
checkedCount === totalToCheck
|
|
648
|
+
checkedCount % chunk === 0 ||
|
|
649
|
+
checkedCount === totalToCheck
|
|
538
650
|
) {
|
|
539
|
-
|
|
651
|
+
updateProgress2(" Analyse: ", checkedCount, totalToCheck, rel);
|
|
540
652
|
}
|
|
653
|
+
|
|
541
654
|
const l = local.get(rel);
|
|
542
655
|
const r = remote.get(rel);
|
|
543
|
-
|
|
544
656
|
const remotePath = path.posix.join(CONNECTION.remoteRoot, rel);
|
|
545
657
|
|
|
546
658
|
if (!r) {
|
|
547
659
|
toAdd.push({ rel, local: l, remotePath });
|
|
548
|
-
|
|
660
|
+
if (!IS_LACONIC) {
|
|
661
|
+
log(`${ADD} ${pc.green("New:")} ${rel}`);
|
|
662
|
+
}
|
|
549
663
|
continue;
|
|
550
664
|
}
|
|
551
665
|
|
|
552
666
|
// 1. size comparison
|
|
553
667
|
if (l.size !== r.size) {
|
|
554
668
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
555
|
-
|
|
669
|
+
if (!IS_LACONIC) {
|
|
670
|
+
log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
|
|
671
|
+
}
|
|
556
672
|
continue;
|
|
557
673
|
}
|
|
558
674
|
|
|
@@ -574,14 +690,16 @@ async function main() {
|
|
|
574
690
|
continue;
|
|
575
691
|
}
|
|
576
692
|
|
|
577
|
-
if (
|
|
693
|
+
if (IS_VERBOSE) {
|
|
578
694
|
const diff = diffWords(remoteStr, localStr);
|
|
579
695
|
const blocks = diff.filter((d) => d.added || d.removed).length;
|
|
580
696
|
vlog(` ${CHA} Text difference (${blocks} blocks) in ${rel}`);
|
|
581
697
|
}
|
|
582
698
|
|
|
583
699
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
584
|
-
|
|
700
|
+
if (!IS_LACONIC) {
|
|
701
|
+
log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
|
|
702
|
+
}
|
|
585
703
|
} else {
|
|
586
704
|
// Binary: Hash comparison with cache
|
|
587
705
|
const localMeta = l;
|
|
@@ -597,25 +715,27 @@ async function main() {
|
|
|
597
715
|
continue;
|
|
598
716
|
}
|
|
599
717
|
|
|
600
|
-
if (
|
|
718
|
+
if (IS_VERBOSE) {
|
|
601
719
|
vlog(` ${CHA} Hash different (binary): ${rel}`);
|
|
602
720
|
vlog(` local: ${localHash}`);
|
|
603
721
|
vlog(` remote: ${remoteHash}`);
|
|
604
722
|
}
|
|
605
723
|
|
|
606
724
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
607
|
-
|
|
725
|
+
if (!IS_LACONIC) {
|
|
726
|
+
log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
|
|
727
|
+
}
|
|
608
728
|
}
|
|
609
729
|
}
|
|
610
730
|
|
|
611
|
-
log(
|
|
612
|
-
"\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
|
|
613
|
-
);
|
|
731
|
+
log("\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
|
|
614
732
|
for (const rel of remoteKeys) {
|
|
615
733
|
if (!localKeys.has(rel)) {
|
|
616
734
|
const r = remote.get(rel);
|
|
617
735
|
toDelete.push({ rel, remotePath: r.remotePath });
|
|
618
|
-
|
|
736
|
+
if (!IS_LACONIC) {
|
|
737
|
+
log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
|
|
738
|
+
}
|
|
619
739
|
}
|
|
620
740
|
}
|
|
621
741
|
|
|
@@ -761,7 +881,7 @@ async function main() {
|
|
|
761
881
|
log(` ${DEL} Deleted: ${toDelete.length}`);
|
|
762
882
|
if (AUTO_EXCLUDED.size > 0) {
|
|
763
883
|
log(
|
|
764
|
-
` ${EXC} Excluded via uploadList | downloadList
|
|
884
|
+
` ${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
|
|
765
885
|
);
|
|
766
886
|
}
|
|
767
887
|
if (toAdd.length || toUpdate.length || toDelete.length) {
|
|
@@ -780,9 +900,7 @@ async function main() {
|
|
|
780
900
|
}
|
|
781
901
|
|
|
782
902
|
log("\n" + pc.bold(pc.green("✅ Sync complete.")));
|
|
783
|
-
log(
|
|
784
|
-
"==================================================================\n\n"
|
|
785
|
-
);
|
|
903
|
+
log("==================================================================\n\n");
|
|
786
904
|
} catch (err) {
|
|
787
905
|
elog(pc.red("❌ Synchronisation error:"), err);
|
|
788
906
|
process.exitCode = 1;
|
|
@@ -800,4 +918,4 @@ async function main() {
|
|
|
800
918
|
}
|
|
801
919
|
}
|
|
802
920
|
|
|
803
|
-
main();
|
|
921
|
+
main();
|