@strapi/provider-upload-aws-s3 4.15.0-alpha.0 → 4.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +146 -104
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +153 -0
- package/dist/index.mjs.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +12 -10
- package/dist/utils.js +0 -70
- package/dist/utils.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,GAAG,MAAM,SAAS,CAAC;AAG1B,UAAU,IAAI;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5C,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,UAAU,WAAY,SAAQ,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,mBAAmB,CAAC;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,mBAAmB,GAAG;QACtC,MAAM,EAAE;YACN,MAAM,EAAE,MAAM,CAAC;YACf,GAAG,CAAC,EAAE,MAAM,CAAC;YACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;SAC3B,CAAC;KACH,CAAC;CACH;;+DAG4D,WAAW;;2BAkEzC,IAAI,GAAG,QAAQ;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;2BA2BrC,IAAI;qBAGV,IAAI;qBAGJ,IAAI,sBAAsB,QAAQ,IAAI,CAAC;;;AApG1D,wBA0HE"}
|
package/dist/index.js
CHANGED
|
@@ -1,112 +1,154 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
const fp = require("lodash/fp");
|
|
3
|
+
const AWS = require("aws-sdk");
|
|
4
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
5
|
+
const AWS__default = /* @__PURE__ */ _interopDefault(AWS);
|
|
6
|
+
const ENDPOINT_PATTERN = /^(.+\.)?s3[.-]([a-z0-9-]+)\./;
|
|
7
|
+
function isUrlFromBucket(fileUrl, bucketName, baseUrl = "") {
|
|
8
|
+
const url = new URL(fileUrl);
|
|
9
|
+
if (baseUrl) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const { bucket } = getBucketFromAwsUrl(fileUrl);
|
|
13
|
+
if (bucket) {
|
|
14
|
+
return bucket === bucketName;
|
|
15
|
+
}
|
|
16
|
+
return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);
|
|
17
|
+
}
|
|
18
|
+
function getBucketFromAwsUrl(fileUrl) {
|
|
19
|
+
const url = new URL(fileUrl);
|
|
20
|
+
if (url.protocol === "s3:") {
|
|
21
|
+
const bucket = url.host;
|
|
22
|
+
if (!bucket) {
|
|
23
|
+
return { err: `Invalid S3 url: no bucket: ${url}` };
|
|
24
|
+
}
|
|
25
|
+
return { bucket };
|
|
26
|
+
}
|
|
27
|
+
if (!url.host) {
|
|
28
|
+
return { err: `Invalid S3 url: no hostname: ${url}` };
|
|
29
|
+
}
|
|
30
|
+
const matches = url.host.match(ENDPOINT_PATTERN);
|
|
31
|
+
if (!matches) {
|
|
32
|
+
return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };
|
|
33
|
+
}
|
|
34
|
+
const prefix = matches[1];
|
|
35
|
+
if (!prefix) {
|
|
36
|
+
if (url.pathname === "/") {
|
|
37
|
+
return { bucket: null };
|
|
38
|
+
}
|
|
39
|
+
const index2 = url.pathname.indexOf("/", 1);
|
|
40
|
+
if (index2 === -1) {
|
|
41
|
+
return { bucket: url.pathname.substring(1) };
|
|
42
|
+
}
|
|
43
|
+
if (index2 === url.pathname.length - 1) {
|
|
44
|
+
return { bucket: url.pathname.substring(1, index2) };
|
|
45
|
+
}
|
|
46
|
+
return { bucket: url.pathname.substring(1, index2) };
|
|
47
|
+
}
|
|
48
|
+
return { bucket: prefix.substring(0, prefix.length - 1) };
|
|
49
|
+
}
|
|
50
|
+
require("aws-sdk/lib/maintenance_mode_message").suppress = true;
|
|
11
51
|
function hasUrlProtocol(url) {
|
|
12
|
-
|
|
13
|
-
return /^\w*:\/\//.test(url);
|
|
52
|
+
return /^\w*:\/\//.test(url);
|
|
14
53
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
54
|
+
const index = {
|
|
55
|
+
init({ baseUrl, rootPath, s3Options, ...legacyS3Options }) {
|
|
56
|
+
if (Object.keys(legacyS3Options).length > 0) {
|
|
57
|
+
process.emitWarning(
|
|
58
|
+
"S3 configuration options passed at root level of the plugin's providerOptions is deprecated and will be removed in a future release. Please wrap them inside the 's3Options:{}' property."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const config = { ...s3Options, ...legacyS3Options };
|
|
62
|
+
const S3 = new AWS__default.default.S3({
|
|
63
|
+
apiVersion: "2006-03-01",
|
|
64
|
+
...config
|
|
65
|
+
});
|
|
66
|
+
const filePrefix = rootPath ? `${rootPath.replace(/\/+$/, "")}/` : "";
|
|
67
|
+
const getFileKey = (file) => {
|
|
68
|
+
const path = file.path ? `${file.path}/` : "";
|
|
69
|
+
return `${filePrefix}${path}${file.hash}${file.ext}`;
|
|
70
|
+
};
|
|
71
|
+
const ACL = fp.getOr("public-read", ["params", "ACL"], config);
|
|
72
|
+
const upload = (file, customParams = {}) => new Promise((resolve, reject) => {
|
|
73
|
+
const fileKey = getFileKey(file);
|
|
74
|
+
if (!file.stream && !file.buffer) {
|
|
75
|
+
reject(new Error("Missing file stream or buffer"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const params = {
|
|
79
|
+
Key: fileKey,
|
|
80
|
+
Bucket: config.params.Bucket,
|
|
81
|
+
Body: file.stream || file.buffer,
|
|
82
|
+
ACL,
|
|
83
|
+
ContentType: file.mime,
|
|
84
|
+
...customParams
|
|
85
|
+
};
|
|
86
|
+
const onUploaded = (err, data) => {
|
|
87
|
+
if (err) {
|
|
88
|
+
return reject(err);
|
|
19
89
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
90
|
+
if (baseUrl) {
|
|
91
|
+
file.url = `${baseUrl}/${fileKey}`;
|
|
92
|
+
} else {
|
|
93
|
+
file.url = hasUrlProtocol(data.Location) ? data.Location : `https://${data.Location}`;
|
|
94
|
+
}
|
|
95
|
+
resolve();
|
|
96
|
+
};
|
|
97
|
+
S3.upload(params, onUploaded);
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
isPrivate() {
|
|
101
|
+
return ACL === "private";
|
|
102
|
+
},
|
|
103
|
+
async getSignedUrl(file) {
|
|
104
|
+
if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {
|
|
105
|
+
return { url: file.url };
|
|
106
|
+
}
|
|
107
|
+
const signedUrlExpires = fp.getOr(15 * 60, ["params", "signedUrlExpires"], config);
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const fileKey = getFileKey(file);
|
|
110
|
+
S3.getSignedUrl(
|
|
111
|
+
"getObject",
|
|
112
|
+
{
|
|
113
|
+
Bucket: config.params.Bucket,
|
|
114
|
+
Key: fileKey,
|
|
115
|
+
Expires: parseInt(signedUrlExpires, 10)
|
|
116
|
+
},
|
|
117
|
+
(err, url) => {
|
|
118
|
+
if (err) {
|
|
119
|
+
return reject(err);
|
|
120
|
+
}
|
|
121
|
+
resolve({ url });
|
|
36
122
|
}
|
|
37
|
-
|
|
38
|
-
Key: fileKey,
|
|
39
|
-
Bucket: config.params.Bucket,
|
|
40
|
-
Body: file.stream || file.buffer,
|
|
41
|
-
ACL,
|
|
42
|
-
ContentType: file.mime,
|
|
43
|
-
...customParams,
|
|
44
|
-
};
|
|
45
|
-
const onUploaded = (err, data) => {
|
|
46
|
-
if (err) {
|
|
47
|
-
return reject(err);
|
|
48
|
-
}
|
|
49
|
-
// set the bucket file url
|
|
50
|
-
if (baseUrl) {
|
|
51
|
-
// Construct the url with the baseUrl
|
|
52
|
-
file.url = `${baseUrl}/${fileKey}`;
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
// Add the protocol if it is missing
|
|
56
|
-
// Some providers like DigitalOcean Spaces return the url without the protocol
|
|
57
|
-
file.url = hasUrlProtocol(data.Location) ? data.Location : `https://${data.Location}`;
|
|
58
|
-
}
|
|
59
|
-
resolve();
|
|
60
|
-
};
|
|
61
|
-
S3.upload(params, onUploaded);
|
|
123
|
+
);
|
|
62
124
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
125
|
+
},
|
|
126
|
+
uploadStream(file, customParams = {}) {
|
|
127
|
+
return upload(file, customParams);
|
|
128
|
+
},
|
|
129
|
+
upload(file, customParams = {}) {
|
|
130
|
+
return upload(file, customParams);
|
|
131
|
+
},
|
|
132
|
+
delete(file, customParams = {}) {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const fileKey = getFileKey(file);
|
|
135
|
+
S3.deleteObject(
|
|
136
|
+
{
|
|
137
|
+
Key: fileKey,
|
|
138
|
+
Bucket: config.params.Bucket,
|
|
139
|
+
...customParams
|
|
66
140
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Expires: parseInt(signedUrlExpires, 10),
|
|
79
|
-
}, (err, url) => {
|
|
80
|
-
if (err) {
|
|
81
|
-
return reject(err);
|
|
82
|
-
}
|
|
83
|
-
resolve({ url });
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
},
|
|
87
|
-
uploadStream(file, customParams = {}) {
|
|
88
|
-
return upload(file, customParams);
|
|
89
|
-
},
|
|
90
|
-
upload(file, customParams = {}) {
|
|
91
|
-
return upload(file, customParams);
|
|
92
|
-
},
|
|
93
|
-
delete(file, customParams = {}) {
|
|
94
|
-
return new Promise((resolve, reject) => {
|
|
95
|
-
// delete file on S3 bucket
|
|
96
|
-
const fileKey = getFileKey(file);
|
|
97
|
-
S3.deleteObject({
|
|
98
|
-
Key: fileKey,
|
|
99
|
-
Bucket: config.params.Bucket,
|
|
100
|
-
...customParams,
|
|
101
|
-
}, (err) => {
|
|
102
|
-
if (err) {
|
|
103
|
-
return reject(err);
|
|
104
|
-
}
|
|
105
|
-
resolve();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
},
|
|
141
|
+
(err) => {
|
|
142
|
+
if (err) {
|
|
143
|
+
return reject(err);
|
|
144
|
+
}
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
111
152
|
};
|
|
112
|
-
|
|
153
|
+
module.exports = index;
|
|
154
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["const ENDPOINT_PATTERN = /^(.+\\.)?s3[.-]([a-z0-9-]+)\\./;\n\ninterface BucketInfo {\n bucket?: string | null;\n err?: string;\n}\n\nexport function isUrlFromBucket(fileUrl: string, bucketName: string, baseUrl = ''): boolean {\n const url = new URL(fileUrl);\n\n // Check if the file URL is using a base URL (e.g. a CDN).\n // In this case do not sign the URL.\n if (baseUrl) {\n return false;\n }\n\n const { bucket } = getBucketFromAwsUrl(fileUrl);\n\n if (bucket) {\n return bucket === bucketName;\n }\n\n // File URL might be of an S3-compatible provider. (or an invalid URL)\n // In this case, check if the bucket name appears in the URL host or path.\n // e.g. https://minio.example.com/bucket-name/object-key\n // e.g. https://bucket.nyc3.digitaloceanspaces.com/folder/img.png\n return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);\n}\n\n/**\n * Parse the bucket name from a URL.\n * See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html\n *\n * @param {string} fileUrl - the URL to parse\n * @returns {object} result\n * @returns {string} result.bucket - the bucket name\n * @returns {string} result.err - if any\n */\nfunction getBucketFromAwsUrl(fileUrl: string): BucketInfo {\n const url = new URL(fileUrl);\n\n // S3://<bucket-name>/<key>\n if (url.protocol === 's3:') {\n const bucket = url.host;\n\n if (!bucket) {\n return { err: `Invalid S3 url: no bucket: ${url}` };\n }\n return { bucket };\n }\n\n if (!url.host) {\n return { err: `Invalid S3 url: no hostname: ${url}` };\n }\n\n const matches = url.host.match(ENDPOINT_PATTERN);\n if (!matches) {\n return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };\n }\n\n const prefix = matches[1];\n // https://s3.amazonaws.com/<bucket-name>\n if (!prefix) {\n if (url.pathname === '/') {\n return { bucket: null };\n }\n\n const index = url.pathname.indexOf('/', 1);\n\n // https://s3.amazonaws.com/<bucket-name>\n if (index === -1) {\n return { bucket: url.pathname.substring(1) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/\n if (index === url.pathname.length - 1) {\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/key\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://<bucket-name>.s3.amazonaws.com/\n return { bucket: prefix.substring(0, prefix.length - 1) };\n}\n","import type { ReadStream } from 'node:fs';\nimport { getOr } from 'lodash/fp';\nimport AWS from 'aws-sdk';\nimport { isUrlFromBucket } from './utils';\n\ninterface File {\n name: string;\n alternativeText?: string;\n caption?: string;\n width?: number;\n height?: number;\n formats?: Record<string, unknown>;\n hash: string;\n ext?: string;\n mime: string;\n size: number;\n url: string;\n previewUrl?: string;\n path?: string;\n provider?: string;\n provider_metadata?: Record<string, unknown>;\n stream?: ReadStream;\n buffer?: Buffer;\n}\n\n// TODO V5: Migrate to aws-sdk v3\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nrequire('aws-sdk/lib/maintenance_mode_message').suppress = true;\n\nfunction hasUrlProtocol(url: string) {\n // Regex to test protocol like \"http://\", \"https://\"\n return /^\\w*:\\/\\//.test(url);\n}\n\ninterface InitOptions extends Partial<AWS.S3.ClientConfiguration> {\n baseUrl?: string;\n rootPath?: string;\n s3Options: AWS.S3.ClientConfiguration & {\n params: {\n Bucket: string; // making it required\n ACL?: string;\n signedUrlExpires?: string;\n };\n };\n}\n\nexport default {\n init({ baseUrl, rootPath, s3Options, ...legacyS3Options }: InitOptions) {\n if (Object.keys(legacyS3Options).length > 0) {\n process.emitWarning(\n \"S3 configuration options passed at root level of the plugin's providerOptions is deprecated and will be removed in a future release. Please wrap them inside the 's3Options:{}' property.\"\n );\n }\n\n const config = { ...s3Options, ...legacyS3Options };\n\n const S3 = new AWS.S3({\n apiVersion: '2006-03-01',\n ...config,\n });\n\n const filePrefix = rootPath ? `${rootPath.replace(/\\/+$/, '')}/` : '';\n\n const getFileKey = (file: File) => {\n const path = file.path ? `${file.path}/` : '';\n\n return `${filePrefix}${path}${file.hash}${file.ext}`;\n };\n\n const ACL = getOr('public-read', ['params', 'ACL'], config);\n\n const upload = (file: File, customParams = {}): Promise<void> =>\n new Promise((resolve, reject) => {\n const fileKey = getFileKey(file);\n\n if (!file.stream && !file.buffer) {\n reject(new Error('Missing file stream or buffer'));\n return;\n }\n\n const params = {\n Key: fileKey,\n Bucket: config.params.Bucket,\n Body: file.stream || file.buffer,\n ACL,\n ContentType: file.mime,\n ...customParams,\n };\n\n const onUploaded = (err: Error, data: AWS.S3.ManagedUpload.SendData) => {\n if (err) {\n return reject(err);\n }\n\n // set the bucket file url\n if (baseUrl) {\n // Construct the url with the baseUrl\n file.url = `${baseUrl}/${fileKey}`;\n } else {\n // Add the protocol if it is missing\n // Some providers like DigitalOcean Spaces return the url without the protocol\n file.url = hasUrlProtocol(data.Location) ? data.Location : `https://${data.Location}`;\n }\n resolve();\n };\n\n S3.upload(params, onUploaded);\n });\n\n return {\n isPrivate() {\n return ACL === 'private';\n },\n async getSignedUrl(file: File): Promise<{ url: string }> {\n // Do not sign the url if it does not come from the same bucket.\n if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {\n return { url: file.url };\n }\n\n const signedUrlExpires: string = getOr(15 * 60, ['params', 'signedUrlExpires'], config); // 15 minutes\n\n return new Promise((resolve, reject) => {\n const fileKey = getFileKey(file);\n\n S3.getSignedUrl(\n 'getObject',\n {\n Bucket: config.params.Bucket,\n Key: fileKey,\n Expires: parseInt(signedUrlExpires, 10),\n },\n (err, url) => {\n if (err) {\n return reject(err);\n }\n resolve({ url });\n }\n );\n });\n },\n uploadStream(file: File, customParams = {}) {\n return upload(file, customParams);\n },\n upload(file: File, customParams = {}) {\n return upload(file, customParams);\n },\n delete(file: File, customParams = {}): Promise<void> {\n return new Promise((resolve, reject) => {\n // delete file on S3 bucket\n const fileKey = getFileKey(file);\n S3.deleteObject(\n {\n Key: fileKey,\n Bucket: config.params.Bucket,\n ...customParams,\n },\n (err) => {\n if (err) {\n return reject(err);\n }\n\n resolve();\n }\n );\n });\n },\n };\n },\n};\n"],"names":["index","AWS","getOr"],"mappings":"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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { getOr } from "lodash/fp";
|
|
2
|
+
import AWS from "aws-sdk";
|
|
3
|
+
const ENDPOINT_PATTERN = /^(.+\.)?s3[.-]([a-z0-9-]+)\./;
|
|
4
|
+
function isUrlFromBucket(fileUrl, bucketName, baseUrl = "") {
|
|
5
|
+
const url = new URL(fileUrl);
|
|
6
|
+
if (baseUrl) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const { bucket } = getBucketFromAwsUrl(fileUrl);
|
|
10
|
+
if (bucket) {
|
|
11
|
+
return bucket === bucketName;
|
|
12
|
+
}
|
|
13
|
+
return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);
|
|
14
|
+
}
|
|
15
|
+
function getBucketFromAwsUrl(fileUrl) {
|
|
16
|
+
const url = new URL(fileUrl);
|
|
17
|
+
if (url.protocol === "s3:") {
|
|
18
|
+
const bucket = url.host;
|
|
19
|
+
if (!bucket) {
|
|
20
|
+
return { err: `Invalid S3 url: no bucket: ${url}` };
|
|
21
|
+
}
|
|
22
|
+
return { bucket };
|
|
23
|
+
}
|
|
24
|
+
if (!url.host) {
|
|
25
|
+
return { err: `Invalid S3 url: no hostname: ${url}` };
|
|
26
|
+
}
|
|
27
|
+
const matches = url.host.match(ENDPOINT_PATTERN);
|
|
28
|
+
if (!matches) {
|
|
29
|
+
return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };
|
|
30
|
+
}
|
|
31
|
+
const prefix = matches[1];
|
|
32
|
+
if (!prefix) {
|
|
33
|
+
if (url.pathname === "/") {
|
|
34
|
+
return { bucket: null };
|
|
35
|
+
}
|
|
36
|
+
const index2 = url.pathname.indexOf("/", 1);
|
|
37
|
+
if (index2 === -1) {
|
|
38
|
+
return { bucket: url.pathname.substring(1) };
|
|
39
|
+
}
|
|
40
|
+
if (index2 === url.pathname.length - 1) {
|
|
41
|
+
return { bucket: url.pathname.substring(1, index2) };
|
|
42
|
+
}
|
|
43
|
+
return { bucket: url.pathname.substring(1, index2) };
|
|
44
|
+
}
|
|
45
|
+
return { bucket: prefix.substring(0, prefix.length - 1) };
|
|
46
|
+
}
|
|
47
|
+
require("aws-sdk/lib/maintenance_mode_message").suppress = true;
|
|
48
|
+
function hasUrlProtocol(url) {
|
|
49
|
+
return /^\w*:\/\//.test(url);
|
|
50
|
+
}
|
|
51
|
+
const index = {
|
|
52
|
+
init({ baseUrl, rootPath, s3Options, ...legacyS3Options }) {
|
|
53
|
+
if (Object.keys(legacyS3Options).length > 0) {
|
|
54
|
+
process.emitWarning(
|
|
55
|
+
"S3 configuration options passed at root level of the plugin's providerOptions is deprecated and will be removed in a future release. Please wrap them inside the 's3Options:{}' property."
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const config = { ...s3Options, ...legacyS3Options };
|
|
59
|
+
const S3 = new AWS.S3({
|
|
60
|
+
apiVersion: "2006-03-01",
|
|
61
|
+
...config
|
|
62
|
+
});
|
|
63
|
+
const filePrefix = rootPath ? `${rootPath.replace(/\/+$/, "")}/` : "";
|
|
64
|
+
const getFileKey = (file) => {
|
|
65
|
+
const path = file.path ? `${file.path}/` : "";
|
|
66
|
+
return `${filePrefix}${path}${file.hash}${file.ext}`;
|
|
67
|
+
};
|
|
68
|
+
const ACL = getOr("public-read", ["params", "ACL"], config);
|
|
69
|
+
const upload = (file, customParams = {}) => new Promise((resolve, reject) => {
|
|
70
|
+
const fileKey = getFileKey(file);
|
|
71
|
+
if (!file.stream && !file.buffer) {
|
|
72
|
+
reject(new Error("Missing file stream or buffer"));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const params = {
|
|
76
|
+
Key: fileKey,
|
|
77
|
+
Bucket: config.params.Bucket,
|
|
78
|
+
Body: file.stream || file.buffer,
|
|
79
|
+
ACL,
|
|
80
|
+
ContentType: file.mime,
|
|
81
|
+
...customParams
|
|
82
|
+
};
|
|
83
|
+
const onUploaded = (err, data) => {
|
|
84
|
+
if (err) {
|
|
85
|
+
return reject(err);
|
|
86
|
+
}
|
|
87
|
+
if (baseUrl) {
|
|
88
|
+
file.url = `${baseUrl}/${fileKey}`;
|
|
89
|
+
} else {
|
|
90
|
+
file.url = hasUrlProtocol(data.Location) ? data.Location : `https://${data.Location}`;
|
|
91
|
+
}
|
|
92
|
+
resolve();
|
|
93
|
+
};
|
|
94
|
+
S3.upload(params, onUploaded);
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
isPrivate() {
|
|
98
|
+
return ACL === "private";
|
|
99
|
+
},
|
|
100
|
+
async getSignedUrl(file) {
|
|
101
|
+
if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {
|
|
102
|
+
return { url: file.url };
|
|
103
|
+
}
|
|
104
|
+
const signedUrlExpires = getOr(15 * 60, ["params", "signedUrlExpires"], config);
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const fileKey = getFileKey(file);
|
|
107
|
+
S3.getSignedUrl(
|
|
108
|
+
"getObject",
|
|
109
|
+
{
|
|
110
|
+
Bucket: config.params.Bucket,
|
|
111
|
+
Key: fileKey,
|
|
112
|
+
Expires: parseInt(signedUrlExpires, 10)
|
|
113
|
+
},
|
|
114
|
+
(err, url) => {
|
|
115
|
+
if (err) {
|
|
116
|
+
return reject(err);
|
|
117
|
+
}
|
|
118
|
+
resolve({ url });
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
uploadStream(file, customParams = {}) {
|
|
124
|
+
return upload(file, customParams);
|
|
125
|
+
},
|
|
126
|
+
upload(file, customParams = {}) {
|
|
127
|
+
return upload(file, customParams);
|
|
128
|
+
},
|
|
129
|
+
delete(file, customParams = {}) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const fileKey = getFileKey(file);
|
|
132
|
+
S3.deleteObject(
|
|
133
|
+
{
|
|
134
|
+
Key: fileKey,
|
|
135
|
+
Bucket: config.params.Bucket,
|
|
136
|
+
...customParams
|
|
137
|
+
},
|
|
138
|
+
(err) => {
|
|
139
|
+
if (err) {
|
|
140
|
+
return reject(err);
|
|
141
|
+
}
|
|
142
|
+
resolve();
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
export {
|
|
151
|
+
index as default
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["const ENDPOINT_PATTERN = /^(.+\\.)?s3[.-]([a-z0-9-]+)\\./;\n\ninterface BucketInfo {\n bucket?: string | null;\n err?: string;\n}\n\nexport function isUrlFromBucket(fileUrl: string, bucketName: string, baseUrl = ''): boolean {\n const url = new URL(fileUrl);\n\n // Check if the file URL is using a base URL (e.g. a CDN).\n // In this case do not sign the URL.\n if (baseUrl) {\n return false;\n }\n\n const { bucket } = getBucketFromAwsUrl(fileUrl);\n\n if (bucket) {\n return bucket === bucketName;\n }\n\n // File URL might be of an S3-compatible provider. (or an invalid URL)\n // In this case, check if the bucket name appears in the URL host or path.\n // e.g. https://minio.example.com/bucket-name/object-key\n // e.g. https://bucket.nyc3.digitaloceanspaces.com/folder/img.png\n return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);\n}\n\n/**\n * Parse the bucket name from a URL.\n * See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html\n *\n * @param {string} fileUrl - the URL to parse\n * @returns {object} result\n * @returns {string} result.bucket - the bucket name\n * @returns {string} result.err - if any\n */\nfunction getBucketFromAwsUrl(fileUrl: string): BucketInfo {\n const url = new URL(fileUrl);\n\n // S3://<bucket-name>/<key>\n if (url.protocol === 's3:') {\n const bucket = url.host;\n\n if (!bucket) {\n return { err: `Invalid S3 url: no bucket: ${url}` };\n }\n return { bucket };\n }\n\n if (!url.host) {\n return { err: `Invalid S3 url: no hostname: ${url}` };\n }\n\n const matches = url.host.match(ENDPOINT_PATTERN);\n if (!matches) {\n return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };\n }\n\n const prefix = matches[1];\n // https://s3.amazonaws.com/<bucket-name>\n if (!prefix) {\n if (url.pathname === '/') {\n return { bucket: null };\n }\n\n const index = url.pathname.indexOf('/', 1);\n\n // https://s3.amazonaws.com/<bucket-name>\n if (index === -1) {\n return { bucket: url.pathname.substring(1) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/\n if (index === url.pathname.length - 1) {\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://s3.amazonaws.com/<bucket-name>/key\n return { bucket: url.pathname.substring(1, index) };\n }\n\n // https://<bucket-name>.s3.amazonaws.com/\n return { bucket: prefix.substring(0, prefix.length - 1) };\n}\n","import type { ReadStream } from 'node:fs';\nimport { getOr } from 'lodash/fp';\nimport AWS from 'aws-sdk';\nimport { isUrlFromBucket } from './utils';\n\ninterface File {\n name: string;\n alternativeText?: string;\n caption?: string;\n width?: number;\n height?: number;\n formats?: Record<string, unknown>;\n hash: string;\n ext?: string;\n mime: string;\n size: number;\n url: string;\n previewUrl?: string;\n path?: string;\n provider?: string;\n provider_metadata?: Record<string, unknown>;\n stream?: ReadStream;\n buffer?: Buffer;\n}\n\n// TODO V5: Migrate to aws-sdk v3\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nrequire('aws-sdk/lib/maintenance_mode_message').suppress = true;\n\nfunction hasUrlProtocol(url: string) {\n // Regex to test protocol like \"http://\", \"https://\"\n return /^\\w*:\\/\\//.test(url);\n}\n\ninterface InitOptions extends Partial<AWS.S3.ClientConfiguration> {\n baseUrl?: string;\n rootPath?: string;\n s3Options: AWS.S3.ClientConfiguration & {\n params: {\n Bucket: string; // making it required\n ACL?: string;\n signedUrlExpires?: string;\n };\n };\n}\n\nexport default {\n init({ baseUrl, rootPath, s3Options, ...legacyS3Options }: InitOptions) {\n if (Object.keys(legacyS3Options).length > 0) {\n process.emitWarning(\n \"S3 configuration options passed at root level of the plugin's providerOptions is deprecated and will be removed in a future release. Please wrap them inside the 's3Options:{}' property.\"\n );\n }\n\n const config = { ...s3Options, ...legacyS3Options };\n\n const S3 = new AWS.S3({\n apiVersion: '2006-03-01',\n ...config,\n });\n\n const filePrefix = rootPath ? `${rootPath.replace(/\\/+$/, '')}/` : '';\n\n const getFileKey = (file: File) => {\n const path = file.path ? `${file.path}/` : '';\n\n return `${filePrefix}${path}${file.hash}${file.ext}`;\n };\n\n const ACL = getOr('public-read', ['params', 'ACL'], config);\n\n const upload = (file: File, customParams = {}): Promise<void> =>\n new Promise((resolve, reject) => {\n const fileKey = getFileKey(file);\n\n if (!file.stream && !file.buffer) {\n reject(new Error('Missing file stream or buffer'));\n return;\n }\n\n const params = {\n Key: fileKey,\n Bucket: config.params.Bucket,\n Body: file.stream || file.buffer,\n ACL,\n ContentType: file.mime,\n ...customParams,\n };\n\n const onUploaded = (err: Error, data: AWS.S3.ManagedUpload.SendData) => {\n if (err) {\n return reject(err);\n }\n\n // set the bucket file url\n if (baseUrl) {\n // Construct the url with the baseUrl\n file.url = `${baseUrl}/${fileKey}`;\n } else {\n // Add the protocol if it is missing\n // Some providers like DigitalOcean Spaces return the url without the protocol\n file.url = hasUrlProtocol(data.Location) ? data.Location : `https://${data.Location}`;\n }\n resolve();\n };\n\n S3.upload(params, onUploaded);\n });\n\n return {\n isPrivate() {\n return ACL === 'private';\n },\n async getSignedUrl(file: File): Promise<{ url: string }> {\n // Do not sign the url if it does not come from the same bucket.\n if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {\n return { url: file.url };\n }\n\n const signedUrlExpires: string = getOr(15 * 60, ['params', 'signedUrlExpires'], config); // 15 minutes\n\n return new Promise((resolve, reject) => {\n const fileKey = getFileKey(file);\n\n S3.getSignedUrl(\n 'getObject',\n {\n Bucket: config.params.Bucket,\n Key: fileKey,\n Expires: parseInt(signedUrlExpires, 10),\n },\n (err, url) => {\n if (err) {\n return reject(err);\n }\n resolve({ url });\n }\n );\n });\n },\n uploadStream(file: File, customParams = {}) {\n return upload(file, customParams);\n },\n upload(file: File, customParams = {}) {\n return upload(file, customParams);\n },\n delete(file: File, customParams = {}): Promise<void> {\n return new Promise((resolve, reject) => {\n // delete file on S3 bucket\n const fileKey = getFileKey(file);\n S3.deleteObject(\n {\n Key: fileKey,\n Bucket: config.params.Bucket,\n ...customParams,\n },\n (err) => {\n if (err) {\n return reject(err);\n }\n\n resolve();\n }\n );\n });\n },\n };\n },\n};\n"],"names":["index"],"mappings":"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"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAOA,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,OAAO,CAoB1F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/provider-upload-aws-s3",
|
|
3
|
-
"version": "4.15.
|
|
3
|
+
"version": "4.15.1",
|
|
4
4
|
"description": "AWS S3 provider for strapi upload",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"upload",
|
|
@@ -30,32 +30,34 @@
|
|
|
30
30
|
}
|
|
31
31
|
],
|
|
32
32
|
"main": "./dist/index.js",
|
|
33
|
+
"module": "./dist/index.mjs",
|
|
34
|
+
"source": "./src/index.ts",
|
|
33
35
|
"types": "./dist/index.d.ts",
|
|
34
36
|
"files": [
|
|
35
37
|
"./dist"
|
|
36
38
|
],
|
|
37
39
|
"scripts": {
|
|
38
|
-
"build": "
|
|
39
|
-
"build:ts": "run -T tsc",
|
|
40
|
-
"watch": "run -T tsc -w --preserveWatchOutput",
|
|
40
|
+
"build": "pack-up build",
|
|
41
41
|
"clean": "run -T rimraf ./dist",
|
|
42
|
+
"lint": "run -T eslint .",
|
|
42
43
|
"prepublishOnly": "yarn clean && yarn build",
|
|
43
44
|
"test:unit": "run -T jest",
|
|
44
45
|
"test:unit:watch": "run -T jest --watch",
|
|
45
|
-
"
|
|
46
|
+
"watch": "pack-up watch"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"aws-sdk": "2.
|
|
49
|
+
"aws-sdk": "2.1472.0",
|
|
49
50
|
"lodash": "4.17.21"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
53
|
+
"@strapi/pack-up": "4.15.1",
|
|
52
54
|
"@types/jest": "29.5.2",
|
|
53
|
-
"eslint-config-custom": "4.15.
|
|
54
|
-
"tsconfig": "4.15.
|
|
55
|
+
"eslint-config-custom": "4.15.1",
|
|
56
|
+
"tsconfig": "4.15.1"
|
|
55
57
|
},
|
|
56
58
|
"engines": {
|
|
57
|
-
"node": ">=
|
|
59
|
+
"node": ">=18.0.0 <=20.x.x",
|
|
58
60
|
"npm": ">=6.0.0"
|
|
59
61
|
},
|
|
60
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "b6c085052f108fcfe47d22972a664dfa85aa0358"
|
|
61
63
|
}
|
package/dist/utils.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isUrlFromBucket = void 0;
|
|
4
|
-
const ENDPOINT_PATTERN = /^(.+\.)?s3[.-]([a-z0-9-]+)\./;
|
|
5
|
-
function isUrlFromBucket(fileUrl, bucketName, baseUrl = '') {
|
|
6
|
-
const url = new URL(fileUrl);
|
|
7
|
-
// Check if the file URL is using a base URL (e.g. a CDN).
|
|
8
|
-
// In this case do not sign the URL.
|
|
9
|
-
if (baseUrl) {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
const { bucket } = getBucketFromAwsUrl(fileUrl);
|
|
13
|
-
if (bucket) {
|
|
14
|
-
return bucket === bucketName;
|
|
15
|
-
}
|
|
16
|
-
// File URL might be of an S3-compatible provider. (or an invalid URL)
|
|
17
|
-
// In this case, check if the bucket name appears in the URL host or path.
|
|
18
|
-
// e.g. https://minio.example.com/bucket-name/object-key
|
|
19
|
-
// e.g. https://bucket.nyc3.digitaloceanspaces.com/folder/img.png
|
|
20
|
-
return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);
|
|
21
|
-
}
|
|
22
|
-
exports.isUrlFromBucket = isUrlFromBucket;
|
|
23
|
-
/**
|
|
24
|
-
* Parse the bucket name from a URL.
|
|
25
|
-
* See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html
|
|
26
|
-
*
|
|
27
|
-
* @param {string} fileUrl - the URL to parse
|
|
28
|
-
* @returns {object} result
|
|
29
|
-
* @returns {string} result.bucket - the bucket name
|
|
30
|
-
* @returns {string} result.err - if any
|
|
31
|
-
*/
|
|
32
|
-
function getBucketFromAwsUrl(fileUrl) {
|
|
33
|
-
const url = new URL(fileUrl);
|
|
34
|
-
// S3://<bucket-name>/<key>
|
|
35
|
-
if (url.protocol === 's3:') {
|
|
36
|
-
const bucket = url.host;
|
|
37
|
-
if (!bucket) {
|
|
38
|
-
return { err: `Invalid S3 url: no bucket: ${url}` };
|
|
39
|
-
}
|
|
40
|
-
return { bucket };
|
|
41
|
-
}
|
|
42
|
-
if (!url.host) {
|
|
43
|
-
return { err: `Invalid S3 url: no hostname: ${url}` };
|
|
44
|
-
}
|
|
45
|
-
const matches = url.host.match(ENDPOINT_PATTERN);
|
|
46
|
-
if (!matches) {
|
|
47
|
-
return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };
|
|
48
|
-
}
|
|
49
|
-
const prefix = matches[1];
|
|
50
|
-
// https://s3.amazonaws.com/<bucket-name>
|
|
51
|
-
if (!prefix) {
|
|
52
|
-
if (url.pathname === '/') {
|
|
53
|
-
return { bucket: null };
|
|
54
|
-
}
|
|
55
|
-
const index = url.pathname.indexOf('/', 1);
|
|
56
|
-
// https://s3.amazonaws.com/<bucket-name>
|
|
57
|
-
if (index === -1) {
|
|
58
|
-
return { bucket: url.pathname.substring(1) };
|
|
59
|
-
}
|
|
60
|
-
// https://s3.amazonaws.com/<bucket-name>/
|
|
61
|
-
if (index === url.pathname.length - 1) {
|
|
62
|
-
return { bucket: url.pathname.substring(1, index) };
|
|
63
|
-
}
|
|
64
|
-
// https://s3.amazonaws.com/<bucket-name>/key
|
|
65
|
-
return { bucket: url.pathname.substring(1, index) };
|
|
66
|
-
}
|
|
67
|
-
// https://<bucket-name>.s3.amazonaws.com/
|
|
68
|
-
return { bucket: prefix.substring(0, prefix.length - 1) };
|
|
69
|
-
}
|
|
70
|
-
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;AAAA,MAAM,gBAAgB,GAAG,8BAA8B,CAAC;AAOxD,SAAgB,eAAe,CAAC,OAAe,EAAE,UAAkB,EAAE,OAAO,GAAG,EAAE;IAC/E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAE7B,0DAA0D;IAC1D,oCAAoC;IACpC,IAAI,OAAO,EAAE;QACX,OAAO,KAAK,CAAC;KACd;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAEhD,IAAI,MAAM,EAAE;QACV,OAAO,MAAM,KAAK,UAAU,CAAC;KAC9B;IAED,sEAAsE;IACtE,0EAA0E;IAC1E,wDAAwD;IACxD,iEAAiE;IACjE,OAAO,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,UAAU,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,UAAU,GAAG,CAAC,CAAC;AAC3F,CAAC;AApBD,0CAoBC;AAED;;;;;;;;GAQG;AACH,SAAS,mBAAmB,CAAC,OAAe;IAC1C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAE7B,2BAA2B;IAC3B,IAAI,GAAG,CAAC,QAAQ,KAAK,KAAK,EAAE;QAC1B,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC;QAExB,IAAI,CAAC,MAAM,EAAE;YACX,OAAO,EAAE,GAAG,EAAE,8BAA8B,GAAG,EAAE,EAAE,CAAC;SACrD;QACD,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB;IAED,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QACb,OAAO,EAAE,GAAG,EAAE,gCAAgC,GAAG,EAAE,EAAE,CAAC;KACvD;IAED,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACjD,IAAI,CAAC,OAAO,EAAE;QACZ,OAAO,EAAE,GAAG,EAAE,uEAAuE,GAAG,EAAE,EAAE,CAAC;KAC9F;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1B,yCAAyC;IACzC,IAAI,CAAC,MAAM,EAAE;QACX,IAAI,GAAG,CAAC,QAAQ,KAAK,GAAG,EAAE;YACxB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;SACzB;QAED,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAE3C,yCAAyC;QACzC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE;YAChB,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9C;QAED,0CAA0C;QAC1C,IAAI,KAAK,KAAK,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE;YACrC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;SACrD;QAED,6CAA6C;QAC7C,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;KACrD;IAED,0CAA0C;IAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;AAC5D,CAAC"}
|