dxfl 0.1.4 → 0.1.5
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/README.md +18 -9
- package/dist/deploy.js +92 -11
- package/dist/index.js +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,18 +4,10 @@ A CLI tool to manage your Deuxfleurs account.
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
From NPM:
|
|
8
|
-
|
|
9
7
|
```
|
|
10
8
|
npm install -g dxfl
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
From git:
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
npm install -g git+https://git.deuxfleurs.fr/Deuxfleurs/dxfl
|
|
17
|
-
```
|
|
18
|
-
|
|
19
11
|
## Usage
|
|
20
12
|
|
|
21
13
|
*Not ready*
|
|
@@ -44,7 +36,24 @@ dxfl deploy example.com _public
|
|
|
44
36
|
git clone https://git.deuxfleurs.fr/Deuxfleurs/dxfl
|
|
45
37
|
cd dxfl
|
|
46
38
|
npm install
|
|
47
|
-
|
|
39
|
+
npm link
|
|
40
|
+
dxfl
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Release
|
|
44
|
+
|
|
45
|
+
First you need an account on npmjs.com and be a maintainer of the `dxfl` package (ask quentin).
|
|
46
|
+
Do not forget also to run `npm login` to bind your account with the CLI.
|
|
47
|
+
|
|
48
|
+
Then to publish a release:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
vim package.json # update the version in this file
|
|
52
|
+
git commit -a -m 'set version 0.1.5' # commit your change
|
|
53
|
+
git push # send update
|
|
54
|
+
git tag -m 'v0.1.5' v0.1.5 # create associated tag
|
|
55
|
+
git push --tags # push tag
|
|
56
|
+
npm publish # build and push the package
|
|
48
57
|
```
|
|
49
58
|
|
|
50
59
|
## License
|
package/dist/deploy.js
CHANGED
|
@@ -7,14 +7,23 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
11
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
12
|
+
var m = o[Symbol.asyncIterator], i;
|
|
13
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
14
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
15
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
16
|
+
};
|
|
10
17
|
import fs from "fs";
|
|
11
18
|
import path from "path";
|
|
19
|
+
import crypto from "crypto";
|
|
12
20
|
import mime from "mime";
|
|
13
21
|
import { WebsiteApi } from "guichet-sdk-ts";
|
|
14
|
-
import { S3Client, ListObjectsV2Command, DeleteObjectsCommand, } from "@aws-sdk/client-s3";
|
|
22
|
+
import { S3Client, ListObjectsV2Command, DeleteObjectsCommand, HeadObjectCommand, } from "@aws-sdk/client-s3";
|
|
15
23
|
import { Upload } from "@aws-sdk/lib-storage";
|
|
16
24
|
import { PromisePool } from "@supercharge/promise-pool";
|
|
17
25
|
import { openApiConf } from "./auth.js";
|
|
26
|
+
const MD5METAFIELD = "dfl-md5sum";
|
|
18
27
|
// Walks through the local directory at path `dir`, and for each file it contains, returns :
|
|
19
28
|
// - `localPath`: its path on the local filesystem (includes `dir`). On windows, this path
|
|
20
29
|
// will typically use `\` as separator.
|
|
@@ -37,9 +46,31 @@ function getLocalFiles(dir, s3Prefix) {
|
|
|
37
46
|
return files.flat();
|
|
38
47
|
});
|
|
39
48
|
}
|
|
49
|
+
function getFileMd5(file) {
|
|
50
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
+
var _a, e_1, _b, _c;
|
|
52
|
+
const hash = crypto.createHash('md5');
|
|
53
|
+
try {
|
|
54
|
+
for (var _d = true, _e = __asyncValues(fs.createReadStream(file)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
55
|
+
_c = _f.value;
|
|
56
|
+
_d = false;
|
|
57
|
+
const chunk = _c;
|
|
58
|
+
hash.update(chunk);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
62
|
+
finally {
|
|
63
|
+
try {
|
|
64
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
65
|
+
}
|
|
66
|
+
finally { if (e_1) throw e_1.error; }
|
|
67
|
+
}
|
|
68
|
+
return hash.digest('hex');
|
|
69
|
+
});
|
|
70
|
+
}
|
|
40
71
|
function getBucketFiles(client, Bucket) {
|
|
41
72
|
return __awaiter(this, void 0, void 0, function* () {
|
|
42
|
-
const files =
|
|
73
|
+
const files = new Map();
|
|
43
74
|
let done = false;
|
|
44
75
|
let cmd = new ListObjectsV2Command({ Bucket });
|
|
45
76
|
while (!done) {
|
|
@@ -49,8 +80,12 @@ function getBucketFiles(client, Bucket) {
|
|
|
49
80
|
console.error(resp);
|
|
50
81
|
process.exit(1);
|
|
51
82
|
}
|
|
52
|
-
|
|
53
|
-
|
|
83
|
+
if (resp.Contents) {
|
|
84
|
+
for (const item of resp.Contents) {
|
|
85
|
+
if (item.Key) {
|
|
86
|
+
files.set(item.Key, { size: item.Size });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
54
89
|
}
|
|
55
90
|
if (resp.NextContinuationToken) {
|
|
56
91
|
cmd = new ListObjectsV2Command({
|
|
@@ -65,7 +100,7 @@ function getBucketFiles(client, Bucket) {
|
|
|
65
100
|
return files;
|
|
66
101
|
});
|
|
67
102
|
}
|
|
68
|
-
function uploadFile(client, Bucket, Key, Body) {
|
|
103
|
+
function uploadFile(client, Bucket, Key, Body, md5) {
|
|
69
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
70
105
|
var _a;
|
|
71
106
|
// use `path.posix` because `Key` is a path in a bucket that uses `/` as separator.
|
|
@@ -74,13 +109,17 @@ function uploadFile(client, Bucket, Key, Body) {
|
|
|
74
109
|
if (ContentType && ContentType.startsWith("text/")) {
|
|
75
110
|
ContentType = ContentType + "; charset=utf-8";
|
|
76
111
|
}
|
|
77
|
-
|
|
112
|
+
// store the md5 checksum in the object metadata; it will be used to skip
|
|
113
|
+
// subsequent uploads if the file has not changed.
|
|
114
|
+
const Metadata = { [MD5METAFIELD]: md5 };
|
|
115
|
+
const params = { Bucket, Key, Body, ContentType, Metadata };
|
|
116
|
+
const parallelUpload = new Upload({ client, params });
|
|
78
117
|
parallelUpload.on("httpUploadProgress", progress => {
|
|
79
118
|
process.stdout.moveCursor(0, -1);
|
|
80
119
|
process.stdout.clearLine(1);
|
|
81
|
-
process.stdout.write(
|
|
120
|
+
process.stdout.write(`Sending ${progress.Key}`);
|
|
82
121
|
if (!(progress.loaded == progress.total && progress.part == 1)) {
|
|
83
|
-
process.stdout.write(
|
|
122
|
+
process.stdout.write(` (${progress.loaded}/${progress.total})`);
|
|
84
123
|
}
|
|
85
124
|
process.stdout.write("\n");
|
|
86
125
|
});
|
|
@@ -100,6 +139,34 @@ function deleteFiles(client, Bucket, files) {
|
|
|
100
139
|
}));
|
|
101
140
|
});
|
|
102
141
|
}
|
|
142
|
+
// Checks whether a remote file needs to be updated by its local copy.
|
|
143
|
+
//
|
|
144
|
+
// We first check whether files differ, and if not compare the md5 checksum we
|
|
145
|
+
// previously stored in the object metadata (if it exists) with the local file's
|
|
146
|
+
// md5 checksum.
|
|
147
|
+
function needsUpdate(client, localFile, localMd5, Bucket, Key, remoteSize) {
|
|
148
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
149
|
+
if (remoteSize) {
|
|
150
|
+
const localSize = (yield fs.promises.stat(localFile)).size;
|
|
151
|
+
if (localSize == 0 /* stat can return 0 in case of error */
|
|
152
|
+
|| localSize != remoteSize) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// fetch metadata for the object and see if we previously stored its md5
|
|
157
|
+
const resp = yield client.send(new HeadObjectCommand({ Bucket, Key }));
|
|
158
|
+
if (resp.$metadata.httpStatusCode != 200) {
|
|
159
|
+
// TODO: better error handling?
|
|
160
|
+
throw resp;
|
|
161
|
+
}
|
|
162
|
+
const remoteMd5 = resp.Metadata ? resp.Metadata[MD5METAFIELD] : null;
|
|
163
|
+
if (!remoteMd5) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
// we have a remote md5, compare it with the local one
|
|
167
|
+
return (localMd5 != remoteMd5);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
103
170
|
export function deploy(vhost, localFolder) {
|
|
104
171
|
return __awaiter(this, void 0, void 0, function* () {
|
|
105
172
|
const conf = yield openApiConf();
|
|
@@ -140,16 +207,30 @@ export function deploy(vhost, localFolder) {
|
|
|
140
207
|
// Delete files that are present in the bucket but not locally.
|
|
141
208
|
// Do this before sending the new files to avoid hitting the size quota
|
|
142
209
|
// unnecessarily.
|
|
143
|
-
const resp = yield deleteFiles(s3client, Bucket, remoteFiles
|
|
210
|
+
const resp = yield deleteFiles(s3client, Bucket, [...remoteFiles]
|
|
211
|
+
.filter(([name, _]) => !localFiles.find(({ s3Path }) => s3Path == name))
|
|
212
|
+
.map(([name, _]) => name));
|
|
144
213
|
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
145
214
|
// TODO: better error handling?
|
|
146
215
|
console.error(resp);
|
|
147
216
|
process.exit(1);
|
|
148
217
|
}
|
|
149
|
-
//
|
|
218
|
+
// Uploads a local file unless the remote copy is the same
|
|
219
|
+
function processFile(localPath, s3Path) {
|
|
220
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
221
|
+
const localMd5 = yield getFileMd5(localPath);
|
|
222
|
+
const remoteFile = remoteFiles.get(s3Path);
|
|
223
|
+
if (!remoteFile ||
|
|
224
|
+
(yield needsUpdate(s3client, localPath, localMd5, Bucket, s3Path, remoteFile.size))) {
|
|
225
|
+
uploadFile(s3client, Bucket, s3Path, fs.createReadStream(localPath), localMd5);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
;
|
|
230
|
+
// Control concurrency while uploading
|
|
150
231
|
yield PromisePool
|
|
151
232
|
.for(localFiles)
|
|
152
233
|
.withConcurrency(6)
|
|
153
|
-
.process(({ localPath, s3Path }) =>
|
|
234
|
+
.process(({ localPath, s3Path }) => processFile(localPath, s3Path));
|
|
154
235
|
});
|
|
155
236
|
}
|
package/dist/index.js
CHANGED
|
File without changes
|