syncodemayo 1.0.9 → 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 +91 -25
- 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 +1 -1
- package/dist/lib/Cli.js +104 -0
- package/dist/lib/Cli.js.map +1 -0
- package/{lib → dist/lib}/Client.d.ts +4 -4
- 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/{lib → dist/lib}/Log.d.ts +1 -1
- package/{lib → dist/lib}/Log.js +9 -16
- 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 -102
- package/index.d.ts +0 -1
- package/index.js +0 -7
- package/lib/Cli.js +0 -130
- package/lib/Client.js +0 -85
- package/lib/Config.d.ts +0 -32
- package/lib/Config.js +0 -114
- 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 -112
- 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 -140
- package/lib/TargetServer.d.ts +0 -22
- package/lib/TargetServer.js +0 -74
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as Log from './Log';
|
|
2
|
-
import { IConfig, ITargetConfig } from './Config';
|
|
3
|
-
import { TargetServer } from './TargetServer';
|
|
1
|
+
import * as Log from './Log.js';
|
|
2
|
+
import { IConfig, ITargetConfig } from './Config.js';
|
|
3
|
+
import { TargetServer } from './TargetServer.js';
|
|
4
4
|
export declare class Client {
|
|
5
5
|
private _path;
|
|
6
6
|
hasConfig: boolean | null;
|
|
@@ -9,7 +9,7 @@ export declare class Client {
|
|
|
9
9
|
parseError: Error;
|
|
10
10
|
whenReady: Promise<Client>;
|
|
11
11
|
DEFAULT_CONFIG: string;
|
|
12
|
-
|
|
12
|
+
get hasError(): boolean;
|
|
13
13
|
constructor(preferredPath: string | undefined);
|
|
14
14
|
setVerbosity(level: Log.LogLevel): void;
|
|
15
15
|
createConfig(preferredPath: string | undefined): Promise<boolean>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as Log from './Log.js';
|
|
2
|
+
import { resolve as resolvePath } from 'path';
|
|
3
|
+
import { ConfigNotFoundError, loadConfigFile, resolveTarget } from './Config.js';
|
|
4
|
+
import { TargetServer } from './TargetServer.js';
|
|
5
|
+
import { DefaultConfig } from './ConfigSchema.js';
|
|
6
|
+
import { writeLocalFile, localFileExists } from './Filelist.js';
|
|
7
|
+
export class Client {
|
|
8
|
+
get hasError() {
|
|
9
|
+
return !!this.loadError || !!this.parseError;
|
|
10
|
+
}
|
|
11
|
+
constructor(preferredPath) {
|
|
12
|
+
this.hasConfig = null;
|
|
13
|
+
this.DEFAULT_CONFIG = "syncodemayo.json";
|
|
14
|
+
this._path = preferredPath;
|
|
15
|
+
this.whenReady = new Promise((resolve) => {
|
|
16
|
+
loadConfigFile(preferredPath)
|
|
17
|
+
.then((config) => {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this._path = this.config._path;
|
|
20
|
+
this.hasConfig = true;
|
|
21
|
+
resolve(this);
|
|
22
|
+
})
|
|
23
|
+
.catch((err) => {
|
|
24
|
+
// No config file is not an error: hasConfig is false and neither error is set.
|
|
25
|
+
// Invalid JSON is a parse error; anything else (a schema error, a read failure) is a
|
|
26
|
+
// load error.
|
|
27
|
+
if (err instanceof SyntaxError)
|
|
28
|
+
this.parseError = err;
|
|
29
|
+
else if (!(err instanceof ConfigNotFoundError))
|
|
30
|
+
this.loadError = err;
|
|
31
|
+
this.hasConfig = false;
|
|
32
|
+
resolve(this);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
setVerbosity(level) {
|
|
37
|
+
Log.setLogLevel(level);
|
|
38
|
+
}
|
|
39
|
+
async createConfig(preferredPath) {
|
|
40
|
+
const path = resolvePath(preferredPath || this._path || this.DEFAULT_CONFIG);
|
|
41
|
+
const alreadyExists = await localFileExists(path);
|
|
42
|
+
if (alreadyExists) {
|
|
43
|
+
throw new Error("Config already exists at " + path);
|
|
44
|
+
}
|
|
45
|
+
writeLocalFile(path, JSON.stringify(DefaultConfig, null, 2));
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
async getTargetNames() {
|
|
49
|
+
if (this.hasConfig === null)
|
|
50
|
+
throw new Error("Config not loaded yet.");
|
|
51
|
+
if (this.hasConfig === false) {
|
|
52
|
+
if (this.hasError) {
|
|
53
|
+
const error = this.loadError || this.parseError;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw new Error("This directory has no SyncoDeMayo config.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Object.keys(this.config.targets).filter(name => this.config.targets[name].enabled);
|
|
61
|
+
}
|
|
62
|
+
getTargetConfig(name) {
|
|
63
|
+
return resolveTarget(this.config, name);
|
|
64
|
+
}
|
|
65
|
+
getTargetServer(name) {
|
|
66
|
+
return TargetServer.create(this.config, name);
|
|
67
|
+
}
|
|
68
|
+
static create(configPath) {
|
|
69
|
+
const client = new Client(configPath);
|
|
70
|
+
return client.whenReady;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=Client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Client.js","sourceRoot":"./","sources":["lib/Client.ts"],"names":[],"mappings":"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","sourcesContent":["import * as Log from './Log.js'\nimport { resolve as resolvePath } from 'path'\nimport { ConfigNotFoundError, IConfig, loadConfigFile, ITargetConfig, resolveTarget } from './Config.js'\nimport { TargetServer } from './TargetServer.js'\nimport { DefaultConfig } from './ConfigSchema.js'\nimport { writeLocalFile, localFileExists } from './Filelist.js'\n\nexport class Client {\n private _path: string | undefined\n hasConfig: boolean | null = null\n config!: IConfig\n loadError!: Error\n parseError!: Error\n whenReady: Promise<Client>\n\n DEFAULT_CONFIG = \"syncodemayo.json\"\n\n get hasError() {\n return !!this.loadError || !!this.parseError\n }\n\n constructor(preferredPath: string | undefined) {\n this._path = preferredPath\n this.whenReady = new Promise((resolve) => {\n loadConfigFile(preferredPath)\n .then((config) => {\n this.config = config\n this._path = this.config._path\n this.hasConfig = true\n resolve(this)\n })\n .catch((err: Error) => {\n // No config file is not an error: hasConfig is false and neither error is set.\n // Invalid JSON is a parse error; anything else (a schema error, a read failure) is a\n // load error.\n if (err instanceof SyntaxError) this.parseError = err\n else if (!(err instanceof ConfigNotFoundError)) this.loadError = err\n this.hasConfig = false\n resolve(this)\n })\n })\n }\n\n setVerbosity(level: Log.LogLevel) {\n Log.setLogLevel(level)\n }\n\n async createConfig(preferredPath: string | undefined): Promise<boolean> {\n const path = resolvePath(preferredPath || this._path || this.DEFAULT_CONFIG)\n const alreadyExists = await localFileExists(path)\n if (alreadyExists) {\n throw new Error(\"Config already exists at \" + path)\n }\n writeLocalFile(path, JSON.stringify(DefaultConfig, null, 2))\n return true\n }\n\n async getTargetNames(): Promise<string[]> {\n if (this.hasConfig === null) throw new Error(\"Config not loaded yet.\")\n if (this.hasConfig === false) {\n if (this.hasError) {\n const error = this.loadError || this.parseError\n throw error\n }\n else {\n throw new Error(\"This directory has no SyncoDeMayo config.\")\n }\n }\n return Object.keys(this.config.targets).filter(name =>\n this.config.targets[name].enabled)\n }\n\n getTargetConfig(name: string | undefined): ITargetConfig {\n return resolveTarget(this.config, name)\n }\n\n getTargetServer(name: string | ITargetConfig | undefined): TargetServer {\n return TargetServer.create(this.config, name)\n }\n\n static create(configPath: string | undefined): Promise<Client> {\n const client = new Client(configPath)\n return client.whenReady\n }\n}"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface IConfig {
|
|
2
|
+
local: ILocalConfig;
|
|
3
|
+
targets: {
|
|
4
|
+
[name: string]: ITargetConfig;
|
|
5
|
+
};
|
|
6
|
+
_path?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ILocalConfig {
|
|
9
|
+
path: string;
|
|
10
|
+
files: string;
|
|
11
|
+
exclude: string[];
|
|
12
|
+
defaultTarget: string;
|
|
13
|
+
deleteRemoteFiles: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ITargetConfig {
|
|
16
|
+
host: string;
|
|
17
|
+
user: string;
|
|
18
|
+
pass?: string;
|
|
19
|
+
path: string;
|
|
20
|
+
port: number;
|
|
21
|
+
cache: string;
|
|
22
|
+
prompt?: boolean | string;
|
|
23
|
+
_name: string;
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
type?: string;
|
|
26
|
+
/** `false` (default): plain FTP. `true`: explicit FTPS. `"implicit"`: implicit FTPS. */
|
|
27
|
+
secure?: boolean | 'implicit';
|
|
28
|
+
/** Default `true`. `false` accepts self-signed or mismatched certificates, with a warning. */
|
|
29
|
+
rejectUnauthorized?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Thrown by `loadConfigFile` when no config file exists: the preferred path is missing, or (with
|
|
33
|
+
* no preferred path) none of the default file names exists in the working directory.
|
|
34
|
+
*/
|
|
35
|
+
export declare class ConfigNotFoundError extends Error {
|
|
36
|
+
constructor(message?: string);
|
|
37
|
+
}
|
|
38
|
+
/** The config file names `loadConfigFile` tries, in order, when it isn't given a path. */
|
|
39
|
+
export declare const CONFIG_FILE_NAMES: string[];
|
|
40
|
+
/**
|
|
41
|
+
* Loads, validates, and fills in the defaults of a config file. With no preferred path it tries
|
|
42
|
+
* each of `CONFIG_FILE_NAMES` in the working directory and loads the first one that exists.
|
|
43
|
+
* Rejects with `ConfigNotFoundError` when there's no config file, with a `SyntaxError` when the
|
|
44
|
+
* file isn't valid JSON, and with a "Config Schema Error: ..." `Error` when it fails validation.
|
|
45
|
+
*/
|
|
46
|
+
export declare function loadConfigFile(preferredPath?: string): Promise<IConfig>;
|
|
47
|
+
export declare function resolvePassword(targetConfig: ITargetConfig): string;
|
|
48
|
+
export declare function resolveTarget(config: IConfig, name: string | ITargetConfig | undefined): ITargetConfig;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { resolve as resolvePath } from 'path';
|
|
2
|
+
import { validateConfig } from './ConfigSchema.js';
|
|
3
|
+
import { localFileExists, readLocalFile } from './Filelist.js';
|
|
4
|
+
import stripJsonComments from '../vendor/strip-json-comments.js';
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by `loadConfigFile` when no config file exists: the preferred path is missing, or (with
|
|
7
|
+
* no preferred path) none of the default file names exists in the working directory.
|
|
8
|
+
*/
|
|
9
|
+
export class ConfigNotFoundError extends Error {
|
|
10
|
+
constructor(message = "Local config not found.") {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'ConfigNotFoundError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** The config file names `loadConfigFile` tries, in order, when it isn't given a path. */
|
|
16
|
+
export const CONFIG_FILE_NAMES = [
|
|
17
|
+
'syncodemayo.json',
|
|
18
|
+
'.syncodemayo.json',
|
|
19
|
+
'sync-config.json',
|
|
20
|
+
];
|
|
21
|
+
// Reads and parses a config file. Resolves undefined (which JSON never parses to) when the file
|
|
22
|
+
// doesn't exist.
|
|
23
|
+
async function evalConfigFromFile(pathname) {
|
|
24
|
+
if (typeof pathname !== 'string')
|
|
25
|
+
throw new Error(`Path must be a string. Got a ${typeof pathname}`);
|
|
26
|
+
const fullpath = resolvePath("./" + pathname);
|
|
27
|
+
if (!(await localFileExists(fullpath)))
|
|
28
|
+
return undefined;
|
|
29
|
+
const source = await readLocalFile(fullpath, false);
|
|
30
|
+
const data = JSON.parse(stripJsonComments(source));
|
|
31
|
+
if (typeof data === 'object' && data !== null)
|
|
32
|
+
data._path = fullpath;
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Loads, validates, and fills in the defaults of a config file. With no preferred path it tries
|
|
37
|
+
* each of `CONFIG_FILE_NAMES` in the working directory and loads the first one that exists.
|
|
38
|
+
* Rejects with `ConfigNotFoundError` when there's no config file, with a `SyntaxError` when the
|
|
39
|
+
* file isn't valid JSON, and with a "Config Schema Error: ..." `Error` when it fails validation.
|
|
40
|
+
*/
|
|
41
|
+
export async function loadConfigFile(preferredPath) {
|
|
42
|
+
const potentialConfigs = (!!preferredPath) ? [preferredPath] : CONFIG_FILE_NAMES;
|
|
43
|
+
for (const path of potentialConfigs) {
|
|
44
|
+
const config = await evalConfigFromFile(path);
|
|
45
|
+
if (config !== undefined)
|
|
46
|
+
return massageConfigData(await validateConfig(config));
|
|
47
|
+
}
|
|
48
|
+
throw new ConfigNotFoundError();
|
|
49
|
+
}
|
|
50
|
+
async function massageConfigData(config) {
|
|
51
|
+
config.local.files = config.local.files || '**/**';
|
|
52
|
+
config.local.exclude = config.local.exclude || [];
|
|
53
|
+
config.local.defaultTarget = config.local.defaultTarget || "staging";
|
|
54
|
+
config.local.deleteRemoteFiles = config.local.deleteRemoteFiles === true;
|
|
55
|
+
Object.keys(config.targets).forEach(name => {
|
|
56
|
+
const target = config.targets[name];
|
|
57
|
+
target._name = name;
|
|
58
|
+
target.port = target.port || (target.secure === 'implicit' ? 990 : 21);
|
|
59
|
+
target.cache = target.cache || ".synco-filelist";
|
|
60
|
+
target.enabled = target.enabled !== false;
|
|
61
|
+
target.type = target.type || 'ftp'; // Will support more eventually
|
|
62
|
+
if (typeof target.prompt === 'undefined')
|
|
63
|
+
target.prompt = true;
|
|
64
|
+
target.secure = target.secure ?? false;
|
|
65
|
+
target.rejectUnauthorized = target.rejectUnauthorized ?? true;
|
|
66
|
+
});
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
69
|
+
export function resolvePassword(targetConfig) {
|
|
70
|
+
const password = targetConfig.pass || process.env[`${targetConfig._name.toUpperCase()}_PWD`];
|
|
71
|
+
if (typeof password === 'undefined') {
|
|
72
|
+
throw new Error("Cannot find password for target: " + targetConfig._name);
|
|
73
|
+
}
|
|
74
|
+
return password;
|
|
75
|
+
}
|
|
76
|
+
export function resolveTarget(config, name) {
|
|
77
|
+
if (typeof name === 'undefined')
|
|
78
|
+
name = config.local.defaultTarget;
|
|
79
|
+
if (typeof name === 'object')
|
|
80
|
+
return name;
|
|
81
|
+
const lookingFor = name;
|
|
82
|
+
const targetName = Object.keys(config.targets).find(namedTarget => (lookingFor.toUpperCase() === config.targets[namedTarget]._name.toUpperCase()));
|
|
83
|
+
if (!targetName) {
|
|
84
|
+
throw new Error("Target '" + name + "' not found.");
|
|
85
|
+
}
|
|
86
|
+
const target = config.targets[targetName];
|
|
87
|
+
if (!target.enabled) {
|
|
88
|
+
throw new Error("Target '" + name + "' not enabled.");
|
|
89
|
+
}
|
|
90
|
+
return target;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=Config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Config.js","sourceRoot":"./","sources":["lib/Config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,MAAM,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,iBAAiB,MAAM,kCAAkC,CAAA;AAkChE;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAO,GAAG,yBAAyB;QAC7C,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;IACnC,CAAC;CACF;AAED,0FAA0F;AAC1F,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,kBAAkB;IAClB,mBAAmB;IACnB,kBAAkB;CACnB,CAAA;AAED,gGAAgG;AAChG,iBAAiB;AACjB,KAAK,UAAU,kBAAkB,CAAC,QAAgB;IAChD,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAC9B,MAAM,IAAI,KAAK,CAAC,gCAAgC,OAAO,QAAQ,EAAE,CAAC,CAAA;IAEpE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAA;IAC7C,IAAI,CAAC,CAAC,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,SAAS,CAAA;IAElB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAW,CAAA;IAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAY,CAAA;IAE7D,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAC3C,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAA;IAEvB,OAAO,IAAI,CAAA;AACb,CAAC;AAGD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,aAAsB;IACzD,MAAM,gBAAgB,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAA;IAEhF,KAAK,MAAM,IAAI,IAAI,gBAAgB,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAA;QAC7C,IAAI,MAAM,KAAK,SAAS;YACtB,OAAO,iBAAiB,CAAC,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC,CAAA;IAC1D,CAAC;IAED,MAAM,IAAI,mBAAmB,EAAE,CAAA;AACjC,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,MAAe;IAC9C,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,OAAO,CAAA;IAClD,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAA;IACjD,MAAM,CAAC,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,aAAa,IAAI,SAAS,CAAA;IACpE,MAAM,CAAC,KAAK,CAAC,iBAAiB,GAAG,MAAM,CAAC,KAAK,CAAC,iBAAiB,KAAK,IAAI,CAAA;IAExE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QACzC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACnC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAA;QACnB,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACtE,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,iBAAiB,CAAA;QAChD,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,KAAK,KAAK,CAAA;QACzC,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,KAAK,CAAA,CAAC,+BAA+B;QAClE,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,WAAW;YACtC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAA;QACtB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,KAAK,CAAA;QACtC,MAAM,CAAC,kBAAkB,GAAG,MAAM,CAAC,kBAAkB,IAAI,IAAI,CAAA;IAC/D,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,YAA2B;IACzD,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,WAAW,EAAE,MAAM,CAAW,CAAA;IACtG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,mCAAmC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;IAC3E,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAe,EAAE,IAAwC;IACrF,IAAI,OAAO,IAAI,KAAK,WAAW;QAAE,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,aAAa,CAAA;IAClE,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAEzC,MAAM,UAAU,GAAG,IAAc,CAAA;IACjC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CACjE,UAAU,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAC7E,CAAC,CAAA;IAEF,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,UAAU,GAAG,IAAI,GAAG,cAAc,CAAC,CAAA;IACrD,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAEzC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,UAAU,GAAG,IAAI,GAAG,gBAAgB,CAAC,CAAA;IACvD,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import { resolve as resolvePath } from 'path'\nimport { validateConfig } from './ConfigSchema.js'\nimport { localFileExists, readLocalFile } from './Filelist.js'\nimport stripJsonComments from '../vendor/strip-json-comments.js'\n\n\nexport interface IConfig {\n local: ILocalConfig\n targets: { [name: string]: ITargetConfig }\n _path?: string\n}\n\nexport interface ILocalConfig {\n path: string\n files: string\n exclude: string[]\n defaultTarget: string\n deleteRemoteFiles: boolean\n}\n\nexport interface ITargetConfig {\n host: string\n user: string\n pass?: string\n path: string\n port: number\n cache: string\n prompt?: boolean | string\n _name: string\n enabled?: boolean\n type?: string\n /** `false` (default): plain FTP. `true`: explicit FTPS. `\"implicit\"`: implicit FTPS. */\n secure?: boolean | 'implicit'\n /** Default `true`. `false` accepts self-signed or mismatched certificates, with a warning. */\n rejectUnauthorized?: boolean\n}\n\n/**\n * Thrown by `loadConfigFile` when no config file exists: the preferred path is missing, or (with\n * no preferred path) none of the default file names exists in the working directory.\n */\nexport class ConfigNotFoundError extends Error {\n constructor(message = \"Local config not found.\") {\n super(message)\n this.name = 'ConfigNotFoundError'\n }\n}\n\n/** The config file names `loadConfigFile` tries, in order, when it isn't given a path. */\nexport const CONFIG_FILE_NAMES = [\n 'syncodemayo.json',\n '.syncodemayo.json',\n 'sync-config.json',\n]\n\n// Reads and parses a config file. Resolves undefined (which JSON never parses to) when the file\n// doesn't exist.\nasync function evalConfigFromFile(pathname: string): Promise<IConfig | undefined> {\n if (typeof pathname !== 'string')\n throw new Error(`Path must be a string. Got a ${typeof pathname}`)\n\n const fullpath = resolvePath(\"./\" + pathname)\n if (!(await localFileExists(fullpath)))\n return undefined\n\n const source = await readLocalFile(fullpath, false) as string\n const data = JSON.parse(stripJsonComments(source)) as IConfig\n\n if (typeof data === 'object' && data !== null)\n data._path = fullpath\n\n return data\n}\n\n\n/**\n * Loads, validates, and fills in the defaults of a config file. With no preferred path it tries\n * each of `CONFIG_FILE_NAMES` in the working directory and loads the first one that exists.\n * Rejects with `ConfigNotFoundError` when there's no config file, with a `SyntaxError` when the\n * file isn't valid JSON, and with a \"Config Schema Error: ...\" `Error` when it fails validation.\n */\nexport async function loadConfigFile(preferredPath?: string): Promise<IConfig> {\n const potentialConfigs = (!!preferredPath) ? [preferredPath] : CONFIG_FILE_NAMES\n\n for (const path of potentialConfigs) {\n const config = await evalConfigFromFile(path)\n if (config !== undefined)\n return massageConfigData(await validateConfig(config))\n }\n\n throw new ConfigNotFoundError()\n}\n\nasync function massageConfigData(config: IConfig): Promise<IConfig> {\n config.local.files = config.local.files || '**/**'\n config.local.exclude = config.local.exclude || []\n config.local.defaultTarget = config.local.defaultTarget || \"staging\"\n config.local.deleteRemoteFiles = config.local.deleteRemoteFiles === true\n\n Object.keys(config.targets).forEach(name => {\n const target = config.targets[name]\n target._name = name\n target.port = target.port || (target.secure === 'implicit' ? 990 : 21)\n target.cache = target.cache || \".synco-filelist\"\n target.enabled = target.enabled !== false\n target.type = target.type || 'ftp' // Will support more eventually\n if (typeof target.prompt === 'undefined')\n target.prompt = true\n target.secure = target.secure ?? false\n target.rejectUnauthorized = target.rejectUnauthorized ?? true\n })\n\n return config\n}\n\nexport function resolvePassword(targetConfig: ITargetConfig): string {\n const password = targetConfig.pass || process.env[`${targetConfig._name.toUpperCase()}_PWD`] as string\n if (typeof password === 'undefined') {\n throw new Error(\"Cannot find password for target: \" + targetConfig._name)\n }\n return password\n}\n\nexport function resolveTarget(config: IConfig, name: string | ITargetConfig | undefined): ITargetConfig {\n if (typeof name === 'undefined') name = config.local.defaultTarget\n if (typeof name === 'object') return name\n\n const lookingFor = name as string\n const targetName = Object.keys(config.targets).find(namedTarget => (\n lookingFor.toUpperCase() === config.targets[namedTarget]._name.toUpperCase()\n ))\n\n if (!targetName) {\n throw new Error(\"Target '\" + name + \"' not found.\")\n }\n\n const target = config.targets[targetName]\n\n if (!target.enabled) {\n throw new Error(\"Target '\" + name + \"' not enabled.\")\n }\n\n return target\n}"]}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2
|
+
const string = (value) => {
|
|
3
|
+
if (typeof value !== 'string')
|
|
4
|
+
return 'must be a string';
|
|
5
|
+
if (value === '')
|
|
6
|
+
return 'must not be empty';
|
|
7
|
+
return undefined;
|
|
8
|
+
};
|
|
9
|
+
// v1 joi.bool(): a boolean, or the string "true"/"false" in any case.
|
|
10
|
+
const boolean = (value) => {
|
|
11
|
+
if (typeof value === 'boolean')
|
|
12
|
+
return undefined;
|
|
13
|
+
if (typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase()))
|
|
14
|
+
return undefined;
|
|
15
|
+
return 'must be a boolean';
|
|
16
|
+
};
|
|
17
|
+
// v1 joi.number(): a finite number, or a string that parseFloat reads and isFinite accepts.
|
|
18
|
+
const number = (value) => {
|
|
19
|
+
if (typeof value === 'number')
|
|
20
|
+
return Number.isFinite(value) ? undefined : 'must be a finite number';
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
const parsed = parseFloat(value);
|
|
23
|
+
if (!Number.isNaN(parsed) && isFinite(value))
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return 'must be a number';
|
|
27
|
+
};
|
|
28
|
+
const stringArray = (value) => {
|
|
29
|
+
if (!Array.isArray(value))
|
|
30
|
+
return 'must be an array';
|
|
31
|
+
for (let i = 0; i < value.length; i++) {
|
|
32
|
+
const problem = string(value[i]);
|
|
33
|
+
if (problem)
|
|
34
|
+
return `item ${i} ${problem}`;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
const booleanOrString = (value) => (boolean(value) && string(value)) ? 'must be a boolean or a non-empty string' : undefined;
|
|
39
|
+
// Target `secure`: exactly true, false or "implicit".
|
|
40
|
+
const secure = (value) => (value === true || value === false || value === 'implicit') ? undefined : 'must be true, false or "implicit"';
|
|
41
|
+
// A JSON boolean only (no "true"/"false" strings).
|
|
42
|
+
const strictBoolean = (value) => typeof value === 'boolean' ? undefined : 'must be a boolean';
|
|
43
|
+
const localShape = {
|
|
44
|
+
path: { check: string, required: true },
|
|
45
|
+
files: { check: string },
|
|
46
|
+
exclude: { check: stringArray },
|
|
47
|
+
defaultTarget: { check: string },
|
|
48
|
+
deleteRemoteFiles: { check: boolean },
|
|
49
|
+
};
|
|
50
|
+
const targetShape = {
|
|
51
|
+
host: { check: string, required: true },
|
|
52
|
+
user: { check: string, required: true },
|
|
53
|
+
path: { check: string, required: true },
|
|
54
|
+
pass: { check: string },
|
|
55
|
+
port: { check: number },
|
|
56
|
+
cache: { check: string },
|
|
57
|
+
prompt: { check: booleanOrString },
|
|
58
|
+
enabled: { check: boolean },
|
|
59
|
+
secure: { check: secure },
|
|
60
|
+
rejectUnauthorized: { check: strictBoolean },
|
|
61
|
+
// For future use...
|
|
62
|
+
type: { check: string },
|
|
63
|
+
};
|
|
64
|
+
// Returns the first problem with `value` against `shape`, as `<path> <problem>`, or undefined.
|
|
65
|
+
function checkShape(value, shape, path) {
|
|
66
|
+
if (!isPlainObject(value))
|
|
67
|
+
return `${path} must be an object`;
|
|
68
|
+
for (const key of Object.keys(shape)) {
|
|
69
|
+
const rule = shape[key];
|
|
70
|
+
const fieldValue = value[key];
|
|
71
|
+
if (fieldValue === undefined) {
|
|
72
|
+
if (rule.required)
|
|
73
|
+
return `${path}.${key} is required`;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const problem = rule.check(fieldValue);
|
|
77
|
+
if (problem)
|
|
78
|
+
return `${path}.${key} ${problem}`;
|
|
79
|
+
}
|
|
80
|
+
for (const key of Object.keys(value)) {
|
|
81
|
+
if (!Object.hasOwn(shape, key) && value[key] !== undefined)
|
|
82
|
+
return `${path}.${key} is not allowed`;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function findProblem(config) {
|
|
87
|
+
if (!isPlainObject(config))
|
|
88
|
+
return 'config must be an object';
|
|
89
|
+
if (config.local === undefined)
|
|
90
|
+
return 'local is required';
|
|
91
|
+
const localProblem = checkShape(config.local, localShape, 'local');
|
|
92
|
+
if (localProblem)
|
|
93
|
+
return localProblem;
|
|
94
|
+
if (config.targets === undefined)
|
|
95
|
+
return 'targets is required';
|
|
96
|
+
if (!isPlainObject(config.targets))
|
|
97
|
+
return 'targets must be an object';
|
|
98
|
+
for (const name of Object.keys(config.targets)) {
|
|
99
|
+
const targetProblem = checkShape(config.targets[name], targetShape, `targets.${name}`);
|
|
100
|
+
if (targetProblem)
|
|
101
|
+
return targetProblem;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
export async function validateConfig(config) {
|
|
106
|
+
const problem = findProblem(config);
|
|
107
|
+
if (problem)
|
|
108
|
+
throw new Error("Config Schema Error: " + problem);
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
111
|
+
export const DefaultConfig = {
|
|
112
|
+
local: {
|
|
113
|
+
path: "public",
|
|
114
|
+
files: "**/**",
|
|
115
|
+
exclude: ["**/*.map", "**/.DS_Store", "**/.git*", "**/.env*"],
|
|
116
|
+
defaultTarget: "staging"
|
|
117
|
+
},
|
|
118
|
+
targets: {
|
|
119
|
+
staging: {
|
|
120
|
+
path: "Example/www/stage",
|
|
121
|
+
host: "www.example.com",
|
|
122
|
+
user: "USERNAME"
|
|
123
|
+
},
|
|
124
|
+
production: {
|
|
125
|
+
path: "Example/www",
|
|
126
|
+
host: "www.example.com",
|
|
127
|
+
user: "USERNAME",
|
|
128
|
+
prompt: "This will upload to PRODUCTION! Are you sure?"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=ConfigSchema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfigSchema.js","sourceRoot":"./","sources":["lib/ConfigSchema.ts"],"names":[],"mappings":"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","sourcesContent":["import { IConfig } from './Config.js'\n\n/*\n * A hand-written validator for the config file. It accepts and rejects what the v1 joi schema did,\n * including where v1 joi and config.schema.json disagree (rework-1.md):\n * - boolean fields also accept the strings \"true\" and \"false\" (in any case);\n * - `port` also accepts a numeric string (parsed the way v1 joi did);\n * - strings must not be empty;\n * - unknown keys inside `local` and inside a target reject.\n * The FTPS fields added in v2 (`secure`, `rejectUnauthorized`) take JSON values only, as in\n * config.schema.json: no boolean strings.\n * Unknown top-level keys are allowed. The config is returned as given: nothing is converted.\n */\n\ntype FieldCheck = (value: unknown) => string | undefined\n\ninterface FieldRule {\n check: FieldCheck\n required?: boolean\n}\n\ntype Shape = { [key: string]: FieldRule }\n\nconst isPlainObject = (value: unknown): value is { [key: string]: unknown } =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst string: FieldCheck = (value) => {\n if (typeof value !== 'string') return 'must be a string'\n if (value === '') return 'must not be empty'\n return undefined\n}\n\n// v1 joi.bool(): a boolean, or the string \"true\"/\"false\" in any case.\nconst boolean: FieldCheck = (value) => {\n if (typeof value === 'boolean') return undefined\n if (typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase())) return undefined\n return 'must be a boolean'\n}\n\n// v1 joi.number(): a finite number, or a string that parseFloat reads and isFinite accepts.\nconst number: FieldCheck = (value) => {\n if (typeof value === 'number') return Number.isFinite(value) ? undefined : 'must be a finite number'\n if (typeof value === 'string') {\n const parsed = parseFloat(value)\n if (!Number.isNaN(parsed) && isFinite(value as unknown as number)) return undefined\n }\n return 'must be a number'\n}\n\nconst stringArray: FieldCheck = (value) => {\n if (!Array.isArray(value)) return 'must be an array'\n for (let i = 0; i < value.length; i++) {\n const problem = string(value[i])\n if (problem) return `item ${i} ${problem}`\n }\n return undefined\n}\n\nconst booleanOrString: FieldCheck = (value) =>\n (boolean(value) && string(value)) ? 'must be a boolean or a non-empty string' : undefined\n\n// Target `secure`: exactly true, false or \"implicit\".\nconst secure: FieldCheck = (value) =>\n (value === true || value === false || value === 'implicit') ? undefined : 'must be true, false or \"implicit\"'\n\n// A JSON boolean only (no \"true\"/\"false\" strings).\nconst strictBoolean: FieldCheck = (value) =>\n typeof value === 'boolean' ? undefined : 'must be a boolean'\n\nconst localShape: Shape = {\n path: { check: string, required: true },\n files: { check: string },\n exclude: { check: stringArray },\n defaultTarget: { check: string },\n deleteRemoteFiles: { check: boolean },\n}\n\nconst targetShape: Shape = {\n host: { check: string, required: true },\n user: { check: string, required: true },\n path: { check: string, required: true },\n pass: { check: string },\n port: { check: number },\n cache: { check: string },\n prompt: { check: booleanOrString },\n enabled: { check: boolean },\n secure: { check: secure },\n rejectUnauthorized: { check: strictBoolean },\n // For future use...\n type: { check: string },\n}\n\n// Returns the first problem with `value` against `shape`, as `<path> <problem>`, or undefined.\nfunction checkShape(value: unknown, shape: Shape, path: string): string | undefined {\n if (!isPlainObject(value)) return `${path} must be an object`\n\n for (const key of Object.keys(shape)) {\n const rule = shape[key]\n const fieldValue = value[key]\n if (fieldValue === undefined) {\n if (rule.required) return `${path}.${key} is required`\n continue\n }\n const problem = rule.check(fieldValue)\n if (problem) return `${path}.${key} ${problem}`\n }\n\n for (const key of Object.keys(value)) {\n if (!Object.hasOwn(shape, key) && value[key] !== undefined) return `${path}.${key} is not allowed`\n }\n\n return undefined\n}\n\nfunction findProblem(config: unknown): string | undefined {\n if (!isPlainObject(config)) return 'config must be an object'\n\n if (config.local === undefined) return 'local is required'\n const localProblem = checkShape(config.local, localShape, 'local')\n if (localProblem) return localProblem\n\n if (config.targets === undefined) return 'targets is required'\n if (!isPlainObject(config.targets)) return 'targets must be an object'\n for (const name of Object.keys(config.targets)) {\n const targetProblem = checkShape(config.targets[name], targetShape, `targets.${name}`)\n if (targetProblem) return targetProblem\n }\n\n return undefined\n}\n\nexport async function validateConfig(config: IConfig): Promise<IConfig> {\n const problem = findProblem(config)\n if (problem) throw new Error(\"Config Schema Error: \" + problem)\n return config\n}\n\nexport const DefaultConfig: any = {\n local: {\n path: \"public\",\n files: \"**/**\",\n exclude: [\"**/*.map\", \"**/.DS_Store\", \"**/.git*\", \"**/.env*\"],\n defaultTarget: \"staging\"\n },\n targets: {\n staging: {\n path: \"Example/www/stage\",\n host: \"www.example.com\",\n user: \"USERNAME\"\n },\n production: {\n path: \"Example/www\",\n host: \"www.example.com\",\n user: \"USERNAME\",\n prompt: \"This will upload to PRODUCTION! Are you sure?\"\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ITargetConfig } from './Config.js';
|
|
2
|
+
import { RemoteConnection } from './RemoteConnection.js';
|
|
3
|
+
/** How to reach an FTP/FTPS server. */
|
|
4
|
+
export interface ConnectionOptions {
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
user: string;
|
|
8
|
+
pass: string;
|
|
9
|
+
/** `false`: plain FTP. `true`: explicit FTPS (AUTH TLS). `"implicit"`: implicit FTPS. */
|
|
10
|
+
secure?: boolean | 'implicit';
|
|
11
|
+
/** `false` accepts self-signed or mismatched TLS certificates (with a warning). Default `true`. */
|
|
12
|
+
rejectUnauthorized?: boolean;
|
|
13
|
+
/** The target name, for messages. */
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A `RemoteConnection` over FTP or FTPS, on `basic-ftp`. It logs in on the first operation and
|
|
18
|
+
* runs operations one at a time, in the order they were called. After creating directories it
|
|
19
|
+
* goes back to the login directory, so relative paths stay relative to it.
|
|
20
|
+
*/
|
|
21
|
+
export declare class Connection implements RemoteConnection {
|
|
22
|
+
private _options;
|
|
23
|
+
private _client;
|
|
24
|
+
private _connected;
|
|
25
|
+
private _queue;
|
|
26
|
+
private _closed;
|
|
27
|
+
constructor(options: ConnectionOptions);
|
|
28
|
+
private _run;
|
|
29
|
+
private _connect;
|
|
30
|
+
private _login;
|
|
31
|
+
getRemoteFile(filename: string): Promise<string>;
|
|
32
|
+
remoteDirectoryFound(pathname: string): Promise<boolean>;
|
|
33
|
+
remoteDirectoryExists(pathname: string): Promise<boolean>;
|
|
34
|
+
remoteFileExists(remotePath: string): Promise<boolean>;
|
|
35
|
+
putBuffer(buff: Buffer, remotePath: string): Promise<boolean>;
|
|
36
|
+
putContent(content: string, remotePath: string): Promise<boolean>;
|
|
37
|
+
putFile(filename: string, remotePath: string): Promise<string>;
|
|
38
|
+
deleteRemoteFile(remotePath: string): Promise<boolean>;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
static create(target: ITargetConfig): Connection;
|
|
41
|
+
}
|
|
@@ -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
|