dxfl 0.1.7 → 0.1.9
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/.editorconfig +22 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +3 -0
- package/CHANGELOG.md +54 -0
- package/README.md +77 -1
- package/dist/_empty.js +64 -0
- package/dist/auth.js +6 -6
- package/dist/bucket.js +174 -0
- package/dist/config.js +185 -0
- package/dist/deploy.js +300 -241
- package/dist/empty.js +73 -0
- package/dist/index.js +23 -14
- package/dist/utils.js +113 -0
- package/dist/website_config.js +406 -0
- package/package.json +19 -4
- package/tsconfig.json +13 -14
package/.editorconfig
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Stop the editor from looking for .editorconfig files in the parent directories
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
indent_style = tab
|
|
6
|
+
# Preferred indentation size is a user preference
|
|
7
|
+
# tab_width = 4
|
|
8
|
+
end_of_line = lf
|
|
9
|
+
insert_final_newline = false
|
|
10
|
+
trim_trailing_whitespace = true
|
|
11
|
+
charset = utf-8
|
|
12
|
+
|
|
13
|
+
[*.md]
|
|
14
|
+
trim_trailing_whitespace = false
|
|
15
|
+
|
|
16
|
+
[*.{yml,yaml}]
|
|
17
|
+
indent_style = space
|
|
18
|
+
indent_size = 2
|
|
19
|
+
|
|
20
|
+
[*.json]
|
|
21
|
+
indent_style = space
|
|
22
|
+
indent_size = 2
|
package/.prettierignore
ADDED
package/.prettierrc.json
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# v0.1.9
|
|
2
|
+
|
|
3
|
+
- `deuxfleurs.toml`: rename redirection property `if_error = 404` to `force = true`, to simplify and improve comprehension.
|
|
4
|
+
|
|
5
|
+
# v0.1.8
|
|
6
|
+
|
|
7
|
+
- `deploy`: add support for redirects, configured from a new `deuxfleurs.toml` file.
|
|
8
|
+
- supports S3 "object redirects" (redirects from a path in the website)
|
|
9
|
+
- supports global "bucket redirects" (redirects from any path in the website
|
|
10
|
+
that starts with a specified prefix)
|
|
11
|
+
- allows specifying a default index page
|
|
12
|
+
- allows specifying a error page for 404 errors
|
|
13
|
+
- new `empty` command: removes all contents of a website (guichet requires emptying
|
|
14
|
+
a website before deleting it)
|
|
15
|
+
- `deploy`: instead of unconditionally deploying by default, ask for interactive
|
|
16
|
+
confirmation after displaying a summary of planned changes. Add a `--yes`
|
|
17
|
+
option to skip confirmation and automatically apply the changes.
|
|
18
|
+
- `deploy`: improve performance by increasing concurrency of S3 operations
|
|
19
|
+
- `deploy`: drop multipart upload support & simplify the strategy for
|
|
20
|
+
incremental uploads: simply use ETags now.
|
|
21
|
+
|
|
22
|
+
# v0.1.7
|
|
23
|
+
|
|
24
|
+
- internal packaging fixes
|
|
25
|
+
|
|
26
|
+
# v0.1.6
|
|
27
|
+
|
|
28
|
+
- improve the output of `deploy`
|
|
29
|
+
- show a full list of uploaded files, keeping multipart uploads on one line
|
|
30
|
+
- list deleted files
|
|
31
|
+
- show human-friendly file sizes instead of number of bytes
|
|
32
|
+
- `deploy`: add a `--dry-run` option to see planned changes without doing anything
|
|
33
|
+
|
|
34
|
+
# v0.1.5
|
|
35
|
+
|
|
36
|
+
- make `deploy` incremental: only upload files that have changed.
|
|
37
|
+
The implementation strategy is by the one used in rclone: store the file md5
|
|
38
|
+
in a custom metadata header.
|
|
39
|
+
- fix a crash in case of empty ListObject reply
|
|
40
|
+
|
|
41
|
+
# v0.1.3, v0.1.4
|
|
42
|
+
|
|
43
|
+
- misc internal tooling changes
|
|
44
|
+
|
|
45
|
+
# v0.1.1, v0.1.2
|
|
46
|
+
|
|
47
|
+
- rename the tool to `dxfl`
|
|
48
|
+
|
|
49
|
+
# v0.1.0
|
|
50
|
+
|
|
51
|
+
- new `login` command: authenticates to guichet & stores credentials locally
|
|
52
|
+
- new `list` command: list websites of user (after logging in)
|
|
53
|
+
- new `deploy` command: deploys a local directory to a user's website.
|
|
54
|
+
Also detects the MIME type of files and sets ContentType accordingly.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install -g dxfl
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
_Warning: software still in an experimental state_
|
|
14
14
|
|
|
15
15
|
Start by login with your username, for example for `john`:
|
|
16
16
|
|
|
@@ -30,6 +30,68 @@ And then to deploy your `_public` folder on `example.com`:
|
|
|
30
30
|
dxfl deploy example.com _public
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
To deploy without confirmation, use the `--yes` option:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
dxfl deploy example.com _public --yes
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or only display the changes without applying them, with the `--dry-run` option:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
dxfl deploy example.com _public --dry-run
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Use the `empty` command to delete all files from a website (required before deleting it):
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
dxfl empty example.com
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Website configuration
|
|
52
|
+
|
|
53
|
+
`dxfl deploy` reads a `deuxfleurs.toml` configuration file (if it exists in the current directory).
|
|
54
|
+
This file can be used to specify website configuration metadata such as redirections.
|
|
55
|
+
|
|
56
|
+
Your `deuxfleurs.toml` should follow the following structure:
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
# Filename added after URLs that point to directories.
|
|
60
|
+
# Default value: "index.html"
|
|
61
|
+
index_page = "index.html"
|
|
62
|
+
# Path to the file to serve in case of 404 error.
|
|
63
|
+
# Default value: none
|
|
64
|
+
error_page = "404.html"
|
|
65
|
+
|
|
66
|
+
# A redirect entry. There can be as many as desired.
|
|
67
|
+
[[redirects]]
|
|
68
|
+
# Source of the redirection.
|
|
69
|
+
from = "old/path"
|
|
70
|
+
# Target of the redirection.
|
|
71
|
+
to = "new/path"
|
|
72
|
+
|
|
73
|
+
# Redirects can also match multiple paths, by specifying
|
|
74
|
+
# a prefix followed by *
|
|
75
|
+
[[redirects]]
|
|
76
|
+
# Source of the redirection: all paths that match the prefix
|
|
77
|
+
# foobar/
|
|
78
|
+
from = "foobar/*"
|
|
79
|
+
# Target: redirect all foobar/XXX to baz/XXX.
|
|
80
|
+
# The target can also be an URL outside of the website, or a
|
|
81
|
+
# single path.
|
|
82
|
+
to = "baz/*"
|
|
83
|
+
# Optional: custom HTTP status code for the redirection.
|
|
84
|
+
# Default value: 302
|
|
85
|
+
status = 301
|
|
86
|
+
# Optional: always apply the redirect.
|
|
87
|
+
# By default, redirections that match multiple paths do not apply
|
|
88
|
+
# if a file that matches the requested path could be served instead.
|
|
89
|
+
# Setting this option to true overrides this behavior and always
|
|
90
|
+
# applies the redirection.
|
|
91
|
+
# Default value: false
|
|
92
|
+
force = true
|
|
93
|
+
```
|
|
94
|
+
|
|
33
95
|
## Development
|
|
34
96
|
|
|
35
97
|
```
|
|
@@ -43,6 +105,18 @@ dxfl
|
|
|
43
105
|
npx dxfl
|
|
44
106
|
```
|
|
45
107
|
|
|
108
|
+
### Code formatting
|
|
109
|
+
|
|
110
|
+
[Prettier](https://prettier.io/) is used to assure a certain consistency in style (and [accessibility](https://adamtuttle.codes/blog/2021/tabs-vs-spaces-its-an-accessibility-issue/)) through the codebase. An [EditorConfig](https://editorconfig.org/) file is also here with a similar goal.
|
|
111
|
+
|
|
112
|
+
You can format your changes with the dedicated npm command lines:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm run prettier-check # check formatting without making change
|
|
116
|
+
npm run prettier # fix formatting
|
|
117
|
+
npm run prettier-watch # watch upcoming changes to fix
|
|
118
|
+
```
|
|
119
|
+
|
|
46
120
|
## Release
|
|
47
121
|
|
|
48
122
|
First you need an account on npmjs.com and be a maintainer of the `dxfl` package (ask quentin).
|
|
@@ -51,9 +125,11 @@ Do not forget also to run `npm login` to bind your account with the CLI.
|
|
|
51
125
|
Then to publish a release:
|
|
52
126
|
|
|
53
127
|
```bash
|
|
128
|
+
vim CHANGELOG.md # update the version and its content in this file
|
|
54
129
|
vim package.json # update the version in this file
|
|
55
130
|
vim index.ts # update the version in this file
|
|
56
131
|
npm install # update the version in the package-lock.json
|
|
132
|
+
npm run prettier # fix potential coding style problem
|
|
57
133
|
git commit -a -m 'set version 0.1.5' # commit your change
|
|
58
134
|
git push # send update
|
|
59
135
|
git tag -m 'v0.1.5' v0.1.5 # create associated tag
|
package/dist/_empty.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// // TODO: Refacto the codebase global before integrate this feature
|
|
2
|
+
export {};
|
|
3
|
+
// export async function empty(
|
|
4
|
+
// vhost: string,
|
|
5
|
+
// options: { dryRun: boolean | undefined },
|
|
6
|
+
// ) {
|
|
7
|
+
// const conf = await openApiConf();
|
|
8
|
+
// // Get website info from guichet (bucket name and keys)
|
|
9
|
+
// const api = new WebsiteApi(conf);
|
|
10
|
+
// let vhostInfo = await api.getWebsite({ vhost }).catch(err => {
|
|
11
|
+
// if (err.response.status == 404) {
|
|
12
|
+
// console.error(`Error: website '${vhost}' does not exist`);
|
|
13
|
+
// } else {
|
|
14
|
+
// console.error(err);
|
|
15
|
+
// }
|
|
16
|
+
// process.exit(1);
|
|
17
|
+
// });
|
|
18
|
+
// // List the files currently stored in the bucket
|
|
19
|
+
// // @FIXME this info could be returned by the guichet API
|
|
20
|
+
// const s3client = new S3Client({
|
|
21
|
+
// endpoint: "https://garage.deuxfleurs.fr",
|
|
22
|
+
// region: "garage",
|
|
23
|
+
// forcePathStyle: true,
|
|
24
|
+
// credentials: {
|
|
25
|
+
// accessKeyId: vhostInfo.accessKeyId!,
|
|
26
|
+
// secretAccessKey: vhostInfo.secretAccessKey!,
|
|
27
|
+
// },
|
|
28
|
+
// });
|
|
29
|
+
// const Bucket = vhostInfo.vhost!.name!;
|
|
30
|
+
// const filesToDelete = [...(await getBucketFiles(s3client, Bucket))].map(
|
|
31
|
+
// ([name, { size }]) => ({
|
|
32
|
+
// name,
|
|
33
|
+
// size,
|
|
34
|
+
// }),
|
|
35
|
+
// );
|
|
36
|
+
// for (const file of filesToDelete) {
|
|
37
|
+
// process.stdout.write(`Deleting ${file.name}\n`);
|
|
38
|
+
// }
|
|
39
|
+
// // If not in dry-run mode, send the delete command
|
|
40
|
+
// if (!options.dryRun) {
|
|
41
|
+
// const resp = await deleteFiles(s3client, Bucket, filesToDelete);
|
|
42
|
+
// if (resp && resp!.$metadata.httpStatusCode != 200) {
|
|
43
|
+
// // TODO: better error handling?
|
|
44
|
+
// console.error(resp);
|
|
45
|
+
// process.exit(1);
|
|
46
|
+
// }
|
|
47
|
+
// }
|
|
48
|
+
// // Display a summary
|
|
49
|
+
// function sum(a: number[]) {
|
|
50
|
+
// return a.reduce((x, y) => x + y, 0);
|
|
51
|
+
// }
|
|
52
|
+
// function formatFiles(n: number) {
|
|
53
|
+
// if (n == 1) {
|
|
54
|
+
// return `${n} file `;
|
|
55
|
+
// } else {
|
|
56
|
+
// return `${n} files`;
|
|
57
|
+
// }
|
|
58
|
+
// }
|
|
59
|
+
// const sizeDeleted = sum(filesToDelete.map(f => f.size ?? 0));
|
|
60
|
+
// process.stdout.write("\nSummary:\n");
|
|
61
|
+
// process.stdout.write(
|
|
62
|
+
// `${formatFiles(filesToDelete.length)} deleted (${formatBytes(sizeDeleted)})\n`,
|
|
63
|
+
// );
|
|
64
|
+
// }
|
package/dist/auth.js
CHANGED
|
@@ -8,9 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { Configuration, WebsiteApi } from "guichet-sdk-ts";
|
|
11
|
-
import { read } from
|
|
12
|
-
import path from
|
|
13
|
-
import fs from
|
|
11
|
+
import { read } from "read";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
14
|
function configPath() {
|
|
15
15
|
let path = ".dfl/config.json";
|
|
16
16
|
if (process.env.XDG_CONFIG_HOME) {
|
|
@@ -27,7 +27,7 @@ export function openApiConf() {
|
|
|
27
27
|
let dictConf;
|
|
28
28
|
const configFile = configPath();
|
|
29
29
|
try {
|
|
30
|
-
strConf = yield fs.readFile(configFile, { encoding:
|
|
30
|
+
strConf = yield fs.readFile(configFile, { encoding: "utf8" });
|
|
31
31
|
}
|
|
32
32
|
catch (err) {
|
|
33
33
|
console.error(err, `\n\nUnable to read ${configFile}, run 'dfl login' first.`);
|
|
@@ -52,7 +52,7 @@ export function login(username) {
|
|
|
52
52
|
const password = yield read({
|
|
53
53
|
prompt: "password: ",
|
|
54
54
|
silent: true,
|
|
55
|
-
replace: "*"
|
|
55
|
+
replace: "*",
|
|
56
56
|
});
|
|
57
57
|
// check config
|
|
58
58
|
const testConf = new Configuration({
|
|
@@ -76,6 +76,6 @@ export function login(username) {
|
|
|
76
76
|
const serializedConfig = JSON.stringify(configData);
|
|
77
77
|
yield fs.writeFile(configFile, serializedConfig, { mode: 0o600 });
|
|
78
78
|
// @FIXME: we would like to avoid storing the password in clear text in the future.
|
|
79
|
-
console.log(
|
|
79
|
+
console.log("ok");
|
|
80
80
|
});
|
|
81
81
|
}
|
package/dist/bucket.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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 mime from "mime";
|
|
12
|
+
import { DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
|
|
13
|
+
import { PromisePool } from "@supercharge/promise-pool";
|
|
14
|
+
import { parseEtag, toChunks, formatBytesHuman } from "./utils.js";
|
|
15
|
+
export function getBucket(api, vhost) {
|
|
16
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
+
// Get website info from guichet (bucket name and keys)
|
|
18
|
+
const bucket = yield api.getWebsite({ vhost }).catch(err => {
|
|
19
|
+
var _a;
|
|
20
|
+
if (((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) == 404) {
|
|
21
|
+
console.error(`Error: website '${vhost}' does not exist`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.error(err);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(err);
|
|
27
|
+
});
|
|
28
|
+
// Initialize a S3 Client
|
|
29
|
+
const client = new S3Client({
|
|
30
|
+
endpoint: "https://garage.deuxfleurs.fr",
|
|
31
|
+
region: "garage",
|
|
32
|
+
forcePathStyle: true,
|
|
33
|
+
credentials: {
|
|
34
|
+
accessKeyId: bucket.accessKeyId,
|
|
35
|
+
secretAccessKey: bucket.secretAccessKey,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
client: client,
|
|
40
|
+
name: bucket.vhost.name,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function getBucketFiles(bucket) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const files = new Map();
|
|
47
|
+
let done = false;
|
|
48
|
+
let cmd = new ListObjectsV2Command({ Bucket: bucket.name });
|
|
49
|
+
while (!done) {
|
|
50
|
+
const resp = yield bucket.client.send(cmd);
|
|
51
|
+
if (resp.$metadata.httpStatusCode != 200) {
|
|
52
|
+
// TODO: better error handling?
|
|
53
|
+
throw resp;
|
|
54
|
+
}
|
|
55
|
+
if (resp.Contents) {
|
|
56
|
+
for (const item of resp.Contents) {
|
|
57
|
+
const etag = item.ETag ? parseEtag(item.ETag) : undefined;
|
|
58
|
+
if (item.Key) {
|
|
59
|
+
files.set(item.Key, { size: item.Size, etag });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (resp.NextContinuationToken) {
|
|
64
|
+
cmd = new ListObjectsV2Command({
|
|
65
|
+
Bucket: bucket.name,
|
|
66
|
+
ContinuationToken: resp.NextContinuationToken,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
done = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return files;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function getBucketFilesDetails(bucket, files) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
let res = new Map();
|
|
79
|
+
function doFile(file) {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
const resp = yield bucket.client.send(new HeadObjectCommand({ Bucket: bucket.name, Key: file }));
|
|
82
|
+
if (resp.$metadata.httpStatusCode != 200) {
|
|
83
|
+
// TODO: better error handling?
|
|
84
|
+
throw resp;
|
|
85
|
+
}
|
|
86
|
+
res.set(file, {
|
|
87
|
+
redirect: resp.WebsiteRedirectLocation,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
yield PromisePool.for(files)
|
|
92
|
+
.withConcurrency(50)
|
|
93
|
+
.handleError(err => {
|
|
94
|
+
throw err;
|
|
95
|
+
})
|
|
96
|
+
.process(doFile);
|
|
97
|
+
return res;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function deleteBucketFile(bucket, name) {
|
|
101
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
102
|
+
const resp = yield bucket.client.send(new DeleteObjectCommand({
|
|
103
|
+
Bucket: bucket.name,
|
|
104
|
+
Key: name,
|
|
105
|
+
}));
|
|
106
|
+
if (resp && resp.$metadata.httpStatusCode != 204) {
|
|
107
|
+
// TODO: better error handling?
|
|
108
|
+
throw resp;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function deleteBucketFiles(bucket, files) {
|
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
const chunks = toChunks(files, 20);
|
|
115
|
+
yield PromisePool.for(chunks)
|
|
116
|
+
.withConcurrency(50)
|
|
117
|
+
.handleError(err => {
|
|
118
|
+
throw err;
|
|
119
|
+
})
|
|
120
|
+
.process((files) => __awaiter(this, void 0, void 0, function* () {
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
process.stdout.write(` Delete ${file.name} (${file.size ? formatBytesHuman(file.size) : "?B"})\n`);
|
|
123
|
+
}
|
|
124
|
+
const resp = yield bucket.client.send(new DeleteObjectsCommand({
|
|
125
|
+
Bucket: bucket.name,
|
|
126
|
+
Delete: {
|
|
127
|
+
Objects: files.map(f => {
|
|
128
|
+
return { Key: f.name };
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
133
|
+
// TODO: better error handling?
|
|
134
|
+
throw resp;
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export function uploadFile(bucket, s3Path, localPath) {
|
|
140
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
var _a;
|
|
142
|
+
// use `path.posix` because `Key` is a path in a bucket that uses `/` as separator.
|
|
143
|
+
let ContentType = (_a = mime.getType(localPath)) !== null && _a !== void 0 ? _a : undefined;
|
|
144
|
+
// add charset=utf-8 by default on text files (TODO: allow the user to override this)
|
|
145
|
+
if (ContentType && ContentType.startsWith("text/")) {
|
|
146
|
+
ContentType = ContentType + "; charset=utf-8";
|
|
147
|
+
}
|
|
148
|
+
const Body = fs.createReadStream(localPath);
|
|
149
|
+
const params = { Bucket: bucket.name, Key: s3Path, Body, ContentType };
|
|
150
|
+
const resp = yield bucket.client.send(new PutObjectCommand(params));
|
|
151
|
+
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
152
|
+
throw resp;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// the 'source' object may or may not exist already in the bucket.
|
|
157
|
+
// if it exists, its contents will be discarded, otherwise it
|
|
158
|
+
// will be created with empty contents.
|
|
159
|
+
export function putEmptyObjectRedirect(bucket, source, target) {
|
|
160
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
161
|
+
// use a non-empty Body to sidestep
|
|
162
|
+
// https://github.com/aws/aws-sdk-js-v3/issues/5722
|
|
163
|
+
const params = {
|
|
164
|
+
Bucket: bucket.name,
|
|
165
|
+
Key: source,
|
|
166
|
+
Body: "source of object redirect",
|
|
167
|
+
WebsiteRedirectLocation: target,
|
|
168
|
+
};
|
|
169
|
+
const resp = yield bucket.client.send(new PutObjectCommand(params));
|
|
170
|
+
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
171
|
+
throw resp;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
const DXFL_CONFIG = "deuxfleurs.toml";
|
|
16
|
+
// Parsing: TOML -> untyped object
|
|
17
|
+
function readConfigFile() {
|
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
+
if (!fs.existsSync(DXFL_CONFIG)) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
let strConf;
|
|
23
|
+
let dictConf;
|
|
24
|
+
try {
|
|
25
|
+
strConf = yield fs.promises.readFile(DXFL_CONFIG, { encoding: "utf8" });
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error(err, `\n\nUnable to read ${DXFL_CONFIG}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
dictConf = TOML.parse(strConf);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error(err, `\n\nUnable to parse ${DXFL_CONFIG} as TOML, please check it for syntax errors.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
return dictConf;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Parsing: untyped object -> RawConfig
|
|
42
|
+
const RawRedirectSchema = zod.object({
|
|
43
|
+
from: zod.string(),
|
|
44
|
+
to: zod.string(),
|
|
45
|
+
if_error: zod.number().int().optional(),
|
|
46
|
+
code: zod.number().int().positive().optional(),
|
|
47
|
+
});
|
|
48
|
+
const RawConfigSchema = zod.object({
|
|
49
|
+
index_page: zod.string().optional(),
|
|
50
|
+
error_page: zod.string().optional(),
|
|
51
|
+
redirects: zod.array(RawRedirectSchema),
|
|
52
|
+
});
|
|
53
|
+
function interpRawConfig(cfg) {
|
|
54
|
+
try {
|
|
55
|
+
return RawConfigSchema.parse(cfg);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const validationError = zodError(err);
|
|
59
|
+
throw validationError.toString();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Parsing: RawConfig -> DxflConfig
|
|
63
|
+
function unescape(s) {
|
|
64
|
+
let out = "";
|
|
65
|
+
const iter = s[Symbol.iterator]();
|
|
66
|
+
while (true) {
|
|
67
|
+
const elt = iter.next();
|
|
68
|
+
if (elt.done == true) {
|
|
69
|
+
return { s: out, is_prefix: false };
|
|
70
|
+
}
|
|
71
|
+
if (elt.value == "*") {
|
|
72
|
+
const next = iter.next();
|
|
73
|
+
if (next.done == true) {
|
|
74
|
+
return { s: out, is_prefix: true };
|
|
75
|
+
}
|
|
76
|
+
else if (next.value == "*") {
|
|
77
|
+
out += "*";
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
throw `a single '*' is only allowed at the end for wildcards; please use '**' instead`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
out += elt.value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function interpConfig(rawcfg) {
|
|
89
|
+
function interpPath(name, str) {
|
|
90
|
+
try {
|
|
91
|
+
return unescape(str);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
throw `${name}: ${err}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function interpRedirect(i, r) {
|
|
98
|
+
const rfrom = interpPath(`redirect ${i}, from`, r.from);
|
|
99
|
+
const rto = interpPath(`redirect ${i}, to`, r.to);
|
|
100
|
+
let redirect;
|
|
101
|
+
if (rfrom.is_prefix) {
|
|
102
|
+
// This is a bucket redirect
|
|
103
|
+
const toURI = URI.parse(rto.s);
|
|
104
|
+
if (toURI.error) {
|
|
105
|
+
throw `redirect ${i}, 'to': ${toURI.error}`;
|
|
106
|
+
}
|
|
107
|
+
if (toURI.scheme && toURI.scheme != "http" && toURI.scheme != "https") {
|
|
108
|
+
throw `redirect ${i}, 'to': unsupported URI scheme ${toURI.scheme}; http or https required`;
|
|
109
|
+
}
|
|
110
|
+
if (toURI.port) {
|
|
111
|
+
throw `redirect ${i}, 'to': specifying a port is not supported`;
|
|
112
|
+
}
|
|
113
|
+
if (toURI.query) {
|
|
114
|
+
throw `redirect ${i}, 'to': specifying a query is not supported`;
|
|
115
|
+
}
|
|
116
|
+
let bredirect = {
|
|
117
|
+
prefix: rfrom.s,
|
|
118
|
+
if_error: undefined,
|
|
119
|
+
hostname: toURI.host,
|
|
120
|
+
protocol: toURI.scheme,
|
|
121
|
+
code: undefined,
|
|
122
|
+
to: rto.is_prefix
|
|
123
|
+
? { kind: "replace_prefix", prefix: rto.s }
|
|
124
|
+
: { kind: "replace", target: rto.s },
|
|
125
|
+
};
|
|
126
|
+
if (r.if_error) {
|
|
127
|
+
if (r.if_error != 404) {
|
|
128
|
+
throw `redirect ${i}: the only currently supportetd value for 'if_error' is 404`;
|
|
129
|
+
}
|
|
130
|
+
bredirect.if_error = 404;
|
|
131
|
+
}
|
|
132
|
+
if (r.code) {
|
|
133
|
+
if ([301, 302, 303, 307, 308].includes(r.code)) {
|
|
134
|
+
bredirect.code = r.code;
|
|
135
|
+
}
|
|
136
|
+
else if ([200, 404].includes(r.code)) {
|
|
137
|
+
if (toURI.host || toURI.scheme) {
|
|
138
|
+
throw `request ${i}: a code of 200 or 404 is not accepted while specifying a protocol and hostname in the destination`;
|
|
139
|
+
}
|
|
140
|
+
bredirect.code = r.code;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
throw `redirect ${i}: unsupported error code ${r.code}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
redirect = { kind: "bucket", r: bredirect };
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// This is an object redirect
|
|
150
|
+
if (r.if_error) {
|
|
151
|
+
throw `redirect ${i}: 'if_error' is not supported for single redirections`;
|
|
152
|
+
}
|
|
153
|
+
if (r.code) {
|
|
154
|
+
throw `redirect ${i}: 'code' is not supported for single redirections`;
|
|
155
|
+
}
|
|
156
|
+
if (rto.is_prefix) {
|
|
157
|
+
throw `redirect ${i}: cannot use a prefix target in 'to' when 'from' is a single address`;
|
|
158
|
+
}
|
|
159
|
+
// pass the 'to' field as-is
|
|
160
|
+
const oredirect = { from: rfrom.s, to: rto.s };
|
|
161
|
+
redirect = { kind: "object", r: oredirect };
|
|
162
|
+
}
|
|
163
|
+
return redirect;
|
|
164
|
+
}
|
|
165
|
+
let cfg = {
|
|
166
|
+
index_page: rawcfg.index_page,
|
|
167
|
+
error_page: rawcfg.error_page,
|
|
168
|
+
redirects: [],
|
|
169
|
+
};
|
|
170
|
+
for (const [i, r] of rawcfg.redirects.entries()) {
|
|
171
|
+
cfg.redirects.push(interpRedirect(i, r));
|
|
172
|
+
}
|
|
173
|
+
return cfg;
|
|
174
|
+
}
|
|
175
|
+
export function readConfig() {
|
|
176
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
177
|
+
try {
|
|
178
|
+
return interpConfig(interpRawConfig(yield readConfigFile()));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(`Error while reading ${DXFL_CONFIG}: ${err}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|