dxfl 0.1.8 → 0.1.10
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 +14 -0
- package/README.md +75 -12
- package/dist/auth.js +94 -6
- package/dist/bucket.js +41 -9
- package/dist/deploy.js +32 -10
- package/dist/empty.js +2 -6
- package/dist/index.js +7 -2
- package/dist/website_config.js +130 -36
- package/doc/workflows/gh-actions.yml +19 -0
- package/doc/workflows/woodpecker.yml +21 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# v0.1.10
|
|
2
|
+
|
|
3
|
+
- new `logout` command: removes dxfl config folder containing the credentials
|
|
4
|
+
- rename authentification config folder from "dfl" to "dxfl", for consistency and clarity
|
|
5
|
+
- `deuxfleurs.toml`: make "error.html" the default `error_page`'s value
|
|
6
|
+
- `deploy`/`empty` commands can now be used without being logged, by passing S3 id and secret keys as enviromnent variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` respectively)
|
|
7
|
+
- `deuxfleurs.toml` can now configure cross-origin requests
|
|
8
|
+
|
|
9
|
+
# v0.1.9
|
|
10
|
+
|
|
11
|
+
- `deuxfleurs.toml`: rename redirection property `if_error = 404` to `force = true`, to simplify and improve comprehension.
|
|
12
|
+
|
|
13
|
+
# v0.1.8
|
|
14
|
+
|
|
1
15
|
- `deploy`: add support for redirects, configured from a new `deuxfleurs.toml` file.
|
|
2
16
|
- supports S3 "object redirects" (redirects from a path in the website)
|
|
3
17
|
- supports global "bucket redirects" (redirects from any path in the website
|
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ npm install -g dxfl
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
_Warning: software still in an experimental state_
|
|
14
14
|
|
|
15
|
-
Start by
|
|
15
|
+
Start by logging in with your username, for example for `john`:
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
dxfl login john
|
|
@@ -30,13 +30,19 @@ And then to deploy your `_public` folder on `example.com`:
|
|
|
30
30
|
dxfl deploy example.com _public
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
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:
|
|
34
40
|
|
|
35
41
|
```
|
|
36
42
|
dxfl deploy example.com _public --dry-run
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
Use the `empty` command to delete all files from a website (required before deleting it):
|
|
40
46
|
|
|
41
47
|
```
|
|
42
48
|
dxfl empty example.com
|
|
@@ -45,7 +51,7 @@ dxfl empty example.com
|
|
|
45
51
|
## Website configuration
|
|
46
52
|
|
|
47
53
|
`dxfl deploy` reads a `deuxfleurs.toml` configuration file (if it exists in the current directory).
|
|
48
|
-
This file can be used to specify website configuration metadata
|
|
54
|
+
This file can be used to specify website configuration metadata such as redirections.
|
|
49
55
|
|
|
50
56
|
Your `deuxfleurs.toml` should follow the following structure:
|
|
51
57
|
|
|
@@ -53,9 +59,9 @@ Your `deuxfleurs.toml` should follow the following structure:
|
|
|
53
59
|
# Filename added after URLs that point to directories.
|
|
54
60
|
# Default value: "index.html"
|
|
55
61
|
index_page = "index.html"
|
|
56
|
-
# Path to the file to serve in case of 404 error
|
|
57
|
-
# Default value:
|
|
58
|
-
error_page = "
|
|
62
|
+
# Path to the file to serve in case of 404 error.
|
|
63
|
+
# Default value: "error.html"
|
|
64
|
+
error_page = "error.html"
|
|
59
65
|
|
|
60
66
|
# A redirect entry. There can be as many as desired.
|
|
61
67
|
[[redirects]]
|
|
@@ -75,10 +81,66 @@ from = "foobar/*"
|
|
|
75
81
|
# single path.
|
|
76
82
|
to = "baz/*"
|
|
77
83
|
# Optional: custom HTTP status code for the redirection.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
# Configuration to allow cross-origin requests.
|
|
95
|
+
# Multiple rules can be specified and will be matched in order.
|
|
96
|
+
# Each rule is defined in its own `[[cors]]` block
|
|
97
|
+
[[cors]]
|
|
98
|
+
# Origins that you want to allow cross-origin requests from.
|
|
99
|
+
allowed_origins = "https://www.example.com"
|
|
100
|
+
# Possible values:
|
|
101
|
+
# - allowed_origins = "*" : wildcard to allow all origins
|
|
102
|
+
# - allowed_origins = "https://www.example.com" : string of origin (protocol+domain)
|
|
103
|
+
# - allowed_origins = ["https://example1.com", "https://example2.com"] : array of origins
|
|
104
|
+
# Required parameter.
|
|
105
|
+
|
|
106
|
+
# HTTP methods allowed in a cross-origin request (in response to a preflight request).
|
|
107
|
+
allowed_method = "GET"
|
|
108
|
+
# Possible values:
|
|
109
|
+
# - allowed_method = "*" : wild card to allow all methods (only GET/HEAD are currently supported by the server)
|
|
110
|
+
# - allowed_method = "GET" : string of http method ("GET" or "HEAD")
|
|
111
|
+
# - allowed_method = ["GET", "HEAD"] : array of string http methods
|
|
112
|
+
# Optional parameter. Default value: ["GET", "HEAD"]
|
|
113
|
+
|
|
114
|
+
# Request headers allowed in a preflight request.
|
|
115
|
+
allowed_header = "Authorization"
|
|
116
|
+
# Possible values:
|
|
117
|
+
# - allowed_header = "*" : wildcard to allow all headers
|
|
118
|
+
# - allowed_header = "Authorization" : string of http header
|
|
119
|
+
# - allowed_header = ["Authorization", "Age"] : array of string http header
|
|
120
|
+
# Optional parameter. Default value : []
|
|
121
|
+
|
|
122
|
+
# Response headers you want to make available to JavaScript in response to a cross-origin request.
|
|
123
|
+
expose_header = "Content-Encoding"
|
|
124
|
+
# Possible values:
|
|
125
|
+
# - expose_header = "Content-Encoding" : string of http header
|
|
126
|
+
# - expose_header = ["Content-Encoding", "Kuma-Revision"] : array of string http header
|
|
127
|
+
# Optional Parameter. Default value : []
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Using dxfl in automated deployments
|
|
131
|
+
|
|
132
|
+
`dxfl login` is designed for interactive use, where you type the password in
|
|
133
|
+
your local computer. For automated deployments (e.g. deploying your website in a
|
|
134
|
+
continuous integration workflows), you can instead directly call `dxfl deploy`
|
|
135
|
+
(or `dxfl empty`) and pass website credentials using environment variables:
|
|
136
|
+
|
|
137
|
+
- `AWS_ACCESS_KEY_ID` for the S3 key id;
|
|
138
|
+
- `AWS_SECRET_ACCESS_KEY` for the S3 secret key.
|
|
139
|
+
|
|
140
|
+
The values for these variables can be found in the Guichet web interface for
|
|
141
|
+
your website in the "S3" tab.
|
|
142
|
+
|
|
143
|
+
Config examples for [Woodpecker CI](doc/workflows/woodpecker.yml) and [GitHub Actions](doc/workflows/gh-actions.yml) are also available to help you get started.
|
|
82
144
|
|
|
83
145
|
## Development
|
|
84
146
|
|
|
@@ -113,6 +175,7 @@ Do not forget also to run `npm login` to bind your account with the CLI.
|
|
|
113
175
|
Then to publish a release:
|
|
114
176
|
|
|
115
177
|
```bash
|
|
178
|
+
vim CHANGELOG.md # update the version and its content in this file
|
|
116
179
|
vim package.json # update the version in this file
|
|
117
180
|
vim index.ts # update the version in this file
|
|
118
181
|
npm install # update the version in the package-lock.json
|
package/dist/auth.js
CHANGED
|
@@ -11,16 +11,62 @@ import { Configuration, WebsiteApi } 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 { confirmationPrompt } from "./utils.js";
|
|
14
15
|
function configPath() {
|
|
15
|
-
let path = ".
|
|
16
|
+
let path = ".dxfl/config.json";
|
|
16
17
|
if (process.env.XDG_CONFIG_HOME) {
|
|
17
|
-
path = process.env.XDG_CONFIG_HOME + "/
|
|
18
|
+
path = process.env.XDG_CONFIG_HOME + "/dxfl/config.json";
|
|
18
19
|
}
|
|
19
20
|
else if (process.env.HOME) {
|
|
20
|
-
path = process.env.HOME + "/.config/
|
|
21
|
+
path = process.env.HOME + "/.config/dxfl/config.json";
|
|
21
22
|
}
|
|
22
23
|
return path;
|
|
23
24
|
}
|
|
25
|
+
// Rename old config folder (from dfl to dxfl)
|
|
26
|
+
function moveOldConfig() {
|
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
function oldConfigPath() {
|
|
29
|
+
let path = ".dfl/config.json";
|
|
30
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
31
|
+
path = process.env.XDG_CONFIG_HOME + "/dfl/config.json";
|
|
32
|
+
}
|
|
33
|
+
else if (process.env.HOME) {
|
|
34
|
+
path = process.env.HOME + "/.config/dfl/config.json";
|
|
35
|
+
}
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
const oldConfigFile = oldConfigPath();
|
|
39
|
+
try {
|
|
40
|
+
yield fs.access(oldConfigFile);
|
|
41
|
+
try {
|
|
42
|
+
const newConfigFile = configPath();
|
|
43
|
+
const oldConfigParent = path.dirname(oldConfigFile);
|
|
44
|
+
const newConfigParent = path.dirname(newConfigFile);
|
|
45
|
+
yield fs.mkdir(newConfigParent, { recursive: true });
|
|
46
|
+
yield fs.rename(oldConfigFile, newConfigFile);
|
|
47
|
+
yield fs.rmdir(oldConfigParent);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error(err);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
// Doing nothing if no old config
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function apiConfExists() {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
try {
|
|
62
|
+
yield fs.access(configPath());
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch (_) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
24
70
|
export function openApiConf() {
|
|
25
71
|
return __awaiter(this, void 0, void 0, function* () {
|
|
26
72
|
let strConf;
|
|
@@ -29,9 +75,15 @@ export function openApiConf() {
|
|
|
29
75
|
try {
|
|
30
76
|
strConf = yield fs.readFile(configFile, { encoding: "utf8" });
|
|
31
77
|
}
|
|
32
|
-
catch (
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
catch (e) {
|
|
79
|
+
try {
|
|
80
|
+
yield moveOldConfig(); // Check for old config folder to rename
|
|
81
|
+
strConf = yield fs.readFile(configFile, { encoding: "utf8" });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.error(err, `\n\nUnable to read ${configFile}, run 'dxfl login' first.`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
35
87
|
}
|
|
36
88
|
try {
|
|
37
89
|
dictConf = JSON.parse(strConf);
|
|
@@ -79,3 +131,39 @@ export function login(username) {
|
|
|
79
131
|
console.log("ok");
|
|
80
132
|
});
|
|
81
133
|
}
|
|
134
|
+
export function logout(options) {
|
|
135
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
const configFile = configPath();
|
|
137
|
+
const parentFolder = path.dirname(configFile);
|
|
138
|
+
// Check if config file exist
|
|
139
|
+
try {
|
|
140
|
+
yield fs.access(configFile);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error(err, `\n\nUnable to find ${configFile}. Are you logged on this device?`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
// If not --yes: ask for confirmation before proceeding
|
|
147
|
+
if (!options.yes) {
|
|
148
|
+
process.stdout.write(`Are you sure to clean dxfl's config folder? (${parentFolder})\n`);
|
|
149
|
+
const ok = yield confirmationPrompt(() => {
|
|
150
|
+
process.stdout.write("Details of planned operations:\n");
|
|
151
|
+
process.stdout.write(` Delete file ${configFile}\n`);
|
|
152
|
+
process.stdout.write(` Delete folder ${parentFolder}\n`);
|
|
153
|
+
});
|
|
154
|
+
if (!ok) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
yield fs.rm(configFile);
|
|
160
|
+
yield fs.rm(parentFolder, { recursive: true });
|
|
161
|
+
process.stdout.write("Config successfully cleaned up!\n");
|
|
162
|
+
process.stdout.write("You can now go outside, feel the sun and touch grass, you are free! ☀️\n");
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.error(err, `\n\nUnable to cleanup config folder. Do you have the permission to delete ${parentFolder}?`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
package/dist/bucket.js
CHANGED
|
@@ -9,36 +9,68 @@ 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";
|
|
12
13
|
import { DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
|
|
13
14
|
import { PromisePool } from "@supercharge/promise-pool";
|
|
15
|
+
import { apiConfExists, openApiConf } from "./auth.js";
|
|
14
16
|
import { parseEtag, toChunks, formatBytesHuman } from "./utils.js";
|
|
15
|
-
export function
|
|
17
|
+
export function getBucketCredentials(name) {
|
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
+
if (yield apiConfExists()) {
|
|
20
|
+
const api = new WebsiteApi(yield openApiConf());
|
|
21
|
+
return yield credentialsFromApi(api, name);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const creds = credentialsFromEnv();
|
|
25
|
+
if (creds === undefined) {
|
|
26
|
+
throw new Error("Failed to load credentials.\n" +
|
|
27
|
+
"You need to run 'dxfl login', " +
|
|
28
|
+
"or define the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.");
|
|
29
|
+
}
|
|
30
|
+
return creds;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function credentialsFromEnv() {
|
|
35
|
+
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
36
|
+
return {
|
|
37
|
+
key: process.env.AWS_ACCESS_KEY_ID,
|
|
38
|
+
secret: process.env.AWS_SECRET_ACCESS_KEY,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function credentialsFromApi(api, website) {
|
|
16
46
|
return __awaiter(this, void 0, void 0, function* () {
|
|
17
47
|
// Get website info from guichet (bucket name and keys)
|
|
18
|
-
const bucket = yield api.getWebsite({ vhost }).catch(err => {
|
|
48
|
+
const bucket = yield api.getWebsite({ vhost: website }).catch(err => {
|
|
19
49
|
var _a;
|
|
20
50
|
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 '${
|
|
51
|
+
console.error(`Error: website '${website}' does not exist`);
|
|
22
52
|
}
|
|
23
53
|
else {
|
|
24
54
|
console.error(err);
|
|
25
55
|
}
|
|
26
56
|
throw new Error(err);
|
|
27
57
|
});
|
|
58
|
+
return { key: bucket.accessKeyId, secret: bucket.secretAccessKey };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function getBucket(name, creds) {
|
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
63
|
// Initialize a S3 Client
|
|
29
64
|
const client = new S3Client({
|
|
30
65
|
endpoint: "https://garage.deuxfleurs.fr",
|
|
31
66
|
region: "garage",
|
|
32
67
|
forcePathStyle: true,
|
|
33
68
|
credentials: {
|
|
34
|
-
accessKeyId:
|
|
35
|
-
secretAccessKey:
|
|
69
|
+
accessKeyId: creds.key,
|
|
70
|
+
secretAccessKey: creds.secret,
|
|
36
71
|
},
|
|
37
72
|
});
|
|
38
|
-
return {
|
|
39
|
-
client: client,
|
|
40
|
-
name: bucket.vhost.name,
|
|
41
|
-
};
|
|
73
|
+
return { client, name };
|
|
42
74
|
});
|
|
43
75
|
}
|
|
44
76
|
export function getBucketFiles(bucket) {
|
package/dist/deploy.js
CHANGED
|
@@ -9,12 +9,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import fs from "fs";
|
|
11
11
|
import path from "path";
|
|
12
|
-
import { WebsiteApi } from "guichet-sdk-ts";
|
|
13
12
|
import { PromisePool } from "@supercharge/promise-pool";
|
|
14
|
-
import {
|
|
15
|
-
import { deleteBucketFile, deleteBucketFiles, getBucket, getBucketFiles, putEmptyObjectRedirect, uploadFile, } from "./bucket.js";
|
|
13
|
+
import { deleteBucketFile, deleteBucketFiles, getBucketCredentials, getBucket, getBucketFiles, putEmptyObjectRedirect, uploadFile, } from "./bucket.js";
|
|
16
14
|
import { confirmationPrompt, filterMap, formatBytesHuman, formatCount, getFileMd5, sum, } from "./utils.js";
|
|
17
|
-
import { equalBucketRedirect, getBucketConfig, putBucketWebsiteConfig, readConfigFile, } from "./website_config.js";
|
|
15
|
+
import { equalBucketRedirect, equalCorsRules, getBucketConfig, putBucketWebsiteConfig, putCorsRules, readConfigFile, } from "./website_config.js";
|
|
18
16
|
// Walks through the local directory at path `dir`, and for each file it contains, returns :
|
|
19
17
|
// - `localPath`: its path on the local filesystem (includes `dir`). On windows, this path
|
|
20
18
|
// will typically use `\` as separator.
|
|
@@ -134,6 +132,10 @@ function computeDeployPlan(localFiles, remoteFiles, localCfg, remoteCfg) {
|
|
|
134
132
|
from: remoteCfg.bucket_redirects,
|
|
135
133
|
to: localCfg.bucket_redirects,
|
|
136
134
|
},
|
|
135
|
+
cors_rules: {
|
|
136
|
+
from: remoteCfg.cors_rules,
|
|
137
|
+
to: localCfg.cors_rules,
|
|
138
|
+
},
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
function diffBucketRedirects(from, to) {
|
|
@@ -163,8 +165,8 @@ function printPlan(plan, details) {
|
|
|
163
165
|
const proto = r.protocol ? `${r.protocol}://` : "";
|
|
164
166
|
const hostname = r.hostname ? `${r.hostname}/` : "";
|
|
165
167
|
const status = r.status ? ` status=${r.status}` : "";
|
|
166
|
-
const
|
|
167
|
-
return `${proto}${hostname}${to}${status}${
|
|
168
|
+
const force = r.force ? ` force=${r.force}` : "";
|
|
169
|
+
return `${proto}${hostname}${to}${status}${force}`;
|
|
168
170
|
}
|
|
169
171
|
function showBucketRedirect(r) {
|
|
170
172
|
return `${r.prefix}* -> ${showBucketRedirectTarget(r)}`;
|
|
@@ -184,6 +186,8 @@ function printPlan(plan, details) {
|
|
|
184
186
|
const oredirects_added = plan.redirects.filter(r => r.action == "add");
|
|
185
187
|
const oredirects_updated = plan.redirects.filter(r => r.action == "update");
|
|
186
188
|
const oredirects_deleted = plan.redirects.filter(r => r.action == "delete");
|
|
189
|
+
// check whether CORS rules changed
|
|
190
|
+
const cors_changed = !equalCorsRules(plan.cors_rules.from, plan.cors_rules.to);
|
|
187
191
|
// print
|
|
188
192
|
if (details == "summary") {
|
|
189
193
|
const sizeRemote = sum([...plan.remoteFiles.values()].map(f => { var _a; return (_a = f.size) !== null && _a !== void 0 ? _a : 0; }));
|
|
@@ -211,6 +215,9 @@ function printPlan(plan, details) {
|
|
|
211
215
|
if (sentence != "") {
|
|
212
216
|
process.stdout.write(` ${sentence}\n`);
|
|
213
217
|
}
|
|
218
|
+
if (cors_changed) {
|
|
219
|
+
process.stdout.write(" CORS rules modified\n");
|
|
220
|
+
}
|
|
214
221
|
process.stdout.write(` size: ${formatBytesHuman(sizeRemote)} -> ${formatBytesHuman(sizeLocal)}\n`);
|
|
215
222
|
}
|
|
216
223
|
else {
|
|
@@ -239,6 +246,20 @@ function printPlan(plan, details) {
|
|
|
239
246
|
for (const { source, prev_target } of oredirects_deleted) {
|
|
240
247
|
process.stdout.write(` Delete redirect ${source} -> ${prev_target}\n`);
|
|
241
248
|
}
|
|
249
|
+
if (cors_changed) {
|
|
250
|
+
process.stdout.write(" Set CORS rules:\n");
|
|
251
|
+
for (const rule of plan.cors_rules.to) {
|
|
252
|
+
function pp(a) {
|
|
253
|
+
return a.length == 1
|
|
254
|
+
? `"${a[0]}"`
|
|
255
|
+
: `[${a.map(s => `"${s}"`).join(", ")}]`;
|
|
256
|
+
}
|
|
257
|
+
process.stdout.write(` - allowed_origins = ${pp(rule.allowed_origins)}\n`);
|
|
258
|
+
process.stdout.write(` allowed_methods = ${pp(rule.allowed_methods)}\n`);
|
|
259
|
+
process.stdout.write(` allowed_headers = ${pp(rule.allowed_headers)}\n`);
|
|
260
|
+
process.stdout.write(` expose_headers = ${pp(rule.expose_headers)}\n`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
242
263
|
}
|
|
243
264
|
// print other (index, error page)
|
|
244
265
|
function s(s) {
|
|
@@ -289,6 +310,10 @@ function applyDeployPlan(bucket, plan) {
|
|
|
289
310
|
}));
|
|
290
311
|
// Apply bucket redirects & global config
|
|
291
312
|
yield putBucketWebsiteConfig(bucket, plan.index_page.to, plan.error_page.to, plan.bucket_redirects.to);
|
|
313
|
+
// Apply CORS rules
|
|
314
|
+
if (!equalCorsRules(plan.cors_rules.from, plan.cors_rules.to)) {
|
|
315
|
+
yield putCorsRules(bucket, plan.cors_rules.to);
|
|
316
|
+
}
|
|
292
317
|
});
|
|
293
318
|
}
|
|
294
319
|
function deployMain(website, localFolder, options) {
|
|
@@ -308,9 +333,7 @@ function deployMain(website, localFolder, options) {
|
|
|
308
333
|
getLocalFilesWithInfo(localFolder),
|
|
309
334
|
// Get the bucket, list of files stored in the bucket, and bucket website config
|
|
310
335
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
311
|
-
const
|
|
312
|
-
const api = new WebsiteApi(guichet);
|
|
313
|
-
const bucket = yield getBucket(api, website);
|
|
336
|
+
const bucket = yield getBucket(website, yield getBucketCredentials(website));
|
|
314
337
|
const remoteFiles = yield getBucketFiles(bucket);
|
|
315
338
|
// This can be slow because it needs to query each object in the bucket.
|
|
316
339
|
const remoteWebsiteConfig = yield getBucketConfig(bucket, [
|
|
@@ -360,7 +383,6 @@ export function deploy(website, localFolder, options) {
|
|
|
360
383
|
console.error(`Error: ${err}`);
|
|
361
384
|
}
|
|
362
385
|
else {
|
|
363
|
-
console.error("Error:");
|
|
364
386
|
console.error(err);
|
|
365
387
|
}
|
|
366
388
|
process.exit(1);
|
package/dist/empty.js
CHANGED
|
@@ -7,9 +7,7 @@ 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 {
|
|
11
|
-
import { openApiConf } from "./auth.js";
|
|
12
|
-
import { getBucket, getBucketFiles, deleteBucketFiles } from "./bucket.js";
|
|
10
|
+
import { getBucket, getBucketCredentials, getBucketFiles, deleteBucketFiles, } from "./bucket.js";
|
|
13
11
|
import { confirmationPrompt, formatBytesHuman, formatCount, sum, } from "./utils.js";
|
|
14
12
|
function emptyMain(website, options) {
|
|
15
13
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -17,9 +15,7 @@ function emptyMain(website, options) {
|
|
|
17
15
|
console.error("Error: options --yes and --dry-run cannot be passed at the same time");
|
|
18
16
|
}
|
|
19
17
|
process.stdout.write("Fetching the website configuration and metadata...\n");
|
|
20
|
-
const
|
|
21
|
-
const api = new WebsiteApi(guichet);
|
|
22
|
-
const bucket = yield getBucket(api, website);
|
|
18
|
+
const bucket = yield getBucket(website, yield getBucketCredentials(website));
|
|
23
19
|
const filesToDelete = [...(yield getBucketFiles(bucket))].map(([name, { size }]) => ({
|
|
24
20
|
name,
|
|
25
21
|
size,
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from "@commander-js/extra-typings";
|
|
3
|
-
import { login } from "./auth.js";
|
|
3
|
+
import { login, logout } from "./auth.js";
|
|
4
4
|
import { deploy } from "./deploy.js";
|
|
5
5
|
import { empty } from "./empty.js";
|
|
6
6
|
import { vhostsList } from "./vhosts.js";
|
|
7
|
-
program.name("dxfl").description("Deuxfleurs CLI tool").version("0.1.
|
|
7
|
+
program.name("dxfl").description("Deuxfleurs CLI tool").version("0.1.10");
|
|
8
8
|
program
|
|
9
9
|
.command("login")
|
|
10
10
|
.description("Link your Deuxfleurs account with this tool.")
|
|
@@ -29,4 +29,9 @@ program
|
|
|
29
29
|
.option("-n, --dry-run", "do a trial run without making actual changes")
|
|
30
30
|
.option("-y, --yes", "apply the changes without asking for confirmation")
|
|
31
31
|
.action(empty);
|
|
32
|
+
program
|
|
33
|
+
.command("logout")
|
|
34
|
+
.description("Cleanup dxfl config containing your credentials")
|
|
35
|
+
.option("-y, --yes", "clean without asking for confirmation")
|
|
36
|
+
.action(logout);
|
|
32
37
|
program.parse();
|
package/dist/website_config.js
CHANGED
|
@@ -12,7 +12,7 @@ import TOML from "smol-toml";
|
|
|
12
12
|
import URI from "fast-uri";
|
|
13
13
|
import { z as zod } from "zod";
|
|
14
14
|
import { fromError as zodError } from "zod-validation-error";
|
|
15
|
-
import { GetBucketWebsiteCommand, PutBucketWebsiteCommand, } from "@aws-sdk/client-s3";
|
|
15
|
+
import { GetBucketCorsCommand, GetBucketWebsiteCommand, PutBucketWebsiteCommand, PutBucketCorsCommand, } from "@aws-sdk/client-s3";
|
|
16
16
|
import { getBucketFilesDetails } from "./bucket.js";
|
|
17
17
|
////////////// Utilities
|
|
18
18
|
export function equalBucketRedirect(b1, b2) {
|
|
@@ -28,12 +28,27 @@ export function equalBucketRedirect(b1, b2) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
return (b1.prefix === b2.prefix &&
|
|
31
|
-
b1.
|
|
31
|
+
b1.force === b2.force &&
|
|
32
32
|
b1.hostname === b2.hostname &&
|
|
33
33
|
b1.status === b2.status &&
|
|
34
34
|
b1.protocol === b2.protocol &&
|
|
35
35
|
equalTo(b1.to, b2.to));
|
|
36
36
|
}
|
|
37
|
+
export function equalCorsRules(c1, c2) {
|
|
38
|
+
function eqArr(eqT, x, y) {
|
|
39
|
+
return x.length === y.length && x.every((v, i) => eqT(y[i], v));
|
|
40
|
+
}
|
|
41
|
+
function eqString(s1, s2) {
|
|
42
|
+
return s1 == s2;
|
|
43
|
+
}
|
|
44
|
+
function eqRule(c1, c2) {
|
|
45
|
+
return (eqArr(eqString, c1.allowed_origins, c2.allowed_origins) &&
|
|
46
|
+
eqArr(eqString, c1.allowed_headers, c2.allowed_headers) &&
|
|
47
|
+
eqArr(eqString, c1.allowed_methods, c2.allowed_methods) &&
|
|
48
|
+
eqArr(eqString, c1.expose_headers, c2.expose_headers));
|
|
49
|
+
}
|
|
50
|
+
return eqArr(eqRule, c1, c2);
|
|
51
|
+
}
|
|
37
52
|
////////////// Parsing from a TOML config file
|
|
38
53
|
// Parsing: TOML -> untyped object
|
|
39
54
|
function readConfigFileObject(filename) {
|
|
@@ -64,13 +79,20 @@ function readConfigFileObject(filename) {
|
|
|
64
79
|
const RawRedirectSchema = zod.object({
|
|
65
80
|
from: zod.string(),
|
|
66
81
|
to: zod.string(),
|
|
67
|
-
|
|
82
|
+
force: zod.boolean().optional(),
|
|
68
83
|
status: zod.number().int().positive().optional(),
|
|
69
84
|
});
|
|
85
|
+
const RawCorsSchema = zod.object({
|
|
86
|
+
allowed_origins: zod.union([zod.string(), zod.string().array()]),
|
|
87
|
+
allowed_methods: zod.union([zod.string(), zod.string().array()]).optional(),
|
|
88
|
+
allowed_headers: zod.union([zod.string(), zod.string().array()]).optional(),
|
|
89
|
+
expose_headers: zod.union([zod.string(), zod.string().array()]).optional(),
|
|
90
|
+
});
|
|
70
91
|
const RawConfigSchema = zod.object({
|
|
71
92
|
index_page: zod.string().optional(),
|
|
72
93
|
error_page: zod.string().optional(),
|
|
73
94
|
redirects: zod.array(RawRedirectSchema).optional(),
|
|
95
|
+
cors: zod.array(RawCorsSchema).optional(),
|
|
74
96
|
});
|
|
75
97
|
function interpRawConfig(cfg) {
|
|
76
98
|
try {
|
|
@@ -114,7 +136,7 @@ function unescape(s) {
|
|
|
114
136
|
}
|
|
115
137
|
}
|
|
116
138
|
function interpConfig(rawcfg) {
|
|
117
|
-
var _a, _b;
|
|
139
|
+
var _a, _b, _c, _d, _e;
|
|
118
140
|
function interpPath(name, str) {
|
|
119
141
|
try {
|
|
120
142
|
return unescape(str);
|
|
@@ -124,7 +146,7 @@ function interpConfig(rawcfg) {
|
|
|
124
146
|
}
|
|
125
147
|
}
|
|
126
148
|
function interpRedirect(i, r) {
|
|
127
|
-
var _a;
|
|
149
|
+
var _a, _b;
|
|
128
150
|
const rfrom = interpPath(`Redirect ${i}, from`, r.from);
|
|
129
151
|
const rto = interpPath(`Redirect ${i}, to`, r.to);
|
|
130
152
|
let redirect;
|
|
@@ -167,7 +189,7 @@ function interpConfig(rawcfg) {
|
|
|
167
189
|
}
|
|
168
190
|
let bredirect = {
|
|
169
191
|
prefix,
|
|
170
|
-
|
|
192
|
+
force: (_b = r.force) !== null && _b !== void 0 ? _b : false,
|
|
171
193
|
hostname: toURI.host,
|
|
172
194
|
protocol: toURI.scheme,
|
|
173
195
|
// Unless specified, set the status to 302.
|
|
@@ -180,12 +202,6 @@ function interpConfig(rawcfg) {
|
|
|
180
202
|
? { kind: "replace_prefix", prefix: path }
|
|
181
203
|
: { kind: "replace", target: path },
|
|
182
204
|
};
|
|
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
205
|
if (r.status) {
|
|
190
206
|
if ([301, 302, 303, 307, 308].includes(r.status)) {
|
|
191
207
|
bredirect.status = r.status;
|
|
@@ -204,8 +220,9 @@ function interpConfig(rawcfg) {
|
|
|
204
220
|
}
|
|
205
221
|
else {
|
|
206
222
|
// This is an object redirect
|
|
207
|
-
if (r.
|
|
208
|
-
throw `Redirect ${i}: '
|
|
223
|
+
if (r.force) {
|
|
224
|
+
throw (`Redirect ${i}: 'force' is not supported for single redirections.\n` +
|
|
225
|
+
"The source of the redirection must not exist already as a file.");
|
|
209
226
|
}
|
|
210
227
|
if (r.status) {
|
|
211
228
|
throw (`Redirect ${i}: 'status' is not supported for single redirections.\n` +
|
|
@@ -221,7 +238,7 @@ function interpConfig(rawcfg) {
|
|
|
221
238
|
if (from.startsWith("/")) {
|
|
222
239
|
from = from.substring(1);
|
|
223
240
|
}
|
|
224
|
-
// for the 'to' field, garage expects
|
|
241
|
+
// for the 'to' field, garage expects the target to start with 'http://', 'https://'
|
|
225
242
|
// or '/'. So if the 'to' field does not start with '/', add one...
|
|
226
243
|
let to = rto.s;
|
|
227
244
|
if (!(to.startsWith("http://") ||
|
|
@@ -233,14 +250,29 @@ function interpConfig(rawcfg) {
|
|
|
233
250
|
}
|
|
234
251
|
return redirect;
|
|
235
252
|
}
|
|
253
|
+
// Default handling for an optional string:
|
|
254
|
+
// undefined -> def
|
|
255
|
+
// "" -> undefined
|
|
256
|
+
// other -> other.
|
|
257
|
+
//
|
|
258
|
+
// `""` is "disable this", while `undefined` is "use the default".
|
|
259
|
+
// This works as long as the empty string is not a valid value for `s`.
|
|
260
|
+
let withDefault = (s, def) => {
|
|
261
|
+
if (s === "") {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
return s !== null && s !== void 0 ? s : def;
|
|
265
|
+
};
|
|
236
266
|
let cfg = {
|
|
237
267
|
// use index.html as default index if not specified explicitly
|
|
238
|
-
index_page: (
|
|
239
|
-
|
|
268
|
+
index_page: withDefault(rawcfg.index_page, "index.html"),
|
|
269
|
+
// use error.html as default error page
|
|
270
|
+
error_page: withDefault(rawcfg.error_page, "error.html"),
|
|
240
271
|
bucket_redirects: [],
|
|
241
272
|
object_redirects: new Map(),
|
|
273
|
+
cors_rules: [],
|
|
242
274
|
};
|
|
243
|
-
for (const [i, raw] of ((
|
|
275
|
+
for (const [i, raw] of ((_a = rawcfg.redirects) !== null && _a !== void 0 ? _a : []).entries()) {
|
|
244
276
|
// `i+1` is only used for display: start counting redirects from 1 instead of 0
|
|
245
277
|
const r = interpRedirect(i + 1, raw);
|
|
246
278
|
if (r.kind == "bucket") {
|
|
@@ -253,6 +285,17 @@ function interpConfig(rawcfg) {
|
|
|
253
285
|
cfg.object_redirects.set(r.from, r.to);
|
|
254
286
|
}
|
|
255
287
|
}
|
|
288
|
+
for (const [_, raw] of ((_b = rawcfg.cors) !== null && _b !== void 0 ? _b : []).entries()) {
|
|
289
|
+
const interpRawArray = (x) => {
|
|
290
|
+
return typeof x === "string" ? [x] : x;
|
|
291
|
+
};
|
|
292
|
+
cfg.cors_rules.push({
|
|
293
|
+
allowed_origins: interpRawArray(raw.allowed_origins),
|
|
294
|
+
allowed_methods: interpRawArray((_c = raw.allowed_methods) !== null && _c !== void 0 ? _c : ["GET", "HEAD"]),
|
|
295
|
+
allowed_headers: interpRawArray((_d = raw.allowed_methods) !== null && _d !== void 0 ? _d : []),
|
|
296
|
+
expose_headers: interpRawArray((_e = raw.expose_headers) !== null && _e !== void 0 ? _e : []),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
256
299
|
return cfg;
|
|
257
300
|
}
|
|
258
301
|
export function readConfigFile(filename) {
|
|
@@ -265,16 +308,27 @@ export function getBucketConfig(bucket, files) {
|
|
|
265
308
|
return __awaiter(this, void 0, void 0, function* () {
|
|
266
309
|
// This function performs the following S3 requests:
|
|
267
310
|
// - GetBucketWebsite to get bucket-level redirects;
|
|
311
|
+
// - GetBucketCors to get the bucket CORS configuration;
|
|
268
312
|
// - A Head command for each file in the bucket to collect object redirects.
|
|
269
313
|
// (This can become relatively slow for buckets with thousands of files,
|
|
270
314
|
// but I don't know of a better way.)
|
|
271
|
-
var _a, _b;
|
|
272
|
-
const [
|
|
315
|
+
var _a, _b, _c, _d, _e, _f;
|
|
316
|
+
const [website, cors, details] = yield Promise.all([
|
|
273
317
|
bucket.client.send(new GetBucketWebsiteCommand({ Bucket: bucket.name })),
|
|
318
|
+
bucket.client.send(new GetBucketCorsCommand({ Bucket: bucket.name })),
|
|
274
319
|
getBucketFilesDetails(bucket, files),
|
|
275
320
|
]);
|
|
276
|
-
if (
|
|
277
|
-
throw `Error sending GetBucketWebsite: ${
|
|
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 = [];
|
|
278
332
|
}
|
|
279
333
|
// Collect object redirects
|
|
280
334
|
const object_redirects = new Map();
|
|
@@ -284,29 +338,30 @@ export function getBucketConfig(bucket, files) {
|
|
|
284
338
|
}
|
|
285
339
|
}
|
|
286
340
|
// Interpret bucket redirects
|
|
287
|
-
if (
|
|
341
|
+
if (website.RedirectAllRequestsTo) {
|
|
288
342
|
// NB: garage does not currently support RedirectAllRequestsTo so this should never happen
|
|
289
343
|
throw (`remote website configuration: RedirectAllRequestsTo is specified; ` +
|
|
290
344
|
`this is currently unsupported by dxfl`);
|
|
291
345
|
}
|
|
292
|
-
const index_page = (_a =
|
|
293
|
-
const error_page = (_b =
|
|
346
|
+
const index_page = (_a = website.IndexDocument) === null || _a === void 0 ? void 0 : _a.Suffix;
|
|
347
|
+
const error_page = (_b = website.ErrorDocument) === null || _b === void 0 ? void 0 : _b.Key;
|
|
294
348
|
let bucket_redirects = [];
|
|
295
|
-
if (
|
|
296
|
-
for (const rule of
|
|
349
|
+
if (website.RoutingRules) {
|
|
350
|
+
for (const rule of website.RoutingRules) {
|
|
297
351
|
// If no Condition is specified, then the redirect always applies, which is equivalent
|
|
298
352
|
// to matching on an empty prefix
|
|
299
353
|
let prefix = "";
|
|
300
|
-
|
|
354
|
+
// If no HttpErrorCodeReturnedEquals is specified, then the redirection always
|
|
355
|
+
// applies, which is equivalent to force=true
|
|
356
|
+
let force = true;
|
|
301
357
|
if (rule.Condition) {
|
|
302
358
|
if (rule.Condition.HttpErrorCodeReturnedEquals) {
|
|
303
359
|
if (rule.Condition.HttpErrorCodeReturnedEquals == "404") {
|
|
304
|
-
|
|
360
|
+
force = false;
|
|
305
361
|
}
|
|
306
362
|
else {
|
|
307
|
-
//
|
|
308
|
-
throw
|
|
309
|
-
`status code than 404; this is currently unsupported by dxfl`);
|
|
363
|
+
// not supported by garage
|
|
364
|
+
throw `remote website configuration: unexpected value for 'HttpErrorCodeReturnedEquals': ${rule.Condition.HttpErrorCodeReturnedEquals}`;
|
|
310
365
|
}
|
|
311
366
|
}
|
|
312
367
|
if (rule.Condition.KeyPrefixEquals) {
|
|
@@ -346,7 +401,7 @@ export function getBucketConfig(bucket, files) {
|
|
|
346
401
|
}
|
|
347
402
|
bucket_redirects.push({
|
|
348
403
|
prefix,
|
|
349
|
-
|
|
404
|
+
force,
|
|
350
405
|
hostname,
|
|
351
406
|
status,
|
|
352
407
|
protocol,
|
|
@@ -354,7 +409,25 @@ export function getBucketConfig(bucket, files) {
|
|
|
354
409
|
});
|
|
355
410
|
}
|
|
356
411
|
}
|
|
357
|
-
|
|
412
|
+
// Interpret CORS rules
|
|
413
|
+
let cors_rules = [];
|
|
414
|
+
if (cors.CORSRules) {
|
|
415
|
+
for (const rule of cors.CORSRules) {
|
|
416
|
+
cors_rules.push({
|
|
417
|
+
allowed_origins: (_c = rule.AllowedOrigins) !== null && _c !== void 0 ? _c : [],
|
|
418
|
+
allowed_methods: (_d = rule.AllowedMethods) !== null && _d !== void 0 ? _d : [],
|
|
419
|
+
allowed_headers: (_e = rule.AllowedHeaders) !== null && _e !== void 0 ? _e : [],
|
|
420
|
+
expose_headers: (_f = rule.ExposeHeaders) !== null && _f !== void 0 ? _f : [],
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
index_page,
|
|
426
|
+
error_page,
|
|
427
|
+
bucket_redirects,
|
|
428
|
+
object_redirects,
|
|
429
|
+
cors_rules,
|
|
430
|
+
};
|
|
358
431
|
});
|
|
359
432
|
}
|
|
360
433
|
////////////// Applying a configuration to S3
|
|
@@ -378,8 +451,8 @@ export function putBucketWebsiteConfig(bucket, index_page, error_page, bucket_re
|
|
|
378
451
|
},
|
|
379
452
|
Redirect: {},
|
|
380
453
|
};
|
|
381
|
-
if (r.
|
|
382
|
-
rule.Condition.HttpErrorCodeReturnedEquals =
|
|
454
|
+
if (!r.force) {
|
|
455
|
+
rule.Condition.HttpErrorCodeReturnedEquals = "404";
|
|
383
456
|
}
|
|
384
457
|
if (r.hostname) {
|
|
385
458
|
rule.Redirect.HostName = r.hostname;
|
|
@@ -408,3 +481,24 @@ export function putBucketWebsiteConfig(bucket, index_page, error_page, bucket_re
|
|
|
408
481
|
}
|
|
409
482
|
});
|
|
410
483
|
}
|
|
484
|
+
// applies CORS rules
|
|
485
|
+
export function putCorsRules(bucket, cors_rules) {
|
|
486
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
487
|
+
let CORSRules = [];
|
|
488
|
+
for (const rule of cors_rules) {
|
|
489
|
+
CORSRules.push({
|
|
490
|
+
AllowedOrigins: rule.allowed_origins,
|
|
491
|
+
AllowedMethods: rule.allowed_methods,
|
|
492
|
+
AllowedHeaders: rule.allowed_headers,
|
|
493
|
+
ExposeHeaders: rule.expose_headers,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
const resp = yield bucket.client.send(new PutBucketCorsCommand({
|
|
497
|
+
Bucket: bucket.name,
|
|
498
|
+
CORSConfiguration: { CORSRules },
|
|
499
|
+
}));
|
|
500
|
+
if (resp && resp.$metadata.httpStatusCode != 200) {
|
|
501
|
+
throw resp;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Deploy with GitHub Actions
|
|
2
|
+
|
|
3
|
+
# You will need to:
|
|
4
|
+
# 1. Get (from Guichet web interface):
|
|
5
|
+
# - Your access keys (ID and secret): in the "S3" tab of your website
|
|
6
|
+
# - The website id: your sub-domain (the part before .web.deuxfleurs.fr) or your full custom domain (e.g. your-domain.com)
|
|
7
|
+
# 2. Setup the access keys as secrets in GitHub web interface (https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/using-secrets-in-github-actions), named in this example key_id and key_secret
|
|
8
|
+
# 3. If you use a deuxfleurs.toml config, be sure the dxfl deploy command is launch in the current directory its in.
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
# Build your website in a dedicated step to avoid to expose your access keys to external software
|
|
13
|
+
deploy:
|
|
14
|
+
runs-on: node:lts-alpine
|
|
15
|
+
steps:
|
|
16
|
+
- run: npx --yes dxfl deploy your-website-id build/ --yes
|
|
17
|
+
env:
|
|
18
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.key_id }}
|
|
19
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.key_secret }}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## Deploy with Woodpecker CI
|
|
2
|
+
#
|
|
3
|
+
# You will need to:
|
|
4
|
+
# 1. Get (from Guichet web interface):
|
|
5
|
+
# - Your access keys (ID and secret): in the "S3" tab of your website
|
|
6
|
+
# - The website id: your sub-domain (the part before .web.deuxfleurs.fr) or your full custom domain (e.g. your-domain.com)
|
|
7
|
+
# 2. Setup the access keys as secrets in Woodpecker web interface (https://woodpecker-ci.org/docs/usage/secrets), named in this example key_id and key_secret
|
|
8
|
+
# 3. If you use a deuxfleurs.toml config, be sure the dxfl deploy command is launch in the current directory its in.
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
- name: build
|
|
12
|
+
# Build your website in a dedicated step to avoid to expose your access keys to external software
|
|
13
|
+
- name: deploy
|
|
14
|
+
image: node:lts-alpine
|
|
15
|
+
environment:
|
|
16
|
+
AWS_ACCESS_KEY_ID:
|
|
17
|
+
from_secret: key_id
|
|
18
|
+
AWS_SECRET_ACCESS_KEY:
|
|
19
|
+
from_secret: key_secret
|
|
20
|
+
commands:
|
|
21
|
+
- npx --yes dxfl deploy your-website-id build/ --yes
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dxfl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "EUPL-1.2",
|
|
6
6
|
"author": "Deuxfleurs Team <coucou@deuxfleurs.fr>",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"cli",
|
|
9
|
+
"deuxfleurs"
|
|
10
|
+
],
|
|
7
11
|
"type": "module",
|
|
8
12
|
"main": "dist/main.js",
|
|
9
13
|
"bin": {
|