dxfl 0.1.3 → 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/auth.js ADDED
@@ -0,0 +1,81 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { Configuration, WebsiteApi } from "guichet-sdk-ts";
11
+ import { read } from 'read';
12
+ import path from 'node:path';
13
+ import fs from 'node:fs/promises';
14
+ function configPath() {
15
+ let path = ".dfl/config.json";
16
+ if (process.env.XDG_CONFIG_HOME) {
17
+ path = process.env.XDG_CONFIG_HOME + "/dfl/config.json";
18
+ }
19
+ else if (process.env.HOME) {
20
+ path = process.env.HOME + "/.config/dfl/config.json";
21
+ }
22
+ return path;
23
+ }
24
+ export function openApiConf() {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ let strConf;
27
+ let dictConf;
28
+ const configFile = configPath();
29
+ try {
30
+ strConf = yield fs.readFile(configFile, { encoding: 'utf8' });
31
+ }
32
+ catch (err) {
33
+ console.error(err, `\n\nUnable to read ${configFile}, run 'dfl login' first.`);
34
+ process.exit(1);
35
+ }
36
+ try {
37
+ dictConf = JSON.parse(strConf);
38
+ }
39
+ catch (err) {
40
+ console.error(err, `\n\nUnable to parse ${configFile} as JSON, check your syntax. Did you manually edit this file?`);
41
+ process.exit(1);
42
+ }
43
+ // @FIXME: we do not validate that the dictConf object really contains a username or password field...
44
+ return new Configuration({
45
+ username: dictConf.username,
46
+ password: dictConf.password,
47
+ });
48
+ });
49
+ }
50
+ export function login(username) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ const password = yield read({
53
+ prompt: "password: ",
54
+ silent: true,
55
+ replace: "*"
56
+ });
57
+ // check config
58
+ const testConf = new Configuration({
59
+ username: username,
60
+ password: password,
61
+ });
62
+ const web = new WebsiteApi(testConf);
63
+ try {
64
+ yield web.listWebsites();
65
+ }
66
+ catch (err) {
67
+ console.error(err, `\n\nLogin failed. Is your username and password correct?`);
68
+ process.exit(1);
69
+ }
70
+ // create config folder if needed
71
+ const configFile = configPath();
72
+ const parent = path.dirname(configFile);
73
+ yield fs.mkdir(parent, { recursive: true });
74
+ // create, serialize, save config data
75
+ const configData = { username, password };
76
+ const serializedConfig = JSON.stringify(configData);
77
+ yield fs.writeFile(configFile, serializedConfig, { mode: 0o600 });
78
+ // @FIXME: we would like to avoid storing the password in clear text in the future.
79
+ console.log('ok');
80
+ });
81
+ }
package/dist/deploy.js ADDED
@@ -0,0 +1,236 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
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
+ };
17
+ import fs from "fs";
18
+ import path from "path";
19
+ import crypto from "crypto";
20
+ import mime from "mime";
21
+ import { WebsiteApi } from "guichet-sdk-ts";
22
+ import { S3Client, ListObjectsV2Command, DeleteObjectsCommand, HeadObjectCommand, } from "@aws-sdk/client-s3";
23
+ import { Upload } from "@aws-sdk/lib-storage";
24
+ import { PromisePool } from "@supercharge/promise-pool";
25
+ import { openApiConf } from "./auth.js";
26
+ const MD5METAFIELD = "dfl-md5sum";
27
+ // Walks through the local directory at path `dir`, and for each file it contains, returns :
28
+ // - `localPath`: its path on the local filesystem (includes `dir`). On windows, this path
29
+ // will typically use `\` as separator.
30
+ // - `s3Path`: an equivalent path as we would store it in an S3 bucket, using '/' as separator.
31
+ // This path includes `s3Prefix` as a prefix if provided. If `s3Prefix` is null, `s3Path`
32
+ // is relative to the root (of the form "a/b/c", instead of "/a/b/c" if `s3Prefix` is "").
33
+ function getLocalFiles(dir, s3Prefix) {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ const entries = yield fs.promises.readdir(dir, { withFileTypes: true });
36
+ const files = yield Promise.all(entries.map(entry => {
37
+ const localPath = path.join(dir, entry.name);
38
+ const s3Path = s3Prefix ? s3Prefix + "/" + entry.name : entry.name;
39
+ if (entry.isDirectory()) {
40
+ return getLocalFiles(localPath, s3Path);
41
+ }
42
+ else {
43
+ return Promise.resolve([{ localPath, s3Path }]);
44
+ }
45
+ }));
46
+ return files.flat();
47
+ });
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
+ }
71
+ function getBucketFiles(client, Bucket) {
72
+ return __awaiter(this, void 0, void 0, function* () {
73
+ const files = new Map();
74
+ let done = false;
75
+ let cmd = new ListObjectsV2Command({ Bucket });
76
+ while (!done) {
77
+ const resp = yield client.send(cmd);
78
+ if (resp.$metadata.httpStatusCode != 200) {
79
+ // TODO: better error handling?
80
+ console.error(resp);
81
+ process.exit(1);
82
+ }
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
+ }
89
+ }
90
+ if (resp.NextContinuationToken) {
91
+ cmd = new ListObjectsV2Command({
92
+ Bucket,
93
+ ContinuationToken: resp.NextContinuationToken
94
+ });
95
+ }
96
+ else {
97
+ done = true;
98
+ }
99
+ }
100
+ return files;
101
+ });
102
+ }
103
+ function uploadFile(client, Bucket, Key, Body, md5) {
104
+ return __awaiter(this, void 0, void 0, function* () {
105
+ var _a;
106
+ // use `path.posix` because `Key` is a path in a bucket that uses `/` as separator.
107
+ let ContentType = (_a = mime.getType(path.posix.extname(Key))) !== null && _a !== void 0 ? _a : undefined;
108
+ // add charset=utf-8 by default on text files (TODO: allow the user to override this)
109
+ if (ContentType && ContentType.startsWith("text/")) {
110
+ ContentType = ContentType + "; charset=utf-8";
111
+ }
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 });
117
+ parallelUpload.on("httpUploadProgress", progress => {
118
+ process.stdout.moveCursor(0, -1);
119
+ process.stdout.clearLine(1);
120
+ process.stdout.write(`Sending ${progress.Key}`);
121
+ if (!(progress.loaded == progress.total && progress.part == 1)) {
122
+ process.stdout.write(` (${progress.loaded}/${progress.total})`);
123
+ }
124
+ process.stdout.write("\n");
125
+ });
126
+ yield parallelUpload.done();
127
+ });
128
+ }
129
+ function deleteFiles(client, Bucket, files) {
130
+ return __awaiter(this, void 0, void 0, function* () {
131
+ if (files.length == 0) {
132
+ return null;
133
+ }
134
+ return yield client.send(new DeleteObjectsCommand({
135
+ Bucket,
136
+ Delete: {
137
+ Objects: files.map(f => { return { Key: f }; }),
138
+ },
139
+ }));
140
+ });
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
+ }
170
+ export function deploy(vhost, localFolder) {
171
+ return __awaiter(this, void 0, void 0, function* () {
172
+ const conf = yield openApiConf();
173
+ // Get paths of the local files to deploy
174
+ const localFiles = yield getLocalFiles(localFolder, "").catch(err => {
175
+ if (err.errno = -2) {
176
+ console.error(`Error: directory '${localFolder}' does not exist`);
177
+ }
178
+ else {
179
+ console.error(err);
180
+ }
181
+ process.exit(1);
182
+ });
183
+ // Get website info from guichet (bucket name and keys)
184
+ const api = new WebsiteApi(conf);
185
+ let vhostInfo = yield api.getWebsite({ vhost }).catch(err => {
186
+ if (err.response.status == 404) {
187
+ console.error(`Error: website '${vhost}' does not exist`);
188
+ }
189
+ else {
190
+ console.error(err);
191
+ }
192
+ process.exit(1);
193
+ });
194
+ // List the files currently stored in the bucket
195
+ // @FIXME this info could be returned by the guichet API
196
+ const s3client = new S3Client({
197
+ endpoint: "https://garage.deuxfleurs.fr",
198
+ region: "garage",
199
+ forcePathStyle: true,
200
+ credentials: {
201
+ accessKeyId: vhostInfo.accessKeyId,
202
+ secretAccessKey: vhostInfo.secretAccessKey,
203
+ },
204
+ });
205
+ const Bucket = vhostInfo.vhost.name;
206
+ const remoteFiles = yield getBucketFiles(s3client, Bucket);
207
+ // Delete files that are present in the bucket but not locally.
208
+ // Do this before sending the new files to avoid hitting the size quota
209
+ // unnecessarily.
210
+ const resp = yield deleteFiles(s3client, Bucket, [...remoteFiles]
211
+ .filter(([name, _]) => !localFiles.find(({ s3Path }) => s3Path == name))
212
+ .map(([name, _]) => name));
213
+ if (resp && resp.$metadata.httpStatusCode != 200) {
214
+ // TODO: better error handling?
215
+ console.error(resp);
216
+ process.exit(1);
217
+ }
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
231
+ yield PromisePool
232
+ .for(localFiles)
233
+ .withConcurrency(6)
234
+ .process(({ localPath, s3Path }) => processFile(localPath, s3Path));
235
+ });
236
+ }
package/dist/vhosts.js ADDED
@@ -0,0 +1,20 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { WebsiteApi } from "guichet-sdk-ts";
11
+ import { openApiConf } from "./auth.js";
12
+ export function vhostsList() {
13
+ return __awaiter(this, void 0, void 0, function* () {
14
+ var _a;
15
+ const conf = yield openApiConf();
16
+ const web = new WebsiteApi(conf);
17
+ const wlist = yield web.listWebsites();
18
+ (_a = wlist.vhosts) === null || _a === void 0 ? void 0 : _a.forEach(v => console.log(v.name));
19
+ });
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dxfl",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "",
5
5
  "license": "EUPL-1.2",
6
6
  "author": "Deuxfleurs Team <coucou@deuxfleurs.fr>",
package/auth.ts DELETED
@@ -1,81 +0,0 @@
1
- import { Configuration, WebsiteApi } from "guichet-sdk-ts";
2
- import { read } from 'read';
3
- import path from 'node:path';
4
- import fs from 'node:fs/promises';
5
-
6
- function configPath(): string {
7
- let path = ".dfl/config.json";
8
- if (process.env.XDG_CONFIG_HOME) {
9
- path = process.env.XDG_CONFIG_HOME + "/dfl/config.json";
10
- } else if (process.env.HOME) {
11
- path = process.env.HOME + "/.config/dfl/config.json";
12
- }
13
- return path
14
- }
15
-
16
- interface PersistedConfig {
17
- username: string,
18
- password: string,
19
- }
20
-
21
- export async function openApiConf() {
22
- let strConf: string;
23
- let dictConf: PersistedConfig;
24
-
25
- const configFile = configPath();
26
-
27
- try {
28
- strConf = await fs.readFile(configFile, { encoding: 'utf8' });
29
- } catch (err) {
30
- console.error(err, `\n\nUnable to read ${configFile}, run 'dfl login' first.`);
31
- process.exit(1);
32
- }
33
-
34
- try {
35
- dictConf = JSON.parse(strConf);
36
- } catch (err) {
37
- console.error(err, `\n\nUnable to parse ${configFile} as JSON, check your syntax. Did you manually edit this file?`);
38
- process.exit(1);
39
- }
40
-
41
- // @FIXME: we do not validate that the dictConf object really contains a username or password field...
42
-
43
- return new Configuration({
44
- username: dictConf.username,
45
- password: dictConf.password,
46
- });
47
- }
48
-
49
- export async function login(username: string) {
50
- const password = await read({
51
- prompt: "password: ",
52
- silent: true,
53
- replace: "*"
54
- });
55
-
56
- // check config
57
- const testConf = new Configuration({
58
- username: username,
59
- password: password,
60
- });
61
- const web = new WebsiteApi(testConf);
62
- try {
63
- await web.listWebsites();
64
- } catch (err) {
65
- console.error(err, `\n\nLogin failed. Is your username and password correct?`);
66
- process.exit(1);
67
- }
68
-
69
- // create config folder if needed
70
- const configFile = configPath();
71
- const parent = path.dirname(configFile);
72
- await fs.mkdir(parent, { recursive: true });
73
-
74
- // create, serialize, save config data
75
- const configData: PersistedConfig = { username, password };
76
- const serializedConfig = JSON.stringify(configData);
77
- await fs.writeFile(configFile, serializedConfig, { mode: 0o600 });
78
- // @FIXME: we would like to avoid storing the password in clear text in the future.
79
-
80
- console.log('ok')
81
- }
package/deploy.ts DELETED
@@ -1,152 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import mime from "mime";
4
- import { WebsiteApi } from "guichet-sdk-ts";
5
- import {
6
- S3Client,
7
- ListObjectsV2Command,
8
- DeleteObjectsCommand,
9
- DeleteObjectsCommandOutput,
10
- } from "@aws-sdk/client-s3";
11
- import { Upload } from "@aws-sdk/lib-storage";
12
- import { PromisePool } from "@supercharge/promise-pool";
13
- import { openApiConf } from "./auth.js";
14
-
15
- // Walks through the local directory at path `dir`, and for each file it contains, returns :
16
- // - `localPath`: its path on the local filesystem (includes `dir`). On windows, this path
17
- // will typically use `\` as separator.
18
- // - `s3Path`: an equivalent path as we would store it in an S3 bucket, using '/' as separator.
19
- // This path includes `s3Prefix` as a prefix if provided. If `s3Prefix` is null, `s3Path`
20
- // is relative to the root (of the form "a/b/c", instead of "/a/b/c" if `s3Prefix` is "").
21
- async function getLocalFiles(dir: string, s3Prefix: string | null): Promise<{ localPath: string, s3Path: string}[]> {
22
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
23
- const files = await Promise.all(entries.map(entry => {
24
- const localPath = path.join(dir, entry.name);
25
- const s3Path = s3Prefix ? s3Prefix + "/" + entry.name : entry.name;
26
- if (entry.isDirectory()) {
27
- return getLocalFiles(localPath, s3Path)
28
- } else {
29
- return Promise.resolve([{ localPath, s3Path }])
30
- }
31
- }));
32
- return files.flat()
33
- }
34
-
35
- async function getBucketFiles(client: S3Client, Bucket: string): Promise<string[]> {
36
- const files = [];
37
- let done = false;
38
- let cmd = new ListObjectsV2Command({ Bucket });
39
- while (!done) {
40
- const resp = await client.send(cmd);
41
- if (resp.$metadata.httpStatusCode != 200) {
42
- // TODO: better error handling?
43
- console.error(resp);
44
- process.exit(1)
45
- }
46
-
47
- for (var item of resp.Contents!) {
48
- files.push(item.Key!)
49
- }
50
-
51
- if (resp.NextContinuationToken) {
52
- cmd = new ListObjectsV2Command({
53
- Bucket,
54
- ContinuationToken: resp.NextContinuationToken
55
- })
56
- } else {
57
- done = true
58
- }
59
- }
60
- return files
61
- }
62
-
63
- async function uploadFile(client: S3Client, Bucket: string, Key: string, Body: any) {
64
- // use `path.posix` because `Key` is a path in a bucket that uses `/` as separator.
65
- let ContentType = mime.getType(path.posix.extname(Key)) ?? undefined;
66
- // add charset=utf-8 by default on text files (TODO: allow the user to override this)
67
- if (ContentType && ContentType.startsWith("text/")) {
68
- ContentType = ContentType + "; charset=utf-8";
69
- }
70
- const parallelUpload = new Upload({ client, params: { Bucket, Key, Body, ContentType } });
71
- parallelUpload.on("httpUploadProgress", progress => {
72
- process.stdout.moveCursor(0, -1)
73
- process.stdout.clearLine(1)
74
- process.stdout.write("Sending " + progress.Key);
75
- if (! (progress.loaded == progress.total && progress.part == 1)) {
76
- process.stdout.write(" (" + progress.loaded + "/" + progress.total + ")");
77
- }
78
- process.stdout.write("\n");
79
- });
80
- await parallelUpload.done();
81
- }
82
-
83
- async function deleteFiles(client: S3Client, Bucket: string, files: string[]): Promise<DeleteObjectsCommandOutput | null> {
84
- if (files.length == 0) {
85
- return null
86
- }
87
- return await client.send(new DeleteObjectsCommand({
88
- Bucket,
89
- Delete: {
90
- Objects: files.map(f => { return { Key: f }}),
91
- },
92
- }));
93
- }
94
-
95
- export async function deploy(vhost: string, localFolder: string) {
96
- const conf = await openApiConf();
97
-
98
- // Get paths of the local files to deploy
99
- const localFiles = await getLocalFiles(localFolder, "").catch(err => {
100
- if (err.errno = -2) {
101
- console.error(`Error: directory '${localFolder}' does not exist`);
102
- } else {
103
- console.error(err);
104
- }
105
- process.exit(1)
106
- });
107
-
108
- // Get website info from guichet (bucket name and keys)
109
- const api = new WebsiteApi(conf);
110
- let vhostInfo = await api.getWebsite({ vhost }).catch(err => {
111
- if (err.response.status == 404) {
112
- console.error(`Error: website '${vhost}' does not exist`);
113
- } else {
114
- console.error(err);
115
- }
116
- process.exit(1)
117
- });
118
-
119
- // List the files currently stored in the bucket
120
- // @FIXME this info could be returned by the guichet API
121
- const s3client = new S3Client({
122
- endpoint: "https://garage.deuxfleurs.fr",
123
- region: "garage",
124
- forcePathStyle: true,
125
- credentials: {
126
- accessKeyId: vhostInfo.accessKeyId!,
127
- secretAccessKey: vhostInfo.secretAccessKey!,
128
- },
129
- });
130
- const Bucket = vhostInfo.vhost!.name!;
131
- const remoteFiles = await getBucketFiles(s3client, Bucket);
132
-
133
- // Delete files that are present in the bucket but not locally.
134
- // Do this before sending the new files to avoid hitting the size quota
135
- // unnecessarily.
136
- const resp = await deleteFiles(
137
- s3client,
138
- Bucket,
139
- remoteFiles.filter(f => !localFiles.find(({ s3Path }) => s3Path == f))
140
- );
141
- if (resp && resp!.$metadata.httpStatusCode != 200) {
142
- // TODO: better error handling?
143
- console.error(resp);
144
- process.exit(1)
145
- }
146
-
147
- // Control concurrence while uploading
148
- await PromisePool
149
- .for(localFiles)
150
- .withConcurrency(6)
151
- .process(({ localPath, s3Path }) => uploadFile(s3client, Bucket, s3Path,fs.createReadStream(localPath)));
152
- }
package/dist/auth.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import { Configuration } from "guichet-sdk-ts";
2
- export declare function openApiConf(): Promise<Configuration>;
3
- export declare function login(username: string): Promise<void>;
package/dist/deploy.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function deploy(vhost: string, localFolder: string): Promise<void>;
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/vhosts.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function vhostsList(): Promise<void>;
package/index.ts DELETED
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
- import { program } from "commander";
3
- import { login } from "./auth.js";
4
- import { deploy } from "./deploy.js";
5
- import { vhostsList } from "./vhosts.js";
6
-
7
- program
8
- .name('dxfl')
9
- .description('Deuxfleurs CLI tool')
10
- .version('0.1.0');
11
-
12
- program.command('login')
13
- .description('Link your Deuxfleurs account with this tool.')
14
- .argument('<username>', 'your account username')
15
- .action(login);
16
-
17
- program.command('list')
18
- .description('List all your websites')
19
- .action(vhostsList);
20
-
21
- program.command('deploy')
22
- .description('Deploy your website')
23
- .argument('<vhost>', 'selected vhost')
24
- .argument('<local_folder>', 'your local folder')
25
- .action(deploy)
26
-
27
- program.parse();
package/vhosts.ts DELETED
@@ -1,9 +0,0 @@
1
- import { Configuration, WebsiteApi } from "guichet-sdk-ts";
2
- import { openApiConf } from "./auth.js";
3
-
4
- export async function vhostsList() {
5
- const conf = await openApiConf();
6
- const web = new WebsiteApi(conf);
7
- const wlist = await web.listWebsites();
8
- wlist.vhosts?.forEach(v => console.log(v.name));
9
- }