@datatruck/cli 0.36.5 → 0.36.6

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.
@@ -1 +1,2 @@
1
1
  export declare function calcFileHash(path: string, algorithm: string): Promise<string>;
2
+ export declare function assertFileChecksum(path: string, checksum: string, algorithm: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.calcFileHash = void 0;
3
+ exports.assertFileChecksum = exports.calcFileHash = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const fs_1 = require("fs");
6
6
  function calcFileHash(path, algorithm) {
@@ -13,3 +13,9 @@ function calcFileHash(path, algorithm) {
13
13
  });
14
14
  }
15
15
  exports.calcFileHash = calcFileHash;
16
+ async function assertFileChecksum(path, checksum, algorithm) {
17
+ const fileChecksum = await calcFileHash(path, algorithm);
18
+ if (fileChecksum !== checksum)
19
+ throw new Error(`Invalid checksum file: ${checksum} != ${fileChecksum}`);
20
+ }
21
+ exports.assertFileChecksum = assertFileChecksum;
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import { DiskStats } from "../fs";
2
3
  import { BasicProgress } from "../progress";
3
4
  import { AbstractFs, FsOptions } from "../virtual-fs";
@@ -12,12 +13,12 @@ export declare class RemoteFs extends AbstractFs {
12
13
  });
13
14
  isLocal(): boolean;
14
15
  protected fetchJson(name: string, params: any[]): Promise<any>;
15
- protected post(name: string, params: any[], data: string): Promise<void>;
16
- existsDir(path: string): Promise<any>;
17
- rename(source: string, target: string): Promise<any>;
18
- mkdir(path: string): Promise<any>;
19
- readFile(path: string): Promise<any>;
20
- readdir(path: string): Promise<any>;
16
+ protected post(name: string, params: any[], data: string): Promise<Response>;
17
+ existsDir(path: string): Promise<boolean>;
18
+ rename(source: string, target: string): Promise<void>;
19
+ mkdir(path: string): Promise<void>;
20
+ readFile(path: string): Promise<string>;
21
+ readdir(path: string): Promise<string[]>;
21
22
  readFileIfExists(path: string): Promise<string | undefined>;
22
23
  ensureEmptyDir(path: string): Promise<void>;
23
24
  writeFile(path: string, contents: string): Promise<void>;
@@ -27,19 +27,19 @@ class RemoteFs extends virtual_fs_1.AbstractFs {
27
27
  return false;
28
28
  }
