dxfl 0.1.7 → 0.1.8

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/empty.js ADDED
@@ -0,0 +1,73 @@
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
+ import { getBucket, getBucketFiles, deleteBucketFiles } from "./bucket.js";
13
+ import { confirmationPrompt, formatBytesHuman, formatCount, sum, } from "./utils.js";
14
+ function emptyMain(website, options) {
15
+ return __awaiter(this, void 0, void 0, function* () {
16
+ if (options.dryRun && options.yes) {
17
+ console.error("Error: options --yes and --dry-run cannot be passed at the same time");
18
+ }
19
+ process.stdout.write("Fetching the website configuration and metadata...\n");
20
+ const guichet = yield openApiConf();
21
+ const api = new WebsiteApi(guichet);
22
+ const bucket = yield getBucket(api, website);
23
+ const filesToDelete = [...(yield getBucketFiles(bucket))].map(([name, { size }]) => ({
24
+ name,
25
+ size,
26
+ }));
27
+ const sizeDeleted = sum(filesToDelete.map(f => { var _a; return (_a = f.size) !== null && _a !== void 0 ? _a : 0; }));
28
+ function formatFile(file) {
29
+ `${file.name} (${file.size ? formatBytesHuman(file.size) : "?B"})`;
30
+ }
31
+ // If --dry-run: display the changes list and summary and return
32
+ if (options.dryRun) {
33
+ for (const file of filesToDelete) {
34
+ process.stdout.write(`Delete ${formatFile(file)}\n`);
35
+ }
36
+ process.stdout.write("\nSummary:\n");
37
+ process.stdout.write(`${formatCount(filesToDelete.length, "file")} deleted (${formatBytesHuman(sizeDeleted)})\n`);
38
+ return;
39
+ }
40
+ // If not --yes: display the summary, ask for confirmation before proceeding
41
+ if (!options.yes) {
42
+ process.stdout.write(`${formatCount(filesToDelete.length, "file")} (${formatBytesHuman(sizeDeleted)}) are going to be deleted:\n`);
43
+ const ok = yield confirmationPrompt(() => {
44
+ process.stdout.write("Files to delete:\n");
45
+ for (const file of filesToDelete) {
46
+ process.stdout.write(` ${formatFile(file)}\n`);
47
+ }
48
+ });
49
+ if (!ok) {
50
+ return;
51
+ }
52
+ }
53
+ // If confirmed
54
+ yield deleteBucketFiles(bucket, filesToDelete);
55
+ });
56
+ }
57
+ export function empty(website, options) {
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ try {
60
+ yield emptyMain(website, options);
61
+ }
62
+ catch (err) {
63
+ if (typeof err == "string" || (err === null || err === void 0 ? void 0 : err.name) === "AbortError") {
64
+ console.error(`Error: ${err}`);
65
+ }
66
+ else {
67
+ console.error("Error:");
68
+ console.error(err);
69
+ }
70
+ process.exit(1);
71
+ }
72
+ });
73
+ }
package/dist/index.js CHANGED
@@ -1,23 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import { program } from "commander";
2
+ import { program } from "@commander-js/extra-typings";
3
3
  import { login } from "./auth.js";
4
4
  import { deploy } from "./deploy.js";
5
+ import { empty } from "./empty.js";
5
6
  import { vhostsList } from "./vhosts.js";
7
+ program.name("dxfl").description("Deuxfleurs CLI tool").version("0.1.8");
6
8
  program
7
- .name('dxfl')
8
- .description('Deuxfleurs CLI tool')
9
- .version('0.1.7');
10
- program.command('login')
11
- .description('Link your Deuxfleurs account with this tool.')
12
- .argument('<username>', 'your account username')
9
+ .command("login")
10
+ .description("Link your Deuxfleurs account with this tool.")
11
+ .argument("<username>", "your account username")
13
12
  .action(login);
14
- program.command('list')
15
- .description('List all your websites')
13
+ program
14
+ .command("list")
15
+ .description("List all your websites")
16
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
- .option('-n, --dry-run', 'do a trial run without making actual changes')
17
+ program
18
+ .command("deploy")
19
+ .description("Deploy your website")
20
+ .argument("<website>", "selected website")
21
+ .argument("<local_folder>", "your local folder")
22
+ .option("-n, --dry-run", "do a trial run without making actual changes")
23
+ .option("-y, --yes", "apply the changes without asking for confirmation")
22
24
  .action(deploy);
