@strapi/provider-upload-aws-s3 0.0.0-next.c58b405b44c71a93df733024d1f15c069cb6bdca → 0.0.0-next.c5f067b5650921187770124e9b6c8186e805e242

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/README.md CHANGED
@@ -49,8 +49,10 @@ module.exports = ({ env }) => ({
49
49
  baseUrl: env('CDN_URL'),
50
50
  rootPath: env('CDN_ROOT_PATH'),
51
51
  s3Options: {
52
- accessKeyId: env('AWS_ACCESS_KEY_ID'),
53
- secretAccessKey: env('AWS_ACCESS_SECRET'),
52
+ credentials: {
53
+ accessKeyId: env('AWS_ACCESS_KEY_ID'),
54
+ secretAccessKey: env('AWS_ACCESS_SECRET'),
55
+ },
54
56
  region: env('AWS_REGION'),
55
57
  params: {
56
58
  ACL: env('AWS_ACL', 'public-read'),
@@ -70,11 +72,13 @@ module.exports = ({ env }) => ({
70
72
  });
71
73
  ```
72
74
 
73
- ### Configuration for a private S3 bucket
75
+ ### Configuration for a private S3 bucket and signed URLs
74
76
 
75
- If your bucket is configured to be private, you will need to set the `ACL` option to `private` in the `params` object. This will ensure that the signed URL is generated with the correct permissions.
77
+ If your bucket is configured to be private, you will need to set the `ACL` option to `private` in the `params` object. This will ensure file URLs are signed.
76
78
 
77
- You can also define the expiration time of the signed URL by setting the `signedUrlExpires` option in the `params` object. The default value is 7 days.
79
+ **Note:** If you are using a CDN, the URLs will not be signed.
80
+
81
+ You can also define the expiration time of the signed URL by setting the `signedUrlExpires` option in the `params` object. The default value is 15 minutes.
78
82
 
79
83
  `./config/plugins.js`
80
84
 
@@ -85,12 +89,14 @@ module.exports = ({ env }) => ({
85
89
  config: {
86
90
  provider: 'aws-s3',
87
91
  providerOptions: {
88
- accessKeyId: env('AWS_ACCESS_KEY_ID'),
89
- secretAccessKey: env('AWS_ACCESS_SECRET'),
92
+ credentials: {
93
+ accessKeyId: env('AWS_ACCESS_KEY_ID'),
94
+ secretAccessKey: env('AWS_ACCESS_SECRET'),
95
+ },
90
96
  region: env('AWS_REGION'),
91
97
  params: {
92
98
  ACL: 'private', // <== set ACL to private
93
- signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 60 * 60 * 24 * 7),
99
+ signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60),
94
100
  Bucket: env('AWS_BUCKET'),
95
101
  },
96
102
  },
@@ -117,8 +123,10 @@ module.exports = ({ env }) => ({
117
123
  config: {
118
124
  provider: 'aws-s3',
119
125
  providerOptions: {
120
- accessKeyId: env('SCALEWAY_ACCESS_KEY_ID'),
121
- secretAccessKey: env('SCALEWAY_ACCESS_SECRET'),
126
+ credentials: {
127
+ accessKeyId: env('SCALEWAY_ACCESS_KEY_ID'),
128
+ secretAccessKey: env('SCALEWAY_ACCESS_SECRET'),
129
+ },
122
130
  endpoint: env('SCALEWAY_ENDPOINT'), // e.g. "s3.fr-par.scw.cloud"
123
131
  params: {
124
132
  Bucket: env('SCALEWAY_BUCKET'),
@@ -150,14 +158,14 @@ module.exports = [
150
158
  "'self'",
151
159
  'data:',
152
160
  'blob:',
153
- 'dl.airtable.com',
161
+ 'market-assets.strapi.io',
154
162
  'yourBucketName.s3.yourRegion.amazonaws.com',
155
163
  ],
156
164
  'media-src': [
157
165
  "'self'",
158
166
  'data:',
159
167
  'blob:',
160
- 'dl.airtable.com',
168
+ 'market-assets.strapi.io',
161
169
  'yourBucketName.s3.yourRegion.amazonaws.com',
162
170
  ],
163
171
  upgradeInsecureRequests: null,
@@ -169,7 +177,7 @@ module.exports = [
169
177
  ];
170
178
  ```
171
179
 
172
- If you use dots in your bucket name, the url of the ressource is in directory style (`s3.yourRegion.amazonaws.com/your.bucket.name/image.jpg`) instead of `yourBucketName.s3.yourRegion.amazonaws.com/image.jpg`. Then only add `s3.yourRegion.amazonaws.com` to img-src and media-src directives.
180
+ If you use dots in your bucket name, the url of the resource is in directory style (`s3.yourRegion.amazonaws.com/your.bucket.name/image.jpg`) instead of `yourBucketName.s3.yourRegion.amazonaws.com/image.jpg` so in that case the img-src and media-src directives to add will be `s3.yourRegion.amazonaws.com` without the bucket name in the url.
173
181
 
174
182
  ## Bucket CORS Configuration
175
183
 
@@ -200,3 +208,47 @@ These are the minimum amount of permissions needed for this provider to work.
200
208
  "s3:PutObjectAcl"
201
209
  ],
202
210
  ```
211
+
212
+ ## Update to AWS SDK V3 and URL Format Change
213
+
214
+ In the recent update of the `@strapi/provider-upload-aws-s3` plugin, we have transitioned from AWS SDK V2 to AWS SDK V3. This significant update brings along a change in the format of the URLs used in Amazon S3 services.
215
+
216
+ ### Understanding the New URL Format
217
+
218
+ AWS SDK V3 adopts the virtual-hosted–style URI format for S3 URLs. This format is recommended by AWS and is likely to become required in the near future, as the path-style URI is being deprecated. More details on this format can be found in the [AWS User Guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access).
219
+
220
+ ### Why the Change?
221
+
222
+ The move to virtual-hosted–style URIs aligns with AWS's recommendation and future-proofing strategies. For an in-depth understanding of AWS's decision behind this transition, you can refer to their detailed post [here](https://aws.amazon.com/es/blogs/aws/amazon-s3-path-deprecation-plan-the-rest-of-the-story/).
223
+
224
+ ### Configuring Your Strapi Application
225
+
226
+ If you wish to continue using the plugin with Strapi 4.15.x versions or newer without changing your URL format, it's possible to specify your desired URL format directly in the plugin's configuration. Below is an example configuration highlighting the critical `baseUrl` property:
227
+
228
+ ```javascript
229
+ upload: {
230
+ config: {
231
+ provider: 'aws-s3',
232
+ providerOptions: {
233
+ credentials: {
234
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
235
+ secretAccessKey: process.env.AWS_ACCESS_SECRET,
236
+ },
237
+ region: process.env.AWS_REGION,
238
+ baseUrl: `https://s3.${region}.amazonaws.com/${bucket}`, // This line sets the custom url format
239
+ params: {
240
+ ACL: process.env.AWS_ACL || 'public-read',
241
+ signedUrlExpires: process.env.AWS_SIGNED_URL_EXPIRES || 15 * 60,
242
+ Bucket: process.env.AWS_BUCKET,
243
+ },
244
+ },
245
+ actionOptions: {
246
+ upload: {},
247
+ uploadStream: {},
248
+ delete: {},
249
+ },
250
+ },
251
+ }
252
+ ```
253
+
254
+ This configuration ensures compatibility with the updated AWS SDK while providing flexibility in URL format selection, catering to various user needs.
@@ -0,0 +1,59 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import type { ReadStream } from 'node:fs';
4
+ import { DeleteObjectCommandOutput, CompleteMultipartUploadCommandOutput, AbortMultipartUploadCommandOutput, S3ClientConfig, ObjectCannedACL } from '@aws-sdk/client-s3';
5
+ import type { AwsCredentialIdentity } from '@aws-sdk/types';
6
+ export interface File {
7
+ name: string;
8
+ alternativeText?: string;
9
+ caption?: string;
10
+ width?: number;
11
+ height?: number;
12
+ formats?: Record<string, unknown>;
13
+ hash: string;
14
+ ext?: string;
15
+ mime: string;
16
+ size: number;
17
+ url: string;
18
+ previewUrl?: string;
19
+ path?: string;
20
+ provider?: string;
21
+ provider_metadata?: Record<string, unknown>;
22
+ stream?: ReadStream;
23
+ buffer?: Buffer;
24
+ }
25
+ export type UploadCommandOutput = (CompleteMultipartUploadCommandOutput | AbortMultipartUploadCommandOutput) & {
26
+ Location: string;
27
+ };
28
+ export interface AWSParams {
29
+ Bucket: string;
30
+ ACL?: ObjectCannedACL;
31
+ signedUrlExpires?: number;
32
+ }
33
+ export interface DefaultOptions extends S3ClientConfig {
34
+ accessKeyId?: AwsCredentialIdentity['accessKeyId'];
35
+ secretAccessKey?: AwsCredentialIdentity['secretAccessKey'];
36
+ credentials?: AwsCredentialIdentity;
37
+ params?: AWSParams;
38
+ [k: string]: any;
39
+ }
40
+ export type InitOptions = (DefaultOptions | {
41
+ s3Options: DefaultOptions;
42
+ }) & {
43
+ baseUrl?: string;
44
+ rootPath?: string;
45
+ [k: string]: any;
46
+ };
47
+ declare const _default: {
48
+ init({ baseUrl, rootPath, s3Options, ...legacyS3Options }: InitOptions): {
49
+ isPrivate(): boolean;
50
+ getSignedUrl(file: File, customParams: any): Promise<{
51
+ url: string;
52
+ }>;
53
+ uploadStream(file: File, customParams?: {}): Promise<void>;
54
+ upload(file: File, customParams?: {}): Promise<void>;
55
+ delete(file: File, customParams?: {}): Promise<DeleteObjectCommandOutput>;
56
+ };
57
+ };
58
+ export default _default;
59
+ //# sourceMappingURL=index.d.ts.map
@@ -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,EAIL,yBAAyB,EAEzB,oCAAoC,EACpC,iCAAiC,EACjC,cAAc,EACd,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAK5D,MAAM,WAAW,IAAI;IACnB,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;AAED,MAAM,MAAM,mBAAmB,GAAG,CAC9B,oCAAoC,GACpC,iCAAiC,CACpC,GAAG;IACF,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAe,SAAQ,cAAc;IAEpD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,eAAe,CAAC,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAC;IAE3D,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CAClB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,cAAc,GAAG;IAAE,SAAS,EAAE,cAAc,CAAA;CAAE,CAAC,GAAG;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CAClB,CAAC;;+DA0B2D,WAAW;;2BAwCzC,IAAI,gBAAgB,GAAG;iBAAkB,MAAM;;2BAqBrD,IAAI;qBAGV,IAAI;qBAGJ,IAAI,sBAAsB,QAAQ,yBAAyB,CAAC;;;AApE/E,wBA8EE"}
package/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ const fp = require("lodash/fp");
3
+ const clientS3 = require("@aws-sdk/client-s3");
4
+ const s3RequestPresigner = require("@aws-sdk/s3-request-presigner");
5
+ const libStorage = require("@aws-sdk/lib-storage");
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
+ const extractCredentials = (options) => {
51
+ if (options.accessKeyId && options.secretAccessKey) {
52
+ return {
53
+ accessKeyId: options.accessKeyId,
54
+ secretAccessKey: options.secretAccessKey
55
+ };
56
+ }
57
+ if (options.s3Options?.accessKeyId && options.s3Options.secretAccessKey) {
58
+ process.emitWarning(
59
+ "Credentials passed directly to s3Options is deprecated and will be removed in a future release. Please wrap them inside a credentials object."
60
+ );
61
+ return {
62
+ accessKeyId: options.s3Options.accessKeyId,
63
+ secretAccessKey: options.s3Options.secretAccessKey
64
+ };
65
+ }
66
+ if (options.s3Options?.credentials) {
67
+ return {
68
+ accessKeyId: options.s3Options.credentials.accessKeyId,
69
+ secretAccessKey: options.s3Options.credentials.secretAccessKey
70
+ };
71
+ }
72
+ return null;
73
+ };
74
+ const assertUrlProtocol = (url) => {
75
+ return /^\w*:\/\//.test(url);
76
+ };
77
+ const getConfig = ({ baseUrl, rootPath, s3Options, ...legacyS3Options }) => {
78
+ if (Object.keys(legacyS3Options).length > 0) {
79
+ process.emitWarning(
80
+ "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."
81
+ );
82
+ }
83
+ const credentials = extractCredentials({ s3Options, ...legacyS3Options });
84
+ const config = {
85
+ ...s3Options,
86
+ ...legacyS3Options,
87
+ ...credentials ? { credentials } : {}
88
+ };
89
+ config.params.ACL = fp.getOr(clientS3.ObjectCannedACL.public_read, ["params", "ACL"], config);
90
+ return config;
91
+ };
92
+ const index = {
93
+ init({ baseUrl, rootPath, s3Options, ...legacyS3Options }) {
94
+ const config = getConfig({ baseUrl, rootPath, s3Options, ...legacyS3Options });
95
+ const s3Client = new clientS3.S3Client(config);
96
+ const filePrefix = rootPath ? `${rootPath.replace(/\/+$/, "")}/` : "";
97
+ const getFileKey = (file) => {
98
+ const path = file.path ? `${file.path}/` : "";
99
+ return `${filePrefix}${path}${file.hash}${file.ext}`;
100
+ };
101
+ const upload = async (file, customParams = {}) => {
102
+ const fileKey = getFileKey(file);
103
+ const uploadObj = new libStorage.Upload({
104
+ client: s3Client,
105
+ params: {
106
+ Bucket: config.params.Bucket,
107
+ Key: fileKey,
108
+ Body: file.stream || Buffer.from(file.buffer, "binary"),
109
+ ACL: config.params.ACL,
110
+ ContentType: file.mime,
111
+ ...customParams
112
+ }
113
+ });
114
+ const upload2 = await uploadObj.done();
115
+ if (assertUrlProtocol(upload2.Location)) {
116
+ file.url = baseUrl ? `${baseUrl}/${fileKey}` : upload2.Location;
117
+ } else {
118
+ file.url = `https://${upload2.Location}`;
119
+ }
120
+ };
121
+ return {
122
+ isPrivate() {
123
+ return config.params.ACL === "private";
124
+ },
125
+ async getSignedUrl(file, customParams) {
126
+ if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {
127
+ return { url: file.url };
128
+ }
129
+ const fileKey = getFileKey(file);
130
+ const url = await s3RequestPresigner.getSignedUrl(
131
+ s3Client,
132
+ new clientS3.GetObjectCommand({
133
+ Bucket: config.params.Bucket,
134
+ Key: fileKey,
135
+ ...customParams
136
+ }),
137
+ {
138
+ expiresIn: fp.getOr(15 * 60, ["params", "signedUrlExpires"], config)
139
+ }
140
+ );
141
+ return { url };
142
+ },
143
+ uploadStream(file, customParams = {}) {
144
+ return upload(file, customParams);
145
+ },
146
+ upload(file, customParams = {}) {
147
+ return upload(file, customParams);
148
+ },
149
+ delete(file, customParams = {}) {
150
+ const command = new clientS3.DeleteObjectCommand({
151
+ Bucket: config.params.Bucket,
152
+ Key: getFileKey(file),
153
+ ...customParams
154
+ });
155
+ return s3Client.send(command);
156
+ }
157
+ };
158
+ }
159
+ };
160
+ module.exports = index;
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport type { InitOptions } from '.';\n\nconst 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\n// TODO Remove this in V5 since we will only support the new config structure\nexport const extractCredentials = (options: InitOptions): AwsCredentialIdentity | null => {\n // legacy\n if (options.accessKeyId && options.secretAccessKey) {\n return {\n accessKeyId: options.accessKeyId,\n secretAccessKey: options.secretAccessKey,\n };\n }\n // Legacy\n if (options.s3Options?.accessKeyId && options.s3Options.secretAccessKey) {\n process.emitWarning(\n 'Credentials passed directly to s3Options is deprecated and will be removed in a future release. Please wrap them inside a credentials object.'\n );\n return {\n accessKeyId: options.s3Options.accessKeyId,\n secretAccessKey: options.s3Options.secretAccessKey,\n };\n }\n // V5\n if (options.s3Options?.credentials) {\n return {\n accessKeyId: options.s3Options.credentials.accessKeyId,\n secretAccessKey: options.s3Options.credentials.secretAccessKey,\n };\n }\n return null;\n};\n","import type { ReadStream } from 'node:fs';\nimport { getOr } from 'lodash/fp';\nimport {\n S3Client,\n GetObjectCommand,\n DeleteObjectCommand,\n DeleteObjectCommandOutput,\n PutObjectCommandInput,\n CompleteMultipartUploadCommandOutput,\n AbortMultipartUploadCommandOutput,\n S3ClientConfig,\n ObjectCannedACL,\n} from '@aws-sdk/client-s3';\nimport type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { Upload } from '@aws-sdk/lib-storage';\nimport { extractCredentials, isUrlFromBucket } from './utils';\n\nexport interface 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\nexport type UploadCommandOutput = (\n | CompleteMultipartUploadCommandOutput\n | AbortMultipartUploadCommandOutput\n) & {\n Location: string;\n};\n\nexport interface AWSParams {\n Bucket: string; // making it required\n ACL?: ObjectCannedACL;\n signedUrlExpires?: number;\n}\n\nexport interface DefaultOptions extends S3ClientConfig {\n // TODO Remove this in V5\n accessKeyId?: AwsCredentialIdentity['accessKeyId'];\n secretAccessKey?: AwsCredentialIdentity['secretAccessKey'];\n // Keep this for V5\n credentials?: AwsCredentialIdentity;\n params?: AWSParams;\n [k: string]: any;\n}\n\nexport type InitOptions = (DefaultOptions | { s3Options: DefaultOptions }) & {\n baseUrl?: string;\n rootPath?: string;\n [k: string]: any;\n};\n\nconst assertUrlProtocol = (url: string) => {\n // Regex to test protocol like \"http://\", \"https://\"\n return /^\\w*:\\/\\//.test(url);\n};\n\nconst getConfig = ({ 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 const credentials = extractCredentials({ s3Options, ...legacyS3Options });\n const config = {\n ...s3Options,\n ...legacyS3Options,\n ...(credentials ? { credentials } : {}),\n };\n\n config.params.ACL = getOr(ObjectCannedACL.public_read, ['params', 'ACL'], config);\n\n return config;\n};\n\nexport default {\n init({ baseUrl, rootPath, s3Options, ...legacyS3Options }: InitOptions) {\n // TODO V5 change config structure to avoid having to do this\n const config = getConfig({ baseUrl, rootPath, s3Options, ...legacyS3Options });\n const s3Client = new S3Client(config);\n const filePrefix = rootPath ? `${rootPath.replace(/\\/+$/, '')}/` : '';\n\n const getFileKey = (file: File) => {\n const path = file.path ? `${file.path}/` : '';\n return `${filePrefix}${path}${file.hash}${file.ext}`;\n };\n\n const upload = async (file: File, customParams: Partial<PutObjectCommandInput> = {}) => {\n const fileKey = getFileKey(file);\n const uploadObj = new Upload({\n client: s3Client,\n params: {\n Bucket: config.params.Bucket,\n Key: fileKey,\n Body: file.stream || Buffer.from(file.buffer as any, 'binary'),\n ACL: config.params.ACL,\n ContentType: file.mime,\n ...customParams,\n },\n });\n\n const upload = (await uploadObj.done()) as UploadCommandOutput;\n\n if (assertUrlProtocol(upload.Location)) {\n file.url = baseUrl ? `${baseUrl}/${fileKey}` : upload.Location;\n } else {\n // Default protocol to https protocol\n file.url = `https://${upload.Location}`;\n }\n };\n\n return {\n isPrivate() {\n return config.params.ACL === 'private';\n },\n\n async getSignedUrl(file: File, customParams: any): 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 const fileKey = getFileKey(file);\n\n const url = await getSignedUrl(\n s3Client,\n new GetObjectCommand({\n Bucket: config.params.Bucket,\n Key: fileKey,\n ...customParams,\n }),\n {\n expiresIn: getOr(15 * 60, ['params', 'signedUrlExpires'], config),\n }\n );\n\n return { url };\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<DeleteObjectCommandOutput> {\n const command = new DeleteObjectCommand({\n Bucket: config.params.Bucket,\n Key: getFileKey(file),\n ...customParams,\n });\n return s3Client.send(command);\n },\n };\n },\n};\n"],"names":["index","getOr","ObjectCannedACL","S3Client","Upload","upload","getSignedUrl","GetObjectCommand","DeleteObjectCommand"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,162 @@
1
+ import { getOr } from "lodash/fp";
2
+ import { S3Client, GetObjectCommand, DeleteObjectCommand, ObjectCannedACL } from "@aws-sdk/client-s3";
3
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
+ import { Upload } from "@aws-sdk/lib-storage";
5
+ const ENDPOINT_PATTERN = /^(.+\.)?s3[.-]([a-z0-9-]+)\./;
6
+ function isUrlFromBucket(fileUrl, bucketName, baseUrl = "") {
7
+ const url = new URL(fileUrl);
8
+ if (baseUrl) {
9
+ return false;
10
+ }
11
+ const { bucket } = getBucketFromAwsUrl(fileUrl);
12
+ if (bucket) {
13
+ return bucket === bucketName;
14
+ }
15
+ return url.host.startsWith(`${bucketName}.`) || url.pathname.includes(`/${bucketName}/`);
16
+ }
17
+ function getBucketFromAwsUrl(fileUrl) {
18
+ const url = new URL(fileUrl);
19
+ if (url.protocol === "s3:") {
20
+ const bucket = url.host;
21
+ if (!bucket) {
22
+ return { err: `Invalid S3 url: no bucket: ${url}` };
23
+ }
24
+ return { bucket };
25
+ }
26
+ if (!url.host) {
27
+ return { err: `Invalid S3 url: no hostname: ${url}` };
28
+ }
29
+ const matches = url.host.match(ENDPOINT_PATTERN);
30
+ if (!matches) {
31
+ return { err: `Invalid S3 url: hostname does not appear to be a valid S3 endpoint: ${url}` };
32
+ }
33
+ const prefix = matches[1];
34
+ if (!prefix) {
35
+ if (url.pathname === "/") {
36
+ return { bucket: null };
37
+ }
38
+ const index2 = url.pathname.indexOf("/", 1);
39
+ if (index2 === -1) {
40
+ return { bucket: url.pathname.substring(1) };
41
+ }
42
+ if (index2 === url.pathname.length - 1) {
43
+ return { bucket: url.pathname.substring(1, index2) };
44
+ }
45
+ return { bucket: url.pathname.substring(1, index2) };
46
+ }
47
+ return { bucket: prefix.substring(0, prefix.length - 1) };
48
+ }
49
+ const extractCredentials = (options) => {
50
+ if (options.accessKeyId && options.secretAccessKey) {
51
+ return {
52
+ accessKeyId: options.accessKeyId,
53
+ secretAccessKey: options.secretAccessKey
54
+ };
55
+ }
56
+ if (options.s3Options?.accessKeyId && options.s3Options.secretAccessKey) {
57
+ process.emitWarning(
58
+ "Credentials passed directly to s3Options is deprecated and will be removed in a future release. Please wrap them inside a credentials object."
59
+ );
60
+ return {
61
+ accessKeyId: options.s3Options.accessKeyId,
62
+ secretAccessKey: options.s3Options.secretAccessKey
63
+ };
64
+ }
65
+ if (options.s3Options?.credentials) {
66
+ return {
67
+ accessKeyId: options.s3Options.credentials.accessKeyId,
68
+ secretAccessKey: options.s3Options.credentials.secretAccessKey
69
+ };
70
+ }
71
+ return null;
72
+ };
73
+ const assertUrlProtocol = (url) => {
74
+ return /^\w*:\/\//.test(url);
75
+ };
76
+ const getConfig = ({ baseUrl, rootPath, s3Options, ...legacyS3Options }) => {
77
+ if (Object.keys(legacyS3Options).length > 0) {
78
+ process.emitWarning(
79
+ "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."
80
+ );
81
+ }
82
+ const credentials = extractCredentials({ s3Options, ...legacyS3Options });
83
+ const config = {
84
+ ...s3Options,
85
+ ...legacyS3Options,
86
+ ...credentials ? { credentials } : {}
87
+ };
88
+ config.params.ACL = getOr(ObjectCannedACL.public_read, ["params", "ACL"], config);
89
+ return config;
90
+ };
91
+ const index = {
92
+ init({ baseUrl, rootPath, s3Options, ...legacyS3Options }) {
93
+ const config = getConfig({ baseUrl, rootPath, s3Options, ...legacyS3Options });
94
+ const s3Client = new S3Client(config);
95
+ const filePrefix = rootPath ? `${rootPath.replace(/\/+$/, "")}/` : "";
96
+ const getFileKey = (file) => {
97
+ const path = file.path ? `${file.path}/` : "";
98
+ return `${filePrefix}${path}${file.hash}${file.ext}`;
99
+ };
100
+ const upload = async (file, customParams = {}) => {
101
+ const fileKey = getFileKey(file);
102
+ const uploadObj = new Upload({
103
+ client: s3Client,
104
+ params: {
105
+ Bucket: config.params.Bucket,
106
+ Key: fileKey,
107
+ Body: file.stream || Buffer.from(file.buffer, "binary"),
108
+ ACL: config.params.ACL,
109
+ ContentType: file.mime,
110
+ ...customParams
111
+ }
112
+ });
113
+ const upload2 = await uploadObj.done();
114
+ if (assertUrlProtocol(upload2.Location)) {
115
+ file.url = baseUrl ? `${baseUrl}/${fileKey}` : upload2.Location;
116
+ } else {
117
+ file.url = `https://${upload2.Location}`;
118
+ }
119
+ };
120
+ return {
121
+ isPrivate() {
122
+ return config.params.ACL === "private";
123
+ },
124
+ async getSignedUrl(file, customParams) {
125
+ if (!isUrlFromBucket(file.url, config.params.Bucket, baseUrl)) {
126
+ return { url: file.url };
127
+ }
128
+ const fileKey = getFileKey(file);
129
+ const url = await getSignedUrl(
130
+ s3Client,
131
+ new GetObjectCommand({
132
+ Bucket: config.params.Bucket,
133
+ Key: fileKey,
134
+ ...customParams
135
+ }),
136
+ {
137
+ expiresIn: getOr(15 * 60, ["params", "signedUrlExpires"], config)
138
+ }
139
+ );
140
+ return { url };
141
+ },
142
+ uploadStream(file, customParams = {}) {
143
+ return upload(file, customParams);
144
+ },
145
+ upload(file, customParams = {}) {
146
+ return upload(file, customParams);
147
+ },
148
+ delete(file, customParams = {}) {
149
+ const command = new DeleteObjectCommand({
150
+ Bucket: config.params.Bucket,
151
+ Key: getFileKey(file),
152
+ ...customParams
153
+ });
154
+ return s3Client.send(command);
155
+ }
156
+ };
157
+ }
158
+ };
159
+ export {
160
+ index as default
161
+ };
162
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport type { InitOptions } from '.';\n\nconst 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\n// TODO Remove this in V5 since we will only support the new config structure\nexport const extractCredentials = (options: InitOptions): AwsCredentialIdentity | null => {\n // legacy\n if (options.accessKeyId && options.secretAccessKey) {\n return {\n accessKeyId: options.accessKeyId,\n secretAccessKey: options.secretAccessKey,\n };\n }\n // Legacy\n if (options.s3Options?.accessKeyId && options.s3Options.secretAccessKey) {\n process.emitWarning(\n 'Credentials passed directly to s3Options is deprecated and will be removed in a future release. Please wrap them inside a credentials object.'\n );\n return {\n accessKeyId: options.s3Options.accessKeyId,\n secretAccessKey: options.s3Options.secretAccessKey,\n };\n }\n // V5\n if (options.s3Options?.credentials) {\n return {\n accessKeyId: options.s3Options.credentials.accessKeyId,\n secretAccessKey: options.s3Options.credentials.secretAccessKey,\n };\n }\n return null;\n};\n","import type { ReadStream } from 'node:fs';\nimport { getOr } from 'lodash/fp';\nimport {\n S3Client,\n GetObjectCommand,\n DeleteObjectCommand,\n DeleteObjectCommandOutput,\n PutObjectCommandInput,\n CompleteMultipartUploadCommandOutput,\n AbortMultipartUploadCommandOutput,\n S3ClientConfig,\n ObjectCannedACL,\n} from '@aws-sdk/client-s3';\nimport type { AwsCredentialIdentity } from '@aws-sdk/types';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { Upload } from '@aws-sdk/lib-storage';\nimport { extractCredentials, isUrlFromBucket } from './utils';\n\nexport interface 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\nexport type UploadCommandOutput = (\n | CompleteMultipartUploadCommandOutput\n | AbortMultipartUploadCommandOutput\n) & {\n Location: string;\n};\n\nexport interface AWSParams {\n Bucket: string; // making it required\n ACL?: ObjectCannedACL;\n signedUrlExpires?: number;\n}\n\nexport interface DefaultOptions extends S3ClientConfig {\n // TODO Remove this in V5\n accessKeyId?: AwsCredentialIdentity['accessKeyId'];\n secretAccessKey?: AwsCredentialIdentity['secretAccessKey'];\n // Keep this for V5\n credentials?: AwsCredentialIdentity;\n params?: AWSParams;\n [k: string]: any;\n}\n\nexport type InitOptions = (DefaultOptions | { s3Options: DefaultOptions }) & {\n baseUrl?: string;\n rootPath?: string;\n [k: string]: any;\n};\n\nconst assertUrlProtocol = (url: string) => {\n // Regex to test protocol like \"http://\", \"https://\"\n return /^\\w*:\\/\\//.test(url);\n};\n\nconst getConfig = ({ 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 const credentials = extractCredentials({ s3Options, ...legacyS3Options });\n const config = {\n ...s3Options,\n ...legacyS3Options,\n ...(credentials ? { credentials } : {}),\n };\n\n config.params.ACL = getOr(ObjectCannedACL.public_read, ['params', 'ACL'], config);\n\n return config;\n};\n\nexport default {\n init({ baseUrl, rootPath, s3Options, ...legacyS3Options }: InitOptions) {\n // TODO V5 change config structure to avoid having to do this\n const config = getConfig({ baseUrl, rootPath, s3Options, ...legacyS3Options });\n const s3Client = new S3Client(config);\n const filePrefix = rootPath ? `${rootPath.replace(/\\/+$/, '')}/` : '';\n\n const getFileKey = (file: File) => {\n const path = file.path ? `${file.path}/` : '';\n return `${filePrefix}${path}${file.hash}${file.ext}`;\n };\n\n const upload = async (file: File, customParams: Partial<PutObjectCommandInput> = {}) => {\n const fileKey = getFileKey(file);\n const uploadObj = new Upload({\n client: s3Client,\n params: {\n Bucket: config.params.Bucket,\n Key: fileKey,\n Body: file.stream || Buffer.from(file.buffer as any, 'binary'),\n ACL: config.params.ACL,\n ContentType: file.mime,\n ...customParams,\n },\n });\n\n const upload = (await uploadObj.done()) as UploadCommandOutput;\n\n if (assertUrlProtocol(upload.Location)) {\n file.url = baseUrl ? `${baseUrl}/${fileKey}` : upload.Location;\n } else {\n // Default protocol to https protocol\n file.url = `https://${upload.Location}`;\n }\n };\n\n return {\n isPrivate() {\n return config.params.ACL === 'private';\n },\n\n async getSignedUrl(file: File, customParams: any): 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 const fileKey = getFileKey(file);\n\n const url = await getSignedUrl(\n s3Client,\n new GetObjectCommand({\n Bucket: config.params.Bucket,\n Key: fileKey,\n ...customParams,\n }),\n {\n expiresIn: getOr(15 * 60, ['params', 'signedUrlExpires'], config),\n }\n );\n\n return { url };\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<DeleteObjectCommandOutput> {\n const command = new DeleteObjectCommand({\n Bucket: config.params.Bucket,\n Key: getFileKey(file),\n ...customParams,\n });\n return s3Client.send(command);\n },\n };\n },\n};\n"],"names":["index","upload"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { AwsCredentialIdentity } from '@aws-sdk/types';
2
+ import type { InitOptions } from '.';
3
+ export declare function isUrlFromBucket(fileUrl: string, bucketName: string, baseUrl?: string): boolean;
4
+ export declare const extractCredentials: (options: InitOptions) => AwsCredentialIdentity | null;
5
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,GAAG,CAAC;AASrC,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,OAAO,CAoB1F;AA6DD,eAAO,MAAM,kBAAkB,YAAa,WAAW,KAAG,qBAAqB,GAAG,IA0BjF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/provider-upload-aws-s3",
3
- "version": "0.0.0-next.c58b405b44c71a93df733024d1f15c069cb6bdca",
3
+ "version": "0.0.0-next.c5f067b5650921187770124e9b6c8186e805e242",
4
4
  "description": "AWS S3 provider for strapi upload",