29
29
  async fetchJson(name, params) {
30
- return await (0, http_1.fetchJson)(`${this.url}/${name}`, {
30
+ const url = (0, http_1.createHref)(`${this.url}/${name}`, {
31
+ params: JSON.stringify(params),
32
+ });
33
+ return await (0, http_1.fetchJson)(url, {
31
34
  headers: this.headers,
32
- query: {
33
- params: JSON.stringify(params),
34
- },
35
35
  });
36
36
  }
37
37
  async post(name, params, data) {
38
- return await (0, http_1.post)(`${this.url}/${name}`, data, {
38
+ const url = (0, http_1.createHref)(`${this.url}/${name}`, {
39
+ params: JSON.stringify(params),
40
+ });
41
+ return await (0, http_1.post)(url, data, {
39
42
  headers: this.headers,
40
- query: {
41
- params: JSON.stringify(params),
42
- },
43
43
  });
44
44
  }
45
45
  async existsDir(path) {
@@ -67,7 +67,7 @@ class RemoteFs extends virtual_fs_1.AbstractFs {
67
67
  await this.post("writeFile", [path], contents);
68
68
  }
69
69
  async rmAll(path) {
70
- await this.fetchJson("rmAll", [path]);
70
+ return await this.fetchJson("rmAll", [path]);
71
71
  }
72
72
  async fetchDiskStats(path) {
73
73
  if (this.options.verbose)
@@ -77,20 +77,23 @@ class RemoteFs extends virtual_fs_1.AbstractFs {
77
77
  async upload(source, target) {
78
78
  if (this.options.verbose)
79
79
  (0, cli_1.logExec)("fs.upload", [source, target]);
80
- await (0, http_1.uploadFile)(`${this.url}/upload`, source, {
80
+ const url = (0, http_1.createHref)(`${this.url}/upload`, {
81
+ params: JSON.stringify([target]),
82
+ });
83
+ return await (0, http_1.uploadFile)(url, source, {
81
84
  headers: this.headers,
82
- query: {
83
- params: JSON.stringify([target]),
84
- },
85
+ checksum: true,
85
86
  });
86
87
  }
87
88
  async download(source, target, options = {}) {
88
89
  if (this.options.verbose)
89
90
  (0, cli_1.logExec)("fs.download", [source, target]);
90
- return await (0, http_1.downloadFile)(`${this.url}/download`, target, {
91
+ const url = (0, http_1.createHref)(`${this.url}/download`, {
92
+ params: JSON.stringify([source]),
93
+ });
94
+ return await (0, http_1.downloadFile)(url, target, {
91
95
  ...options,
92
96
  headers: this.headers,
93
- query: { params: JSON.stringify([source]) },
94
97
  });
95
98
  }
96
99
  }
@@ -6,10 +6,7 @@ const cli_1 = require("../cli");
6
6
  const http_1 = require("../http");
7
7
  const math_1 = require("../math");
8
8
  const virtual_fs_1 = require("../virtual-fs");
9
- const fs_1 = require("fs");
10
- const promises_1 = require("fs/promises");
11
9
  const http_2 = require("http");
12
- const promises_2 = require("stream/promises");
13
10
  exports.headerKey = {
14
11
  user: "x-dtt-user",
15
12
  password: "x-dtt-password",
@@ -91,16 +88,12 @@ function createDatatruckRepositoryServer(inOptions, config = {}) {
91
88
  else if (action === "upload") {
92
89
  const [target] = params;
93
90
  const path = fs.resolvePath(target);
94
- const file = (0, fs_1.createWriteStream)(path);
95
- await (0, promises_2.pipeline)(req, file);
91
+ await (0, http_1.recvFile)(req, res, path);
96
92
  }
97
93
  else if (action === "download") {
98
94
  const [target] = params;
99
95
  const path = fs.resolvePath(target);
100
- const file = (0, fs_1.createReadStream)(path);
101
- const fileStat = await (0, promises_1.stat)(path);
102
- res.setHeader("Content-Length", fileStat.size);
103
- await (0, promises_2.pipeline)(file, res, { end: false });
96
+ await (0, http_1.sendFile)(req, res, path);
104
97
  }
105
98
  else if (action === "writeFile") {
106
99
  const data = await (0, http_1.readRequestData)(req);
@@ -123,7 +116,7 @@ function createDatatruckRepositoryServer(inOptions, config = {}) {
123
116
  (0, cli_1.logJson)("repository-server", "request failed", { id });
124
117
  console.error(error);
125
118
  }
126
- if (!res.headersSent)
119
+ if (!res.writableEnded && !res.headersSent)
127
120
  res.writeHead(500, error.message);
128
121
  }
129
122
  finally {
@@ -135,12 +128,8 @@ function createDatatruckRepositoryServer(inOptions, config = {}) {
135
128
  (0, cli_1.logJson)("repository-server", "response error", { id });
136
129
  console.error(responseError);
137
130
  }
138
- if (requestError || responseError) {
139
- res.destroy();
140
- }
141
- else {
131
+ if (!res.writableEnded)
142
132
  res.end();
143
- }
144
133
  }
145
134
  });
146
135
  }
@@ -1,31 +1,27 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { BasicProgress } from "./progress";
3
- import { IncomingMessage, Server } from "http";
4
+ import { IncomingMessage, Server, ServerResponse } from "http";
5
+ export declare function createHref(inUrl: string, query?: Record<string, string>): string;
4
6
  export declare function closeServer(server: Server): Promise<void>;
5
7
  export declare function readRequestData(req: IncomingMessage): Promise<string | undefined>;
6
- export type FetchOptions = {
7
- headers?: Record<string, string>;
8
- query?: Record<string, string>;
9
- statusError?: boolean;
10
- };
11
- export declare function fetch(url: string, options?: FetchOptions): Promise<{
12
- data: string | undefined;
13
- status: number | undefined;
14
- }>;
15
- export declare function fetchJson<T = any>(url: string, options?: FetchOptions): Promise<T | undefined>;
16
- export declare function post(url: string, data: string, options?: {
17
- headers?: Record<string, string>;
18
- query?: Record<string, string>;
8
+ export declare const safeFetch: typeof fetch;
9
+ export declare function fetchJson<T = any>(url: string, options?: RequestInit): Promise<T | undefined>;
10
+ export declare function post(url: string, data: string, options?: Omit<RequestInit, "method" | "body">): Promise<Response>;
11
+ export declare function parseContentLength(value: string | undefined): number;
12
+ export declare function sendFile(req: IncomingMessage, res: ServerResponse, path: string, options?: {
13
+ end?: boolean;
14
+ checksum?: boolean;
15
+ }): Promise<void>;
16
+ export declare function recvFile(req: IncomingMessage, res: ServerResponse, path: string, options?: {
17
+ end?: boolean;
19
18
  }): Promise<void>;
20
- export declare function downloadFile(url: string, output: string, options?: {
21
- headers?: Record<string, string>;
22
- query?: Record<string, string>;
19
+ export declare function downloadFile(url: string, output: string, options?: Omit<RequestInit, "signal"> & {
23
20
  timeout?: number;
24
21
  onProgress?: (progress: BasicProgress) => void;
25
22
  }): Promise<{
26
23
  bytes: number;
27
24
  }>;
28
- export declare function uploadFile(url: string, path: string, options?: {
29
- headers?: Record<string, string>;
30
- query?: Record<string, string>;
25
+ export declare function uploadFile(url: string, path: string, options?: Omit<RequestInit, "method" | "body"> & {
26
+ checksum?: boolean;
31
27
  }): Promise<void>;
package/lib/utils/http.js CHANGED
@@ -1,22 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.uploadFile = exports.downloadFile = exports.post = exports.fetchJson = exports.fetch = exports.readRequestData = exports.closeServer = void 0;
3
+ exports.uploadFile = exports.downloadFile = exports.recvFile = exports.sendFile = exports.parseContentLength = exports.post = exports.fetchJson = exports.safeFetch = exports.readRequestData = exports.closeServer = exports.createHref = void 0;
4
+ const crypto_1 = require("./crypto");
4
5
  const math_1 = require("./math");
5
6
  const fs_1 = require("fs");
6
7
  const promises_1 = require("fs/promises");
7
- const http_1 = require("http");
8
- const https_1 = require("https");
9
- const request = (url, options, callback) => {
10
- return url.startsWith("https://")
11
- ? (0, https_1.request)(url, options, callback)
12
- : (0, http_1.request)(url, options, callback);
13
- };
14
- function href(inUrl, query) {
8
+ const stream_1 = require("stream");
9
+ const promises_2 = require("stream/promises");
10
+ function createHref(inUrl, query) {
15
11
  const url = new URL(inUrl);
16
12
  for (const key in query || {})
17
13
  url.searchParams.set(key, query[key]);
18
14
  return url.href;
19
15
  }
16
+ exports.createHref = createHref;
20
17
  async function closeServer(server) {
21
18
  await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
22
19
  }
@@ -37,140 +34,150 @@ function readRequestData(req) {
37
34
  });
38
35
  }
39
36
  exports.readRequestData = readRequestData;
40
- async function fetch(url, options = {}) {
41
- const throwStatusCodeError = options.statusError ?? true;
42
- return new Promise((resolve, reject) => {
43
- let data;
44
- request(href(url, options.query), {
45
- method: "GET",
46
- headers: options.headers,
47
- }, (res) => {
48
- if (throwStatusCodeError && res.statusCode !== 200)
49
- return reject(new Error(`GET failed: ${res.statusCode} ${res.statusMessage}`));
50
- res
51
- .on("data", (chunk) => {
52
- if (data === undefined)
53
- data = "";
54
- data += chunk;
55
- })
56
- .on("error", reject)
57
- .on("close", () => {
58
- resolve({ data, status: res.statusCode });
59
- });
60
- })
61
- .on("error", reject)
62
- .end();
63
- });
64
- }
65
- exports.fetch = fetch;
37
+ const safeFetch = async (...args) => {
38
+ const res = await fetch(...args);
39
+ if (res.status !== 200)
40
+ throw new Error(`Fetch request failed: ${res.status} ${res.statusText}`);
41
+ return res;
42
+ };
43
+ exports.safeFetch = safeFetch;
66
44
  async function fetchJson(url, options = {}) {
67
- const res = await fetch(url, options);
68
- if (res.data === undefined)
69
- return;
70
- return JSON.parse(res.data);
45
+ const res = await (0, exports.safeFetch)(url, options);
46
+ const data = await res.text();
47
+ return data.length ? JSON.parse(data) : undefined;
71
48
  }
72
49
  exports.fetchJson = fetchJson;
73
50
  async function post(url, data, options = {}) {
74
- await new Promise((resolve, reject) => {
75
- const req = request(href(url, options.query), { method: "POST", headers: options.headers }, (res) => {
51
+ return await (0, exports.safeFetch)(url, { ...options, method: "POST", body: data });
52
+ }
53
+ exports.post = post;
54
+ function parseContentLength(value) {
55
+ if (!value || !/^\d+$/.test(value))
56
+ throw new Error(`Invalid 'content-length': ${value}`);
57
+ return Number(value);
58
+ }
59
+ exports.parseContentLength = parseContentLength;
60
+ async function sendFile(req, res, path, options = {}) {
61
+ let file;
62
+ try {
63
+ file = (0, fs_1.createReadStream)(path);
64
+ const fileStat = await (0, promises_1.stat)(path);
65
+ res.setHeader("Content-Length", fileStat.size);
66
+ if (options.checksum)
67
+ res.setHeader("x-checksum", await (0, crypto_1.calcFileHash)(path, "sha1"));
68
+ file.pipe(res);
69
+ await new Promise((resolve, reject) => {
70
+ file.on("error", reject);
71
+ req.on("error", reject);
72
+ res.on("error", reject).on("close", resolve);
73
+ });
74
+ }
75
+ finally {
76
+ file?.close();
77
+ if (options.end)
78
+ res.end();
79
+ }
80
+ }
81
+ exports.sendFile = sendFile;
82
+ async function recvFile(req, res, path, options = {}) {
83
+ let file;
84
+ try {
85
+ file = (0, fs_1.createWriteStream)(path);
86
+ req.pipe(file);
87
+ await new Promise((resolve, reject) => {
88
+ file.on("error", reject).on("close", resolve);
89
+ req.on("error", reject);
76
90
  res.on("error", reject);
77
- if (res.statusCode !== 200) {
78
- reject(new Error(`Post failed: ${res.statusCode} ${res.statusMessage}`));
79
- }
80
- else {
81
- resolve();
82
- }
83
91
  });
84
- req.on("error", reject);
85
- req.write(data);
86
- req.end();
87
- });
92
+ const checksum = res.getHeader("x-checksum");
93
+ if (typeof checksum === "string")
94
+ await (0, crypto_1.assertFileChecksum)(path, checksum, "sha1");
95
+ }
96
+ finally {
97
+ file?.close();
98
+ if (options.end)
99
+ res.end();
100
+ }
88
101
  }
89
- exports.post = post;
102
+ exports.recvFile = recvFile;
90
103
  async function downloadFile(url, output, options = {}) {
91
- const timeout = options.timeout ?? 3600 * 1000; // 60m
104
+ const { timeout, onProgress, ...fetchOptions } = options;
92
105
  const file = (0, fs_1.createWriteStream)(output);
93
- let total = 0;
94
- await new Promise((resolve, reject) => {
95
- const req = request(href(url, options.query), {
96
- headers: options.headers,
97
- }, (res) => {
98
- const contentLength = res.headers["content-length"] ?? "";
99
- if (!/^\d+$/.test(contentLength))
100
- return reject(new Error(`Invalid 'content-length': ${contentLength}`));
101
- total = Number(contentLength);
102
- let current = 0;
103
- if (res.statusCode === 200) {
104
- if (options.onProgress) {
105
- res.on("data", (chunk) => {
106
- current += chunk.byteLength;
107
- options.onProgress({
108
- percent: (0, math_1.progressPercent)(total, current),
109
- current,
110
- total,
111
- });
112
- });
113
- }
114
- res
115
- .on("error", async (error) => {
106
+ let checksum;
107
+ const length = { total: 0, current: 0 };
108
+ let requestError;
109
+ try {
110
+ const res = await (0, exports.safeFetch)(url, {
111
+ ...fetchOptions,
112
+ signal: AbortSignal.timeout(timeout ?? 3600 * 1000), // 60m
113
+ });
114
+ length.total = parseContentLength(res.headers.get("content-length") ?? undefined);
115
+ checksum = res.headers.get("x-checksum") ?? undefined;
116
+ const body = stream_1.Readable.fromWeb(res.body);
117
+ const progress = onProgress &&
118
+ new stream_1.Transform({
119
+ transform(chunk, encoding, callback) {
120
+ let error;
116
121
  try {
117
- file.destroy();
122
+ length.current += chunk.byteLength;
123
+ onProgress({
124
+ percent: (0, math_1.progressPercent)(length.total, length.current),
125
+ current: length.current,
126
+ total: length.total,
127
+ });
118
128
  }
119
- catch (_) { }
120
- try {
121
- await (0, promises_1.unlink)(output);
129
+ catch (progressError) {
130
+ error = progressError;
122
131
  }
123
- catch (_) { }
124
- reject(error);
125
- })
126
- .pipe(file);
127
- file.on("finish", () => {
128
- file.close((error) => {
129
- error ? reject(error) : resolve();
130
- });
131
- });
132
- }
133
- else {
134
- reject(new Error(`Download failed: ${res.statusCode} ${res.statusMessage}`));
135
- }
136
- }).on("error", async (error) => {
137
- try {
138
- await (0, promises_1.unlink)(output);
139
- }
140
- catch (_) { }
141
- reject(error);
142
- });
143
- req.setTimeout(timeout, () => {
144
- req.destroy();
145
- reject(new Error(`Request timeout after ${timeout / 1000}s`));
146
- });
147
- req.end();
148
- });
149
- const { size: bytes } = await (0, promises_1.stat)(output);
150
- if (total !== bytes)
151
- throw new Error(`Invalid download size: ${total} != ${bytes}`);
152
- return { bytes };
132
+ callback(error, chunk);
133
+ },
134
+ });
135
+ if (progress) {
136
+ await (0, promises_2.pipeline)(body, progress, file);
137
+ }
138
+ else {
139
+ await (0, promises_2.pipeline)(body, file);
140
+ }
141
+ const { size: fileLength } = await (0, promises_1.stat)(output);
142
+ if (length.total !== fileLength)
143
+ throw new Error(`Invalid download size: ${length.total} != ${fileLength}`);
144
+ }
145
+ catch (error) {
146
+ try {
147
+ await (0, promises_1.unlink)(output);
148
+ }
149
+ catch (_) { }
150
+ throw error;
151
+ }
152
+ if (checksum)
153
+ await (0, crypto_1.assertFileChecksum)(output, checksum, "sha1");
154
+ if (requestError)
155
+ throw requestError;
156
+ return { bytes: length.total };
153
157
  }
154
158
  exports.downloadFile = downloadFile;
155
159
  async function uploadFile(url, path, options = {}) {
156
160
  const { size } = await (0, promises_1.stat)(path);
157
- const readStream = (0, fs_1.createReadStream)(path);
158
- await new Promise((resolve, reject) => {
159
- const req = request(href(url, options.query), {
161
+ const file = (0, fs_1.createReadStream)(path);
162
+ try {
163
+ const res = await fetch(url, {
164
+ ...options,
160
165
  method: "POST",
166
+ duplex: "half",
161
167
  headers: {
162
168
  ...options.headers,
163
- "Content-length": size,
169
+ "Content-Length": size.toString(),
170
+ ...(options.checksum && {
171
+ "x-checksum": await (0, crypto_1.calcFileHash)(path, "sha1"),
172
+ }),
164
173
  },
165
- }, (res) => {
166
- if (res.statusCode !== 200) {
167
- reject(new Error(`Upload failed: ${res.statusCode} ${res.statusMessage}`));
168
- }
169
- else {
170
- resolve();
171
- }
172
- }).on("error", reject);
173
- readStream.on("error", reject).pipe(req);
174
- });
174
+ body: file,
175
+ });
176
+ if (res.status !== 200)
177
+ new Error(`Upload failed: ${res.status} ${res.statusText}`);
178
+ }
179
+ finally {
180
+ file.close();
181
+ }
175
182
  }
176
183
  exports.uploadFile = uploadFile;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datatruck/cli",
3
- "version": "0.36.5",
3
+ "version": "0.36.6",
4
4
  "description": "Tool for creating and managing backups",
5
5
  "homepage": "https://github.com/swordev/datatruck#readme",
6
6
  "bugs": {