25
+ program
26
+ .command("empty")
27
+ .description("Empty your website from its contents")
28
+ .argument("<website>", "selected website")
29
+ .option("-n, --dry-run", "do a trial run without making actual changes")
30
+ .option("-y, --yes", "apply the changes without asking for confirmation")
31
+ .action(empty);
23
32
  program.parse();
package/dist/utils.js ADDED
@@ -0,0 +1,113 @@
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 crypto from "crypto";
19
+ import { stdin, stdout } from "process";
20
+ import readline from "readline/promises";
21
+ export function getFileMd5(file) {
22
+ return __awaiter(this, void 0, void 0, function* () {
23
+ var _a, e_1, _b, _c;
24
+ const hash = crypto.createHash("md5");
25
+ try {
26
+ for (var _d = true, _e = __asyncValues(fs.createReadStream(file)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
27
+ _c = _f.value;
28
+ _d = false;
29
+ const chunk = _c;
30
+ hash.update(chunk);
31
+ }
32
+ }
33
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
34
+ finally {
35
+ try {
36
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
37
+ }
38
+ finally { if (e_1) throw e_1.error; }
39
+ }
40
+ return hash.digest("hex");
41
+ });
42
+ }
43
+ export function formatBytesHuman(bytes) {
44
+ if (bytes < 1000) {
45
+ return `${bytes}B`;
46
+ }
47
+ bytes /= 1000;
48
+ if (bytes < 1000) {
49
+ return `${bytes.toFixed(2)}KB`;
50
+ }
51
+ bytes /= 1000;
52
+ if (bytes < 1000) {
53
+ return `${bytes.toFixed(2)}MB`;
54
+ }
55
+ bytes /= 1000;
56
+ return `${bytes.toFixed(2)}GB`;
57
+ }
58
+ export function toChunks(a, chunkSize) {
59
+ let chunks = [];
60
+ let i = 0;
61
+ while (i < a.length) {
62
+ chunks.push(a.slice(i, i + chunkSize));
63
+ i = i + chunkSize;
64
+ }
65
+ return chunks;
66
+ }
67
+ export function sum(a) {
68
+ return a.reduce((x, y) => x + y, 0);
69
+ }
70
+ export function formatCount(nb, thing) {
71
+ if (nb == 1) {
72
+ return `${nb} ${thing}`;
73
+ }
74
+ else {
75
+ return `${nb} ${thing}s`;
76
+ }
77
+ }
78
+ export function parseEtag(s) {
79
+ if (s.startsWith('"') && s.endsWith('"')) {
80
+ return s.slice(1, s.length - 1);
81
+ }
82
+ else {
83
+ return undefined;
84
+ }
85
+ }
86
+ export function confirmationPrompt(details) {
87
+ return __awaiter(this, void 0, void 0, function* () {
88
+ const rl = readline.createInterface({ input: stdin, output: stdout });
89
+ let ok = true;
90
+ while (true) {
91
+ const a = yield rl.question(`Proceed? y (yes), n (no), d (details): `);
92
+ if (a == "y" || a == "yes") {
93
+ break;
94
+ }
95
+ else if (a == "n" || a == "no") {
96
+ ok = false;
97
+ break;
98
+ }
99
+ else if (a == "d" || a == "details") {
100
+ details();
101
+ process.stdout.write("\n");
102
+ }
103
+ }
104
+ rl.close();
105
+ return ok;
106
+ });
107
+ }
108
+ export function filterMap(a, f) {
109
+ return a.flatMap(x => {
110
+ let y = f(x);
111
+ return y ? [y] : [];
112
+ });
113
+ }
@@ -0,0 +1,410 @@
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 TOML from "smol-toml";
12
+ import URI from "fast-uri";
13
+ import { z as zod } from "zod";
14
+ import { fromError as zodError } from "zod-validation-error";
15
+ import { GetBucketWebsiteCommand, PutBucketWebsiteCommand, } from "@aws-sdk/client-s3";
16
+ import { getBucketFilesDetails } from "./bucket.js";
17
+ ////////////// Utilities
18
+ export function equalBucketRedirect(b1, b2) {
19
+ function equalTo(x, y) {
20
+ if (x.kind == "replace_prefix" && y.kind == "replace_prefix") {
21
+ return x.prefix === y.prefix;
22
+ }
23
+ else if (x.kind == "replace" && y.kind == "replace") {
24
+ return x.target === y.target;
25
+ }
26
+ else {
27
+ return false;
28
+ }
29
+ }
30
+ return (b1.prefix === b2.prefix &&
31
+ b1.if_error === b2.if_error &&
32
+ b1.hostname === b2.hostname &&
33
+ b1.status === b2.status &&
34
+ b1.protocol === b2.protocol &&
35
+ equalTo(b1.to, b2.to));
36
+ }
37
+ ////////////// Parsing from a TOML config file
38
+ // Parsing: TOML -> untyped object
39
+ function readConfigFileObject(filename) {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ if (!fs.existsSync(filename)) {
42
+ return {};
43
+ }
44
+ let strConf;
45
+ let dictConf;
46
+ try {
47
+ strConf = yield fs.promises.readFile(filename, { encoding: "utf8" });
48
+ }
49
+ catch (err) {
50
+ console.error(err, `\n\nUnable to read ${filename}`);
51
+ process.exit(1);
52
+ }
53
+ try {
54
+ dictConf = TOML.parse(strConf);
55
+ }
56
+ catch (err) {
57
+ console.error(err, `\n\nUnable to parse ${filename} as TOML, please check it for syntax errors.`);
58
+ process.exit(1);
59
+ }
60
+ return dictConf;
61
+ });
62
+ }
63
+ // Parsing: untyped object -> RawConfig
64
+ const RawRedirectSchema = zod.object({
65
+ from: zod.string(),
66
+ to: zod.string(),
67
+ if_error: zod.number().int().optional(),
68
+ status: zod.number().int().positive().optional(),
69
+ });
70
+ const RawConfigSchema = zod.object({
71
+ index_page: zod.string().optional(),
72
+ error_page: zod.string().optional(),
73
+ redirects: zod.array(RawRedirectSchema).optional(),
74
+ });
75
+ function interpRawConfig(cfg) {
76
+ try {
77
+ return RawConfigSchema.parse(cfg);
78
+ }
79
+ catch (err) {
80
+ const validationError = zodError(err);
81
+ throw validationError.toString();
82
+ }
83
+ }
84
+ // Parsing: RawConfig -> WebsiteConfig
85
+ // Parses the escaping scheme used for 'from' and 'to' paths
86
+ // in redirects. These fields may specify a '*' at the end
87
+ // (in that case they indicate a prefix) or not (then they are
88
+ // absolute URLs.
89
+ // Occurences of '*' that are part of the path itself must be
90
+ // doubled as '**' in order to avoid any ambiguity.
91
+ function unescape(s) {
92
+ let out = "";
93
+ const iter = s[Symbol.iterator]();
94
+ while (true) {
95
+ const elt = iter.next();
96
+ if (elt.done == true) {
97
+ return { s: out, is_prefix: false };
98
+ }
99
+ if (elt.value == "*") {
100
+ const next = iter.next();
101
+ if (next.done == true) {
102
+ return { s: out, is_prefix: true };
103
+ }
104
+ else if (next.value == "*") {
105
+ out += "*";
106
+ }
107
+ else {
108
+ throw `a single '*' is only allowed at the end for wildcards; please use '**' instead`;
109
+ }
110
+ }
111
+ else {
112
+ out += elt.value;
113
+ }
114
+ }
115
+ }
116
+ function interpConfig(rawcfg) {
117
+ var _a, _b;
118
+ function interpPath(name, str) {
119
+ try {
120
+ return unescape(str);
121
+ }
122
+ catch (err) {
123
+ throw `${name}: ${err}`;
124
+ }
125
+ }
126
+ function interpRedirect(i, r) {
127
+ var _a;
128
+ const rfrom = interpPath(`Redirect ${i}, from`, r.from);
129
+ const rto = interpPath(`Redirect ${i}, to`, r.to);
130
+ let redirect;
131
+ if (rfrom.is_prefix) {
132
+ // This is a bucket redirect
133
+ const toURI = URI.parse(rto.s);
134
+ if (toURI.error) {
135
+ throw `Redirect ${i}, 'to': ${toURI.error}`;
136
+ }
137
+ if (toURI.scheme && toURI.scheme != "http" && toURI.scheme != "https") {
138
+ throw `Redirect ${i}, 'to': unsupported URI scheme ${toURI.scheme}; http or https required`;
139
+ }
140
+ if (toURI.port) {
141
+ throw `Redirect ${i}, 'to': specifying a port is not supported`;
142
+ }
143
+ if (toURI.query) {
144
+ throw `Redirect ${i}, 'to': specifying a query is not supported`;
145
+ }
146
+ if (toURI.fragment) {
147
+ throw `Redirect ${i}, 'to': specifying a fragment is not supported`;
148
+ }
149
+ let path = (_a = toURI.path) !== null && _a !== void 0 ? _a : "";
150
+ // - In case of an absolute URL e.g. `https://foo.net/abc`, toURI.path will
151
+ // be equal to `/abc`, leading to redirect URLs with a double slash e.g.
152
+ // `https://foo.net//abc`. This works but removing the extra / in nicer.
153
+ // - In case of a "relative" URL e.g. `/abc`, setting this as redirect
154
+ // will result in garage sending `//abc` as redirect, which does not
155
+ // result in a working relative path. So we remove the extra / in this
156
+ // case as well.
157
+ if (path.startsWith("/")) {
158
+ path = path.substring(1);
159
+ }
160
+ // Similarly, if the user used `from = "/a*"` they likely meant to ask for
161
+ // `from = "a*"`, so we remove a level of `/` here as well. Otherwise,
162
+ // the redirect would only match for queries with double slashes, e.g.
163
+ // `https://foo.net//a`.
164
+ let prefix = rfrom.s;
165
+ if (prefix.startsWith("/")) {
166
+ prefix = prefix.substring(1);
167
+ }
168
+ let bredirect = {
169
+ prefix,
170
+ if_error: undefined,
171
+ hostname: toURI.host,
172
+ protocol: toURI.scheme,
173
+ // Unless specified, set the status to 302.
174
+ // Garage returns status=302 when reading back a bucket redirection
175
+ // that was written with status=undefined, and we want to be able to
176
+ // read back the same redirect as we wrote to be able to properly
177
+ // detect changes.
178
+ status: 302,
179
+ to: rto.is_prefix
180
+ ? { kind: "replace_prefix", prefix: path }
181
+ : { kind: "replace", target: path },
182
+ };
183
+ if (r.if_error) {
184
+ if (r.if_error != 404) {
185
+ throw `Redirect ${i}: the only currently supported value for 'if_error' is 404`;
186
+ }
187
+ bredirect.if_error = 404;
188
+ }
189
+ if (r.status) {
190
+ if ([301, 302, 303, 307, 308].includes(r.status)) {
191
+ bredirect.status = r.status;
192
+ }
193
+ else if ([200, 404].includes(r.status)) {
194
+ if (toURI.host || toURI.scheme) {
195
+ throw `Redirect ${i}: a status of 200 or 404 is not accepted while specifying a protocol and hostname in the destination`;
196
+ }
197
+ bredirect.status = r.status;
198
+ }
199
+ else {
200
+ throw `Redirect ${i}: unsupported status code ${r.status}`;
201
+ }
202
+ }
203
+ redirect = { kind: "bucket", r: bredirect };
204
+ }
205
+ else {
206
+ // This is an object redirect
207
+ if (r.if_error) {
208
+ throw `Redirect ${i}: 'if_error' is not supported for single redirections`;
209
+ }
210
+ if (r.status) {
211
+ throw (`Redirect ${i}: 'status' is not supported for single redirections.\n` +
212
+ "All single redirections have status code 301.");
213
+ }
214
+ if (rto.is_prefix) {
215
+ throw `Redirect ${i}: cannot use a prefix target in 'to' when 'from' is a single address`;
216
+ }
217
+ // if 'from' starts with a /, consider that the user meant to refer to an object
218
+ // at the root of the bucket and not an object with "/" in its name. Thus we should
219
+ // remove the /.
220
+ let from = rfrom.s;
221
+ if (from.startsWith("/")) {
222
+ from = from.substring(1);
223
+ }
224
+ // for the 'to' field, garage expects a target that starts with 'http://', 'https://'
225
+ // or '/'. So if the 'to' field does not start with '/', add one...
226
+ let to = rto.s;
227
+ if (!(to.startsWith("http://") ||
228
+ to.startsWith("https://") ||
229
+ to.startsWith("/"))) {
230
+ to = "/".concat(to);
231
+ }
232
+ redirect = { kind: "object", from, to };
233
+ }
234
+ return redirect;
235
+ }
236
+ let cfg = {
237
+ // use index.html as default index if not specified explicitly
238
+ index_page: (_a = rawcfg.index_page) !== null && _a !== void 0 ? _a : "index.html",
239
+ error_page: rawcfg.error_page,
240
+ bucket_redirects: [],
241
+ object_redirects: new Map(),
242
+ };
243
+ for (const [i, raw] of ((_b = rawcfg.redirects) !== null && _b !== void 0 ? _b : []).entries()) {
244
+ // `i+1` is only used for display: start counting redirects from 1 instead of 0
245
+ const r = interpRedirect(i + 1, raw);
246
+ if (r.kind == "bucket") {
247
+ cfg.bucket_redirects.push(r.r);
248
+ }
249
+ else {
250
+ if (cfg.object_redirects.has(r.from)) {
251
+ throw `Cannot have two redirects from the same path ${r.from}`;
252
+ }
253
+ cfg.object_redirects.set(r.from, r.to);
254
+ }
255
+ }
256
+ return cfg;
257
+ }
258
+ export function readConfigFile(filename) {
259
+ return __awaiter(this, void 0, void 0, function* () {
260
+ return interpConfig(interpRawConfig(yield readConfigFileObject(filename)));
261
+ });
262
+ }
263
+ ////////////// Reading from a S3 bucket
264
+ export function getBucketConfig(bucket, files) {
265
+ return __awaiter(this, void 0, void 0, function* () {
266
+ // This function performs the following S3 requests:
267
+ // - GetBucketWebsite to get bucket-level redirects;
268
+ // - A Head command for each file in the bucket to collect object redirects.
269
+ // (This can become relatively slow for buckets with thousands of files,
270
+ // but I don't know of a better way.)
271
+ var _a, _b;
272
+ const [response, details] = yield Promise.all([
273
+ bucket.client.send(new GetBucketWebsiteCommand({ Bucket: bucket.name })),
274
+ getBucketFilesDetails(bucket, files),
275
+ ]);
276
+ if (response && response.$metadata.httpStatusCode != 200) {
277
+ throw `Error sending GetBucketWebsite: ${response}`;
278
+ }
279
+ // Collect object redirects
280
+ const object_redirects = new Map();
281
+ for (const [file, { redirect }] of details) {
282
+ if (redirect) {
283
+ object_redirects.set(file, redirect);
284
+ }
285
+ }
286
+ // Interpret bucket redirects
287
+ if (response.RedirectAllRequestsTo) {
288
+ // NB: garage does not currently support RedirectAllRequestsTo so this should never happen
289
+ throw (`remote website configuration: RedirectAllRequestsTo is specified; ` +
290
+ `this is currently unsupported by dxfl`);
291
+ }
292
+ const index_page = (_a = response.IndexDocument) === null || _a === void 0 ? void 0 : _a.Suffix;
293
+ const error_page = (_b = response.ErrorDocument) === null || _b === void 0 ? void 0 : _b.Key;
294
+ let bucket_redirects = [];
295
+ if (response.RoutingRules) {
296
+ for (const rule of response.RoutingRules) {
297
+ // If no Condition is specified, then the redirect always applies, which is equivalent
298
+ // to matching on an empty prefix
299
+ let prefix = "";
300
+ let if_error = undefined;
301
+ if (rule.Condition) {
302
+ if (rule.Condition.HttpErrorCodeReturnedEquals) {
303
+ if (rule.Condition.HttpErrorCodeReturnedEquals == "404") {
304
+ if_error = 404;
305
+ }
306
+ else {
307
+ // currently not supported by garage
308
+ throw (`remote website configuration: 'if_error' specified with a different ` +
309
+ `status code than 404; this is currently unsupported by dxfl`);
310
+ }
311
+ }
312
+ if (rule.Condition.KeyPrefixEquals) {
313
+ prefix = rule.Condition.KeyPrefixEquals;
314
+ }
315
+ }
316
+ const hostname = rule.Redirect.HostName;
317
+ const status = rule.Redirect.HttpRedirectCode
318
+ ? parseInt(rule.Redirect.HttpRedirectCode)
319
+ : undefined;
320
+ let protocol = undefined;
321
+ if (rule.Redirect.Protocol) {
322
+ if (rule.Redirect.Protocol == "http" ||
323
+ rule.Redirect.Protocol == "https") {
324
+ protocol = rule.Redirect.Protocol;
325
+ }
326
+ else {
327
+ // currently not supported by garage
328
+ throw (`remote website configuration: 'protocol' is neither http or https; ` +
329
+ `this is currently unsupported by dxfl`);
330
+ }
331
+ }
332
+ let to;
333
+ if (rule.Redirect.ReplaceKeyPrefixWith) {
334
+ to = {
335
+ kind: "replace_prefix",
336
+ prefix: rule.Redirect.ReplaceKeyPrefixWith,
337
+ };
338
+ }
339
+ else if (rule.Redirect.ReplaceKeyWith) {
340
+ to = { kind: "replace", target: rule.Redirect.ReplaceKeyWith };
341
+ }
342
+ else {
343
+ // not completely sure whether this is the correct behavior, but it should
344
+ // match garage's implementation
345
+ to = { kind: "replace", target: "" };
346
+ }
347
+ bucket_redirects.push({
348
+ prefix,
349
+ if_error,
350
+ hostname,
351
+ status,
352
+ protocol,
353
+ to,
354
+ });
355
+ }
356
+ }
357
+ return { index_page, error_page, bucket_redirects, object_redirects };
358
+ });
359
+ }
360
+ ////////////// Applying a configuration to S3
361
+ // applies the bucket-wide part of the website configuration
362
+ export function putBucketWebsiteConfig(bucket, index_page, error_page, bucket_redirects) {
363
+ return __awaiter(this, void 0, void 0, function* () {
364
+ // TODO: printing to show what changes are being applied?
365
+ const WebsiteConfiguration = {};
366
+ if (error_page) {
367
+ WebsiteConfiguration.ErrorDocument = { Key: error_page };
368
+ }
369
+ if (index_page) {
370
+ WebsiteConfiguration.IndexDocument = { Suffix: index_page };
371
+ }
372
+ if (bucket_redirects.length > 0) {
373
+ WebsiteConfiguration.RoutingRules = [];
374
+ for (const r of bucket_redirects) {
375
+ const rule = {
376
+ Condition: {
377
+ KeyPrefixEquals: r.prefix,
378
+ },
379
+ Redirect: {},
380
+ };
381
+ if (r.if_error) {
382
+ rule.Condition.HttpErrorCodeReturnedEquals = r.if_error.toString();
383
+ }
384
+ if (r.hostname) {
385
+ rule.Redirect.HostName = r.hostname;
386
+ }
387
+ if (r.status) {
388
+ rule.Redirect.HttpRedirectCode = r.status;
389
+ }
390
+ if (r.protocol) {
391
+ rule.Redirect.Protocol = r.protocol;
392
+ }
393
+ if (r.to.kind == "replace_prefix") {
394
+ rule.Redirect.ReplaceKeyPrefixWith = r.to.prefix;
395
+ }
396
+ else if (r.to.kind == "replace") {
397
+ rule.Redirect.ReplaceKeyWith = r.to.target;
398
+ }
399
+ WebsiteConfiguration.RoutingRules.push(rule);
400
+ }
401
+ }
402
+ const resp = yield bucket.client.send(new PutBucketWebsiteCommand({
403
+ Bucket: bucket.name,
404
+ WebsiteConfiguration,
405
+ }));
406
+ if (resp && resp.$metadata.httpStatusCode != 200) {
407
+ throw resp;
408
+ }
409
+ });
410
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dxfl",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "",
5
5
  "license": "EUPL-1.2",
6
6
  "author": "Deuxfleurs Team <coucou@deuxfleurs.fr>",
@@ -10,17 +10,28 @@
10
10
  "dxfl": "dist/index.js"
11
11
  },
