dxfl 0.1.11 → 0.2.0
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/CHANGELOG.md +5 -0
- package/README.md +0 -2
- package/dist/auth.js +21 -21
- package/dist/bucket.js +15 -45
- package/dist/deploy.js +14 -30
- package/dist/empty.js +3 -19
- package/dist/error.js +113 -0
- package/dist/guichet.js +64 -0
- package/dist/index.js +7 -6
- package/dist/utils.js +25 -14
- package/dist/website_config.js +51 -62
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# v0.2.0
|
|
2
|
+
|
|
3
|
+
- hint towards new website names (XXX.web.deuxfleurs.fr), following an overhaul in website name handling in deuxfleurs
|
|
4
|
+
- thorough error handling overhaul: dxfl now handles errors gracefully in most cases
|
|
5
|
+
|
|
1
6
|
# v0.1.11
|
|
2
7
|
|
|
3
8
|
- Lower files upload concurrency (from 50 to 20) to avoid IO issues
|
package/README.md
CHANGED
package/dist/auth.js
CHANGED
|
@@ -7,10 +7,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { Configuration
|
|
10
|
+
import { Configuration } from "guichet-sdk-ts";
|
|
11
11
|
import { read } from "read";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import fs from "node:fs/promises";
|
|
14
|
+
import { ErrorMsg } from "./error.js";
|
|
15
|
+
import { GuichetApi } from "./guichet.js";
|
|
14
16
|
import { confirmationPrompt } from "./utils.js";
|
|
15
17
|
function configPath() {
|
|
16
18
|
let path = ".dxfl/config.json";
|
|
@@ -80,17 +82,23 @@ export function openApiConf() {
|
|
|
80
82
|
yield moveOldConfig(); // Check for old config folder to rename
|
|
81
83
|
strConf = yield fs.readFile(configFile, { encoding: "utf8" });
|
|
82
84
|
}
|
|
83
|
-
catch (
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
catch (e) {
|
|
86
|
+
let msg = `failed to read ${configFile}`;
|
|
87
|
+
if (e instanceof Error && "code" in e && e.code) {
|
|
88
|
+
msg = msg + ` (${e.code})`;
|
|
89
|
+
}
|
|
90
|
+
throw new ErrorMsg(msg);
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
try {
|
|
89
94
|
dictConf = JSON.parse(strConf);
|
|
90
95
|
}
|
|
91
|
-
catch (
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
catch (e) {
|
|
97
|
+
let msg = `Unable to parse ${configFile} as JSON. Did you manually edit this file?`;
|
|
98
|
+
if (e instanceof Error) {
|
|
99
|
+
msg = msg + ` (${e.message})`;
|
|
100
|
+
}
|
|
101
|
+
throw new ErrorMsg(msg);
|
|
94
102
|
}
|
|
95
103
|
// @FIXME: we do not validate that the dictConf object really contains a username or password field...
|
|
96
104
|
return new Configuration({
|
|
@@ -111,14 +119,8 @@ export function login(username) {
|
|
|
111
119
|
username: username,
|
|
112
120
|
password: password,
|
|
113
121
|
});
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
yield web.listWebsites();
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
console.error(err, `\n\nLogin failed. Is your username and password correct?`);
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
+
const guichet = new GuichetApi(testConf);
|
|
123
|
+
yield guichet.listWebsites();
|
|
122
124
|
// create config folder if needed
|
|
123
125
|
const configFile = configPath();
|
|
124
126
|
const parent = path.dirname(configFile);
|
|
@@ -128,7 +130,7 @@ export function login(username) {
|
|
|
128
130
|
const serializedConfig = JSON.stringify(configData);
|
|
129
131
|
yield fs.writeFile(configFile, serializedConfig, { mode: 0o600 });
|
|
130
132
|
// @FIXME: we would like to avoid storing the password in clear text in the future.
|
|
131
|
-
console.log("
|
|
133
|
+
console.log("Success.");
|
|
132
134
|
});
|
|
133
135
|
}
|
|
134
136
|
export function logout(options) {
|
|
@@ -140,12 +142,11 @@ export function logout(options) {
|
|
|
140
142
|
yield fs.access(configFile);
|
|
141
143
|
}
|
|
142
144
|
catch (err) {
|
|
143
|
-
|
|
144
|
-
process.exit(1);
|
|
145
|
+
throw new ErrorMsg(`Unable to find ${configFile}. Are you logged on this device?`);
|
|
145
146
|
}
|
|
146
147
|
// If not --yes: ask for confirmation before proceeding
|
|
147
148
|
if (!options.yes) {
|
|
148
|
-
process.stdout.write(`
|
|
149
|
+
process.stdout.write(`This will clear dxfl's config folder. Proceed? (${parentFolder})\n`);
|
|
149
150
|
const ok = yield confirmationPrompt(() => {
|
|
150
151
|
process.stdout.write("Details of planned operations:\n");
|
|
151
152
|
process.stdout.write(` Delete file ${configFile}\n`);
|
|
@@ -162,8 +163,7 @@ export function logout(options) {
|
|
|
162
163
|
process.stdout.write("You can now go outside, feel the sun and touch grass, you are free! ☀️\n");
|
|
163
164
|
}
|
|
164
165
|
catch (err) {
|
|
165
|
-
|
|
166
|
-
process.exit(1);
|
|
166
|
+
throw new ErrorMsg(`Unable to cleanup config folder. Do you have the permission to delete ${parentFolder}?`);
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
}
|
package/dist/bucket.js
CHANGED
|
@@ -9,21 +9,22 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import fs from "fs";
|
|
11
11
|
import mime from "mime";
|
|
12
|
-
import { WebsiteApi } from "guichet-sdk-ts";
|
|
13
12
|
import { DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
|
|
14
13
|
import { PromisePool } from "@supercharge/promise-pool";
|
|
15
14
|
import { apiConfExists, openApiConf } from "./auth.js";
|
|
15
|
+
import { ErrorMsg, wrapS3Call } from "./error.js";
|
|
16
|
+
import { GuichetApi } from "./guichet.js";
|
|
16
17
|
import { parseEtag, toChunks, formatBytesHuman } from "./utils.js";
|
|
17
18
|
export function getBucketCredentials(name) {
|
|
18
19
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19
20
|
if (yield apiConfExists()) {
|
|
20
|
-
const
|
|
21
|
-
return yield credentialsFromApi(
|
|
21
|
+
const guichet = new GuichetApi(yield openApiConf());
|
|
22
|
+
return yield credentialsFromApi(guichet, name);
|
|
22
23
|
}
|
|
23
24
|
else {
|
|
24
25
|
const creds = credentialsFromEnv();
|
|
25
26
|
if (creds === undefined) {
|
|
26
|
-
throw new
|
|
27
|
+
throw new ErrorMsg("Failed to load credentials.\n" +
|
|
27
28
|
"You need to run 'dxfl login', " +
|
|
28
29
|
"or define the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.");
|
|
29
30
|
}
|
|
@@ -42,19 +43,10 @@ function credentialsFromEnv() {
|
|
|
42
43
|
return undefined;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
function credentialsFromApi(
|
|
46
|
+
function credentialsFromApi(guichet, website) {
|
|
46
47
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
48
|
// Get website info from guichet (bucket name and keys)
|
|
48
|
-
const bucket = yield
|
|
49
|
-
var _a;
|
|
50
|
-
if (((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) == 404) {
|
|
51
|
-
console.error(`Error: website '${website}' does not exist`);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
console.error(err);
|
|
55
|
-
}
|
|
56
|
-
throw new Error(err);
|
|
57
|
-
});
|
|
49
|
+
const bucket = yield guichet.getWebsite(website);
|
|
58
50
|
return { key: bucket.accessKeyId, secret: bucket.secretAccessKey };
|
|
59
51
|
});
|
|
60
52
|
}
|
|
@@ -79,11 +71,7 @@ export function getBucketFiles(bucket) {
|
|
|
79
71
|
let done = false;
|
|
80
72
|
let cmd = new ListObjectsV2Command({ Bucket: bucket.name });
|
|
81
73
|
while (!done) {
|
|
82
|
-
const resp = yield bucket.client.send(cmd);
|
|
83
|
-
if (resp.$metadata.httpStatusCode != 200) {
|
|
84
|
-
// TODO: better error handling?
|
|
85
|
-
throw resp;
|
|
86
|
-
}
|
|
74
|
+
const resp = yield wrapS3Call("list bucket files", [200], () => bucket.client.send(cmd));
|
|
87
75
|
if (resp.Contents) {
|
|
88
76
|
for (const item of resp.Contents) {
|
|
89
77
|
const etag = item.ETag ? parseEtag(item.ETag) : undefined;
|
|
@@ -110,11 +98,7 @@ export function getBucketFilesDetails(bucket, files) {
|
|
|
110
98
|
let res = new Map();
|
|
111
99
|
function doFile(file) {
|
|
112
100
|
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
-
const resp = yield bucket.client.send(new HeadObjectCommand({ Bucket: bucket.name, Key: file }));
|
|
114
|
-
if (resp.$metadata.httpStatusCode != 200) {
|
|
115
|
-
// TODO: better error handling?
|
|
116
|
-
throw resp;
|
|
117
|
-
}
|
|
101
|
+
const resp = yield wrapS3Call(`read metadata of file "${file}"`, [200], () => bucket.client.send(new HeadObjectCommand({ Bucket: bucket.name, Key: file })));
|
|
118
102
|
res.set(file, {
|
|
119
103
|
redirect: resp.WebsiteRedirectLocation,
|
|
120
104
|
});
|
|
@@ -131,14 +115,10 @@ export function getBucketFilesDetails(bucket, files) {
|
|
|
131
115
|
}
|
|
132
116
|
export function deleteBucketFile(bucket, name) {
|
|
133
117
|
return __awaiter(this, void 0, void 0, function* () {
|
|
134
|
-
|
|
118
|
+
yield wrapS3Call(`delete file "${name}"`, [204], () => bucket.client.send(new DeleteObjectCommand({
|
|
135
119
|
Bucket: bucket.name,
|
|
136
120
|
Key: name,
|
|
137
|
-
}));
|
|
138
|
-
if (resp && resp.$metadata.httpStatusCode != 204) {
|
|
139
|
-
// TODO: better error handling?
|
|
140
|
-
throw resp;
|
|
141
|
-
}
|
|
121
|
+
})));
|
|
142
122
|
});
|
|
143
123
|
}
|
|
144
124
|
export function deleteBucketFiles(bucket, files) {
|
|
@@ -153,18 +133,14 @@ export function deleteBucketFiles(bucket, files) {
|
|
|
153
133
|
for (const file of files) {
|
|
154
134
|
process.stdout.write(` Delete ${file.name} (${file.size ? formatBytesHuman(file.size) : "?B"})\n`);
|
|
155
135
|
}
|
|
156
|
-
|
|
136
|
+
yield wrapS3Call(`deleting ${files.length} files`, [200], () => bucket.client.send(new DeleteObjectsCommand({
|
|
157
137
|
Bucket: bucket.name,
|
|
158
138
|
Delete: {
|
|
159
139
|
Objects: files.map(f => {
|
|
160
140
|
return { Key: f.name };
|
|
161
141
|
}),
|
|
162
142
|
},
|
|
163
|
-
}));
|
|
164
|
-
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
165
|
-
// TODO: better error handling?
|
|
166
|
-
throw resp;
|
|
167
|
-
}
|
|
143
|
+
})));
|
|
168
144
|
}));
|
|
169
145
|
});
|
|
170
146
|
}
|
|
@@ -179,10 +155,7 @@ export function uploadFile(bucket, s3Path, localPath) {
|
|
|
179
155
|
}
|
|
180
156
|
const Body = fs.createReadStream(localPath);
|
|
181
157
|
const params = { Bucket: bucket.name, Key: s3Path, Body, ContentType };
|
|
182
|
-
|
|
183
|
-
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
184
|
-
throw resp;
|
|
185
|
-
}
|
|
158
|
+
yield wrapS3Call(`upload "${s3Path}"`, [200], () => bucket.client.send(new PutObjectCommand(params)));
|
|
186
159
|
});
|
|
187
160
|
}
|
|
188
161
|
// the 'source' object may or may not exist already in the bucket.
|
|
@@ -198,9 +171,6 @@ export function putEmptyObjectRedirect(bucket, source, target) {
|
|
|
198
171
|
Body: "source of object redirect",
|
|
199
172
|
WebsiteRedirectLocation: target,
|
|
200
173
|
};
|
|
201
|
-
|
|
202
|
-
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
203
|
-
throw resp;
|
|
204
|
-
}
|
|
174
|
+
yield wrapS3Call(`create redirection source object ${source}`, [200], () => bucket.client.send(new PutObjectCommand(params)));
|
|
205
175
|
});
|
|
206
176
|
}
|
package/dist/deploy.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from "fs";
|
|
|
11
11
|
import path from "path";
|
|
12
12
|
import { PromisePool } from "@supercharge/promise-pool";
|
|
13
13
|
import { deleteBucketFile, deleteBucketFiles, getBucketCredentials, getBucket, getBucketFiles, putEmptyObjectRedirect, uploadFile, } from "./bucket.js";
|
|
14
|
+
import { ErrorMsg } from "./error.js";
|
|
14
15
|
import { confirmationPrompt, filterMap, formatBytesHuman, formatCount, getFileMd5, sum, } from "./utils.js";
|
|
15
16
|
import { equalBucketRedirect, equalCorsRules, getBucketConfig, putBucketWebsiteConfig, putCorsRules, readConfigFile, } from "./website_config.js";
|
|
16
17
|
// Walks through the local directory at path `dir`, and for each file it contains, returns :
|
|
@@ -39,10 +40,10 @@ function getLocalFilesWithInfo(localFolder) {
|
|
|
39
40
|
return __awaiter(this, void 0, void 0, function* () {
|
|
40
41
|
const localPaths = yield getLocalFiles(localFolder).catch(err => {
|
|
41
42
|
if (err.errno == -2) {
|
|
42
|
-
throw `directory '${localFolder}' does not exist
|
|
43
|
+
throw new ErrorMsg(`directory '${localFolder}' does not exist`);
|
|
43
44
|
}
|
|
44
45
|
else {
|
|
45
|
-
throw err;
|
|
46
|
+
throw new ErrorMsg(`failed to read directory ${localFolder} (${err.message})`);
|
|
46
47
|
}
|
|
47
48
|
});
|
|
48
49
|
return yield Promise.all(localPaths.map((_a) => __awaiter(this, [_a], void 0, function* ({ localPath, s3Path }) {
|
|
@@ -73,14 +74,15 @@ function computeDeployPlan(localFiles, remoteFiles, localCfg, remoteCfg) {
|
|
|
73
74
|
return ret;
|
|
74
75
|
});
|
|
75
76
|
if (localRedirectSources.length > 0) {
|
|
76
|
-
|
|
77
|
+
let msg = "the following local files are also the source of a redirection:\n";
|
|
77
78
|
for (const [f, target] of localRedirectSources) {
|
|
78
79
|
console.error(` ${f.localPath} (redirect to: ${target})`);
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
msg +=
|
|
82
|
+
"It does not make sense for a local file to also be the source of a redirection,\n" +
|
|
83
|
+
"because its contents will never be read.\n" +
|
|
84
|
+
"Please delete these files or the corresponding redirections.";
|
|
85
|
+
throw new ErrorMsg(msg);
|
|
84
86
|
}
|
|
85
87
|
// Compute object redirects that need to be set because the redirection is new,
|
|
86
88
|
// the target was changed, or because the source object changed.
|
|
@@ -165,8 +167,8 @@ function printPlan(plan, details) {
|
|
|
165
167
|
const proto = r.protocol ? `${r.protocol}://` : "";
|
|
166
168
|
const hostname = r.hostname ? `${r.hostname}/` : "";
|
|
167
169
|
const status = r.status ? ` status=${r.status}` : "";
|
|
168
|
-
const force = r.force ? `
|
|
169
|
-
return `${proto}${hostname}${to}${status}${force}`;
|
|
170
|
+
const force = r.force ? ` force=${r.force}` : "";
|
|
171
|
+
return `${proto}${hostname}${to} ${status}${force}`;
|
|
170
172
|
}
|
|
171
173
|
function showBucketRedirect(r) {
|
|
172
174
|
return `${r.prefix}* -> ${showBucketRedirectTarget(r)}`;
|
|
@@ -316,17 +318,15 @@ function applyDeployPlan(bucket, plan) {
|
|
|
316
318
|
}
|
|
317
319
|
});
|
|
318
320
|
}
|
|
319
|
-
function
|
|
321
|
+
export function deploy(website, localFolder, options) {
|
|
320
322
|
return __awaiter(this, void 0, void 0, function* () {
|
|
321
323
|
if (options.dryRun && options.yes) {
|
|
322
|
-
throw "options --yes and --dry-run cannot be passed at the same time";
|
|
324
|
+
throw new ErrorMsg("options --yes and --dry-run cannot be passed at the same time");
|
|
323
325
|
}
|
|
324
326
|
// TODO: make this configurable
|
|
325
327
|
const website_config_path = "deuxfleurs.toml";
|
|
326
328
|
// Read and validate the local configuration file before doing anything else
|
|
327
|
-
const localWebsiteConfig = yield readConfigFile(website_config_path)
|
|
328
|
-
throw `while reading ${website_config_path}:\n${err}`;
|
|
329
|
-
});
|
|
329
|
+
const localWebsiteConfig = yield readConfigFile(website_config_path);
|
|
330
330
|
process.stdout.write("Fetching the website configuration and metadata...\n");
|
|
331
331
|
const [localFiles, [bucket, remoteFiles, remoteWebsiteConfig]] = yield Promise.all([
|
|
332
332
|
// Get paths & size of the local files to deploy
|
|
@@ -373,19 +373,3 @@ function deployMain(website, localFolder, options) {
|
|
|
373
373
|
}
|
|
374
374
|
});
|
|
375
375
|
}
|
|
376
|
-
export function deploy(website, localFolder, options) {
|
|
377
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
378
|
-
try {
|
|
379
|
-
yield deployMain(website, localFolder, options);
|
|
380
|
-
}
|
|
381
|
-
catch (err) {
|
|
382
|
-
if (typeof err == "string" || (err === null || err === void 0 ? void 0 : err.name) === "AbortError") {
|
|
383
|
-
console.error(`Error: ${err}`);
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
console.error(err);
|
|
387
|
-
}
|
|
388
|
-
process.exit(1);
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
}
|
package/dist/empty.js
CHANGED
|
@@ -8,11 +8,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { getBucket, getBucketCredentials, getBucketFiles, deleteBucketFiles, } from "./bucket.js";
|
|
11
|
+
import { ErrorMsg } from "./error.js";
|
|
11
12
|
import { confirmationPrompt, formatBytesHuman, formatCount, sum, } from "./utils.js";
|
|
12
|
-
function
|
|
13
|
+
export function empty(website, options) {
|
|
13
14
|
return __awaiter(this, void 0, void 0, function* () {
|
|
14
15
|
if (options.dryRun && options.yes) {
|
|
15
|
-
|
|
16
|
+
throw new ErrorMsg("options --yes and --dry-run cannot be passed at the same time");
|
|
16
17
|
}
|
|
17
18
|
process.stdout.write("Fetching the website configuration and metadata...\n");
|
|
18
19
|
const bucket = yield getBucket(website, yield getBucketCredentials(website));
|
|
@@ -50,20 +51,3 @@ function emptyMain(website, options) {
|
|
|
50
51
|
yield deleteBucketFiles(bucket, filesToDelete);
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
|
-
export function empty(website, options) {
|
|
54
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
-
try {
|
|
56
|
-
yield emptyMain(website, options);
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
if (typeof err == "string" || (err === null || err === void 0 ? void 0 : err.name) === "AbortError") {
|
|
60
|
-
console.error(`Error: ${err}`);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
console.error("Error:");
|
|
64
|
-
console.error(err);
|
|
65
|
-
}
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
package/dist/error.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
|
+
import { S3ServiceException } from "@aws-sdk/client-s3";
|
|
11
|
+
export class ErrorCtx {
|
|
12
|
+
constructor(ctx, error) {
|
|
13
|
+
this.ctx = ctx;
|
|
14
|
+
this.error = error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Wraps any error returned by the function with some context.
|
|
18
|
+
export function withErrorCtx(ctx, f) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
try {
|
|
21
|
+
return yield f();
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
throw new ErrorCtx(ctx, e);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export class ErrorMsg {
|
|
29
|
+
constructor(msg) {
|
|
30
|
+
this.msg = msg;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function withErrorMsg(f, mapMsg) {
|
|
34
|
+
try {
|
|
35
|
+
return f();
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof ErrorMsg) {
|
|
39
|
+
throw new ErrorMsg(mapMsg(e.msg));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class ErrorGuichet {
|
|
47
|
+
constructor(cause) {
|
|
48
|
+
this.cause = cause;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Wraps a call to the S3 API, turning exceptions and unexpected error codes
|
|
52
|
+
// into a formatted error message
|
|
53
|
+
export function wrapS3Call(context, expectedCodes, f) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
const ctx = `when sending a remote command to ${context}:\n `;
|
|
56
|
+
try {
|
|
57
|
+
const resp = yield f();
|
|
58
|
+
if (!resp) {
|
|
59
|
+
throw new ErrorMsg(`${ctx}Empty response`);
|
|
60
|
+
}
|
|
61
|
+
if (!resp.$metadata.httpStatusCode) {
|
|
62
|
+
throw new ErrorMsg(`${ctx}Empty response code`);
|
|
63
|
+
}
|
|
64
|
+
if (!expectedCodes.includes(resp.$metadata.httpStatusCode)) {
|
|
65
|
+
throw new ErrorMsg(`${ctx}Unexpected response code: ${resp.$metadata.httpStatusCode}` +
|
|
66
|
+
` (expected: ${expectedCodes.join(", ")})`);
|
|
67
|
+
}
|
|
68
|
+
return resp;
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (e instanceof S3ServiceException) {
|
|
72
|
+
throw new ErrorMsg(`${ctx}${e.message}`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Must only be used in the toplevel function that implements a command.
|
|
81
|
+
// The entire program gets terminated if `e` is one of the errors above,
|
|
82
|
+
// otherwise it is thrown as an exception.
|
|
83
|
+
export function handleError(e) {
|
|
84
|
+
if (e instanceof ErrorMsg) {
|
|
85
|
+
console.error(`Error: ${e.msg}`);
|
|
86
|
+
}
|
|
87
|
+
else if (e instanceof ErrorGuichet) {
|
|
88
|
+
let msg = "Error: ";
|
|
89
|
+
if (e.cause.kind == "Unauthorized") {
|
|
90
|
+
msg +=
|
|
91
|
+
"failed to authenticate with Guichet. Is your password and login correct?";
|
|
92
|
+
}
|
|
93
|
+
else if (e.cause.kind == "WebsiteNotFound") {
|
|
94
|
+
msg += `website ${e.cause.website} does not exist`;
|
|
95
|
+
}
|
|
96
|
+
console.error(msg);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error("Unexpected error:\n", e);
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
// Wraps a function and calls `handleError` should it return an exception.
|
|
104
|
+
export function withHandleErrors(f) {
|
|
105
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
try {
|
|
107
|
+
return yield f();
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return handleError(e);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
package/dist/guichet.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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 { ErrorGuichet } from "./error.js";
|
|
12
|
+
// The GuichetApi class wraps WebsiteApi (from the guichet sdk). It catches
|
|
13
|
+
// errors that can happen in normal use, and it turn them into a dedicated error type
|
|
14
|
+
// (ErrorGuichet) that we know how to display nicely.
|
|
15
|
+
export class GuichetApi {
|
|
16
|
+
constructor(cfg) {
|
|
17
|
+
this.api = new WebsiteApi(cfg);
|
|
18
|
+
}
|
|
19
|
+
// NOTE: using `err instanceof ResponseError` will not work here
|
|
20
|
+
// because ResponseError extends Error and the following issue:
|
|
21
|
+
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
|
|
22
|
+
listWebsites() {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
try {
|
|
25
|
+
return yield this.api.listWebsites();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if ((err === null || err === void 0 ? void 0 : err.name) == "ResponseError" && err.response.status == 401) {
|
|
29
|
+
throw new ErrorGuichet({ kind: "Unauthorized" });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
getWebsite(vhost) {
|
|
38
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
try {
|
|
40
|
+
const websiteInfo = yield this.api.getWebsite({ vhost });
|
|
41
|
+
if (websiteInfo.vhost == `${vhost}.web.deuxfleurs.fr`) {
|
|
42
|
+
// This means that `vhost` is a website name from before
|
|
43
|
+
// the migration of websites *.web.deuxfleurs.fr to bucket
|
|
44
|
+
// names with the full domain name.
|
|
45
|
+
// We just warn that this is a working but deprecated name.
|
|
46
|
+
console.log(`Warning: the name "${vhost}" is now deprecated for this website,` +
|
|
47
|
+
` you should use "${vhost}.web.deuxfleurs.fr" instead.`);
|
|
48
|
+
}
|
|
49
|
+
return websiteInfo;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if ((err === null || err === void 0 ? void 0 : err.name) == "ResponseError" && err.response.status == 401) {
|
|
53
|
+
throw new ErrorGuichet({ kind: "Unauthorized" });
|
|
54
|
+
}
|
|
55
|
+
else if ((err === null || err === void 0 ? void 0 : err.name) == "ResponseError" && err.response.status == 404) {
|
|
56
|
+
throw new ErrorGuichet({ kind: "WebsiteNotFound", website: vhost });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from "@commander-js/extra-typings";
|
|
3
|
+
import { withHandleErrors } from "./error.js";
|
|
3
4
|
import { login, logout } from "./auth.js";
|
|
4
5
|
import { deploy } from "./deploy.js";
|
|
5
6
|
import { empty } from "./empty.js";
|
|
6
7
|
import { vhostsList } from "./vhosts.js";
|
|
7
|
-
program.name("dxfl").description("Deuxfleurs CLI tool").version("0.
|
|
8
|
+
program.name("dxfl").description("Deuxfleurs CLI tool").version("0.2.0");
|
|
8
9
|
program
|
|
9
10
|
.command("login")
|
|
10
11
|
.description("Link your Deuxfleurs account with this tool.")
|
|
11
12
|
.argument("<username>", "your account username")
|
|
12
|
-
.action(login);
|
|
13
|
+
.action(username => withHandleErrors(() => login(username)));
|
|
13
14
|
program
|
|
14
15
|
.command("list")
|
|
15
16
|
.description("List all your websites")
|
|
16
|
-
.action(vhostsList);
|
|
17
|
+
.action(() => withHandleErrors(vhostsList));
|
|
17
18
|
program
|
|
18
19
|
.command("deploy")
|
|
19
20
|
.description("Deploy your website")
|
|
@@ -21,17 +22,17 @@ program
|
|
|
21
22
|
.argument("<local_folder>", "your local folder")
|
|
22
23
|
.option("-n, --dry-run", "do a trial run without making actual changes")
|
|
23
24
|
.option("-y, --yes", "apply the changes without asking for confirmation")
|
|
24
|
-
.action(deploy);
|
|
25
|
+
.action((website, localFolder, options) => withHandleErrors(() => deploy(website, localFolder, options)));
|
|
25
26
|
program
|
|
26
27
|
.command("empty")
|
|
27
28
|
.description("Empty your website from its contents")
|
|
28
29
|
.argument("<website>", "selected website")
|
|
29
30
|
.option("-n, --dry-run", "do a trial run without making actual changes")
|
|
30
31
|
.option("-y, --yes", "apply the changes without asking for confirmation")
|
|
31
|
-
.action(empty);
|
|
32
|
+
.action((website, options) => withHandleErrors(() => empty(website, options)));
|
|
32
33
|
program
|
|
33
34
|
.command("logout")
|
|
34
35
|
.description("Cleanup dxfl config containing your credentials")
|
|
35
36
|
.option("-y, --yes", "clean without asking for confirmation")
|
|
36
|
-
.action(logout);
|
|
37
|
+
.action(options => withHandleErrors(() => logout(options)));
|
|
37
38
|
program.parse();
|
package/dist/utils.js
CHANGED
|
@@ -18,6 +18,7 @@ import fs from "fs";
|
|
|
18
18
|
import crypto from "crypto";
|
|
19
19
|
import { stdin, stdout } from "process";
|
|
20
20
|
import readline from "readline/promises";
|
|
21
|
+
import { ErrorMsg } from "./error.js";
|
|
21
22
|
export function getFileMd5(file) {
|
|
22
23
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23
24
|
var _a, e_1, _b, _c;
|
|
@@ -85,24 +86,34 @@ export function parseEtag(s) {
|
|
|
85
86
|
}
|
|
86
87
|
export function confirmationPrompt(details) {
|
|
87
88
|
return __awaiter(this, void 0, void 0, function* () {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
try {
|
|
90
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
91
|
+
let ok = true;
|
|
92
|
+
while (true) {
|
|
93
|
+
const a = yield rl.question(`Proceed? y (yes), n (no), d (details): `);
|
|
94
|
+
if (a == "y" || a == "yes") {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
else if (a == "n" || a == "no") {
|
|
98
|
+
ok = false;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
else if (a == "d" || a == "details") {
|
|
102
|
+
details();
|
|
103
|
+
process.stdout.write("\n");
|
|
104
|
+
}
|
|
94
105
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
rl.close();
|
|
107
|
+
return ok;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (e.code == "ABORT_ERR") {
|
|
111
|
+
throw new ErrorMsg("Aborted with Ctrl+C");
|
|
98
112
|
}
|
|
99
|
-
else
|
|
100
|
-
|
|
101
|
-
process.stdout.write("\n");
|
|
113
|
+
else {
|
|
114
|
+
throw e;
|
|
102
115
|
}
|
|
103
116
|
}
|
|
104
|
-
rl.close();
|
|
105
|
-
return ok;
|
|
106
117
|
});
|
|
107
118
|
}
|
|
108
119
|
export function filterMap(a, f) {
|
package/dist/website_config.js
CHANGED
|
@@ -13,6 +13,7 @@ import URI from "fast-uri";
|
|
|
13
13
|
import { z as zod } from "zod";
|
|
14
14
|
import { fromError as zodError } from "zod-validation-error";
|
|
15
15
|
import { GetBucketCorsCommand, GetBucketWebsiteCommand, PutBucketWebsiteCommand, PutBucketCorsCommand, } from "@aws-sdk/client-s3";
|
|
16
|
+
import { ErrorMsg, withErrorMsg, wrapS3Call } from "./error.js";
|
|
16
17
|
import { getBucketFilesDetails } from "./bucket.js";
|
|
17
18
|
////////////// Utilities
|
|
18
19
|
export function equalBucketRedirect(b1, b2) {
|
|
@@ -61,16 +62,22 @@ function readConfigFileObject(filename) {
|
|
|
61
62
|
try {
|
|
62
63
|
strConf = yield fs.promises.readFile(filename, { encoding: "utf8" });
|
|
63
64
|
}
|
|
64
|
-
catch (
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
catch (e) {
|
|
66
|
+
let msg = `failed to read ${filename}`;
|
|
67
|
+
if (e instanceof Error && "code" in e && e.code) {
|
|
68
|
+
msg += ` (${e.code})`;
|
|
69
|
+
}
|
|
70
|
+
throw new ErrorMsg(msg);
|
|
67
71
|
}
|
|
68
72
|
try {
|
|
69
73
|
dictConf = TOML.parse(strConf);
|
|
70
74
|
}
|
|
71
|
-
catch (
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
catch (e) {
|
|
76
|
+
let msg = `Unable to parse ${filename} as TOML`;
|
|
77
|
+
if (e instanceof Error) {
|
|
78
|
+
msg += `:\n${e.message}`;
|
|
79
|
+
}
|
|
80
|
+
return new ErrorMsg(msg);
|
|
74
81
|
}
|
|
75
82
|
return dictConf;
|
|
76
83
|
});
|
|
@@ -100,7 +107,7 @@ function interpRawConfig(cfg) {
|
|
|
100
107
|
}
|
|
101
108
|
catch (err) {
|
|
102
109
|
const validationError = zodError(err);
|
|
103
|
-
throw validationError.toString();
|
|
110
|
+
throw new ErrorMsg(validationError.toString());
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
// Parsing: RawConfig -> WebsiteConfig
|
|
@@ -127,7 +134,7 @@ function unescape(s) {
|
|
|
127
134
|
out += "*";
|
|
128
135
|
}
|
|
129
136
|
else {
|
|
130
|
-
throw `a single '*' is only allowed at the end for wildcards; please use '**' instead
|
|
137
|
+
throw new ErrorMsg(`a single '*' is only allowed at the end for wildcards; please use '**' instead`);
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
else {
|
|
@@ -137,36 +144,28 @@ function unescape(s) {
|
|
|
137
144
|
}
|
|
138
145
|
function interpConfig(rawcfg) {
|
|
139
146
|
var _a, _b, _c, _d, _e;
|
|
140
|
-
function interpPath(name, str) {
|
|
141
|
-
try {
|
|
142
|
-
return unescape(str);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
throw `${name}: ${err}`;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
147
|
function interpRedirect(i, r) {
|
|
149
148
|
var _a, _b;
|
|
150
|
-
const rfrom =
|
|
151
|
-
const rto =
|
|
149
|
+
const rfrom = withErrorMsg(() => unescape(r.from), msg => `from: ${msg}`);
|
|
150
|
+
const rto = withErrorMsg(() => unescape(r.to), msg => `to: ${msg}`);
|
|
152
151
|
let redirect;
|
|
153
152
|
if (rfrom.is_prefix) {
|
|
154
153
|
// This is a bucket redirect
|
|
155
154
|
const toURI = URI.parse(rto.s);
|
|
156
155
|
if (toURI.error) {
|
|
157
|
-
throw `
|
|
156
|
+
throw new ErrorMsg(`'to': ${toURI.error}`);
|
|
158
157
|
}
|
|
159
158
|
if (toURI.scheme && toURI.scheme != "http" && toURI.scheme != "https") {
|
|
160
|
-
throw `
|
|
159
|
+
throw new ErrorMsg(`'to': unsupported URI scheme ${toURI.scheme}; http or https required`);
|
|
161
160
|
}
|
|
162
161
|
if (toURI.port) {
|
|
163
|
-
throw `
|
|
162
|
+
throw new ErrorMsg(`'to': specifying a port is not supported`);
|
|
164
163
|
}
|
|
165
164
|
if (toURI.query) {
|
|
166
|
-
throw `
|
|
165
|
+
throw new ErrorMsg(`'to': specifying a query is not supported`);
|
|
167
166
|
}
|
|
168
167
|
if (toURI.fragment) {
|
|
169
|
-
throw `
|
|
168
|
+
throw new ErrorMsg(`'to': specifying a fragment is not supported`);
|
|
170
169
|
}
|
|
171
170
|
let path = (_a = toURI.path) !== null && _a !== void 0 ? _a : "";
|
|
172
171
|
// - In case of an absolute URL e.g. `https://foo.net/abc`, toURI.path will
|
|
@@ -208,12 +207,13 @@ function interpConfig(rawcfg) {
|
|
|
208
207
|
}
|
|
209
208
|
else if ([200, 404].includes(r.status)) {
|
|
210
209
|
if (toURI.host || toURI.scheme) {
|
|
211
|
-
throw `
|
|
210
|
+
throw new ErrorMsg(`a status of 200 or 404 is not accepted while specifying` +
|
|
211
|
+
` a protocol and hostname in the destination`);
|
|
212
212
|
}
|
|
213
213
|
bredirect.status = r.status;
|
|
214
214
|
}
|
|
215
215
|
else {
|
|
216
|
-
throw `
|
|
216
|
+
throw new ErrorMsg(`unsupported status code ${r.status}`);
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
redirect = { kind: "bucket", r: bredirect };
|
|
@@ -221,15 +221,15 @@ function interpConfig(rawcfg) {
|
|
|
221
221
|
else {
|
|
222
222
|
// This is an object redirect
|
|
223
223
|
if (r.force) {
|
|
224
|
-
throw (`
|
|
224
|
+
throw new ErrorMsg(`'force' is not supported for single redirections.\n` +
|
|
225
225
|
"The source of the redirection must not exist already as a file.");
|
|
226
226
|
}
|
|
227
227
|
if (r.status) {
|
|
228
|
-
throw (`
|
|
228
|
+
throw new ErrorMsg(`'status' is not supported for single redirections.\n` +
|
|
229
229
|
"All single redirections have status code 301.");
|
|
230
230
|
}
|
|
231
231
|
if (rto.is_prefix) {
|
|
232
|
-
throw `
|
|
232
|
+
throw new ErrorMsg(`cannot use a prefix target in 'to' when 'from' is a single address`);
|
|
233
233
|
}
|
|
234
234
|
// if 'from' starts with a /, consider that the user meant to refer to an object
|
|
235
235
|
// at the root of the bucket and not an object with "/" in its name. Thus we should
|
|
@@ -263,24 +263,26 @@ function interpConfig(rawcfg) {
|
|
|
263
263
|
}
|
|
264
264
|
return s !== null && s !== void 0 ? s : def;
|
|
265
265
|
};
|
|
266
|
+
// use index.html as default index page
|
|
267
|
+
const index_page = withDefault(rawcfg.index_page, "index.html");
|
|
268
|
+
// use error.html as default error page
|
|
269
|
+
const error_page = withDefault(rawcfg.error_page, "error.html");
|
|
266
270
|
let cfg = {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// use error.html as default error page
|
|
270
|
-
error_page: withDefault(rawcfg.error_page, "error.html"),
|
|
271
|
+
index_page,
|
|
272
|
+
error_page,
|
|
271
273
|
bucket_redirects: [],
|
|
272
274
|
object_redirects: new Map(),
|
|
273
275
|
cors_rules: [],
|
|
274
276
|
};
|
|
275
277
|
for (const [i, raw] of ((_a = rawcfg.redirects) !== null && _a !== void 0 ? _a : []).entries()) {
|
|
276
278
|
// `i+1` is only used for display: start counting redirects from 1 instead of 0
|
|
277
|
-
const r = interpRedirect(i + 1, raw);
|
|
279
|
+
const r = withErrorMsg(() => interpRedirect(i + 1, raw), msg => `Redirect ${i + 1}: ${msg}`);
|
|
278
280
|
if (r.kind == "bucket") {
|
|
279
281
|
cfg.bucket_redirects.push(r.r);
|
|
280
282
|
}
|
|
281
283
|
else {
|
|
282
284
|
if (cfg.object_redirects.has(r.from)) {
|
|
283
|
-
throw `Cannot have two redirects from the same path ${r.from}
|
|
285
|
+
throw new ErrorMsg(`Cannot have two redirects from the same path ${r.from}`);
|
|
284
286
|
}
|
|
285
287
|
cfg.object_redirects.set(r.from, r.to);
|
|
286
288
|
}
|
|
@@ -300,7 +302,9 @@ function interpConfig(rawcfg) {
|
|
|
300
302
|
}
|
|
301
303
|
export function readConfigFile(filename) {
|
|
302
304
|
return __awaiter(this, void 0, void 0, function* () {
|
|
303
|
-
|
|
305
|
+
const obj = yield readConfigFileObject(filename);
|
|
306
|
+
const raw = withErrorMsg(() => interpRawConfig(obj), msg => `Failed to parse ${filename}:\n${msg}`);
|
|
307
|
+
return interpConfig(raw);
|
|
304
308
|
});
|
|
305
309
|
}
|
|
306
310
|
////////////// Reading from a S3 bucket
|
|
@@ -314,22 +318,12 @@ export function getBucketConfig(bucket, files) {
|
|
|
314
318
|
// but I don't know of a better way.)
|
|
315
319
|
var _a, _b, _c, _d, _e, _f;
|
|
316
320
|
const [website, cors, details] = yield Promise.all([
|
|
317
|
-
|
|
318
|
-
bucket.client.send(new
|
|
321
|
+
// 204 "No Content" is returned when there is no existing website config
|
|
322
|
+
wrapS3Call(`read the bucket website config`, [200, 204], () => bucket.client.send(new GetBucketWebsiteCommand({ Bucket: bucket.name }))),
|
|
323
|
+
// 204 "No Content" is returned when there are no existing CORS rules
|
|
324
|
+
wrapS3Call(`read the bucket CORS config`, [200, 204], () => bucket.client.send(new GetBucketCorsCommand({ Bucket: bucket.name }))),
|
|
319
325
|
getBucketFilesDetails(bucket, files),
|
|
320
326
|
]);
|
|
321
|
-
if (website && website.$metadata.httpStatusCode != 200) {
|
|
322
|
-
throw `Error sending GetBucketWebsite: ${JSON.stringify(website)}`;
|
|
323
|
-
}
|
|
324
|
-
if (!cors ||
|
|
325
|
-
(cors.$metadata.httpStatusCode != 200 &&
|
|
326
|
-
cors.$metadata.httpStatusCode != 204)) {
|
|
327
|
-
throw `Error sending GetBucketCors: ${JSON.stringify(cors)}`;
|
|
328
|
-
}
|
|
329
|
-
// 204 "No Content" is returned when there are no existing CORS rules
|
|
330
|
-
if (cors.$metadata.httpStatusCode == 204) {
|
|
331
|
-
cors.CORSRules = [];
|
|
332
|
-
}
|
|
333
327
|
// Collect object redirects
|
|
334
328
|
const object_redirects = new Map();
|
|
335
329
|
for (const [file, { redirect }] of details) {
|
|
@@ -340,7 +334,7 @@ export function getBucketConfig(bucket, files) {
|
|
|
340
334
|
// Interpret bucket redirects
|
|
341
335
|
if (website.RedirectAllRequestsTo) {
|
|
342
336
|
// NB: garage does not currently support RedirectAllRequestsTo so this should never happen
|
|
343
|
-
throw (`remote website configuration: RedirectAllRequestsTo is specified; ` +
|
|
337
|
+
throw new ErrorMsg(`remote website configuration: RedirectAllRequestsTo is specified; ` +
|
|
344
338
|
`this is currently unsupported by dxfl`);
|
|
345
339
|
}
|
|
346
340
|
const index_page = (_a = website.IndexDocument) === null || _a === void 0 ? void 0 : _a.Suffix;
|
|
@@ -361,7 +355,8 @@ export function getBucketConfig(bucket, files) {
|
|
|
361
355
|
}
|
|
362
356
|
else {
|
|
363
357
|
// not supported by garage
|
|
364
|
-
throw `remote website configuration: unexpected value for
|
|
358
|
+
throw new ErrorMsg(`remote website configuration: unexpected value for` +
|
|
359
|
+
` 'HttpErrorCodeReturnedEquals': ${rule.Condition.HttpErrorCodeReturnedEquals}`);
|
|
365
360
|
}
|
|
366
361
|
}
|
|
367
362
|
if (rule.Condition.KeyPrefixEquals) {
|
|
@@ -380,7 +375,7 @@ export function getBucketConfig(bucket, files) {
|
|
|
380
375
|
}
|
|
381
376
|
else {
|
|
382
377
|
// currently not supported by garage
|
|
383
|
-
throw (`remote website configuration: 'protocol' is neither http or https; ` +
|
|
378
|
+
throw new ErrorMsg(`remote website configuration: 'protocol' is neither http or https; ` +
|
|
384
379
|
`this is currently unsupported by dxfl`);
|
|
385
380
|
}
|
|
386
381
|
}
|
|
@@ -472,13 +467,10 @@ export function putBucketWebsiteConfig(bucket, index_page, error_page, bucket_re
|
|
|
472
467
|
WebsiteConfiguration.RoutingRules.push(rule);
|
|
473
468
|
}
|
|
474
469
|
}
|
|
475
|
-
|
|
470
|
+
yield wrapS3Call(`write the bucket website config`, [200], () => bucket.client.send(new PutBucketWebsiteCommand({
|
|
476
471
|
Bucket: bucket.name,
|
|
477
472
|
WebsiteConfiguration,
|
|
478
|
-
}));
|
|
479
|
-
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
480
|
-
throw resp;
|
|
481
|
-
}
|
|
473
|
+
})));
|
|
482
474
|
});
|
|
483
475
|
}
|
|
484
476
|
// applies CORS rules
|
|
@@ -493,12 +485,9 @@ export function putCorsRules(bucket, cors_rules) {
|
|
|
493
485
|
ExposeHeaders: rule.expose_headers,
|
|
494
486
|
});
|
|
495
487
|
}
|
|
496
|
-
|
|
488
|
+
yield wrapS3Call(`when writing the bucket CORS config`, [200], () => bucket.client.send(new PutBucketCorsCommand({
|
|
497
489
|
Bucket: bucket.name,
|
|
498
490
|
CORSConfiguration: { CORSRules },
|
|
499
|
-
}));
|
|
500
|
-
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
501
|
-
throw resp;
|
|
502
|
-
}
|
|
491
|
+
})));
|
|
503
492
|
});
|
|
504
493
|
}
|