phio 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/README.md +44 -35
- package/package.json +32 -22
- package/src/cli.ts +9 -4
- package/src/commands/DevCommand.ts +17 -6
- package/src/commands/InfoCommand.ts +27 -4
- package/src/commands/LinkCommand.ts +1 -1
- package/src/commands/LogsCommand.ts +14 -11
- package/src/commands/SftpCommand.ts +99 -0
- package/src/lib/constants.ts +2 -0
- package/src/lib/defaultInstanceId.ts +58 -36
- package/src/lib/deployKey.ts +222 -0
- package/src/lib/fetchEventSource.ts +3 -0
- package/src/lib/getClient.ts +30 -2
- package/src/lib/phioRoot.ts +67 -0
- package/src/lib/sftpConnection.ts +33 -0
- package/src/lib/sshPublicKey.ts +128 -0
- package/vendor/ftp-deploy/HashDiff.ts +122 -0
- package/vendor/ftp-deploy/LICENSE +21 -0
- package/vendor/ftp-deploy/README.md +3 -0
- package/vendor/ftp-deploy/deploy.ts +226 -0
- package/vendor/ftp-deploy/errorHandling.ts +67 -0
- package/vendor/ftp-deploy/localFiles.ts +47 -0
- package/vendor/ftp-deploy/module.ts +28 -0
- package/vendor/ftp-deploy/sftpDeploy.ts +233 -0
- package/vendor/ftp-deploy/sftpSyncProvider.ts +188 -0
- package/vendor/ftp-deploy/sshPrivateKey.ts +66 -0
- package/vendor/ftp-deploy/syncProvider.ts +189 -0
- package/vendor/ftp-deploy/types.ts +226 -0
- package/vendor/ftp-deploy/utilities.ts +217 -0
- package/src/commands/WhoAmICommand.ts +0 -15
- package/src/global.d.ts +0 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { IDiff, IFileList, Record } from "./types";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
|
|
5
|
+
export async function fileHash(filename: string, algorithm: "md5" | "sha1" | "sha256" | "sha512"): Promise<string> {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
// Algorithm depends on availability of OpenSSL on platform
|
|
8
|
+
// Another algorithms: "sha1", "md5", "sha256", "sha512" ...
|
|
9
|
+
let shasum = crypto.createHash(algorithm);
|
|
10
|
+
try {
|
|
11
|
+
let s = fs.createReadStream(filename);
|
|
12
|
+
s.on("data", function (data: any) {
|
|
13
|
+
shasum.update(data)
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
s.on("error", function (error) {
|
|
17
|
+
reject(error);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// making digest
|
|
21
|
+
s.on("end", function () {
|
|
22
|
+
const hash = shasum.digest("hex")
|
|
23
|
+
return resolve(hash);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return reject("calc fail");
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class HashDiff implements IDiff {
|
|
33
|
+
getDiffs(localFiles: IFileList, serverFiles: IFileList) {
|
|
34
|
+
const uploadList: Record[] = [];
|
|
35
|
+
const deleteList: Record[] = [];
|
|
36
|
+
const replaceList: Record[] = [];
|
|
37
|
+
|
|
38
|
+
const sameList: Record[] = [];
|
|
39
|
+
|
|
40
|
+
let sizeUpload = 0;
|
|
41
|
+
let sizeDelete = 0;
|
|
42
|
+
let sizeReplace = 0;
|
|
43
|
+
|
|
44
|
+
// alphabetize each list based off path
|
|
45
|
+
const localFilesSorted = localFiles.data.sort((first, second) => first.name.localeCompare(second.name));
|
|
46
|
+
const serverFilesSorted = serverFiles.data.sort((first, second) => first.name.localeCompare(second.name));
|
|
47
|
+
|
|
48
|
+
let localPosition = 0;
|
|
49
|
+
let serverPosition = 0;
|
|
50
|
+
while (localPosition + serverPosition < localFilesSorted.length + serverFilesSorted.length) {
|
|
51
|
+
let localFile: Record | undefined = localFilesSorted[localPosition];
|
|
52
|
+
let serverFile: Record | undefined = serverFilesSorted[serverPosition];
|
|
53
|
+
|
|
54
|
+
let fileNameCompare = 0;
|
|
55
|
+
if (localFile === undefined) {
|
|
56
|
+
fileNameCompare = 1;
|
|
57
|
+
}
|
|
58
|
+
if (serverFile === undefined) {
|
|
59
|
+
fileNameCompare = -1;
|
|
60
|
+
}
|
|
61
|
+
if (localFile !== undefined && serverFile !== undefined) {
|
|
62
|
+
fileNameCompare = localFile.name.localeCompare(serverFile.name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (fileNameCompare < 0) {
|
|
66
|
+
uploadList.push(localFile);
|
|
67
|
+
sizeUpload += localFile.size ?? 0;
|
|
68
|
+
localPosition += 1;
|
|
69
|
+
}
|
|
70
|
+
else if (fileNameCompare > 0) {
|
|
71
|
+
deleteList.push(serverFile);
|
|
72
|
+
sizeDelete += serverFile.size ?? 0;
|
|
73
|
+
serverPosition += 1;
|
|
74
|
+
}
|
|
75
|
+
else if (fileNameCompare === 0) {
|
|
76
|
+
// paths are a match
|
|
77
|
+
if (localFile.type === "file" && serverFile.type === "file") {
|
|
78
|
+
if (localFile.hash === serverFile.hash) {
|
|
79
|
+
sameList.push(localFile);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
sizeReplace += localFile.size ?? 0;
|
|
83
|
+
replaceList.push(localFile);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
localPosition += 1;
|
|
88
|
+
serverPosition += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// optimize modifications
|
|
93
|
+
let foldersToDelete = deleteList.filter((item) => item.type === "folder");
|
|
94
|
+
|
|
95
|
+
// remove files/folders that have a nested parent folder we plan on deleting
|
|
96
|
+
const optimizedDeleteList = deleteList.filter((itemToDelete) => {
|
|
97
|
+
const parentFolderIsBeingDeleted = foldersToDelete.find((folder) => {
|
|
98
|
+
const isSameFile = itemToDelete.name === folder.name;
|
|
99
|
+
const parentFolderExists = itemToDelete.name.startsWith(folder.name);
|
|
100
|
+
|
|
101
|
+
return parentFolderExists && !isSameFile;
|
|
102
|
+
}) !== undefined;
|
|
103
|
+
|
|
104
|
+
if (parentFolderIsBeingDeleted) {
|
|
105
|
+
// a parent folder is being deleted, no need to delete this one
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
upload: uploadList,
|
|
114
|
+
delete: optimizedDeleteList,
|
|
115
|
+
replace: replaceList,
|
|
116
|
+
same: sameList,
|
|
117
|
+
sizeDelete,
|
|
118
|
+
sizeReplace,
|
|
119
|
+
sizeUpload
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 SamKirkland
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Vendored from [benallfree/ftp-deploy](https://github.com/benallfree/ftp-deploy) @ `132389e` (fork of [SamKirkland/FTP-Deploy-Action](https://github.com/SamKirkland/FTP-Deploy-Action) sync engine).
|
|
2
|
+
|
|
3
|
+
Incremental FTPS/SFTP deploy: hash diff, include/exclude globs, `.ftp-deploy-sync-state.json`. phio uses `protocol: sftp` via `ssh2-sftp-client`. See `LICENSE`.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as ftp from "basic-ftp";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { IFileList, IDiff, syncFileDescription, currentSyncFileVersion, IFtpDeployArgumentsWithDefaults } from "./types";
|
|
4
|
+
import { HashDiff } from "./HashDiff";
|
|
5
|
+
import { ILogger, retryRequest, ITimings, applyExcludeFilter, formatNumber } from "./utilities";
|
|
6
|
+
import prettyBytes from "pretty-bytes";
|
|
7
|
+
import { prettyError } from "./errorHandling";
|
|
8
|
+
import { ensureDir, FTPSyncProvider } from "./syncProvider";
|
|
9
|
+
import { getLocalFiles } from "./localFiles";
|
|
10
|
+
import { deploySftp } from "./sftpDeploy";
|
|
11
|
+
|
|
12
|
+
async function downloadFileList(client: ftp.Client, logger: ILogger, path: string): Promise<IFileList> {
|
|
13
|
+
// note: originally this was using a writable stream instead of a buffer file
|
|
14
|
+
// basic-ftp doesn't seam to close the connection when using steams over some ftps connections. This appears to be dependent on the ftp server
|
|
15
|
+
const tempFileNameHack = ".ftp-deploy-sync-server-state-buffer-file---delete.json";
|
|
16
|
+
|
|
17
|
+
await retryRequest(logger, async () => await client.downloadTo(tempFileNameHack, path));
|
|
18
|
+
|
|
19
|
+
const fileAsString = fs.readFileSync(tempFileNameHack, { encoding: "utf-8" });
|
|
20
|
+
const fileAsObject = JSON.parse(fileAsString) as IFileList;
|
|
21
|
+
|
|
22
|
+
fs.unlinkSync(tempFileNameHack);
|
|
23
|
+
|
|
24
|
+
return fileAsObject;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createLocalState(localFiles: IFileList, logger: ILogger, args: IFtpDeployArgumentsWithDefaults): void {
|
|
28
|
+
logger.verbose(`Creating local state at ${args["local-dir"]}${args["state-name"]}`);
|
|
29
|
+
fs.writeFileSync(`${args["local-dir"]}${args["state-name"]}`, JSON.stringify(localFiles, undefined, 4), { encoding: "utf8" });
|
|
30
|
+
logger.verbose("Local state created");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function connect(client: ftp.Client, args: IFtpDeployArgumentsWithDefaults, logger: ILogger) {
|
|
34
|
+
let secure: boolean | "implicit" = false;
|
|
35
|
+
if (args.protocol === "ftps") {
|
|
36
|
+
secure = true;
|
|
37
|
+
}
|
|
38
|
+
else if (args.protocol === "ftps-legacy") {
|
|
39
|
+
secure = "implicit";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
client.ftp.verbose = args["log-level"] === "verbose";
|
|
43
|
+
|
|
44
|
+
const rejectUnauthorized = args.security === "strict";
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await client.access({
|
|
48
|
+
host: args.server,
|
|
49
|
+
user: args.username,
|
|
50
|
+
password: args.password,
|
|
51
|
+
port: args.port,
|
|
52
|
+
secure: secure,
|
|
53
|
+
secureOptions: {
|
|
54
|
+
rejectUnauthorized: rejectUnauthorized
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.all("Failed to connect, are you sure your server works via FTP or FTPS? Users sometimes get this error when the server only supports SFTP.");
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (args["log-level"] === "verbose") {
|
|
64
|
+
client.trackProgress(info => {
|
|
65
|
+
logger.verbose(`${info.type} progress for "${info.name}". Progress: ${info.bytes} bytes of ${info.bytesOverall} bytes`);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getServerFiles(client: ftp.Client, logger: ILogger, timings: ITimings, args: IFtpDeployArgumentsWithDefaults): Promise<IFileList> {
|
|
71
|
+
try {
|
|
72
|
+
await ensureDir(client, logger, timings, args["server-dir"]);
|
|
73
|
+
|
|
74
|
+
if (args["dangerous-clean-slate"]) {
|
|
75
|
+
logger.all(`----------------------------------------------------------------`);
|
|
76
|
+
logger.all("🗑️ Removing all files on the server because 'dangerous-clean-slate' was set, this will make the deployment very slow...");
|
|
77
|
+
if (args["dry-run"] === false) {
|
|
78
|
+
await client.clearWorkingDir();
|
|
79
|
+
}
|
|
80
|
+
logger.all("Clear complete");
|
|
81
|
+
|
|
82
|
+
throw new Error("dangerous-clean-slate was run");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const serverFiles = await downloadFileList(client, logger, args["state-name"]);
|
|
86
|
+
logger.all(`----------------------------------------------------------------`);
|
|
87
|
+
logger.all(`Last published on 📅 ${new Date(serverFiles.generatedTime).toLocaleDateString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "numeric" })}`);
|
|
88
|
+
|
|
89
|
+
// apply exclude options to server
|
|
90
|
+
if (args.exclude.length > 0) {
|
|
91
|
+
const filteredData = serverFiles.data.filter((item) => applyExcludeFilter({ path: item.name, isDirectory: () => item.type === "folder" }, args.exclude));
|
|
92
|
+
serverFiles.data = filteredData;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return serverFiles;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
logger.all(`----------------------------------------------------------------`);
|
|
99
|
+
logger.all(`No file exists on the server "${args["server-dir"] + args["state-name"]}" - this must be your first publish! 🎉`);
|
|
100
|
+
logger.all(`The first publish will take a while... but once the initial sync is done only differences are published!`);
|
|
101
|
+
logger.all(`If you get this message and its NOT your first publish, something is wrong.`);
|
|
102
|
+
|
|
103
|
+
// set the server state to nothing, because we don't know what the server state is
|
|
104
|
+
return {
|
|
105
|
+
description: syncFileDescription,
|
|
106
|
+
version: currentSyncFileVersion,
|
|
107
|
+
generatedTime: new Date().getTime(),
|
|
108
|
+
data: [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function deploy(args: IFtpDeployArgumentsWithDefaults, logger: ILogger, timings: ITimings): Promise<void> {
|
|
114
|
+
if (args.protocol === "sftp") {
|
|
115
|
+
return deploySftp(args, logger, timings);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
timings.start("total");
|
|
119
|
+
|
|
120
|
+
// header
|
|
121
|
+
logger.all(`----------------------------------------------------------------`);
|
|
122
|
+
logger.all(`🚀 Thanks for using ftp-deploy. Let's deploy some stuff! `);
|
|
123
|
+
logger.all(`----------------------------------------------------------------`);
|
|
124
|
+
logger.all(`If you found this project helpful, please support it`);
|
|
125
|
+
logger.all(`by giving it a ⭐ on Github --> https://github.com/SamKirkland/FTP-Deploy-Action`);
|
|
126
|
+
logger.all(`or add a badge 🏷️ to your projects readme --> https://github.com/SamKirkland/FTP-Deploy-Action#badge`);
|
|
127
|
+
logger.verbose(`Using the following include filters: ${JSON.stringify(args.include)}`);
|
|
128
|
+
logger.verbose(`Using the following excludes filters: ${JSON.stringify(args.exclude)}`);
|
|
129
|
+
|
|
130
|
+
timings.start("hash");
|
|
131
|
+
const localFiles = await getLocalFiles(args, logger);
|
|
132
|
+
timings.stop("hash");
|
|
133
|
+
|
|
134
|
+
createLocalState(localFiles, logger, args);
|
|
135
|
+
|
|
136
|
+
const client = new ftp.Client(args.timeout);
|
|
137
|
+
|
|
138
|
+
global.reconnect = async function () {
|
|
139
|
+
timings.start("connecting");
|
|
140
|
+
await connect(client, args, logger);
|
|
141
|
+
timings.stop("connecting");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
let totalBytesUploaded = 0;
|
|
146
|
+
try {
|
|
147
|
+
await global.reconnect();
|
|
148
|
+
|
|
149
|
+
const serverFiles = await getServerFiles(client, logger, timings, args);
|
|
150
|
+
|
|
151
|
+
timings.start("logging");
|
|
152
|
+
const diffTool: IDiff = new HashDiff();
|
|
153
|
+
|
|
154
|
+
logger.standard(`----------------------------------------------------------------`);
|
|
155
|
+
logger.standard(`Local Files:\t${formatNumber(localFiles.data.length)}`);
|
|
156
|
+
logger.standard(`Server Files:\t${formatNumber(serverFiles.data.length)}`);
|
|
157
|
+
logger.standard(`----------------------------------------------------------------`);
|
|
158
|
+
logger.standard(`Calculating differences between client & server`);
|
|
159
|
+
logger.standard(`----------------------------------------------------------------`);
|
|
160
|
+
logger.verbose(`Local files:`, JSON.stringify(localFiles,null,2));
|
|
161
|
+
logger.verbose(`Server files:`, JSON.stringify(serverFiles,null,2));
|
|
162
|
+
|
|
163
|
+
const diffs = diffTool.getDiffs(localFiles, serverFiles);
|
|
164
|
+
|
|
165
|
+
diffs.upload.filter((itemUpload) => itemUpload.type === "folder").map((itemUpload) => {
|
|
166
|
+
logger.standard(`📁 Create: ${itemUpload.name}`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
diffs.upload.filter((itemUpload) => itemUpload.type === "file").map((itemUpload) => {
|
|
170
|
+
logger.standard(`📄 Upload: ${itemUpload.name}`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
diffs.replace.map((itemReplace) => {
|
|
174
|
+
logger.standard(`🔁 File replace: ${itemReplace.name}`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
diffs.delete.filter((itemUpload) => itemUpload.type === "file").map((itemDelete) => {
|
|
178
|
+
logger.standard(`📄 Delete: ${itemDelete.name} `);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
diffs.delete.filter((itemUpload) => itemUpload.type === "folder").map((itemDelete) => {
|
|
182
|
+
logger.standard(`📁 Delete: ${itemDelete.name} `);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
diffs.same.map((itemSame) => {
|
|
186
|
+
if (itemSame.type === "file") {
|
|
187
|
+
logger.standard(`⚖️ File content is the same, doing nothing: ${itemSame.name}`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
timings.stop("logging");
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
totalBytesUploaded = diffs.sizeUpload + diffs.sizeReplace;
|
|
194
|
+
|
|
195
|
+
timings.start("upload");
|
|
196
|
+
try {
|
|
197
|
+
const syncProvider = new FTPSyncProvider(client, logger, timings, args["local-dir"], args["server-dir"], args["state-name"], args["dry-run"]);
|
|
198
|
+
await syncProvider.syncLocalToServer(diffs);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
timings.stop("upload");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
prettyError(logger, args, error);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
client.close();
|
|
210
|
+
timings.stop("total");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
const uploadSpeed = prettyBytes(totalBytesUploaded / (timings.getTime("upload") / 1000));
|
|
215
|
+
|
|
216
|
+
// footer
|
|
217
|
+
logger.all(`----------------------------------------------------------------`);
|
|
218
|
+
logger.all(`Time spent hashing: ${timings.getTimeFormatted("hash")}`);
|
|
219
|
+
logger.all(`Time spent connecting to server: ${timings.getTimeFormatted("connecting")}`);
|
|
220
|
+
logger.all(`Time spent deploying: ${timings.getTimeFormatted("upload")} (${uploadSpeed}/second)`);
|
|
221
|
+
logger.all(` - changing dirs: ${timings.getTimeFormatted("changingDir")}`);
|
|
222
|
+
logger.all(` - logging: ${timings.getTimeFormatted("logging")}`);
|
|
223
|
+
logger.all(`----------------------------------------------------------------`);
|
|
224
|
+
logger.all(`Total time: ${timings.getTimeFormatted("total")}`);
|
|
225
|
+
logger.all(`----------------------------------------------------------------`);
|
|
226
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ILogger } from "./utilities";
|
|
2
|
+
import { IFtpDeployArgumentsWithDefaults, ErrorCode } from "./types";
|
|
3
|
+
import { FTPError } from "basic-ftp";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
function logOriginalError(logger: ILogger, error: any) {
|
|
7
|
+
logger.all();
|
|
8
|
+
logger.all(`----------------------------------------------------------------`);
|
|
9
|
+
logger.all(`---------------------- full error below ----------------------`);
|
|
10
|
+
logger.all(`----------------------------------------------------------------`);
|
|
11
|
+
logger.all();
|
|
12
|
+
logger.all(error);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts a exception to helpful debug info
|
|
18
|
+
* @param error exception
|
|
19
|
+
*/
|
|
20
|
+
export function prettyError(logger: ILogger, args: IFtpDeployArgumentsWithDefaults, error: any): void {
|
|
21
|
+
logger.all();
|
|
22
|
+
logger.all(`----------------------------------------------------------------`);
|
|
23
|
+
logger.all(`-------------- 🔥🔥🔥 an error occurred 🔥🔥🔥 --------------`);
|
|
24
|
+
logger.all(`----------------------------------------------------------------`);
|
|
25
|
+
|
|
26
|
+
const ftpError = error as FTPError;
|
|
27
|
+
if (typeof error.code === "string") {
|
|
28
|
+
const errorCode = error.code as string;
|
|
29
|
+
|
|
30
|
+
if (errorCode === "ENOTFOUND") {
|
|
31
|
+
logger.all(`The server "${args.server}" doesn't seem to exist. Do you have a typo?`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (typeof error.name === "string") {
|
|
35
|
+
const errorName = error.name as string;
|
|
36
|
+
|
|
37
|
+
if (errorName.includes("ERR_TLS_CERT_ALTNAME_INVALID")) {
|
|
38
|
+
logger.all(`The certificate for "${args.server}" is likely shared. The host did not place your server on the list of valid domains for this cert.`);
|
|
39
|
+
logger.all(`This is a common issue with shared hosts. You have a few options:`);
|
|
40
|
+
logger.all(` - Ignore this error by setting security back to loose`);
|
|
41
|
+
logger.all(` - Contact your hosting provider and ask them for your servers hostname`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (args.protocol === "sftp") {
|
|
45
|
+
const message = `${error?.message ?? error}`.toLowerCase();
|
|
46
|
+
if (message.includes("authentication") || message.includes("all configured")) {
|
|
47
|
+
logger.all(`Could not authenticate over SFTP with username "${args.username}".`);
|
|
48
|
+
logger.all(`Ensure the Phio deploy key is registered under Account → Keys.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (typeof ftpError.code === "number") {
|
|
52
|
+
if (ftpError.code === ErrorCode.NotLoggedIn) {
|
|
53
|
+
const serverRequiresFTPS = ftpError.message.toLowerCase().includes("must use encryption");
|
|
54
|
+
|
|
55
|
+
if (serverRequiresFTPS) {
|
|
56
|
+
logger.all(`The server you are connecting to requires encryption (ftps)`);
|
|
57
|
+
logger.all(`Enable FTPS by using the protocol option.`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
logger.all(`Could not login with the username "${args.username}" and password "${args.password}".`);
|
|
61
|
+
logger.all(`Make sure you can login with those credentials. If you have a space or a quote in your username or password be sure to escape them!`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logOriginalError(logger, error);
|
|
67
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Record, IFileList, syncFileDescription, currentSyncFileVersion, IFtpDeployArgumentsWithDefaults } from "./types";
|
|
2
|
+
import { fileHash } from "./HashDiff";
|
|
3
|
+
import { globSync} from 'glob'
|
|
4
|
+
import { lstatSync } from "fs";
|
|
5
|
+
import { ILogger } from "./utilities";
|
|
6
|
+
|
|
7
|
+
export async function getLocalFiles(args: IFtpDeployArgumentsWithDefaults, logger: ILogger): Promise<IFileList> {
|
|
8
|
+
const files = globSync(args['include'], {ignore: args['exclude'], cwd: args['local-dir'], });
|
|
9
|
+
logger.verbose(`Local files:`, JSON.stringify({files},null,2));
|
|
10
|
+
|
|
11
|
+
const records: Record[] = [];
|
|
12
|
+
|
|
13
|
+
for (const path of files) {
|
|
14
|
+
const stat = lstatSync(path);
|
|
15
|
+
if (stat.isDirectory()) {
|
|
16
|
+
records.push({
|
|
17
|
+
type: "folder",
|
|
18
|
+
name: path,
|
|
19
|
+
size: undefined
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (stat.isFile()) {
|
|
26
|
+
records.push({
|
|
27
|
+
type: "file",
|
|
28
|
+
name: path,
|
|
29
|
+
size: stat.size,
|
|
30
|
+
hash: await fileHash(args["local-dir"] + path, "sha256")
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (stat.isSymbolicLink()) {
|
|
37
|
+
console.warn("This script is currently unable to handle symbolic links - please add a feature request if you need this");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
description: syncFileDescription,
|
|
43
|
+
version: currentSyncFileVersion,
|
|
44
|
+
generatedTime: new Date().getTime(),
|
|
45
|
+
data: records
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { deploy as deployCustom } from "./deploy";
|
|
2
|
+
import { IFtpDeployArguments } from "./types";
|
|
3
|
+
import { getDefaultSettings, Logger, Timings } from "./utilities";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default includes, everything by default
|
|
7
|
+
*/
|
|
8
|
+
export const includeDefaults = ["**/*"];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default excludes, ignores all git files and the node_modules folder
|
|
12
|
+
* **\/.git* ignores all FILES that start with .git(in any folder or sub-folder)
|
|
13
|
+
* **\/.git*\/** ignores all FOLDERS that start with .git (in any folder or sub-folder)
|
|
14
|
+
* **\/node_modules\/** ignores all FOLDERS named node_modules (in any folder or sub-folder)
|
|
15
|
+
*/
|
|
16
|
+
export const excludeDefaults = ["**/.git*", "**/.git*/**", "**/node_modules/**"];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Syncs a local folder with a remote folder over FTP.
|
|
20
|
+
* After the initial sync only differences are synced, making deployments super fast!
|
|
21
|
+
*/
|
|
22
|
+
export async function deploy(args: IFtpDeployArguments): Promise<void> {
|
|
23
|
+
const argsWithDefaults = getDefaultSettings(args);
|
|
24
|
+
const logger = new Logger(argsWithDefaults["log-level"]);
|
|
25
|
+
const timings = new Timings();
|
|
26
|
+
|
|
27
|
+
await deployCustom(argsWithDefaults, logger, timings);
|
|
28
|
+
}
|