5
5
  "keywords": [
6
6
  "upload",
@@ -29,22 +29,37 @@
29
29
  "url": "https://strapi.io"
30
30
  }
31
31
  ],
32
- "main": "./lib",
33
- "directories": {
34
- "lib": "./lib"
35
- },
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.mjs",
34
+ "source": "./src/index.ts",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "./dist"
38
+ ],
36
39
  "scripts": {
40
+ "build": "pack-up build",
41
+ "clean": "run -T rimraf ./dist",
42
+ "lint": "run -T eslint .",
37
43
  "test:unit": "run -T jest",
38
44
  "test:unit:watch": "run -T jest --watch",
39
- "lint": "run -T eslint ."
45
+ "watch": "pack-up watch"
40
46
  },
41
47
  "dependencies": {
42
- "aws-sdk": "2.1287.0",
48
+ "@aws-sdk/client-s3": "3.433.0",
49
+ "@aws-sdk/lib-storage": "3.433.0",
50
+ "@aws-sdk/s3-request-presigner": "3.433.0",
51
+ "@aws-sdk/types": "3.433.0",
43
52
  "lodash": "4.17.21"
44
53
  },
54
+ "devDependencies": {
55
+ "@strapi/pack-up": "0.0.0-next.c5f067b5650921187770124e9b6c8186e805e242",
56
+ "@types/jest": "29.5.2",
57
+ "eslint-config-custom": "0.0.0-next.c5f067b5650921187770124e9b6c8186e805e242",
58
+ "tsconfig": "0.0.0-next.c5f067b5650921187770124e9b6c8186e805e242"
59
+ },
45
60
  "engines": {
46
- "node": ">=14.19.1 <=18.x.x",
61
+ "node": ">=18.0.0 <=20.x.x",
47
62
  "npm": ">=6.0.0"
48
63
  },
