dxfl 0.1.2 → 0.1.4

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/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,155 @@
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 fs from "fs";
11
+ import path from "path";
12
+ import mime from "mime";
13
+ import { WebsiteApi } from "guichet-sdk-ts";
14
+ import { S3Client, ListObjectsV2Command, DeleteObjectsCommand, } from "@aws-sdk/client-s3";
15
+ import { Upload } from "@aws-sdk/lib-storage";
16
+ import { PromisePool } from "@supercharge/promise-pool";
17
+ import { openApiConf } from "./auth.js";
18
+ // Walks through the local directory at path `dir`, and for each file it contains, returns :
19
+ // - `localPath`: its path on the local filesystem (includes `dir`). On windows, this path
20
+ // will typically use `\` as separator.
21
+ // - `s3Path`: an equivalent path as we would store it in an S3 bucket, using '/' as separator.
22
+ // This path includes `s3Prefix` as a prefix if provided. If `s3Prefix` is null, `s3Path`
23
+ // is relative to the root (of the form "a/b/c", instead of "/a/b/c" if `s3Prefix` is "").
24
+ function getLocalFiles(dir, s3Prefix) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ const entries = yield fs.promises.readdir(dir, { withFileTypes: true });
27
+ const files = yield Promise.all(entries.map(entry => {
28
+ const localPath = path.join(dir, entry.name);
29
+ const s3Path = s3Prefix ? s3Prefix + "/" + entry.name : entry.name;
30
+ if (entry.isDirectory()) {
31
+ return getLocalFiles(localPath, s3Path);
32
+ }
33
+ else {
34
+ return Promise.resolve([{ localPath, s3Path }]);
35
+ }
36
+ }));
37
+ return files.flat();
38
+ });
39
+ }
40
+ function getBucketFiles(client, Bucket) {
41
+ return __awaiter(this, void 0, void 0, function* () {
42
+ const files = [];
43
+ let done = false;
44
+ let cmd = new ListObjectsV2Command({ Bucket });
45
+ while (!done) {
46
+ const resp = yield client.send(cmd);
47
+ if (resp.$metadata.httpStatusCode != 200) {
48
+ // TODO: better error handling?
49
+ console.error(resp);
50
+ process.exit(1);
51
+ }
52
+ for (var item of resp.Contents) {
53
+ files.push(item.Key);
54
+ }
55
+ if (resp.NextContinuationToken) {
56
+ cmd = new ListObjectsV2Command({
57
+ Bucket,
58
+ ContinuationToken: resp.NextContinuationToken
59
+ });
60
+ }
61
+ else {
62
+ done = true;
63
+ }
64
+ }
65
+ return files;
66
+ });
67
+ }
68
+ function uploadFile(client, Bucket, Key, Body) {
69
+ return __awaiter(this, void 0, void 0, function* () {
70
+ var _a;
71
+ // use `path.posix` because `Key` is a path in a bucket that uses `/` as separator.
72
+ let ContentType = (_a = mime.getType(path.posix.extname(Key))) !== null && _a !== void 0 ? _a : undefined;
73
+ // add charset=utf-8 by default on text files (TODO: allow the user to override this)
74
+ if (ContentType && ContentType.startsWith("text/")) {
75
+ ContentType = ContentType + "; charset=utf-8";
76
+ }
77
+ const parallelUpload = new Upload({ client, params: { Bucket, Key, Body, ContentType } });
78
+ parallelUpload.on("httpUploadProgress", progress => {
79
+ process.stdout.moveCursor(0, -1);
80
+ process.stdout.clearLine(1);
81
+ process.stdout.write("Sending " + progress.Key);
82
+ if (!(progress.loaded == progress.total && progress.part == 1)) {
83
+ process.stdout.write(" (" + progress.loaded + "/" + progress.total + ")");
84
+ }
85
+ process.stdout.write("\n");
86
+ });
87
+ yield parallelUpload.done();
88
+ });
89
+ }
90
+ function deleteFiles(client, Bucket, files) {
91
+ return __awaiter(this, void 0, void 0, function* () {
92
+ if (files.length == 0) {
93
+ return null;
94
+ }
95
+ return yield client.send(new DeleteObjectsCommand({
96
+ Bucket,
97
+ Delete: {
98
+ Objects: files.map(f => { return { Key: f }; }),
99
+ },
100
+ }));
101
+ });
102
+ }
103
+ export function deploy(vhost, localFolder) {
104
+ return __awaiter(this, void 0, void 0, function* () {
105
+ const conf = yield openApiConf();
106
+ // Get paths of the local files to deploy
107
+ const localFiles = yield getLocalFiles(localFolder, "").catch(err => {
108
+ if (err.errno = -2) {
109
+ console.error(`Error: directory '${localFolder}' does not exist`);
110
+ }
111
+ else {
112
+ console.error(err);
113
+ }
114
+ process.exit(1);
115
+ });
116
+ // Get website info from guichet (bucket name and keys)
117
+ const api = new WebsiteApi(conf);
118
+ let vhostInfo = yield api.getWebsite({ vhost }).catch(err => {
119
+ if (err.response.status == 404) {
120
+ console.error(`Error: website '${vhost}' does not exist`);
121
+ }
122
+ else {
123
+ console.error(err);
124
+ }
125
+ process.exit(1);
126
+ });
127
+ // List the files currently stored in the bucket
128
+ // @FIXME this info could be returned by the guichet API
129
+ const s3client = new S3Client({
130
+ endpoint: "https://garage.deuxfleurs.fr",
131
+ region: "garage",
132
+ forcePathStyle: true,
133
+ credentials: {
134
+ accessKeyId: vhostInfo.accessKeyId,
135
+ secretAccessKey: vhostInfo.secretAccessKey,
136
+ },
137
+ });
138
+ const Bucket = vhostInfo.vhost.name;
139
+ const remoteFiles = yield getBucketFiles(s3client, Bucket);
140
+ // Delete files that are present in the bucket but not locally.
141
+ // Do this before sending the new files to avoid hitting the size quota
142
+ // unnecessarily.
143
+ const resp = yield deleteFiles(s3client, Bucket, remoteFiles.filter(f => !localFiles.find(({ s3Path }) => s3Path == f)));
144
+ if (resp && resp.$metadata.httpStatusCode != 200) {
145
+ // TODO: better error handling?
146
+ console.error(resp);
147
+ process.exit(1);
148
+ }
149
+ // Control concurrence while uploading
150
+ yield PromisePool
151
+ .for(localFiles)
152
+ .withConcurrency(6)
153
+ .process(({ localPath, s3Path }) => uploadFile(s3client, Bucket, s3Path, fs.createReadStream(localPath)));
154
+ });
155
+ }
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
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
+ program
7
+ .name('dxfl')
8
+ .description('Deuxfleurs CLI tool')
9
+ .version('0.1.0');
10
+ program.command('login')
11
+ .description('Link your Deuxfleurs account with this tool.')
12
+ .argument('<username>', 'your account username')
13
+ .action(login);
14
+ program.command('list')
15
+ .description('List all your websites')
16
+ .action(vhostsList);
17
+ program.command('deploy')
18
+ .description('Deploy your website')
19
+ .argument('<vhost>', 'selected vhost')
20
+ .argument('<local_folder>', 'your local folder')
21
+ .action(deploy);
22
+ program.parse();
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,16 +1,16 @@
1
1
  {
2
2
  "name": "dxfl",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "",
5
5
  "license": "EUPL-1.2",
6
6
  "author": "Deuxfleurs Team <coucou@deuxfleurs.fr>",
7
- "type": "commonjs",
8
- "main": "bootstrap.js",
7
+ "type": "module",
8
+ "main": "dist/main.js",
9
9
  "bin": {
10
- "dxfl": "./bootstrap.js"
10
+ "dxfl": "./dist/index.js"
11
11
  },
12
12
  "scripts": {
13
- "test": "echo \"Error: no test specified\" && exit 1"
13
+ "prepare": "npx tsc"
14
14
  },
15
15
  "dependencies": {
16
16
  "@aws-sdk/client-s3": "^3.750.0",
@@ -18,9 +18,9 @@
18
18
  "@supercharge/promise-pool": "^3.2.0",
19
19
  "@types/node": "^22.13.5",
20
20
  "commander": "^13.1.0",
21
- "guichet-sdk-ts": "git+https://git.deuxfleurs.fr/Deuxfleurs/guichet-sdk-ts",
21
+ "guichet-sdk-ts": "^0.1.0",
22
22
  "mime": "^4.0.6",
23
23
  "read": "^4.1.0",
24
- "tsx": "^4.19.3"
24
+ "typescript": "^5.7.3"
25
25
  }
26
26
  }
