diff --git a/.gitignore b/.gitignore index b7cc346..cbde930 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ node_modules dist .DS_Store .pnpm-debug.log* - diff --git a/.npmignore b/.npmignore index 5d9934e..3b3561a 100644 --- a/.npmignore +++ b/.npmignore @@ -6,3 +6,4 @@ lib pnpm-lock.yaml tsconfig.json .npmignore +.husky diff --git a/README.md b/README.md index c98e637..f24e8d4 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,12 @@ npm install strapi-provider-upload-tencent-cloud-storage --save - SecretKey: Tencent Cloud API SecretKey - Region: Tencent Cloud API Region - Bucket: Tencent Cloud API Bucket - - ACL: (optional) ACL applied to the uploaded files. If you set it to `private` you will need to configure the [security middleware](#security-middleware-configuration) to properly see thumbnail previews in the Media Library. + - ACL: (optional) ACL applied to the uploaded files. - Expires: (optional) Expiration time of the signed URL. Default value is 360 seconds (6 minutes). - - initOptions: (optional) Options passed to the constructor of the provider. You can find the complete list of [options here](https://cloud.tencent.com/document/product/436/8629#:~:text=%E5%8F%82%E8%A7%81%20demo%20%E7%A4%BA%E4%BE%8B%E3%80%82-,%E9%85%8D%E7%BD%AE%E9%A1%B9,-%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0) - - uploadOptions: (optional) Options passed to the `upload` method. You can find the complete list of [options here](https://cloud.tencent.com/document/product/436/64980#.E7.AE.80.E5.8D.95.E4.B8.8A.E4.BC.A0.E5.AF.B9.E8.B1.A1) + - initOptions: (optional) Options passed to the constructor of the provider. You can find the complete list of [options here](https://cloud.tencent.com/document/product/436/8629#:~:text=%E5%8F%82%E8%A7%81%20demo%20%E7%A4%BA%E4%BE%8B%E3%80%82-,%E9%85%8D%E7%BD%AE%E9%A1%B9,-%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0). + - uploadOptions: (optional) Options passed to the `upload` method. You can find the complete list of [options here](https://cloud.tencent.com/document/product/436/64980#.E7.AE.80.E5.8D.95.E4.B8.8A.E4.BC.A0.E5.AF.B9.E8.B1.A1). + - CDNDomain: (optional) CDN Accelerated Domain. + - StorageRootPath: (optional) The storage path of the file in the bucket. See the [documentation about using a provider](https://docs.strapi.io/developer-docs/latest/plugins/upload.html#using-a-provider) for information on installing and using a provider. To understand how environment variables are used in Strapi, please refer to the [documentation about environment variables](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#environment-variables). @@ -46,12 +48,12 @@ module.exports = ({ env }) => ({ // ... upload: { config: { - provider: 'strapi-provider-upload-tencent-cloud-storage', + provider: "strapi-provider-upload-tencent-cloud-storage", providerOptions: { - SecretId: env('COS_SecretId'), - SecretKey: env('COS_SecretKey'), - Region: env('COS_Region'), - Bucket: env('COS_Bucket'), + SecretId: env("COS_SecretId"), + SecretKey: env("COS_SecretKey"), + Region: env("COS_Region"), + Bucket: env("COS_Bucket"), }, }, }, @@ -63,6 +65,8 @@ module.exports = ({ env }) => ({ 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. +**Note:** If you are using a CDN, the URLs will not be signed. + You can also define the expiration time of the signed URL by setting the `Expires` option in the `providerOptions` object. The default value is 360 seconds (6 minutes). `./config/plugins.js` or `./config/plugins.ts` for TypeScript projects: @@ -72,13 +76,13 @@ module.exports = ({ env }) => ({ // ... upload: { config: { - provider: 'strapi-provider-upload-tencent-cloud-storage', + provider: "strapi-provider-upload-tencent-cloud-storage", providerOptions: { - SecretId: env('COS_SecretId'), - SecretKey: env('COS_SecretKey'), - Region: env('COS_Region'), - Bucket: env('COS_Bucket'), - ACL: 'private', // <= set ACL to private + SecretId: env("COS_SecretId"), + SecretKey: env("COS_SecretKey"), + Region: env("COS_Region"), + Bucket: env("COS_Bucket"), + ACL: "private", // <= set ACL to private }, }, }, @@ -96,25 +100,25 @@ Due to the default settings in the Strapi Security Middleware you will need to m module.exports = [ // ... { - name: 'strapi::security', + name: "strapi::security", config: { contentSecurityPolicy: { useDefaults: true, directives: { - 'connect-src': ["'self'", 'https:'], - 'img-src': [ + "connect-src": ["'self'", "https:"], + "img-src": [ "'self'", - 'data:', - 'blob:', - 'market-assets.strapi.io', - 'yourBucketName.cos.yourRegion.myqcloud.com', + "data:", + "blob:", + "market-assets.strapi.io", + "yourBucketName.cos.yourRegion.myqcloud.com", ], - 'media-src': [ + "media-src": [ "'self'", - 'data:', - 'blob:', - 'market-assets.strapi.io', - 'yourBucketName.cos.yourRegion.myqcloud.com' + "data:", + "blob:", + "market-assets.strapi.io", + "yourBucketName.cos.yourRegion.myqcloud.com", ], upgradeInsecureRequests: null, }, @@ -125,78 +129,7 @@ module.exports = [ ]; ``` -### Configure the access domain (CDN/global acceleration) - -#### Default CDN Acceleration Domain - -`./config/plugins.js` or `./config/plugins.ts` for TypeScript projects: - -```js -module.exports = ({ env }) => ({ - // ... - upload: { - config: { - provider: 'strapi-provider-upload-tencent-cloud-storage', - providerOptions: { - Domain: '{Bucket}.file.myqcloud.com', // <= Custom acceleration domain name, the Domain parameter supports templates. In this example, {Bucket} will be automatically replaced with the provided Bucket during the request. - SecretId: env('COS_SecretId'), - SecretKey: env('COS_SecretKey'), - Region: env('COS_Region'), - Bucket: env('COS_Bucket'), - }, - }, - }, - // ... -}); -``` - -#### Custom CDN Acceleration Domain - -`./config/plugins.js` or `./config/plugins.ts` for TypeScript projects: - -```js -module.exports = ({ env }) => ({ - // ... - upload: { - config: { - provider: 'strapi-provider-upload-tencent-cloud-storage', - providerOptions: { - Domain: 'example-cdn-domain.com', // <= Custom Accelerated Domain - SecretId: env('COS_SecretId'), - SecretKey: env('COS_SecretKey'), - Region: env('COS_Region'), - Bucket: env('COS_Bucket'), - }, - }, - }, - // ... -}); -``` - -#### Custom Source Site Domain - -`./config/plugins.js` or `./config/plugins.ts` for TypeScript projects: - -```js -module.exports = ({ env }) => ({ - // ... - upload: { - config: { - provider: 'strapi-provider-upload-tencent-cloud-storage', - providerOptions: { - Domain: 'example-cos-domain.com', // <= Custom Source Site Domain - SecretId: env('COS_SecretId'), - SecretKey: env('COS_SecretKey'), - Region: env('COS_Region'), - Bucket: env('COS_Bucket'), - }, - }, - }, - // ... -}); -``` - -#### Global Accelerated Domain +### Configure the access domain (CDN acceleration) `./config/plugins.js` or `./config/plugins.ts` for TypeScript projects: @@ -205,13 +138,13 @@ module.exports = ({ env }) => ({ // ... upload: { config: { - provider: 'strapi-provider-upload-tencent-cloud-storage', + provider: "strapi-provider-upload-tencent-cloud-storage", providerOptions: { - UseAccelerate: true, - SecretId: env('COS_SecretId'), - SecretKey: env('COS_SecretKey'), - Region: env('COS_Region'), - Bucket: env('COS_Bucket'), + CDNDomain: "example-cdn-domain.com", // <= CDN Accelerated Domain + SecretId: env("COS_SecretId"), + SecretKey: env("COS_SecretKey"), + Region: env("COS_Region"), + Bucket: env("COS_Bucket"), }, }, }, @@ -219,6 +152,6 @@ module.exports = ({ env }) => ({ }); ``` -### Contribution +## Contribution Feel free to fork and make a Pull Request to this plugin project. All the input is warmly welcome! diff --git a/lib/index.ts b/lib/index.ts index 49237f3..9ccbb86 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,7 +1,7 @@ -import type {ReadStream} from 'node:fs'; -import COS from 'cos-nodejs-sdk-v5'; -import type {COSOptions, PutObjectParams} from 'cos-nodejs-sdk-v5' -import * as utils from '@strapi/utils'; +import type { ReadStream } from "node:fs"; +import COS from "cos-nodejs-sdk-v5"; +import type { COSOptions, PutObjectParams } from "cos-nodejs-sdk-v5"; +import * as utils from "@strapi/utils"; interface File { name: string; @@ -30,34 +30,57 @@ interface ConfigOptions { initOptions?: COSOptions; Bucket: string; Region: string; - uploadOptions?: Exclude + uploadOptions?: Exclude< + PutObjectParams, + "Bucket" | "Region" | "Key" | "Body" | "ContentLength" | "ContentType" + >; // access control list for the bucket - ACL?: 'private' | 'default'; + ACL?: "private" | "default"; // the expiration time of the signature file(millisecond) Expires?: number; + CDNDomain?: string; + StorageRootPath?: string; } const { PayloadTooLargeError } = utils.errors; const { kbytesToBytes, bytesToHumanReadable } = utils.file; const log = (...args: any) => { - if (process.env.NODE_ENV !== 'production') { - console.debug('>>>>>>> upload cos <<<<<<<'); + if (process.env.NODE_ENV !== "production") { + console.debug(">>>>>>> upload cos <<<<<<<"); console.debug(...args); } }; - export = { init(config: ConfigOptions) { - const {SecretId, SecretKey, Bucket, Region, ACL = 'default', Expires = 360} = config + const { + SecretId, + SecretKey, + Bucket, + Region, + ACL = "default", + Expires = 360, + StorageRootPath, + CDNDomain, + } = config; const COSInitConfig = { SecretId, SecretKey, - ...config.initOptions - } - if((COSInitConfig.SecretId && COSInitConfig.SecretKey) || COSInitConfig.getAuthorization){} - else throw new Error('getAuthorization or SecretId and SecretKey must be provided') + ...config.initOptions, + }; + if ( + (COSInitConfig.SecretId && COSInitConfig.SecretKey) || + COSInitConfig.getAuthorization + ) { + } else + throw new Error( + "getAuthorization or SecretId and SecretKey must be provided", + ); + + const filePrefix = StorageRootPath + ? `${StorageRootPath.replace(/\/+$/, "")}/` + : ""; // init COS const cos = new COS(COSInitConfig); @@ -67,67 +90,77 @@ export = { // uploadStream: upload, delete(file: File): Promise { - const Key = getFileKey(file) + const Key = getFileKey(file); return new Promise((resolve, reject) => { cos.deleteObject( { Bucket, Region, - Key + Key, }, function (err) { - log({err}) - if (err) return reject(err) - resolve() - } - ) - }) + log({ err }); + if (err) return reject(err); + resolve(); + }, + ); + }); }, - checkFileSize(file: File, { sizeLimit}: { - sizeLimit?: number; - }) { - const maxSize = 5 * 1024 * 1024 * 1024 - const limit = sizeLimit ? Math.min(sizeLimit, maxSize) : maxSize + checkFileSize( + file: File, + { + sizeLimit, + }: { + sizeLimit?: number; + }, + ) { + const maxSize = 5 * 1024 * 1024 * 1024; + const limit = sizeLimit ? Math.min(sizeLimit, maxSize) : maxSize; if (kbytesToBytes(file.size) > limit) { throw new PayloadTooLargeError( - `${file.name} exceeds size limit of ${bytesToHumanReadable(limit)}.` + `${file.name} exceeds size limit of ${bytesToHumanReadable( + limit, + )}.`, ); } }, - getSignedUrl(file: File): Promise<{url: string}> { - const Key = getFileKey(file) - return new Promise((resolve, reject) => {cos.getObjectUrl( - { - Bucket, - Region, - Key, - Sign: true, - Expires, - Protocol: 'https' - }, - function (err, data) { - log({err, data}) - if (err) return reject(err) - resolve({url: data.Url}) - } - )}) + getSignedUrl(file: File): Promise<{ url: string }> { + const Key = getFileKey(file); + return new Promise((resolve, reject) => { + cos.getObjectUrl( + { + Bucket, + Region, + Key, + Sign: true, + Expires, + Protocol: "https", + }, + function (err, data) { + log({ err, data }); + if (err) return reject(err); + resolve({ url: data.Url }); + }, + ); + }); }, isPrivate() { - return ACL === 'private'; + return ACL === "private"; }, }; function getFileKey(file: File): string { - const path = file.path ? `${file.path}/` : ''; - return `${path}${file.hash}${file.ext}`; + const path = file.path ? `${file.path}/` : ""; + return `${filePrefix}${path}${file.hash}${file.ext}`; } function upload(file: File): Promise { - if (!file.stream && !file.buffer) return Promise.reject(new Error('Missing Readable Stream or Buffer')); - const Key = getFileKey(file) + if (!file.stream && !file.buffer) + return Promise.reject(new Error("Missing Readable Stream or Buffer")); + const Key = getFileKey(file); return new Promise((resolve, reject) => { cos.putObject( { @@ -138,14 +171,18 @@ export = { Body: file.stream || file.buffer, }, function (err, data) { - log({err, data, size: file.size}) - if (err) return reject(err) - file.url = `https://${data.Location}` - file.provider = 'strapi-provider-upload-tencent-cloud-storage' - resolve() - } - ) - }) + log({ err, data, size: file.size }); + if (err) return reject(err); + if (CDNDomain) { + file.url = `${CDNDomain}/${Key}`; + } else { + file.url = `https://${data.Location}`; + } + file.provider = "strapi-provider-upload-tencent-cloud-storage"; + resolve(); + }, + ); + }); } - } + }, }; diff --git a/package.json b/package.json index f31d041..b297ff0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "strapi-provider-upload-tencent-cloud-storage", - "version": "1.1.2", + "version": "1.1.3", "description": "A integration of Tencent Cloud COS as a file storage solution within Strapi.", "main": "dist/index.js", "directories": {