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 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
- node bootstrap.js
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
- for (var item of resp.Contents) {
53
- files.push(item.Key);
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
- const parallelUpload = new Upload({ client, params: { Bucket, Key, Body, ContentType } });
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("Sending " + progress.Key);
120
+ process.stdout.write(`Sending ${progress.Key}`);
82
121
  if (!(progress.loaded == progress.total && progress.part == 1)) {
83
- process.stdout.write(" (" + progress.loaded + "/" + progress.total + ")");
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.filter(f => !localFiles.find(({ s3Path }) => s3Path == f)));
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
- // Control concurrence while uploading
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 }) => uploadFile(s3client, Bucket, s3Path, fs.createReadStream(localPath)));
234
+ .process(({ localPath, s3Path }) => processFile(localPath, s3Path));
154
235
  });
155
236
  }
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dxfl",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "",
5
5
  "license": "EUPL-1.2",
6
6
  "author": "Deuxfleurs Team <coucou@deuxfleurs.fr>",