package/tsconfig.json CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  /* Language and Environment */
14
14
  "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15
- // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
15
+ "lib": [ "es6" ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16
16
  // "jsx": "preserve", /* Specify what JSX code is generated. */
17
17
  // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18
18
  // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
@@ -25,13 +25,15 @@
25
25
  // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26
26
 
27
27
  /* Modules */
28
- "module": "commonjs", /* Specify what module code is generated. */
28
+ "module": "nodenext", /* Specify what module code is generated. */
29
29
  // "rootDir": "./", /* Specify the root folder within your source files. */
30
- // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
30
+ //"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31
31
  // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32
32
  // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33
33
  // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34
- // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
34
+ "typeRoots": [
35
+ "node_modules/@types"
36
+ ], /* Specify multiple folders that act like './node_modules/@types'. */
35
37
  // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36
38
  // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37
39
  // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
@@ -51,14 +53,14 @@
51
53
  // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
52
54
 
53
55
  /* Emit */
54
- // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
56
+ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
55
57
  // "declarationMap": true, /* Create sourcemaps for d.ts files. */
56
58
  // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
57
59
  // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
58
60
  // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59
61
  // "noEmit": true, /* Disable emitting files from a compilation. */
60
62
  // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
61
- // "outDir": "./", /* Specify an output folder for all emitted files. */
63
+ "outDir": "dist", /* Specify an output folder for all emitted files. */
62
64
  // "removeComments": true, /* Disable emitting comments. */
63
65
  // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
64
66
  // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
@@ -107,5 +109,9 @@
107
109
  /* Completeness */
108
110
  // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
109
111
  "skipLibCheck": true /* Skip type checking all .d.ts files. */
110
- }
112
+ },
113
+ "exclude": [
114
+ "dist",
115
+ "node_modules"
116
+ ]
111
117
  }
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/bootstrap.js DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- require('tsx/cjs')
3
- require('./index.ts')
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 { openApiConf } from "./auth";
13
- import Pool from "@supercharge/promise-pool";
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 Pool
149
- .for(localFiles)
150
- .withConcurrency(6)
151
- .process(({ localPath, s3Path }) => uploadFile(s3client, Bucket, s3Path,fs.createReadStream(localPath)));
152
- }
package/index.ts DELETED
@@ -1,26 +0,0 @@
1
- import { program } from "commander";
2
- import { login } from "./auth";
3
- import { deploy } from "./deploy";
4
- import { vhostsList } from "./vhosts";
5
-
6
- program
7
- .name('dxfl')
8
- .description('Deuxfleurs CLI tool')
9
- .version('0.1.0');
10
-
11
- program.command('login')
12
- .description('Link your Deuxfleurs account with this tool.')
13
- .argument('<username>', 'your account username')
14
- .action(login);
15
-
16
- program.command('list')
17
- .description('List all your websites')
18
- .action(vhostsList);
19
-
20
- program.command('deploy')
21
- .description('Deploy your website')
22
- .argument('<vhost>', 'selected vhost')
23
- .argument('<local_folder>', 'your local folder')
24
- .action(deploy)
25
-
26
- program.parse();
package/vhosts.ts DELETED
@@ -1,9 +0,0 @@
1
- import { Configuration, WebsiteApi } from "guichet-sdk-ts";
2
- import { openApiConf } from "./auth";
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
- }