49
- "gitHead": "c58b405b44c71a93df733024d1f15c069cb6bdca"
64
+ "gitHead": "c5f067b5650921187770124e9b6c8186e805e242"
50
65
  }
package/.eslintignore DELETED
@@ -1,2 +0,0 @@
1
- node_modules/
2
- .eslintrc.js
package/.eslintrc.js DELETED
@@ -1,4 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- extends: ['custom/back'],
4
- };
package/lib/index.js DELETED
@@ -1,137 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Module dependencies
5
- */
6
-
7
- /* eslint-disable no-unused-vars */
8
- // Public node modules.
9
- const { getOr } = require('lodash/fp');
10
- const AWS = require('aws-sdk');
11
- const { getBucketFromUrl } = require('./utils');
12
-
13
- function assertUrlProtocol(url) {
14
- // Regex to test protocol like "http://", "https://"
15
- return /^\w*:\/\//.test(url);
16
- }
17
-
18
- module.exports = {
19
- init({ baseUrl = null, rootPath = null, s3Options, ...legacyS3Options }) {
20
- if (legacyS3Options) {
21
- process.emitWarning(
22
- "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."
23
- );
24
- }
25
-
26
- const config = { ...s3Options, ...legacyS3Options };
27
-
28
- const S3 = new AWS.S3({
29
- apiVersion: '2006-03-01',
30
- ...config,
31
- });
32
-
33
- const filePrefix = rootPath ? `${rootPath.replace(/\/+$/, '')}/` : '';
34
-
35
- const getFileKey = (file) => {
36
- const path = file.path ? `${file.path}/` : '';
37
-
38
- return `${filePrefix}${path}${file.hash}${file.ext}`;
39
- };
40
-
41
- const ACL = getOr('public-read', ['params', 'ACL'], config);
42
-
43
- const upload = (file, customParams = {}) =>
44
- new Promise((resolve, reject) => {
45
- // upload file on S3 bucket
46
- const fileKey = getFileKey(file);
47
- S3.upload(
48
- {
49
- Key: fileKey,
50
- Body: file.stream || Buffer.from(file.buffer, 'binary'),
51
- ACL,
52
- ContentType: file.mime,
53
- ...customParams,
54
- },
55
- (err, data) => {
56
- if (err) {
57
- return reject(err);
58
- }
59
-
60
- // set the bucket file url
61
- if (assertUrlProtocol(data.Location)) {
62
- file.url = baseUrl ? `${baseUrl}/${fileKey}` : data.Location;
63
- } else {
64
- // Default protocol to https protocol
65
- file.url = `https://${data.Location}`;
66
- }
67
- resolve();
68
- }
69
- );
70
- });
71
-
72
- return {
73
- isPrivate() {
74
- return ACL === 'private';
75
- },
76
- /**
77
- * @param {Object} file
78
- * @param {string} file.path
79
- * @param {string} file.hash
80
- * @param {string} file.ext
81
- * @param {Object} customParams
82
- * @returns {Promise<{url: string}>}
83
- */
84
- getSignedUrl(file, customParams = {}) {
85
- // Do not sign the url if it does not come from the same bucket.
86
- const { bucket } = getBucketFromUrl(file.url);
87
- if (bucket !== config.params.Bucket) {
88
- return { url: file.url };
89
- }
90
-
91
- return new Promise((resolve, reject) => {
92
- const fileKey = getFileKey(file);
93
-
94
- S3.getSignedUrl(
95
- 'getObject',
96
- {
97
- Bucket: config.params.Bucket,
98
- Key: fileKey,
99
- Expires: getOr(15 * 60, ['params', 'signedUrlExpires'], config), // 15 minutes
100
- },
101
- (err, url) => {
102
- if (err) {
103
- return reject(err);
104
- }
105
- resolve({ url });
106
- }
107
- );
108
- });
109
- },
110
- uploadStream(file, customParams = {}) {
111
- return upload(file, customParams);
112
- },
113
- upload(file, customParams = {}) {
114
- return upload(file, customParams);
115
- },
116
- delete(file, customParams = {}) {
117
- return new Promise((resolve, reject) => {
118
- // delete file on S3 bucket
119
- const fileKey = getFileKey(file);
120
- S3.deleteObject(
121
- {
122
- Key: fileKey,
123
- ...customParams,
124
- },
125
- (err) => {
126
- if (err) {
127
- return reject(err);
128
- }
129
-
130
- resolve();
131
- }
132
- );
133
- });
134
- },
135
- };
136
- },
137
- };
package/lib/utils.js DELETED
@@ -1,63 +0,0 @@
1
- 'use strict';
2
-
3
- const ENDPOINT_PATTERN = /^(.+\.)?s3[.-]([a-z0-9-]+)\./;
4
-
5
- /**
6
- * Parse the bucket name from a URL.
7
- * See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html
8
- *
9
- * @param {string} fileUrl - the URL to parse
10
- * @returns {object} result
11
- * @returns {string} result.bucket - the bucket name
12
- * @returns {string} result.error - if any
13
- */
14
- function getBucketFromUrl(fileUrl) {
15
- const uri = new URL(fileUrl);
16
-
17
- // S3://<bucket-name>/<key>
18
- if (uri.protocol === 's3:') {
19
- const bucket = uri.host;
20
-
21
- if (!bucket) {
22
- return { err: `Invalid S3 URI: no bucket: ${uri}` };
23
- }
24
- return { bucket };
25
- }
26
-
27
- if (!uri.host) {
28
- return { err: `Invalid S3 URI: no hostname: ${uri}` };
29
- }
30
-
31
- const matches = uri.host.match(ENDPOINT_PATTERN);
32
- if (!matches) {
33
- return { err: `Invalid S3 URI: hostname does not appear to be a valid S3 endpoint: ${uri}` };
34
- }
35
-
36
- const prefix = matches[1];
37
- // https://s3.amazonaws.com/<bucket-name>
38
- if (!prefix) {
39
- if (uri.pathname === '/') {
40
- return { bucket: null };
41
- }
42
-
43
- const index = uri.pathname.indexOf('/', 1);
44
-
45
- // https://s3.amazonaws.com/<bucket-name>
46
- if (index === -1) {
47
- return { bucket: uri.pathname.substring(1) };
48
- }
49
-
50
- // https://s3.amazonaws.com/<bucket-name>/
51
- if (index === uri.pathname.length - 1) {
52
- return { bucket: uri.pathname.substring(1, index) };
53
- }
54
-
55
- // https://s3.amazonaws.com/<bucket-name>/key
56
- return { bucket: uri.pathname.substring(1, index) };
57
- }
58
-
59
- // https://<bucket-name>.s3.amazonaws.com/
60
- return { bucket: prefix.substring(0, prefix.length - 1) };
61
- }
62
-
63
- module.exports = { getBucketFromUrl };