syncodemayo 1.0.8 → 2.0.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/CHANGELOG.md +81 -0
- package/README.md +99 -30
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +66 -0
- package/dist/cli.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +17 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/Args.d.ts +21 -0
- package/dist/lib/Args.js +99 -0
- package/dist/lib/Args.js.map +1 -0
- package/{lib → dist/lib}/Cli.d.ts +5 -0
- package/dist/lib/Cli.js +104 -0
- package/dist/lib/Cli.js.map +1 -0
- package/{lib → dist/lib}/Client.d.ts +5 -3
- package/dist/lib/Client.js +73 -0
- package/dist/lib/Client.js.map +1 -0
- package/dist/lib/Config.d.ts +48 -0
- package/dist/lib/Config.js +92 -0
- package/dist/lib/Config.js.map +1 -0
- package/dist/lib/ConfigSchema.d.ts +3 -0
- package/dist/lib/ConfigSchema.js +132 -0
- package/dist/lib/ConfigSchema.js.map +1 -0
- package/dist/lib/Connection.d.ts +41 -0
- package/dist/lib/Connection.js +176 -0
- package/dist/lib/Connection.js.map +1 -0
- package/dist/lib/Filelist.d.ts +42 -0
- package/dist/lib/Filelist.js +156 -0
- package/dist/lib/Filelist.js.map +1 -0
- package/dist/lib/Log.d.ts +12 -0
- package/dist/lib/Log.js +46 -0
- package/dist/lib/Log.js.map +1 -0
- package/dist/lib/Prompt.d.ts +15 -0
- package/dist/lib/Prompt.js +31 -0
- package/dist/lib/Prompt.js.map +1 -0
- package/dist/lib/RemoteConnection.d.ts +37 -0
- package/dist/lib/RemoteConnection.js +2 -0
- package/dist/lib/RemoteConnection.js.map +1 -0
- package/dist/lib/Sync.d.ts +18 -0
- package/dist/lib/Sync.js +127 -0
- package/dist/lib/Sync.js.map +1 -0
- package/dist/lib/TargetServer.d.ts +31 -0
- package/dist/lib/TargetServer.js +76 -0
- package/dist/lib/TargetServer.js.map +1 -0
- package/dist/lib/index.d.ts +8 -0
- package/dist/lib/index.js +9 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/vendor/strip-json-comments.d.ts +23 -0
- package/dist/vendor/strip-json-comments.js +126 -0
- package/dist/vendor/strip-json-comments.js.map +1 -0
- package/package.json +24 -26
- package/.npmignore +0 -9
- package/cli.d.ts +0 -0
- package/cli.js +0 -93
- package/index.d.ts +0 -1
- package/index.js +0 -7
- package/lib/Cli.js +0 -122
- package/lib/Client.js +0 -81
- package/lib/Config.d.ts +0 -32
- package/lib/Config.js +0 -115
- package/lib/ConfigSchema.d.ts +0 -7
- package/lib/ConfigSchema.js +0 -65
- package/lib/Connection.d.ts +0 -14
- package/lib/Connection.js +0 -111
- package/lib/Filelist.d.ts +0 -11
- package/lib/Filelist.js +0 -59
- package/lib/Sync.d.ts +0 -18
- package/lib/Sync.js +0 -141
- package/lib/TargetServer.d.ts +0 -22
- package/lib/TargetServer.js +0 -73
- package/lib/index.d.ts +0 -7
- package/lib/index.js +0 -13
- package/missing-types.d.ts +0 -14
- package/tsconfig.json +0 -48
- package/yarn.lock +0 -975
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { Client as FtpClient } from 'basic-ftp';
|
|
3
|
+
import * as Log from './Log.js';
|
|
4
|
+
import { resolvePassword } from './Config.js';
|
|
5
|
+
import { readLocalFile } from './Filelist.js';
|
|
6
|
+
// Node TLS error codes meaning the server's certificate was not trusted.
|
|
7
|
+
const UNTRUSTED_CERT_CODES = new Set([
|
|
8
|
+
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
9
|
+
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
10
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
11
|
+
'UNABLE_TO_GET_ISSUER_CERT',
|
|
12
|
+
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
|
|
13
|
+
'CERT_HAS_EXPIRED',
|
|
14
|
+
'CERT_NOT_YET_VALID',
|
|
15
|
+
'CERT_UNTRUSTED',
|
|
16
|
+
'CERT_REJECTED',
|
|
17
|
+
'CERT_SIGNATURE_FAILURE',
|
|
18
|
+
'ERR_TLS_CERT_ALTNAME_INVALID',
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* A `RemoteConnection` over FTP or FTPS, on `basic-ftp`. It logs in on the first operation and
|
|
22
|
+
* runs operations one at a time, in the order they were called. After creating directories it
|
|
23
|
+
* goes back to the login directory, so relative paths stay relative to it.
|
|
24
|
+
*/
|
|
25
|
+
export class Connection {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this._queue = Promise.resolve();
|
|
28
|
+
this._closed = false;
|
|
29
|
+
this._options = options;
|
|
30
|
+
this._client = new FtpClient();
|
|
31
|
+
}
|
|
32
|
+
// Runs `task` after every operation queued before it, so the client is never used concurrently.
|
|
33
|
+
_run(task) {
|
|
34
|
+
if (this._closed)
|
|
35
|
+
return Promise.reject(new Error("Connection is closed."));
|
|
36
|
+
const result = this._queue.then(async () => task(await this._connect()));
|
|
37
|
+
this._queue = result.catch(() => undefined);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
// Connects and logs in once; resolves the login directory.
|
|
41
|
+
_connect() {
|
|
42
|
+
if (!this._connected)
|
|
43
|
+
this._connected = this._login();
|
|
44
|
+
return this._connected;
|
|
45
|
+
}
|
|
46
|
+
async _login() {
|
|
47
|
+
const { host, port, user, pass, name } = this._options;
|
|
48
|
+
const secure = this._options.secure ?? false;
|
|
49
|
+
const rejectUnauthorized = this._options.rejectUnauthorized !== false;
|
|
50
|
+
if (secure && !rejectUnauthorized) {
|
|
51
|
+
Log.urgent(`Warning: TLS certificate checks are off for target "${name ?? host}" (rejectUnauthorized: false). The certificate of ${host} is not verified.`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await this._client.access({ host, port, user, password: pass, secure, secureOptions: { rejectUnauthorized } });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
throw describeConnectError(err, this._options);
|
|
58
|
+
}
|
|
59
|
+
return this._client.pwd();
|
|
60
|
+
}
|
|
61
|
+
getRemoteFile(filename) {
|
|
62
|
+
return this._run(async () => {
|
|
63
|
+
Log.log(" <-", filename);
|
|
64
|
+
const chunks = [];
|
|
65
|
+
const sink = new Writable({
|
|
66
|
+
write(chunk, _encoding, callback) {
|
|
67
|
+
chunks.push(chunk);
|
|
68
|
+
callback();
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
await this._client.downloadTo(sink, filename);
|
|
72
|
+
return Buffer.concat(chunks).toString();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Changes into the directory and back to the login directory: creates nothing.
|
|
76
|
+
remoteDirectoryFound(pathname) {
|
|
77
|
+
return this._run(async (startDir) => {
|
|
78
|
+
try {
|
|
79
|
+
await this._client.cd(pathname);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await this._client.cd(startDir);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// As in v1, this creates the directory and any missing parents (mkdir -p), and resolves
|
|
91
|
+
// whether that worked.
|
|
92
|
+
remoteDirectoryExists(pathname) {
|
|
93
|
+
return this._run(async (startDir) => {
|
|
94
|
+
try {
|
|
95
|
+
await this._client.ensureDir(pathname);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await this._client.cd(startDir);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
remoteFileExists(remotePath) {
|
|
107
|
+
return this._run(async () => {
|
|
108
|
+
try {
|
|
109
|
+
await this._client.size(remotePath);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
putBuffer(buff, remotePath) {
|
|
118
|
+
return this._run(async () => {
|
|
119
|
+
Log.log(" ->", remotePath);
|
|
120
|
+
await this._client.uploadFrom(Readable.from([buff]), remotePath);
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
putContent(content, remotePath) {
|
|
125
|
+
return this.putBuffer(Buffer.from(content), remotePath);
|
|
126
|
+
}
|
|
127
|
+
async putFile(filename, remotePath) {
|
|
128
|
+
const buffer = await readLocalFile(filename, true);
|
|
129
|
+
await this.putBuffer(buffer, remotePath);
|
|
130
|
+
return remotePath;
|
|
131
|
+
}
|
|
132
|
+
deleteRemoteFile(remotePath) {
|
|
133
|
+
return this._run(async () => {
|
|
134
|
+
Log.log(" x", remotePath);
|
|
135
|
+
try {
|
|
136
|
+
await this._client.remove(remotePath);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
Log.log("Error removing", remotePath, err.message);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Waits for queued operations, then closes the control and data sockets. Safe to call twice.
|
|
146
|
+
async close() {
|
|
147
|
+
if (this._closed)
|
|
148
|
+
return;
|
|
149
|
+
this._closed = true;
|
|
150
|
+
await this._queue;
|
|
151
|
+
this._client.close();
|
|
152
|
+
}
|
|
153
|
+
static create(target) {
|
|
154
|
+
const pass = resolvePassword(target);
|
|
155
|
+
return new Connection({
|
|
156
|
+
host: target.host,
|
|
157
|
+
port: target.port,
|
|
158
|
+
user: target.user,
|
|
159
|
+
pass,
|
|
160
|
+
secure: target.secure,
|
|
161
|
+
rejectUnauthorized: target.rejectUnauthorized,
|
|
162
|
+
name: target._name,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// An untrusted certificate becomes an error that names the host and the opt-out; anything
|
|
167
|
+
// else is returned as is.
|
|
168
|
+
function describeConnectError(err, options) {
|
|
169
|
+
const code = err?.code;
|
|
170
|
+
if (typeof code !== 'string' || !UNTRUSTED_CERT_CODES.has(code))
|
|
171
|
+
return err;
|
|
172
|
+
const target = options.name ? ` on target "${options.name}"` : '';
|
|
173
|
+
return new Error(`The TLS certificate of ${options.host} was not trusted (${err.message}). ` +
|
|
174
|
+
`If you trust this server, set "rejectUnauthorized": false${target} to accept its certificate.`, { cause: err });
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=Connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Connection.js","sourceRoot":"./","sources":["lib/Connection.ts"],"names":[],"mappings":"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","sourcesContent":["import { Readable, Writable } from 'node:stream'\nimport { Client as FtpClient } from 'basic-ftp'\nimport * as Log from './Log.js'\nimport { ITargetConfig, resolvePassword } from './Config.js'\nimport { readLocalFile } from './Filelist.js'\nimport { RemoteConnection } from './RemoteConnection.js'\n\n/** How to reach an FTP/FTPS server. */\nexport interface ConnectionOptions {\n host: string\n port: number\n user: string\n pass: string\n /** `false`: plain FTP. `true`: explicit FTPS (AUTH TLS). `\"implicit\"`: implicit FTPS. */\n secure?: boolean | 'implicit'\n /** `false` accepts self-signed or mismatched TLS certificates (with a warning). Default `true`. */\n rejectUnauthorized?: boolean\n /** The target name, for messages. */\n name?: string\n}\n\n// Node TLS error codes meaning the server's certificate was not trusted.\nconst UNTRUSTED_CERT_CODES = new Set([\n 'DEPTH_ZERO_SELF_SIGNED_CERT',\n 'SELF_SIGNED_CERT_IN_CHAIN',\n 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',\n 'UNABLE_TO_GET_ISSUER_CERT',\n 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',\n 'CERT_HAS_EXPIRED',\n 'CERT_NOT_YET_VALID',\n 'CERT_UNTRUSTED',\n 'CERT_REJECTED',\n 'CERT_SIGNATURE_FAILURE',\n 'ERR_TLS_CERT_ALTNAME_INVALID',\n])\n\n/**\n * A `RemoteConnection` over FTP or FTPS, on `basic-ftp`. It logs in on the first operation and\n * runs operations one at a time, in the order they were called. After creating directories it\n * goes back to the login directory, so relative paths stay relative to it.\n */\nexport class Connection implements RemoteConnection {\n private _options: ConnectionOptions\n private _client: FtpClient\n private _connected: Promise<string> | undefined\n private _queue: Promise<unknown> = Promise.resolve()\n private _closed = false\n\n constructor(options: ConnectionOptions) {\n this._options = options\n this._client = new FtpClient()\n }\n\n // Runs `task` after every operation queued before it, so the client is never used concurrently.\n private _run<T>(task: (startDir: string) => Promise<T>): Promise<T> {\n if (this._closed) return Promise.reject(new Error(\"Connection is closed.\"))\n const result = this._queue.then(async () => task(await this._connect()))\n this._queue = result.catch(() => undefined)\n return result\n }\n\n // Connects and logs in once; resolves the login directory.\n private _connect(): Promise<string> {\n if (!this._connected) this._connected = this._login()\n return this._connected\n }\n\n private async _login(): Promise<string> {\n const { host, port, user, pass, name } = this._options\n const secure = this._options.secure ?? false\n const rejectUnauthorized = this._options.rejectUnauthorized !== false\n\n if (secure && !rejectUnauthorized) {\n Log.urgent(`Warning: TLS certificate checks are off for target \"${name ?? host}\" (rejectUnauthorized: false). The certificate of ${host} is not verified.`)\n }\n\n try {\n await this._client.access({ host, port, user, password: pass, secure, secureOptions: { rejectUnauthorized } })\n }\n catch (err) {\n throw describeConnectError(err, this._options)\n }\n return this._client.pwd()\n }\n\n getRemoteFile(filename: string): Promise<string> {\n return this._run(async () => {\n Log.log(\" <-\", filename)\n const chunks: Buffer[] = []\n const sink = new Writable({\n write(chunk: Buffer, _encoding, callback) {\n chunks.push(chunk)\n callback()\n },\n })\n await this._client.downloadTo(sink, filename)\n return Buffer.concat(chunks).toString()\n })\n }\n\n // Changes into the directory and back to the login directory: creates nothing.\n remoteDirectoryFound(pathname: string): Promise<boolean> {\n return this._run(async (startDir) => {\n try {\n await this._client.cd(pathname)\n return true\n }\n catch {\n return false\n }\n finally {\n await this._client.cd(startDir)\n }\n })\n }\n\n // As in v1, this creates the directory and any missing parents (mkdir -p), and resolves\n // whether that worked.\n remoteDirectoryExists(pathname: string): Promise<boolean> {\n return this._run(async (startDir) => {\n try {\n await this._client.ensureDir(pathname)\n return true\n }\n catch {\n return false\n }\n finally {\n await this._client.cd(startDir)\n }\n })\n }\n\n remoteFileExists(remotePath: string): Promise<boolean> {\n return this._run(async () => {\n try {\n await this._client.size(remotePath)\n return true\n }\n catch {\n return false\n }\n })\n }\n\n putBuffer(buff: Buffer, remotePath: string): Promise<boolean> {\n return this._run(async () => {\n Log.log(\" ->\", remotePath)\n await this._client.uploadFrom(Readable.from([buff]), remotePath)\n return true\n })\n }\n\n putContent(content: string, remotePath: string): Promise<boolean> {\n return this.putBuffer(Buffer.from(content), remotePath)\n }\n\n async putFile(filename: string, remotePath: string): Promise<string> {\n const buffer = await readLocalFile(filename, true) as Buffer\n await this.putBuffer(buffer, remotePath)\n return remotePath\n }\n\n deleteRemoteFile(remotePath: string): Promise<boolean> {\n return this._run(async () => {\n Log.log(\" x\", remotePath)\n try {\n await this._client.remove(remotePath)\n return true\n }\n catch (err) {\n Log.log(\"Error removing\", remotePath, (err as Error).message)\n return false\n }\n })\n }\n\n // Waits for queued operations, then closes the control and data sockets. Safe to call twice.\n async close(): Promise<void> {\n if (this._closed) return\n this._closed = true\n await this._queue\n this._client.close()\n }\n\n static create(target: ITargetConfig): Connection {\n const pass = resolvePassword(target)\n return new Connection({\n host: target.host,\n port: target.port,\n user: target.user,\n pass,\n secure: target.secure,\n rejectUnauthorized: target.rejectUnauthorized,\n name: target._name,\n })\n }\n}\n\n// An untrusted certificate becomes an error that names the host and the opt-out; anything\n// else is returned as is.\nfunction describeConnectError(err: unknown, options: ConnectionOptions): unknown {\n const code = (err as NodeJS.ErrnoException | undefined)?.code\n if (typeof code !== 'string' || !UNTRUSTED_CERT_CODES.has(code)) return err\n const target = options.name ? ` on target \"${options.name}\"` : ''\n return new Error(\n `The TLS certificate of ${options.host} was not trusted (${(err as Error).message}). ` +\n `If you trust this server, set \"rejectUnauthorized\": false${target} to accept its certificate.`,\n { cause: err },\n )\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ILocalConfig, ITargetConfig } from './Config.js';
|
|
2
|
+
import { RemoteConnection } from './RemoteConnection.js';
|
|
3
|
+
export interface IFilelist {
|
|
4
|
+
[filename: string]: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function localFileExists(path: string): Promise<boolean>;
|
|
7
|
+
export declare function readLocalFile(path: string, asBuffer?: boolean): Promise<string | Buffer>;
|
|
8
|
+
export declare function writeLocalFile(path: string, content: string): Promise<any>;
|
|
9
|
+
/**
|
|
10
|
+
* Reads and parses the remote file list at `<target.path>/<target.cache>`. Rejects with an `Error`
|
|
11
|
+
* that tells the user to run `syncodemayo init` when the list is missing or isn't valid JSON.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRemoteFilelist(target: ITargetConfig, conn: RemoteConnection): Promise<IFilelist>;
|
|
14
|
+
/**
|
|
15
|
+
* Converts a path that uses `sep` as its separator to POSIX (`/`) separators.
|
|
16
|
+
*/
|
|
17
|
+
export declare function toPosixPath(filepath: string, sep?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* The cache key for a file: its POSIX path relative to `local.path`, prefixed
|
|
20
|
+
* with `local.path`. Matches the keys v1 got from glob 7, which collapsed
|
|
21
|
+
* repeated slashes (so `public/` and `public//` both give `public/<file>`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function filelistKey(localPath: string, relativePath: string, sep?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* The remote path for a cache key: `target.path` (without trailing slashes), one `/`, then the
|
|
26
|
+
* key with its `filelistKey(localPath, …)` prefix removed. Resolves null when the key doesn't
|
|
27
|
+
* start with that prefix (for example, `local.path` changed since the file list was written).
|
|
28
|
+
*/
|
|
29
|
+
export declare function remotePathForKey(localPath: string, targetPath: string, key: string, sep?: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* The CRC32 of a file's bytes as an unsigned 32-bit integer, the same value
|
|
32
|
+
* `crc` 3.4.1's `crc32` gave v1.
|
|
33
|
+
*/
|
|
34
|
+
export declare function fileCrc32(filepath: string): number;
|
|
35
|
+
/**
|
|
36
|
+
* `path.matchesGlob` with minimatch's `{ dot: true }` meaning, as v1 used it:
|
|
37
|
+
* `*`, `**`, and `?` also match path segments that start with `.`.
|
|
38
|
+
* `filepath` uses POSIX separators.
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchesGlobDot(filepath: string, pattern: string): boolean;
|
|
41
|
+
export declare function listLocalFiles(root: string): string[];
|
|
42
|
+
export declare function buildLocalFilelist(config: ILocalConfig): Promise<IFilelist>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { crc32 } from 'zlib';
|
|
4
|
+
import * as Log from './Log.js';
|
|
5
|
+
export async function localFileExists(path) {
|
|
6
|
+
return fs.existsSync(path);
|
|
7
|
+
}
|
|
8
|
+
export async function readLocalFile(path, asBuffer = false) {
|
|
9
|
+
const buff = fs.readFileSync(path);
|
|
10
|
+
return asBuffer ? buff : buff.toString();
|
|
11
|
+
}
|
|
12
|
+
export async function writeLocalFile(path, content) {
|
|
13
|
+
return fs.writeFileSync(path, content);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads and parses the remote file list at `<target.path>/<target.cache>`. Rejects with an `Error`
|
|
17
|
+
* that tells the user to run `syncodemayo init` when the list is missing or isn't valid JSON.
|
|
18
|
+
*/
|
|
19
|
+
export async function getRemoteFilelist(target, conn) {
|
|
20
|
+
const path = `${target.path}/${target.cache}`;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(await conn.getRemoteFile(path));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
Log.debug("Missing or error parsing remote file list.", err);
|
|
26
|
+
throw new Error(`The remote file list ${path} is missing or unreadable (${err?.message ?? err}). ` +
|
|
27
|
+
`Run "syncodemayo init" to set up SyncoDeMayo on the server.`, { cause: err });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Converts a path that uses `sep` as its separator to POSIX (`/`) separators.
|
|
32
|
+
*/
|
|
33
|
+
export function toPosixPath(filepath, sep = path.sep) {
|
|
34
|
+
return sep === '/' ? filepath : filepath.split(sep).join('/');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The cache key for a file: its POSIX path relative to `local.path`, prefixed
|
|
38
|
+
* with `local.path`. Matches the keys v1 got from glob 7, which collapsed
|
|
39
|
+
* repeated slashes (so `public/` and `public//` both give `public/<file>`).
|
|
40
|
+
*/
|
|
41
|
+
export function filelistKey(localPath, relativePath, sep = path.sep) {
|
|
42
|
+
const prefix = toPosixPath(localPath, sep).replace(/\/+/g, '/').replace(/\/$/, '');
|
|
43
|
+
return `${prefix}/${toPosixPath(relativePath, sep)}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The remote path for a cache key: `target.path` (without trailing slashes), one `/`, then the
|
|
47
|
+
* key with its `filelistKey(localPath, …)` prefix removed. Resolves null when the key doesn't
|
|
48
|
+
* start with that prefix (for example, `local.path` changed since the file list was written).
|
|
49
|
+
*/
|
|
50
|
+
export function remotePathForKey(localPath, targetPath, key, sep = path.sep) {
|
|
51
|
+
const prefix = filelistKey(localPath, '', sep);
|
|
52
|
+
if (!key.startsWith(prefix))
|
|
53
|
+
return null;
|
|
54
|
+
const relative = key.slice(prefix.length);
|
|
55
|
+
if (relative === '')
|
|
56
|
+
return null;
|
|
57
|
+
return `${targetPath.replace(/\/+$/, '')}/${relative}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The CRC32 of a file's bytes as an unsigned 32-bit integer, the same value
|
|
61
|
+
* `crc` 3.4.1's `crc32` gave v1.
|
|
62
|
+
*/
|
|
63
|
+
export function fileCrc32(filepath) {
|
|
64
|
+
return crc32(fs.readFileSync(filepath)) >>> 0;
|
|
65
|
+
}
|
|
66
|
+
// A private-use character that stands in for a leading `.` in a path segment.
|
|
67
|
+
// `path.matchesGlob` never lets `*`, `**`, or `?` match a segment that starts
|
|
68
|
+
// with `.`, but it does match this character.
|
|
69
|
+
const DOT_MASK = '\uE000';
|
|
70
|
+
const maskPathDots = (posixPath) => posixPath
|
|
71
|
+
.split('/')
|
|
72
|
+
.map(segment => segment.startsWith('.') && segment !== '.' && segment !== '..'
|
|
73
|
+
? DOT_MASK + segment.slice(1)
|
|
74
|
+
: segment)
|
|
75
|
+
.join('/');
|
|
76
|
+
// A literal `.` that starts a pattern segment (or a brace, extglob, or
|
|
77
|
+
// alternative inside one) must match the masked dot in the path.
|
|
78
|
+
const maskPatternDots = (pattern) => pattern.replace(/(^|[/{,(|])\./g, `$1${DOT_MASK}`);
|
|
79
|
+
/**
|
|
80
|
+
* `path.matchesGlob` with minimatch's `{ dot: true }` meaning, as v1 used it:
|
|
81
|
+
* `*`, `**`, and `?` also match path segments that start with `.`.
|
|
82
|
+
* `filepath` uses POSIX separators.
|
|
83
|
+
*/
|
|
84
|
+
export function matchesGlobDot(filepath, pattern) {
|
|
85
|
+
return path.matchesGlob(filepath, pattern)
|
|
86
|
+
|| path.matchesGlob(maskPathDots(filepath), maskPatternDots(pattern));
|
|
87
|
+
}
|
|
88
|
+
// Every file (no directories) under `root`, dotfiles and files inside
|
|
89
|
+
// dot-directories included, as sorted POSIX paths relative to `root`.
|
|
90
|
+
//
|
|
91
|
+
// Each directory is globbed with `*` and `.*` from inside it. `fs.globSync`'s
|
|
92
|
+
// `**` can't do the walk: it never lists a dot entry or enters a
|
|
93
|
+
// dot-directory, on Node 22.17 `**/.*` finds no dot entries at all, and it
|
|
94
|
+
// follows a symlinked directory only one level deep.
|
|
95
|
+
//
|
|
96
|
+
// Symlinks follow glob 7's `**/**` rule (the v1 default `files`): a symlinked
|
|
97
|
+
// directory reached through real directories is walked in full, but a
|
|
98
|
+
// symlinked directory inside one is not entered. No path crosses more than one
|
|
99
|
+
// symlink, so a symlink loop ends instead of failing with ELOOP. Entries that
|
|
100
|
+
// can't be resolved (dangling symlinks, self-referencing symlinks) are skipped.
|
|
101
|
+
// lstat and stat for one entry, or null when it can't be resolved (a dangling
|
|
102
|
+
// symlink, or a symlink loop): such entries are skipped.
|
|
103
|
+
function statEntry(fullpath) {
|
|
104
|
+
try {
|
|
105
|
+
return {
|
|
106
|
+
isSymlink: fs.lstatSync(fullpath).isSymbolicLink(),
|
|
107
|
+
isDirectory: fs.statSync(fullpath).isDirectory(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const code = err.code;
|
|
112
|
+
if (code === 'ENOENT' || code === 'ELOOP')
|
|
113
|
+
return null;
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function listLocalFiles(root) {
|
|
118
|
+
const files = new Set();
|
|
119
|
+
const pending = [{ dir: '', inSymlink: false }];
|
|
120
|
+
while (pending.length > 0) {
|
|
121
|
+
const { dir, inSymlink } = pending.pop();
|
|
122
|
+
const entries = new Set(fs.globSync(['*', '.*'], { cwd: path.join(root, dir) }));
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
const relative = dir ? `${dir}/${toPosixPath(entry)}` : toPosixPath(entry);
|
|
125
|
+
const fullpath = path.join(root, relative);
|
|
126
|
+
const stats = statEntry(fullpath);
|
|
127
|
+
if (stats == null)
|
|
128
|
+
continue;
|
|
129
|
+
const { isSymlink, isDirectory } = stats;
|
|
130
|
+
if (!isDirectory) {
|
|
131
|
+
files.add(relative);
|
|
132
|
+
}
|
|
133
|
+
else if (!isSymlink) {
|
|
134
|
+
pending.push({ dir: relative, inSymlink });
|
|
135
|
+
}
|
|
136
|
+
else if (!inSymlink) {
|
|
137
|
+
pending.push({ dir: relative, inSymlink: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [...files].sort();
|
|
142
|
+
}
|
|
143
|
+
export async function buildLocalFilelist(config) {
|
|
144
|
+
// Create the CRCs...
|
|
145
|
+
const isListed = (relative) => matchesGlobDot(relative, config.files);
|
|
146
|
+
const isExcluded = (key) => config.exclude.some(pattern => matchesGlobDot(key, pattern));
|
|
147
|
+
const filelist = {};
|
|
148
|
+
for (const relative of listLocalFiles(config.path).filter(isListed)) {
|
|
149
|
+
const key = filelistKey(config.path, relative);
|
|
150
|
+
if (isExcluded(key))
|
|
151
|
+
continue;
|
|
152
|
+
filelist[key] = fileCrc32(path.join(config.path, relative));
|
|
153
|
+
}
|
|
154
|
+
return filelist;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=Filelist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Filelist.js","sourceRoot":"./","sources":["lib/Filelist.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs'\nimport * as path from 'path'\nimport { crc32 } from 'zlib'\nimport * as Log from './Log.js'\nimport { ILocalConfig, ITargetConfig } from './Config.js'\nimport { RemoteConnection } from './RemoteConnection.js'\n\nexport interface IFilelist {\n [filename: string]: number\n}\n\nexport async function localFileExists(path: string): Promise<boolean> {\n return fs.existsSync(path)\n}\n\nexport async function readLocalFile(path: string, asBuffer = false): Promise<string | Buffer> {\n const buff = fs.readFileSync(path)\n return asBuffer ? buff : buff.toString()\n}\n\nexport async function writeLocalFile(path: string, content: string): Promise<any> {\n return fs.writeFileSync(path, content)\n}\n\n/**\n * Reads and parses the remote file list at `<target.path>/<target.cache>`. Rejects with an `Error`\n * that tells the user to run `syncodemayo init` when the list is missing or isn't valid JSON.\n */\nexport async function getRemoteFilelist(target: ITargetConfig, conn: RemoteConnection): Promise<IFilelist> {\n const path = `${target.path}/${target.cache}`\n try {\n return JSON.parse(await conn.getRemoteFile(path)) as IFilelist\n }\n catch (err) {\n Log.debug(\"Missing or error parsing remote file list.\", err)\n throw new Error(\n `The remote file list ${path} is missing or unreadable (${(err as Error)?.message ?? err}). ` +\n `Run \"syncodemayo init\" to set up SyncoDeMayo on the server.`,\n { cause: err },\n )\n }\n}\n\n/**\n * Converts a path that uses `sep` as its separator to POSIX (`/`) separators.\n */\nexport function toPosixPath(filepath: string, sep: string = path.sep): string {\n return sep === '/' ? filepath : filepath.split(sep).join('/')\n}\n\n/**\n * The cache key for a file: its POSIX path relative to `local.path`, prefixed\n * with `local.path`. Matches the keys v1 got from glob 7, which collapsed\n * repeated slashes (so `public/` and `public//` both give `public/<file>`).\n */\nexport function filelistKey(localPath: string, relativePath: string, sep: string = path.sep): string {\n const prefix = toPosixPath(localPath, sep).replace(/\\/+/g, '/').replace(/\\/$/, '')\n return `${prefix}/${toPosixPath(relativePath, sep)}`\n}\n\n/**\n * The remote path for a cache key: `target.path` (without trailing slashes), one `/`, then the\n * key with its `filelistKey(localPath, …)` prefix removed. Resolves null when the key doesn't\n * start with that prefix (for example, `local.path` changed since the file list was written).\n */\nexport function remotePathForKey(localPath: string, targetPath: string, key: string, sep: string = path.sep): string | null {\n const prefix = filelistKey(localPath, '', sep)\n if (!key.startsWith(prefix)) return null\n const relative = key.slice(prefix.length)\n if (relative === '') return null\n return `${targetPath.replace(/\\/+$/, '')}/${relative}`\n}\n\n/**\n * The CRC32 of a file's bytes as an unsigned 32-bit integer, the same value\n * `crc` 3.4.1's `crc32` gave v1.\n */\nexport function fileCrc32(filepath: string): number {\n return crc32(fs.readFileSync(filepath)) >>> 0\n}\n\n// A private-use character that stands in for a leading `.` in a path segment.\n// `path.matchesGlob` never lets `*`, `**`, or `?` match a segment that starts\n// with `.`, but it does match this character.\nconst DOT_MASK = '\\uE000'\n\nconst maskPathDots = (posixPath: string) =>\n posixPath\n .split('/')\n .map(segment => segment.startsWith('.') && segment !== '.' && segment !== '..'\n ? DOT_MASK + segment.slice(1)\n : segment)\n .join('/')\n\n// A literal `.` that starts a pattern segment (or a brace, extglob, or\n// alternative inside one) must match the masked dot in the path.\nconst maskPatternDots = (pattern: string) =>\n pattern.replace(/(^|[/{,(|])\\./g, `$1${DOT_MASK}`)\n\n/**\n * `path.matchesGlob` with minimatch's `{ dot: true }` meaning, as v1 used it:\n * `*`, `**`, and `?` also match path segments that start with `.`.\n * `filepath` uses POSIX separators.\n */\nexport function matchesGlobDot(filepath: string, pattern: string): boolean {\n return path.matchesGlob(filepath, pattern)\n || path.matchesGlob(maskPathDots(filepath), maskPatternDots(pattern))\n}\n\n// Every file (no directories) under `root`, dotfiles and files inside\n// dot-directories included, as sorted POSIX paths relative to `root`.\n//\n// Each directory is globbed with `*` and `.*` from inside it. `fs.globSync`'s\n// `**` can't do the walk: it never lists a dot entry or enters a\n// dot-directory, on Node 22.17 `**/.*` finds no dot entries at all, and it\n// follows a symlinked directory only one level deep.\n//\n// Symlinks follow glob 7's `**/**` rule (the v1 default `files`): a symlinked\n// directory reached through real directories is walked in full, but a\n// symlinked directory inside one is not entered. No path crosses more than one\n// symlink, so a symlink loop ends instead of failing with ELOOP. Entries that\n// can't be resolved (dangling symlinks, self-referencing symlinks) are skipped.\n// lstat and stat for one entry, or null when it can't be resolved (a dangling\n// symlink, or a symlink loop): such entries are skipped.\nfunction statEntry(fullpath: string): { isSymlink: boolean, isDirectory: boolean } | null {\n try {\n return {\n isSymlink: fs.lstatSync(fullpath).isSymbolicLink(),\n isDirectory: fs.statSync(fullpath).isDirectory(),\n }\n }\n catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ENOENT' || code === 'ELOOP') return null\n throw err\n }\n}\n\nexport function listLocalFiles(root: string): string[] {\n const files = new Set<string>()\n const pending: { dir: string, inSymlink: boolean }[] = [{ dir: '', inSymlink: false }]\n\n while (pending.length > 0) {\n const { dir, inSymlink } = pending.pop()!\n const entries = new Set(fs.globSync(['*', '.*'], { cwd: path.join(root, dir) }))\n for (const entry of entries) {\n const relative = dir ? `${dir}/${toPosixPath(entry)}` : toPosixPath(entry)\n const fullpath = path.join(root, relative)\n const stats = statEntry(fullpath)\n if (stats == null) continue\n const { isSymlink, isDirectory } = stats\n if (!isDirectory) {\n files.add(relative)\n }\n else if (!isSymlink) {\n pending.push({ dir: relative, inSymlink })\n }\n else if (!inSymlink) {\n pending.push({ dir: relative, inSymlink: true })\n }\n }\n }\n\n return [...files].sort()\n}\n\nexport async function buildLocalFilelist(config: ILocalConfig): Promise<IFilelist> {\n // Create the CRCs...\n const isListed = (relative: string) =>\n matchesGlobDot(relative, config.files)\n const isExcluded = (key: string) =>\n config.exclude.some(pattern => matchesGlobDot(key, pattern))\n\n const filelist: IFilelist = {}\n for (const relative of listLocalFiles(config.path).filter(isListed)) {\n const key = filelistKey(config.path, relative)\n if (isExcluded(key)) continue\n filelist[key] = fileCrc32(path.join(config.path, relative))\n }\n return filelist\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const LOG_LEVEL_ENV = "SYNCODEMAYO_LOG_LEVEL";
|
|
2
|
+
export declare enum LogLevel {
|
|
3
|
+
Debug = 0,
|
|
4
|
+
Default = 1,
|
|
5
|
+
Info = 2,
|
|
6
|
+
Urgent = 3
|
|
7
|
+
}
|
|
8
|
+
export declare function log(...args: any[]): void;
|
|
9
|
+
export declare function debug(...args: any[]): void;
|
|
10
|
+
export declare function info(...args: any[]): void;
|
|
11
|
+
export declare function urgent(...args: any[]): void;
|
|
12
|
+
export declare function setLogLevel(level: LogLevel): void;
|
package/dist/lib/Log.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const LOG_LEVEL_ENV = 'SYNCODEMAYO_LOG_LEVEL';
|
|
2
|
+
export var LogLevel;
|
|
3
|
+
(function (LogLevel) {
|
|
4
|
+
LogLevel[LogLevel["Debug"] = 0] = "Debug";
|
|
5
|
+
LogLevel[LogLevel["Default"] = 1] = "Default";
|
|
6
|
+
LogLevel[LogLevel["Info"] = 2] = "Info";
|
|
7
|
+
LogLevel[LogLevel["Urgent"] = 3] = "Urgent";
|
|
8
|
+
})(LogLevel || (LogLevel = {}));
|
|
9
|
+
const getLogLevel = (level) => {
|
|
10
|
+
if (typeof level == 'string') {
|
|
11
|
+
switch (level.toLowerCase()) {
|
|
12
|
+
case 'info': return LogLevel.Info;
|
|
13
|
+
case 'debug': return LogLevel.Debug;
|
|
14
|
+
case 'error':
|
|
15
|
+
case 'none':
|
|
16
|
+
case 'urgent': return LogLevel.Urgent;
|
|
17
|
+
default: return LogLevel.Default;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return level;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
let logger = defaultLogger;
|
|
25
|
+
let logLevel = getLogLevel(process.env[LOG_LEVEL_ENV] || 'Default');
|
|
26
|
+
export function log(...args) {
|
|
27
|
+
logger(LogLevel.Default, args);
|
|
28
|
+
}
|
|
29
|
+
export function debug(...args) {
|
|
30
|
+
logger(LogLevel.Debug, args);
|
|
31
|
+
}
|
|
32
|
+
export function info(...args) {
|
|
33
|
+
logger(LogLevel.Info, args);
|
|
34
|
+
}
|
|
35
|
+
export function urgent(...args) {
|
|
36
|
+
logger(LogLevel.Urgent, args);
|
|
37
|
+
}
|
|
38
|
+
export function setLogLevel(level) {
|
|
39
|
+
logLevel = level;
|
|
40
|
+
}
|
|
41
|
+
function defaultLogger(level, args) {
|
|
42
|
+
if (level >= logLevel) {
|
|
43
|
+
console.log(...args);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=Log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Log.js","sourceRoot":"./","sources":["lib/Log.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG,uBAAuB,CAAA;AAEpD,MAAM,CAAN,IAAY,QAKX;AALD,WAAY,QAAQ;IAClB,yCAAK,CAAA;IACL,6CAAO,CAAA;IACP,uCAAI,CAAA;IACJ,2CAAM,CAAA;AACR,CAAC,EALW,QAAQ,KAAR,QAAQ,QAKnB;AAED,MAAM,WAAW,GAAG,CAAC,KAAwB,EAAE,EAAE;IAC/C,IAAI,OAAO,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,QAAQ,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YAC5B,KAAK,MAAM,CAAC,CAAC,OAAO,QAAQ,CAAC,IAAI,CAAA;YACjC,KAAK,OAAO,CAAC,CAAC,OAAO,QAAQ,CAAC,KAAK,CAAA;YACnC,KAAK,OAAO,CAAC;YACb,KAAK,MAAM,CAAC;YACZ,KAAK,QAAQ,CAAC,CAAC,OAAO,QAAQ,CAAC,MAAM,CAAA;YACrC,OAAO,CAAC,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAA;QAClC,CAAC;IACH,CAAC;SACI,CAAC;QACJ,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AAED,IAAI,MAAM,GAAG,aAAa,CAAA;AAC1B,IAAI,QAAQ,GAAa,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,SAAS,CAAC,CAAA;AAE7E,MAAM,UAAU,GAAG,CAAC,GAAG,IAAW;IAChC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;AAChC,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,GAAG,IAAW;IAClC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;AAC9B,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,GAAG,IAAW;IACjC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,GAAG,IAAW;IACnC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAe;IACzC,QAAQ,GAAG,KAAK,CAAA;AAClB,CAAC;AAGD,SAAS,aAAa,CAAC,KAAe,EAAE,IAAW;IACjD,IAAI,KAAK,IAAI,QAAQ,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACtB,CAAC;AACH,CAAC","sourcesContent":["export const LOG_LEVEL_ENV = 'SYNCODEMAYO_LOG_LEVEL'\n\nexport enum LogLevel {\n Debug,\n Default,\n Info,\n Urgent,\n}\n\nconst getLogLevel = (level: LogLevel | string) => {\n if (typeof level == 'string') {\n switch (level.toLowerCase()) {\n case 'info': return LogLevel.Info\n case 'debug': return LogLevel.Debug\n case 'error':\n case 'none':\n case 'urgent': return LogLevel.Urgent\n default: return LogLevel.Default\n }\n }\n else {\n return level\n }\n}\n\nlet logger = defaultLogger\nlet logLevel: LogLevel = getLogLevel(process.env[LOG_LEVEL_ENV] || 'Default')\n\nexport function log(...args: any[]): void {\n logger(LogLevel.Default, args)\n}\n\nexport function debug(...args: any[]): void {\n logger(LogLevel.Debug, args)\n}\n\nexport function info(...args: any[]): void {\n logger(LogLevel.Info, args)\n}\n\nexport function urgent(...args: any[]): void {\n logger(LogLevel.Urgent, args)\n}\n\nexport function setLogLevel(level: LogLevel) {\n logLevel = level\n}\n\n\nfunction defaultLogger(level: LogLevel, args: any[]) {\n if (level >= logLevel) {\n console.log(...args)\n }\n}"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ConfirmStreams {
|
|
2
|
+
input?: NodeJS.ReadableStream;
|
|
3
|
+
output?: NodeJS.WritableStream;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* True when an answer confirms: `y` or `yes` in any case, after trimming. Anything else,
|
|
7
|
+
* including an empty answer, declines.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isYes(answer: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Asks `<message> - (Yes will upload)` and resolves whether the answer confirms (see `isYes`).
|
|
12
|
+
* The input and output streams default to stdin and stdout, and can be injected for tests.
|
|
13
|
+
* If input ends before an answer, it resolves false.
|
|
14
|
+
*/
|
|
15
|
+
export declare function confirmUpload(message: string, streams?: ConfirmStreams): Promise<boolean>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
/**
|
|
3
|
+
* True when an answer confirms: `y` or `yes` in any case, after trimming. Anything else,
|
|
4
|
+
* including an empty answer, declines.
|
|
5
|
+
*/
|
|
6
|
+
export function isYes(answer) {
|
|
7
|
+
const normalized = answer.trim().toLowerCase();
|
|
8
|
+
return normalized === 'y' || normalized === 'yes';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Asks `<message> - (Yes will upload)` and resolves whether the answer confirms (see `isYes`).
|
|
12
|
+
* The input and output streams default to stdin and stdout, and can be injected for tests.
|
|
13
|
+
* If input ends before an answer, it resolves false.
|
|
14
|
+
*/
|
|
15
|
+
export async function confirmUpload(message, streams = {}) {
|
|
16
|
+
const rl = createInterface({
|
|
17
|
+
input: streams.input ?? process.stdin,
|
|
18
|
+
output: streams.output ?? process.stdout,
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
const answer = await new Promise((resolve, reject) => {
|
|
22
|
+
rl.once('close', () => resolve(''));
|
|
23
|
+
rl.question(`${message} - (Yes will upload) `).then(resolve, reject);
|
|
24
|
+
});
|
|
25
|
+
return isYes(answer);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
rl.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=Prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Prompt.js","sourceRoot":"./","sources":["lib/Prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAOxD;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,MAAc;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC9C,OAAO,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK,CAAA;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAe,EAAE,UAA0B,EAAE;IAC/E,MAAM,EAAE,GAAG,eAAe,CAAC;QACzB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK;QACrC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM;KACzC,CAAC,CAAA;IACF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3D,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;YACnC,EAAE,CAAC,QAAQ,CAAC,GAAG,OAAO,uBAAuB,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QACtE,CAAC,CAAC,CAAA;QACF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAA;IACtB,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAA;IACZ,CAAC;AACH,CAAC","sourcesContent":["import { createInterface } from 'node:readline/promises'\n\nexport interface ConfirmStreams {\n input?: NodeJS.ReadableStream\n output?: NodeJS.WritableStream\n}\n\n/**\n * True when an answer confirms: `y` or `yes` in any case, after trimming. Anything else,\n * including an empty answer, declines.\n */\nexport function isYes(answer: string): boolean {\n const normalized = answer.trim().toLowerCase()\n return normalized === 'y' || normalized === 'yes'\n}\n\n/**\n * Asks `<message> - (Yes will upload)` and resolves whether the answer confirms (see `isYes`).\n * The input and output streams default to stdin and stdout, and can be injected for tests.\n * If input ends before an answer, it resolves false.\n */\nexport async function confirmUpload(message: string, streams: ConfirmStreams = {}): Promise<boolean> {\n const rl = createInterface({\n input: streams.input ?? process.stdin,\n output: streams.output ?? process.stdout,\n })\n try {\n const answer = await new Promise<string>((resolve, reject) => {\n rl.once('close', () => resolve(''))\n rl.question(`${message} - (Yes will upload) `).then(resolve, reject)\n })\n return isYes(answer)\n } finally {\n rl.close()\n }\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ITargetConfig } from './Config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The remote operations `Sync`, `TargetServer` and `getRemoteFilelist` use. The FTP/FTPS
|
|
4
|
+
* implementation is `Connection`; tests use an in-memory fake.
|
|
5
|
+
*
|
|
6
|
+
* Remote paths are used as given: a relative path is relative to the login directory, for the
|
|
7
|
+
* whole life of the connection.
|
|
8
|
+
*/
|
|
9
|
+
export interface RemoteConnection {
|
|
10
|
+
/** Reads a remote file as text. Rejects when it can't be read. */
|
|
11
|
+
getRemoteFile(remotePath: string): Promise<string>;
|
|
12
|
+
/** Whether a remote file exists. */
|
|
13
|
+
remoteFileExists(remotePath: string): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a remote directory exists. Only reads: it never creates anything. `init` uses it to
|
|
16
|
+
* check `target.path`, which it must not create.
|
|
17
|
+
*/
|
|
18
|
+
remoteDirectoryFound(remoteDir: string): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Ensures a remote directory exists, creating it and any missing parents (mkdir -p).
|
|
21
|
+
* Resolves whether it exists afterwards. `Sync` uses it for subfolders under `target.path`;
|
|
22
|
+
* to check a directory without creating it, use `remoteDirectoryFound`.
|
|
23
|
+
*/
|
|
24
|
+
remoteDirectoryExists(remoteDir: string): Promise<boolean>;
|
|
25
|
+
/** Uploads a buffer to a remote file. */
|
|
26
|
+
putBuffer(buffer: Buffer, remotePath: string): Promise<boolean>;
|
|
27
|
+
/** Uploads a string to a remote file. */
|
|
28
|
+
putContent(content: string, remotePath: string): Promise<boolean>;
|
|
29
|
+
/** Uploads a local file to a remote file, and resolves the remote path. */
|
|
30
|
+
putFile(localPath: string, remotePath: string): Promise<string>;
|
|
31
|
+
/** Deletes a remote file. Resolves false, rather than rejecting, when it can't. */
|
|
32
|
+
deleteRemoteFile(remotePath: string): Promise<boolean>;
|
|
33
|
+
/** Closes the connection. */
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Opens a connection to a target. */
|
|
37
|
+
export type ConnectionFactory = (target: ITargetConfig) => RemoteConnection;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteConnection.js","sourceRoot":"./","sources":["lib/RemoteConnection.ts"],"names":[],"mappings":"","sourcesContent":["import { ITargetConfig } from './Config.js'\n\n/**\n * The remote operations `Sync`, `TargetServer` and `getRemoteFilelist` use. The FTP/FTPS\n * implementation is `Connection`; tests use an in-memory fake.\n *\n * Remote paths are used as given: a relative path is relative to the login directory, for the\n * whole life of the connection.\n */\nexport interface RemoteConnection {\n /** Reads a remote file as text. Rejects when it can't be read. */\n getRemoteFile(remotePath: string): Promise<string>\n\n /** Whether a remote file exists. */\n remoteFileExists(remotePath: string): Promise<boolean>\n\n /**\n * Whether a remote directory exists. Only reads: it never creates anything. `init` uses it to\n * check `target.path`, which it must not create.\n */\n remoteDirectoryFound(remoteDir: string): Promise<boolean>\n\n /**\n * Ensures a remote directory exists, creating it and any missing parents (mkdir -p).\n * Resolves whether it exists afterwards. `Sync` uses it for subfolders under `target.path`;\n * to check a directory without creating it, use `remoteDirectoryFound`.\n */\n remoteDirectoryExists(remoteDir: string): Promise<boolean>\n\n /** Uploads a buffer to a remote file. */\n putBuffer(buffer: Buffer, remotePath: string): Promise<boolean>\n\n /** Uploads a string to a remote file. */\n putContent(content: string, remotePath: string): Promise<boolean>\n\n /** Uploads a local file to a remote file, and resolves the remote path. */\n putFile(localPath: string, remotePath: string): Promise<string>\n\n /** Deletes a remote file. Resolves false, rather than rejecting, when it can't. */\n deleteRemoteFile(remotePath: string): Promise<boolean>\n\n /** Closes the connection. */\n close(): Promise<void>\n}\n\n/** Opens a connection to a target. */\nexport type ConnectionFactory = (target: ITargetConfig) => RemoteConnection\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { IConfig, ITargetConfig } from './Config.js';
|
|
2
|
+
import { RemoteConnection } from './RemoteConnection.js';
|
|
3
|
+
export interface IChangeset {
|
|
4
|
+
added: string[];
|
|
5
|
+
changed: string[];
|
|
6
|
+
removed: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare class Sync {
|
|
9
|
+
private _conn;
|
|
10
|
+
private _config;
|
|
11
|
+
private _target;
|
|
12
|
+
constructor(conn: RemoteConnection, config: IConfig, target: string | ITargetConfig);
|
|
13
|
+
run(isDryRun?: boolean, forceConfirmation?: boolean): Promise<IChangeset | null>;
|
|
14
|
+
private _confirmUpload;
|
|
15
|
+
private _getFilelists;
|
|
16
|
+
private _buildChangeset;
|
|
17
|
+
static create(conn: RemoteConnection, config: IConfig, target: string | ITargetConfig): Sync;
|
|
18
|
+
}
|