12
12
  "scripts": {
13
- "prepare": "npx tsc"
13
+ "prepare": "npx tsc",
14
+ "prettier": "npx prettier . --write",
15
+ "prettier-watch": "npx -y onchange \"**/*\" -- npx prettier --write --ignore-unknown {{changed}}",
16
+ "prettier-check": "npx prettier . --check"
14
17
  },
15
18
  "dependencies": {
16
19
  "@aws-sdk/client-s3": "^3.750.0",
17
- "@aws-sdk/lib-storage": "^3.750.0",
20
+ "@commander-js/extra-typings": "^13.1.0",
18
21
  "@supercharge/promise-pool": "^3.2.0",
19
22
  "@types/node": "^22.13.5",
20
23
  "commander": "^13.1.0",
24
+ "fast-uri": "^3.0.6",
21
25
  "guichet-sdk-ts": "^0.1.0",
22
26
  "mime": "^4.0.6",
23
27
  "read": "^4.1.0",
24
- "typescript": "^5.7.3"
28
+ "smol-toml": "^1.3.4",
29
+ "typescript": "^5.7.3",
30
+ "zod": "^3.24.4",
31
+ "zod-validation-error": "^3.4.1"
32
+ },
33
+ "devDependencies": {
34
+ "onchange": "^7.1.0",
35
+ "prettier": "3.5.3"
25
36
  }
26
37
  }