@datatruck/cli 0.29.1 → 0.30.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.
@@ -153,7 +153,7 @@ class BackupAction {
153
153
  .map((item) => [
154
154
  (0, cli_1.renderResult)(item.error),
155
155
  renderTitle(item, true),
156
- renderData(item, true),
156
+ renderData(item, true, result),
157
157
  (0, date_1.duration)(item.elapsed),
158
158
  (0, cli_1.renderError)(item.error, options.verbose),
159
159
  ]),
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @datatruck/cli
2
2
 
3
+ ## 0.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`e28b12d`](https://github.com/swordev/datatruck/commit/e28b12d08c36844317e506552443825ab3333139) Thanks [@juanrgm](https://github.com/juanrgm)! - Add `allowlist` option to the datatruck server
8
+
9
+ ### Patch Changes
10
+
11
+ - [`2f63e67`](https://github.com/swordev/datatruck/commit/2f63e67ee532892fda3a9d06e46336a42834e5ed) Thanks [@juanrgm](https://github.com/juanrgm)! - Add download progress in datatruck repository
12
+
13
+ - [`258e933`](https://github.com/swordev/datatruck/commit/258e93385d9b6f93a435a28a2b26e53ea1763755) Thanks [@juanrgm](https://github.com/juanrgm)! - Copy backups safely
14
+
15
+ - [`82b4c67`](https://github.com/swordev/datatruck/commit/82b4c67c55eee4f40753b48eb91345e6d6789f72) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix exit event
16
+
3
17
  ## 0.29.1
4
18
 
5
19
  ### Patch Changes
@@ -1,13 +1,10 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.CleanCacheCommand = void 0;
7
4
  const CleanCacheAction_1 = require("../Action/CleanCacheAction");
8
5
  const DataFormat_1 = require("../utils/DataFormat");
6
+ const bytes_1 = require("../utils/bytes");
9
7
  const CommandAbstract_1 = require("./CommandAbstract");
10
- const bytes_1 = __importDefault(require("bytes"));
11
8
  class CleanCacheCommand extends CommandAbstract_1.CommandAbstract {
12
9
  onOptions() {
13
10
  return this.returnsOptions({});
@@ -29,7 +26,7 @@ class CleanCacheCommand extends CommandAbstract_1.CommandAbstract {
29
26
  value: "Freed disk space",
30
27
  },
31
28
  ],
32
- rows: () => [[result.path, (0, bytes_1.default)(result.freedSize)]],
29
+ rows: () => [[result.path, (0, bytes_1.formatBytes)(result.freedSize)]],
33
30
  },
34
31
  });
35
32
  await cleanCache.exec();
@@ -1,15 +1,12 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.SnapshotsCommand = void 0;
7
4
  const ConfigAction_1 = require("../Action/ConfigAction");
8
5
  const SnapshotsAction_1 = require("../Action/SnapshotsAction");
9
6
  const DataFormat_1 = require("../utils/DataFormat");
7
+ const bytes_1 = require("../utils/bytes");
10
8
  const string_1 = require("../utils/string");
11
9
  const CommandAbstract_1 = require("./CommandAbstract");
12
- const bytes_1 = __importDefault(require("bytes"));
13
10
  class SnapshotsCommand extends CommandAbstract_1.CommandAbstract {
14
11
  onOptions() {
15
12
  const groupByValues = [
@@ -139,7 +136,7 @@ class SnapshotsCommand extends CommandAbstract_1.CommandAbstract {
139
136
  item.date.replace("T", " ").replace("Z", ""),
140
137
  item.packageName,
141
138
  item.packageTaskName || "",
142
- (0, bytes_1.default)(item.size),
139
+ (0, bytes_1.formatBytes)(item.size),
143
140
  item.repositoryName,
144
141
  item.repositoryType,
145
142
  ]),
package/Config/Config.js CHANGED
@@ -39,6 +39,7 @@ exports.configDefinition = {
39
39
  additionalProperties: false,
40
40
  properties: {
41
41
  path: { type: "string" },
42
+ log: { type: "boolean" },
42
43
  users: {
43
44
  type: "array",
44
45
  items: {
@@ -58,6 +59,27 @@ exports.configDefinition = {
58
59
  address: { type: "string" },
59
60
  },
60
61
  },
62
+ trustProxy: {
63
+ anyOf: [
64
+ { type: "boolean" },
65
+ {
66
+ type: "object",
67
+ additionalProperties: false,
68
+ required: ["remoteAddressHeader"],
69
+ properties: {
70
+ remoteAddressHeader: { type: "string" },
71
+ },
72
+ },
73
+ ],
74
+ },
75
+ allowlist: {
76
+ type: "object",
77
+ additionalProperties: false,
78
+ properties: {
79
+ enabled: { type: "boolean" },
80
+ remoteAddreses: (0, DefinitionEnum_1.makeRef)(DefinitionEnum_1.DefinitionEnum.stringListUtil),
81
+ },
82
+ },
61
83
  },
62
84
  },
63
85
  prunePolicy: (0, DefinitionEnum_1.makeRef)(DefinitionEnum_1.DefinitionEnum.prunePolicy),
@@ -58,7 +58,9 @@ class DatatruckRepository extends RepositoryAbstract_1.RepositoryAbstract {
58
58
  static zipBasenameTpl = `.*.dd.tar.gz`;
59
59
  static buildSnapshotName(snapshot, pkg) {
60
60
  const date = snapshot.date.replace(/:/g, "-");
61
- const pkgName = encodeURIComponent(pkg.name).replace(/%40/g, "@");
61
+ const pkgName = encodeURIComponent(pkg.name)
62
+ .replace(/%40/g, "@")
63
+ .replace("_", "%5F");
62
64
  const snapshotShortId = snapshot.id.slice(0, 8);
63
65
  return `${date}_${pkgName}_${snapshotShortId}`;
64
66
  }
@@ -260,38 +262,62 @@ class DatatruckRepository extends RepositoryAbstract_1.RepositoryAbstract {
260
262
  const snapshotName = DatatruckRepository.buildSnapshotName(data.snapshot, data.package);
261
263
  if (data.options.verbose)
262
264
  (0, cli_1.logExec)(`Copying backup files to ${data.mirrorRepositoryConfig.backend}`);
263
- await targetFs.mkdir(snapshotName);
264
- await targetFs.ensureEmptyDir(snapshotName);
265
+ const tmpSnapshotName = `${snapshotName}_tmp`;
266
+ if (await targetFs.existsDir(snapshotName))
267
+ await targetFs.ensureEmptyDir(snapshotName);
268
+ try {
269
+ await targetFs.rmAll(tmpSnapshotName);
270
+ }
271
+ catch (_) { }
272
+ await targetFs.mkdir(tmpSnapshotName);
273
+ await targetFs.ensureEmptyDir(tmpSnapshotName);
265
274
  const entries = await sourceFs.readdir(snapshotName);
266
275
  const total = entries.length;
267
276
  let current = 0;
268
277
  for (const entry of entries) {
269
- data.onProgress({
270
- absolute: {
271
- current,
272
- description: "Copying",
273
- payload: entry,
274
- total,
275
- percent: (0, math_1.progressPercent)(total, current),
276
- },
277
- });
278
+ const absolute = {
279
+ current,
280
+ description: "Copying",
281
+ payload: entry,
282
+ total,
283
+ percent: (0, math_1.progressPercent)(total, current),
284
+ };
285
+ data.onProgress({ absolute });
278
286
  current++;
279
287
  const sourceEntry = `${snapshotName}/${entry}`;
288
+ const targetEntry = `${tmpSnapshotName}/${entry}`;
280
289
  if (targetFs.isLocal()) {
281
- await sourceFs.download(sourceEntry, targetFs.resolvePath(sourceEntry));
290
+ await sourceFs.download(sourceEntry, targetFs.resolvePath(targetEntry), {
291
+ onProgress: (progress) => data.onProgress({
292
+ absolute,
293
+ relative: {
294
+ description: "Downloading",
295
+ ...progress,
296
+ },
297
+ }),
298
+ });
282
299
  }
283
300
  else {
284
301
  const tempDir = await (0, temp_1.mkTmpDir)(exports.datatruckRepositoryName, "repo", "remote-copy", entry);
285
302
  const tempFile = (0, path_1.join)(tempDir, entry);
286
303
  try {
287
- await sourceFs.download(sourceEntry, tempFile);
288
- await targetFs.upload(tempFile, sourceEntry);
304
+ await sourceFs.download(sourceEntry, tempFile, {
305
+ onProgress: (progress) => data.onProgress({
306
+ absolute,
307
+ relative: {
308
+ description: "Downloading",
309
+ ...progress,
310
+ },
311
+ }),
312
+ });
313
+ await targetFs.upload(tempFile, targetEntry);
289
314
  }
290
315
  finally {
291
316
  await (0, fs_1.tryRm)(tempFile);
292
317
  }
293
318
  }
294
319
  }
320
+ await targetFs.rename(tmpSnapshotName, snapshotName);
295
321
  }
296
322
  async restore(data) {
297
323
  const fs = (0, client_1.createFs)(this.config.backend);
package/cli.js CHANGED
@@ -9,8 +9,8 @@ const AppError_1 = require("./Error/AppError");
9
9
  const CommandFactory_1 = require("./Factory/CommandFactory");
10
10
  const globalData_1 = __importDefault(require("./globalData"));
11
11
  const cli_1 = require("./utils/cli");
12
+ const exit_1 = require("./utils/exit");
12
13
  const fs_1 = require("./utils/fs");
13
- const process_1 = require("./utils/process");
14
14
  const string_1 = require("./utils/string");
15
15
  const temp_1 = require("./utils/temp");
16
16
  const chalk_1 = require("chalk");
@@ -112,7 +112,7 @@ exports.buildArgs = buildArgs;
112
112
  function parseArgs(args) {
113
113
  program.parse(args);
114
114
  const verbose = getGlobalOptions().verbose;
115
- (0, process_1.onExit)((eventName, error) => {
115
+ (0, exit_1.onExit)((eventName, error) => {
116
116
  if (eventName !== "exit") {
117
117
  process.stdout.write(cli_1.showCursorCommand);
118
118
  console.info(`\nClosing... (reason: ${eventName})`);
@@ -1051,6 +1051,9 @@
1051
1051
  "path": {
1052
1052
  "type": "string"
1053
1053
  },
1054
+ "log": {
1055
+ "type": "boolean"
1056
+ },
1054
1057
  "users": {
1055
1058
  "type": "array",
1056
1059
  "items": {
@@ -1077,6 +1080,37 @@
1077
1080
  "type": "string"
1078
1081
  }
1079
1082
  }
1083
+ },
1084
+ "trustProxy": {
1085
+ "anyOf": [
1086
+ {
1087
+ "type": "boolean"
1088
+ },
1089
+ {
1090
+ "type": "object",
1091
+ "additionalProperties": false,
1092
+ "required": [
1093
+ "remoteAddressHeader"
1094
+ ],
1095
+ "properties": {
1096
+ "remoteAddressHeader": {
1097
+ "type": "string"
1098
+ }
1099
+ }
1100
+ }
1101
+ ]
1102
+ },
1103
+ "allowlist": {
1104
+ "type": "object",
1105
+ "additionalProperties": false,
1106
+ "properties": {
1107
+ "enabled": {
1108
+ "type": "boolean"
1109
+ },
1110
+ "remoteAddreses": {
1111
+ "$ref": "#/definitions/stringlist-util"
1112
+ }
1113
+ }
1080
1114
  }
1081
1115
  }
1082
1116
  },
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@datatruck/cli",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "dependencies": {
5
5
  "@supercharge/promise-pool": "^3.1.0",
6
6
  "ajv": "^8.12.0",
7
7
  "async": "^3.2.4",
8
- "bytes": "^3.1.2",
9
8
  "chalk": "^4.1.2",
10
9
  "commander": "^11.0.0",
11
10
  "dayjs": "^1.11.10",
@@ -0,0 +1,2 @@
1
+ export declare function formatBytes(bytes: number): string;
2
+ export declare function parseSize(size: string): number;
package/utils/bytes.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSize = exports.formatBytes = void 0;
4
+ const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
5
+ const sizeRegex = new RegExp(`^(\\d+(?:\\.\\d+)?)\\s*(${units.join("|")})$`, "i");
6
+ function formatBytes(bytes) {
7
+ let u = 0;
8
+ let n = bytes;
9
+ if (bytes < 0n)
10
+ throw new Error(`Invalid bytes: ${bytes.toString()}`);
11
+ while (n >= 1024n && ++u)
12
+ n = n / 1024;
13
+ return Number(n).toFixed(n < 10 && u > 0 ? 1 : 0) + units[u];
14
+ }
15
+ exports.formatBytes = formatBytes;
16
+ function parseSize(size) {
17
+ const matches = sizeRegex.exec(size);
18
+ if (!matches)
19
+ throw new Error(`Invalid size: ${size}`);
20
+ const [, value, unit] = matches;
21
+ const unitIndex = units.findIndex((v) => v === unit.toUpperCase());
22
+ if (unitIndex === -1)
23
+ throw new Error(`Unit not found: ${unit}`);
24
+ let result = Number(value);
25
+ for (let i = 0; i < unitIndex; ++i)
26
+ result *= 1024;
27
+ return result;
28
+ }
29
+ exports.parseSize = parseSize;
@@ -1,4 +1,5 @@
1
1
  import { DiskStats } from "../fs";
2
+ import { BasicProgress } from "../progress";
2
3
  import { AbstractFs, FsOptions } from "../virtual-fs";
3
4
  export declare class RemoteFs extends AbstractFs {
4
5
  readonly options: FsOptions;
@@ -9,6 +10,7 @@ export declare class RemoteFs extends AbstractFs {
9
10
  protected fetchJson(name: string, params: any[]): Promise<any>;
10
11
  protected post(name: string, params: any[], data: string): Promise<void>;
11
12
  existsDir(path: string): Promise<any>;
13
+ rename(source: string, target: string): Promise<any>;
12
14
  mkdir(path: string): Promise<any>;
13
15
  readFile(path: string): Promise<any>;
14
16
  readdir(path: string): Promise<any>;
@@ -18,7 +20,10 @@ export declare class RemoteFs extends AbstractFs {
18
20
  rmAll(path: string): Promise<void>;
19
21
  fetchDiskStats(path: string): Promise<DiskStats>;
20
22
  upload(source: string, target: string): Promise<void>;
21
- download(source: string, target: string, timeout?: number): Promise<void>;
23
+ download(source: string, target: string, options?: {
24
+ timeout?: number;
25
+ onProgress?: (progress: BasicProgress) => void;
26
+ }): Promise<void>;
22
27
  }
23
28
  export declare function isRemoteBackend(backend: string): boolean;
24
29
  export declare function createFs(backend: string): AbstractFs;
@@ -44,6 +44,9 @@ class RemoteFs extends virtual_fs_1.AbstractFs {
44
44
  async existsDir(path) {
45
45
  return await this.fetchJson("existsDir", [path]);
46
46
  }
47
+ async rename(source, target) {
48
+ return await this.fetchJson("rename", [source, target]);
49
+ }
47
50
  async mkdir(path) {
48
51
  return await this.fetchJson("mkdir", [path]);
49
52
  }
@@ -76,13 +79,11 @@ class RemoteFs extends virtual_fs_1.AbstractFs {
76
79
  },
77
80
  });
78
81
  }
79
- async download(source, target, timeout = 100000) {
82
+ async download(source, target, options = {}) {
80
83
  await (0, http_1.downloadFile)(`${this.url}/download`, target, {
81
- timeout,
84
+ ...options,
82
85
  headers: this.headers,
83
- query: {
84
- params: JSON.stringify([source]),
85
- },
86
+ query: { params: JSON.stringify([source]) },
86
87
  });
87
88
  }
88
89
  }
@@ -12,6 +12,16 @@ export type DatatruckServerOptions = {
12
12
  address?: string;
13
13
  };
14
14
  users?: User[];
15
+ trustProxy?: true | {
16
+ remoteAddressHeader: string;
17
+ };
18
+ allowlist?: {
19
+ /**
20
+ * @default true
21
+ */
22
+ enabled?: boolean;
23
+ remoteAddreses?: string[];
24
+ };
15
25
  };
16
26
  export declare const headerKey: {
17
27
  user: string;
@@ -4,6 +4,7 @@ exports.createDatatruckServer = exports.headerKey = void 0;
4
4
  const http_1 = require("../http");
5
5
  const virtual_fs_1 = require("../virtual-fs");
6
6
  const fs_1 = require("fs");
7
+ const promises_1 = require("fs/promises");
7
8
  const http_2 = require("http");
8
9
  function parseUrl(inUrl) {
9
10
  const url = new URL(`http://127.0.0.1${inUrl}`);
@@ -23,14 +24,26 @@ exports.headerKey = {
23
24
  user: "x-dtt-user",
24
25
  password: "x-dtt-password",
25
26
  };
26
- function validateRequest(req, users) {
27
- const name = req.headers[exports.headerKey.user];
28
- const password = req.headers[exports.headerKey.password];
29
- return users.some((user) => user.name.length &&
30
- user.password.length &&
31
- user.name === name &&
32
- user.password === password);
27
+ function validateRequest(req, options) {
28
+ const list = options.allowlist;
29
+ if (list && (list.enabled ?? true) && list.remoteAddreses) {
30
+ const remoteAddress = getRemoteAddress(req, options);
31
+ if (!remoteAddress || list.remoteAddreses.includes(remoteAddress))
32
+ return false;
33
+ }
34
+ const name = req.headers[exports.headerKey.user]?.toString().trim();
35
+ const password = req.headers[exports.headerKey.password]?.toString().trim();
36
+ if (!name?.length || !password?.length)
37
+ return;
38
+ return (options.users?.some((user) => user.name === name && user.password === password) || false);
33
39
  }
40
+ const getRemoteAddress = (req, options) => {
41
+ return ((options.trustProxy
42
+ ? options.trustProxy === true
43
+ ? req.headers["x-real-ip"]?.toString()
44
+ : req.headers[options.trustProxy.remoteAddressHeader]?.toString()
45
+ : undefined) ?? req.socket.remoteAddress);
46
+ };
34
47
  function createDatatruckServer(options) {
35
48
  const log = options.log ?? true;
36
49
  return (0, http_2.createServer)(async (req, res) => {
@@ -38,7 +51,7 @@ function createDatatruckServer(options) {
38
51
  if (req.url === "/" || req.url === "/favicon.ico") {
39
52
  return res.end();
40
53
  }
41
- else if (!validateRequest(req, options.users || [])) {
54
+ else if (!validateRequest(req, options)) {
42
55
  res.statusCode = 401;
43
56
  return res.end();
44
57
  }
@@ -50,7 +63,8 @@ function createDatatruckServer(options) {
50
63
  const { action, params } = parseUrl(req.url);
51
64
  if (action === "upload") {
52
65
  const [target] = params;
53
- const file = (0, fs_1.createWriteStream)(fs.resolvePath(target));
66
+ const path = fs.resolvePath(target);
67
+ const file = (0, fs_1.createWriteStream)(path);
54
68
  req.pipe(file);
55
69
  await new Promise((resolve, reject) => {
56
70
  req.on("error", reject);
@@ -60,7 +74,10 @@ function createDatatruckServer(options) {
60
74
  }
61
75
  else if (action === "download") {
62
76
  const [target] = params;
63
- const file = (0, fs_1.createReadStream)(fs.resolvePath(target));
77
+ const path = fs.resolvePath(target);
78
+ const file = (0, fs_1.createReadStream)(path);
79
+ const fileStat = await (0, promises_1.stat)(path);
80
+ res.setHeader("Content-Length", fileStat.size);
64
81
  file.pipe(res);
65
82
  await new Promise((resolve, reject) => {
66
83
  req.on("error", reject);
@@ -82,14 +99,16 @@ function createDatatruckServer(options) {
82
99
  if (json !== undefined)
83
100
  res.write(JSON.stringify(json));
84
101
  }
85
- res.end();
86
102
  if (log)
87
103
  console.info(`<${action}`);
104
+ res.end();
88
105
  }
89
106
  catch (error) {
90
107
  if (log)
91
108
  console.error(`<${req.url}`, error);
92
109
  res.statusCode = 500;
110
+ res.statusMessage = error.message;
111
+ res.end();
93
112
  }
94
113
  });
95
114
  }
@@ -0,0 +1,6 @@
1
+ type EventNameType = "exit" | "SIGINT" | "SIGUSR1" | "SIGUSR2" | "SIGTERM" | "uncaughtException";
2
+ export declare function triggerExitEvent(eventName: EventNameType, ...args: any[]): void;
3
+ export declare function enableExitEvents(): void;
4
+ export declare function disableExitEvents(): void;
5
+ export declare function onExit(cb: (eventName: EventNameType, ...args: any[]) => void, priority?: number): () => boolean;
6
+ export {};
package/utils/exit.js ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.onExit = exports.disableExitEvents = exports.enableExitEvents = exports.triggerExitEvent = void 0;
4
+ const eventNames = [
5
+ `exit`,
6
+ `SIGINT`,
7
+ `SIGUSR1`,
8
+ `SIGUSR2`,
9
+ `uncaughtException`,
10
+ `SIGTERM`,
11
+ ];
12
+ const listeners = new Set();
13
+ const disableExitDisposes = new Set();
14
+ function triggerExitEvent(eventName, ...args) {
15
+ if (!disableExitDisposes.size) {
16
+ process.emit(eventName, ...args);
17
+ return;
18
+ }
19
+ try {
20
+ disableExitEvents();
21
+ const items = [...listeners].sort((a, b) => b.priority - a.priority);
22
+ for (const { cb } of items) {
23
+ try {
24
+ cb(eventName, ...args);
25
+ }
26
+ catch (_) { }
27
+ }
28
+ }
29
+ catch (_) {
30
+ process.exit(5);
31
+ }
32
+ }
33
+ exports.triggerExitEvent = triggerExitEvent;
34
+ function enableExitEvents() {
35
+ disableExitEvents();
36
+ for (const eventName of eventNames) {
37
+ const listener = (...args) => triggerExitEvent(eventName, ...args);
38
+ process.on(eventName, listener);
39
+ disableExitDisposes.add(() => process.off(eventName, listener));
40
+ }
41
+ }
42
+ exports.enableExitEvents = enableExitEvents;
43
+ function disableExitEvents() {
44
+ for (const dispose of disableExitDisposes)
45
+ dispose();
46
+ disableExitDisposes.clear();
47
+ }
48
+ exports.disableExitEvents = disableExitEvents;
49
+ function onExit(cb, priority) {
50
+ if (!disableExitDisposes.size)
51
+ enableExitEvents();
52
+ const listener = { cb, priority: priority ?? 0 };
53
+ listeners.add(listener);
54
+ return () => listeners.delete(listener);
55
+ }
56
+ exports.onExit = onExit;
package/utils/fs.d.ts CHANGED
@@ -133,4 +133,7 @@ export declare function fetchDiskStats(path: string): Promise<DiskStats>;
133
133
  export declare function checkFreeDiskSpace(stat: DiskStats, inSize: string | number): Promise<void>;
134
134
  export declare function ensureFreeDiskSpace(input: string[] | DiskStats, inSize: number | string): Promise<void>;
135
135
  export declare function groupFiles(inFiles: string[], suffixes?: string[], gzSuffix?: string): [string[], Record<string, string>];
136
+ export declare function asFile(input: string | {
137
+ path: string;
138
+ }): Promise<[string, (() => Promise<void>) | undefined]>;
136
139
  export {};
package/utils/fs.js CHANGED
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.groupFiles = exports.ensureFreeDiskSpace = exports.checkFreeDiskSpace = exports.fetchDiskStats = exports.initEmptyDir = exports.tryRm = exports.safeRename = exports.fetchData = exports.countFileLines = exports.createWriteStreamPool = exports.createFileScanner = exports.createProgress = exports.cpy = exports.isNotFoundError = exports.updateFileStats = exports.copyFileWithStreams = exports.waitForClose = exports.writeGitIgnoreList = exports.fastglobToGitIgnore = exports.forEachFile = exports.readDir = exports.readPartialFile = exports.fastFolderSizeAsync = exports.findFile = exports.parsePackageFile = exports.parseFile = exports.parseFileExtensions = exports.writeJSONFile = exports.existsFile = exports.existsDir = exports.safeStat = exports.ensureExistsDir = exports.ensureSingleFile = exports.ensureEmptyDir = exports.mkdirIfNotExists = exports.isLocalDir = exports.isEmptyDir = exports.isWSLSystem = void 0;
6
+ exports.asFile = exports.groupFiles = exports.ensureFreeDiskSpace = exports.checkFreeDiskSpace = exports.fetchDiskStats = exports.initEmptyDir = exports.tryRm = exports.safeRename = exports.fetchData = exports.countFileLines = exports.createWriteStreamPool = exports.createFileScanner = exports.createProgress = exports.cpy = exports.isNotFoundError = exports.updateFileStats = exports.copyFileWithStreams = exports.waitForClose = exports.writeGitIgnoreList = exports.fastglobToGitIgnore = exports.forEachFile = exports.readDir = exports.readPartialFile = exports.fastFolderSizeAsync = exports.findFile = exports.parsePackageFile = exports.parseFile = exports.parseFileExtensions = exports.writeJSONFile = exports.existsFile = exports.existsDir = exports.safeStat = exports.ensureExistsDir = exports.ensureSingleFile = exports.ensureEmptyDir = exports.mkdirIfNotExists = exports.isLocalDir = exports.isEmptyDir = exports.isWSLSystem = void 0;
7
+ const bytes_1 = require("./bytes");
7
8
  const math_1 = require("./math");
8
9
  const path_1 = require("./path");
9
10
  const string_1 = require("./string");
11
+ const temp_1 = require("./temp");
10
12
  const async_1 = require("async");
11
- const bytes_1 = __importDefault(require("bytes"));
12
13
  const fast_folder_size_1 = __importDefault(require("fast-folder-size"));
13
14
  const fast_glob_1 = __importDefault(require("fast-glob"));
14
15
  const fs_1 = require("fs");
@@ -238,7 +239,7 @@ async function writeGitIgnoreList(options) {
238
239
  }
239
240
  exports.writeGitIgnoreList = writeGitIgnoreList;
240
241
  async function waitForClose(stream) {
241
- return new Promise(async (resolve, reject) => {
242
+ return new Promise((resolve, reject) => {
242
243
  stream.on("close", resolve);
243
244
  stream.on("error", reject);
244
245
  return stream;
@@ -566,10 +567,10 @@ async function fetchDiskStats(path) {
566
567
  }
567
568
  exports.fetchDiskStats = fetchDiskStats;
568
569
  async function checkFreeDiskSpace(stat, inSize) {
569
- const humanSize = typeof inSize === "number" ? (0, bytes_1.default)(inSize) : inSize;
570
- const size = bytes_1.default.parse(inSize);
570
+ const humanSize = typeof inSize === "number" ? (0, bytes_1.formatBytes)(inSize) : inSize;
571
+ const size = typeof inSize === "number" ? inSize : (0, bytes_1.parseSize)(inSize);
571
572
  if (stat.free < size)
572
- throw new Error(`Free disk space is less than ${humanSize}: ${(0, bytes_1.default)(stat.free)}/${(0, bytes_1.default)(stat.total)}`);
573
+ throw new Error(`Free disk space is less than ${humanSize}: ${(0, bytes_1.formatBytes)(stat.free)}/${(0, bytes_1.formatBytes)(stat.total)}`);
573
574
  }
574
575
  exports.checkFreeDiskSpace = checkFreeDiskSpace;
575
576
  async function ensureFreeDiskSpace(input, inSize) {
@@ -608,3 +609,20 @@ function groupFiles(inFiles, suffixes, gzSuffix = ".tar.gz") {
608
609
  return [Object.keys(grouped), compressed];
609
610
  }
610
611
  exports.groupFiles = groupFiles;
612
+ async function asFile(input) {
613
+ if (typeof input === "string") {
614
+ const dir = await (0, temp_1.mkTmpDir)("text-as-file");
615
+ const path = (0, path_2.join)(dir, "contents.txt");
616
+ await (0, promises_1.writeFile)(path, input);
617
+ return [
618
+ path,
619
+ async () => {
620
+ await (0, promises_1.rm)(dir, { recursive: true });
621
+ },
622
+ ];
623
+ }
624
+ else {
625
+ return [input.path, undefined];
626
+ }
627
+ }
628
+ exports.asFile = asFile;
package/utils/http.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ import { BasicProgress } from "./progress";
2
3
  import { IncomingMessage, Server } from "http";
3
4
  export declare function closeServer(server: Server): Promise<void>;
4
5
  export declare function readRequestData(req: IncomingMessage): Promise<string | undefined>;
@@ -14,6 +15,7 @@ export declare function downloadFile(url: string, output: string, options?: {
14
15
  headers?: Record<string, string>;
15
16
  query?: Record<string, string>;
16
17
  timeout?: number;
18
+ onProgress?: (progress: BasicProgress) => void;
17
19
  }): Promise<void>;
18
20
  export declare function uploadFile(url: string, path: string, options?: {
19
21
  headers?: Record<string, string>;
package/utils/http.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.uploadFile = exports.downloadFile = exports.post = exports.fetchJson = exports.readRequestData = exports.closeServer = void 0;
4
+ const math_1 = require("./math");
4
5
  const fs_1 = require("fs");
5
6
  const promises_1 = require("fs/promises");
6
7
  const http_1 = require("http");
@@ -89,13 +90,28 @@ async function post(url, data, options = {}) {
89
90
  }
90
91
  exports.post = post;
91
92
  async function downloadFile(url, output, options = {}) {
92
- const timeout = options.timeout ?? 3600 * 100;
93
+ const timeout = options.timeout ?? 3600 * 1000; // 60m
93
94
  const file = (0, fs_1.createWriteStream)(output);
94
95
  await new Promise((resolve, reject) => {
95
96
  const req = request(href(url, options.query), {
96
97
  headers: options.headers,
97
98
  }, (res) => {
99
+ const contentLength = res.headers["content-length"] ?? "";
100
+ if (!/^\d+$/.test(contentLength))
101
+ return reject(new Error(`Invalid 'content-length': ${contentLength}`));
102
+ const total = Number(contentLength);
103
+ let current = 0;
98
104
  if (res.statusCode === 200) {
105
+ if (options.onProgress) {
106
+ res.on("data", (chunk) => {
107
+ current += chunk.byteLength;
108
+ options.onProgress({
109
+ percent: (0, math_1.progressPercent)(total, current),
110
+ current,
111
+ total,
112
+ });
113
+ });
114
+ }
99
115
  res
100
116
  .on("error", async (error) => {
101
117
  file.destroy();
package/utils/list.d.ts CHANGED
@@ -46,6 +46,7 @@ export declare class Listr3<T extends Listr3Context> extends Listr<void, "defaul
46
46
  };
47
47
  readonly resultMap: Record<string, Listr3TaskResult<T>>;
48
48
  readonly resultList: Listr3TaskResult<T>[];
49
+ readonly logger: List3Logger;
49
50
  protected execTimer: Timer;
50
51
  constructor($options: {
51
52
  streams?: Streams;
@@ -59,6 +60,7 @@ export declare class Listr3<T extends Listr3Context> extends Listr<void, "defaul
59
60
  add(tasks: ListrTask<void, ListrGetRendererClassFromValue<"default">> | ListrTask<void, ListrGetRendererClassFromValue<"default">>[]): this;
60
61
  getSummaryResult(): SummaryResult;
61
62
  getResult(): (SummaryResult | Listr3TaskResult<T>)[];
63
+ protected release(): void;
62
64
  exec(): Promise<(Listr3TaskResult<T> | SummaryResult)[]>;
63
65
  }
64
66
  export {};
package/utils/list.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Listr3 = exports.List3Logger = void 0;
4
4
  const date_1 = require("./date");
5
+ const exit_1 = require("./exit");
5
6
  const stream_1 = require("./stream");
6
7
  const listr2_1 = require("listr2");
7
8
  class List3Logger extends listr2_1.ListrLogger {
@@ -17,8 +18,10 @@ class Listr3 extends listr2_1.Listr {
17
18
  $options;
18
19
  resultMap = {};
19
20
  resultList = [];
21
+ logger;
20
22
  execTimer;
21
23
  constructor($options) {
24
+ const logger = new List3Logger();
22
25
  super([], {
23
26
  renderer: "default",
24
27
  collectErrors: "minimal",
@@ -27,12 +30,12 @@ class Listr3 extends listr2_1.Listr {
27
30
  }),
28
31
  fallbackRenderer: "simple",
29
32
  fallbackRendererOptions: {
30
- logger: new List3Logger(),
33
+ logger: logger,
31
34
  timestamp: listr2_1.PRESET_TIMESTAMP,
32
35
  timer: listr2_1.PRESET_TIMER,
33
36
  },
34
37
  rendererOptions: {
35
- logger: new List3Logger(),
38
+ logger: logger,
36
39
  collapseSubtasks: false,
37
40
  collapseErrors: false,
38
41
  timer: listr2_1.PRESET_TIMER,
@@ -40,6 +43,7 @@ class Listr3 extends listr2_1.Listr {
40
43
  });
41
44
  this.$options = $options;
42
45
  this.execTimer = (0, date_1.createTimer)();
46
+ this.logger = logger;
43
47
  }
44
48
  serializeKeyIndex(keyIndex) {
45
49
  return typeof keyIndex !== "undefined"
@@ -127,7 +131,18 @@ class Listr3 extends listr2_1.Listr {
127
131
  getResult() {
128
132
  return [...this.resultList, this.getSummaryResult()];
129
133
  }
134
+ release() {
135
+ for (const task of this.tasks)
136
+ if (task.isPending())
137
+ task.state$ = listr2_1.ListrTaskState.FAILED;
138
+ this["renderer"].end(new Error("Interrupted."));
139
+ }
130
140
  async exec() {
141
+ const dispose = (0, exit_1.onExit)(() => {
142
+ this.$options.progressManager?.dispose();
143
+ this.execTimer.reset();
144
+ this.release();
145
+ }, 1);
131
146
  try {
132
147
  this.$options.progressManager?.start();
133
148
  this.execTimer.reset();
@@ -138,7 +153,7 @@ class Listr3 extends listr2_1.Listr {
138
153
  throw error;
139
154
  }
140
155
  finally {
141
- this.$options.progressManager?.dispose();
156
+ dispose();
142
157
  }
143
158
  }
144
159
  }
@@ -30,7 +30,7 @@ export interface ExecSettingsInterface {
30
30
  stream: Readable;
31
31
  };
32
32
  log?: ExecLogSettingsType | boolean;
33
- onSpawn?: (p: ChildProcess) => any;
33
+ onSpawn?: (p: ChildProcess) => void | undefined;
34
34
  stdout?: {
35
35
  save?: boolean;
36
36
  parseLines?: boolean | "skip-empty";
@@ -103,6 +103,4 @@ export declare function createProcess<O1 extends boolean, O2 extends boolean>(co
103
103
  stderr: string;
104
104
  } : {})>;
105
105
  export declare function exec(command: string, argv?: string[], options?: SpawnOptions | null, settings?: ExecSettingsInterface): Promise<ExecResultType>;
106
- type EventNameType = "exit" | "SIGINT" | "SIGUSR1" | "SIGUSR2" | "SIGTERM" | "uncaughtException";
107
- export declare function onExit(cb: (eventName: EventNameType, ...args: any[]) => void): void;
108
106
  export {};
package/utils/process.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.onExit = exports.exec = exports.createProcess = exports.logProcessExec = exports.waitForClose = exports.parseStreamData = exports.logExecStderr = exports.logExecStdout = void 0;
6
+ exports.exec = exports.createProcess = exports.logProcessExec = exports.waitForClose = exports.parseStreamData = exports.logExecStderr = exports.logExecStdout = void 0;
7
7
  const cli_1 = require("./cli");
8
8
  const fs_1 = require("./fs");
9
9
  const math_1 = require("./math");
@@ -179,46 +179,45 @@ async function exec(command, argv = [], options = null, settings = {}) {
179
179
  else if (settings.log) {
180
180
  log = settings.log;
181
181
  }
182
- return new Promise(async (resolve, reject) => {
183
- if (log.exec) {
184
- logProcessExec(command, argv, {
185
- env: options?.env,
186
- envNames: log.envNames,
187
- pipe: pipe?.stream,
188
- toStderr: log.allToStderr,
189
- });
190
- }
191
- if (typeof options?.cwd === "string" && !(await (0, fs_1.existsDir)(options.cwd)))
192
- return reject(new Error(`Current working directory does not exist: ${options.cwd}`));
193
- if (pipe?.stream instanceof fs_2.ReadStream && "onReadProgress" in pipe) {
194
- const fileInfo = await (0, promises_1.stat)(pipe.stream.path);
195
- const totalBytes = fileInfo.size;
196
- let currentBytes = 0;
197
- pipe.stream.on("data", (data) => {
198
- currentBytes += data.length;
199
- pipe.onReadProgress?.({
200
- totalBytes: totalBytes,
201
- currentBytes: currentBytes,
202
- progress: (0, math_1.progressPercent)(totalBytes, currentBytes),
203
- });
182
+ if (log.exec)
183
+ logProcessExec(command, argv, {
184
+ env: options?.env,
185
+ envNames: log.envNames,
186
+ pipe: pipe?.stream,
187
+ toStderr: log.allToStderr,
188
+ });
189
+ if (typeof options?.cwd === "string" && !(await (0, fs_1.existsDir)(options.cwd)))
190
+ throw new Error(`Current working directory does not exist: ${options.cwd}`);
191
+ if (pipe?.stream instanceof fs_2.ReadStream && "onReadProgress" in pipe) {
192
+ const fileInfo = await (0, promises_1.stat)(pipe.stream.path);
193
+ const totalBytes = fileInfo.size;
194
+ let currentBytes = 0;
195
+ pipe.stream.on("data", (data) => {
196
+ currentBytes += data.length;
197
+ pipe.onReadProgress?.({
198
+ totalBytes: totalBytes,
199
+ currentBytes: currentBytes,
200
+ progress: (0, math_1.progressPercent)(totalBytes, currentBytes),
204
201
  });
205
- }
206
- const p = (0, child_process_1.spawn)(command, argv, options ?? {});
207
- await settings.onSpawn?.(p);
208
- let spawnError;
209
- const spawnData = {
210
- stdout: "",
211
- stderr: "",
212
- exitCode: 0,
213
- };
214
- let finishListeners = 1;
215
- if (pipe?.stream instanceof fs_2.WriteStream)
216
- finishListeners++;
217
- if (settings.stdout?.parseLines)
218
- finishListeners++;
219
- if (settings.stderr?.parseLines)
220
- finishListeners++;
221
- let streamError;
202
+ });
203
+ }
204
+ const p = (0, child_process_1.spawn)(command, argv, options ?? {});
205
+ settings.onSpawn?.(p);
206
+ let spawnError;
207
+ const spawnData = {
208
+ stdout: "",
209
+ stderr: "",
210
+ exitCode: 0,
211
+ };
212
+ let finishListeners = 1;
213
+ if (pipe?.stream instanceof fs_2.WriteStream)
214
+ finishListeners++;
215
+ if (settings.stdout?.parseLines)
216
+ finishListeners++;
217
+ if (settings.stderr?.parseLines)
218
+ finishListeners++;
219
+ let streamError;
220
+ return new Promise((resolve, reject) => {
222
221
  const tryFinish = () => {
223
222
  if (!--finishListeners)
224
223
  finish();
@@ -336,17 +335,3 @@ async function exec(command, argv = [], options = null, settings = {}) {
336
335
  });
337
336
  }
338
337
  exports.exec = exec;
339
- const eventNames = [
340
- `exit`,
341
- `SIGINT`,
342
- `SIGUSR1`,
343
- `SIGUSR2`,
344
- `uncaughtException`,
345
- `SIGTERM`,
346
- ];
347
- function onExit(cb) {
348
- for (const eventName of eventNames) {
349
- process.on(eventName, (...args) => cb(eventName, ...args));
350
- }
351
- }
352
- exports.onExit = onExit;
@@ -1,5 +1,10 @@
1
1
  /// <reference types="node" />
2
2
  import { Timer } from "./date";
3
+ export type BasicProgress = {
4
+ percent: number;
5
+ current: number;
6
+ total: number;
7
+ };
3
8
  export type ProgressStats = {
4
9
  percent?: number;
5
10
  total?: number;
@@ -24,7 +29,7 @@ export declare class ProgressManager {
24
29
  };
25
30
  protected timer: Timer;
26
31
  protected interval: Timer | undefined;
27
- protected keydownListener: ((data: Buffer) => void) | undefined;
32
+ protected keydownListener: ((data: Buffer | undefined) => void) | undefined;
28
33
  readonly tty: boolean;
29
34
  readonly enabled: boolean | "interval";
30
35
  constructor(options: {
package/utils/progress.js CHANGED
@@ -1,12 +1,10 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.renderProgressStats = exports.renderProgress = exports.ProgressManager = void 0;
4
+ const bytes_1 = require("./bytes");
7
5
  const cli_1 = require("./cli");
8
6
  const date_1 = require("./date");
9
- const bytes_1 = __importDefault(require("bytes"));
7
+ const exit_1 = require("./exit");
10
8
  const chalk_1 = require("chalk");
11
9
  const readline_1 = require("readline");
12
10
  class ProgressManager {
@@ -41,10 +39,10 @@ class ProgressManager {
41
39
  process.stdin?.resume();
42
40
  process.stdin?.setEncoding("utf8");
43
41
  process.stdin?.on("keypress", (this.keydownListener = (inKey) => {
44
- const key = inKey.toString();
42
+ const key = inKey?.toString() || "";
45
43
  if (key === "\u0003") {
46
44
  process.stdin.setRawMode?.(false);
47
- process.emit("SIGINT");
45
+ (0, exit_1.triggerExitEvent)("SIGINT");
48
46
  }
49
47
  else if (/^(\r\n)|\r|\n$/.test(key)) {
50
48
  this.interval = undefined;
@@ -89,7 +87,7 @@ function renderProgressStats(stats, progressBar) {
89
87
  text.push(`${stats.percent.toFixed(2).padStart(5, " ")}%`);
90
88
  }
91
89
  if (typeof stats.current === "number" || typeof stats.total === "number") {
92
- const format = (value) => stats.format === "size" ? (0, bytes_1.default)(value) : value.toString();
90
+ const format = (value) => stats.format === "size" ? (0, bytes_1.formatBytes)(value) : value.toString();
93
91
  const pad = 8;
94
92
  let values = [];
95
93
  if (typeof stats.current === "number" && typeof stats.total === "number") {
package/utils/tar.d.ts CHANGED
@@ -1,12 +1,8 @@
1
+ import { BasicProgress } from "./progress";
1
2
  import type { JSONSchema7 } from "json-schema";
2
- export type Progress = {
3
- percent: number;
4
- current: number;
5
- total: number;
6
- };
7
3
  export type TarEntry = {
8
4
  path: string;
9
- progress: Progress;
5
+ progress: BasicProgress;
10
6
  };
11
7
  export type CoresOptions = number | {
12
8
  percent: number;
@@ -1,4 +1,5 @@
1
1
  import { DiskStats } from "./fs";
2
+ import { BasicProgress } from "./progress";
2
3
  export declare function resolvePath(path: string): string;
3
4
  export type FsOptions = {
4
5
  backend: string;
@@ -10,6 +11,7 @@ export declare abstract class AbstractFs {
10
11
  abstract isLocal(): boolean;
11
12
  isRemote(): boolean;
12
13
  abstract existsDir(path: string): Promise<boolean>;
14
+ abstract rename(source: string, target: string): Promise<void>;
13
15
  abstract mkdir(path: string): Promise<void>;
14
16
  abstract readFile(path: string): Promise<string>;
15
17
  abstract rmAll(path: string): Promise<void>;
@@ -18,12 +20,16 @@ export declare abstract class AbstractFs {
18
20
  abstract ensureEmptyDir(path: string): Promise<void>;
19
21
  abstract writeFile(path: string, contents: string): Promise<void>;
20
22
  abstract upload(source: string, target: string): Promise<void>;
21
- abstract download(source: string, target: string): Promise<void>;
23
+ abstract download(source: string, target: string, options?: {
24
+ timeout?: number;
25
+ onProgress?: (progress: BasicProgress) => void;
26
+ }): Promise<void>;
22
27
  abstract fetchDiskStats(source: string): Promise<DiskStats>;
23
28
  }
24
29
  export declare class LocalFs extends AbstractFs {
25
30
  isLocal(): boolean;
26
31
  existsDir(path: string): Promise<boolean>;
32
+ rename(source: string, target: string): Promise<void>;
27
33
  mkdir(path: string): Promise<void>;
28
34
  ensureEmptyDir(path: string): Promise<void>;
29
35
  readFile(path: string): Promise<string>;
@@ -29,6 +29,9 @@ class LocalFs extends AbstractFs {
29
29
  async existsDir(path) {
30
30
  return (0, fs_1.existsDir)(this.resolvePath(path));
31
31
  }
32
+ async rename(source, target) {
33
+ await (0, promises_1.rename)(this.resolvePath(source), this.resolvePath(target));
34
+ }
32
35
  async mkdir(path) {
33
36
  await (0, fs_1.mkdirIfNotExists)(this.resolvePath(path));
34
37
  }