From e19991e323d4793414411f805aac609cfa18cd88 Mon Sep 17 00:00:00 2001 From: Robin Genz Date: Wed, 11 Jan 2023 09:33:48 +0100 Subject: [PATCH] feat: add methods `uploadResourceAsBlob` and `downloadResourceAsBlob` (#9) --- README.md | 4 ++ docs/utils/README.md | 117 +++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- src/definitions.ts | 82 ++++++++++++++++++++++++++++++ src/utils.ts | 104 ++++++++++++++++++++++++++++++++++++++ src/web.ts | 92 ++++------------------------------ 6 files changed, 321 insertions(+), 82 deletions(-) create mode 100644 docs/utils/README.md create mode 100644 src/utils.ts diff --git a/README.md b/README.md index 6d95dc7..f58173d 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,10 @@ further processing after downloading. +## Utils + +See [docs/utils/README.md](/docs/utils/README.md). + ## Changelog See [CHANGELOG.md](https://github.com/capawesome-team/capacitor-cloudinary/blob/main/CHANGELOG.md). diff --git a/docs/utils/README.md b/docs/utils/README.md new file mode 100644 index 0000000..2aedbee --- /dev/null +++ b/docs/utils/README.md @@ -0,0 +1,117 @@ +# Utils + +## API + + + +* [`uploadResourceAsBlob(...)`](#uploadresourceasblob) +* [`downloadResourceAsBlob(...)`](#downloadresourceasblob) +* [Interfaces](#interfaces) +* [Type Aliases](#type-aliases) +* [Enums](#enums) + + + + + + +### uploadResourceAsBlob(...) + +```typescript +uploadResourceAsBlob(options: UploadResourceAsBlobOptions) => Promise +``` + +Upload a file to Cloudinary as a blob. + +| Param | Type | +| ------------- | ----------------------------------------------------------------------------------- | +| **`options`** | UploadResourceAsBlobOptions | + +**Returns:** Promise<UploadResourceResult> + +**Since:** 0.1.1 + +-------------------- + + +### downloadResourceAsBlob(...) + +```typescript +downloadResourceAsBlob(options: DownloadResourceAsBlobOptions) => Promise +``` + +Download a file from Cloudinary as a blob. + +| Param | Type | +| ------------- | --------------------------------------------------------------------------- | +| **`options`** | DownloadResourceOptions | + +**Returns:** Promise<DownloadResourceAsBlobResult> + +**Since:** 0.1.1 + +-------------------- + + +### Interfaces + + +#### UploadResourceResult + +| Prop | Type | Description | Since | +| ---------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----- | +| **`assetId`** | string | The unique asset identifier of the uploaded resource. Only available on Android and Web. | 0.0.1 | +| **`bytes`** | number | The number of bytes of the uploaded resource. Only available on Android and Web. | 0.0.1 | +| **`createdAt`** | string | The timestamp at which the resource was uploaded. | 0.0.1 | +| **`format`** | string | The format of the uploaded resource. | 0.0.1 | +| **`originalFilename`** | string | The original filename of the uploaded resource. Only available on Android and iOS. | 0.0.1 | +| **`resourceType`** | ResourceType | The resource type of the uploaded resource. | 0.0.1 | +| **`publicId`** | string | The unique public identifier of the uploaded resource. | 0.0.1 | +| **`url`** | string | The url of the uploaded resource. | 0.0.1 | + + +#### UploadResourceAsBlobOptions + +| Prop | Type | Description | Since | +| ------------------ | ----------------------------------------------------- | ----------------------------------------------------------------------------------- | ----- | +| **`cloudName`** | string | The cloud name of your app which you can find in the Cloudinary Management Console. | 0.1.1 | +| **`resourceType`** | ResourceType | The resource type to upload. | 0.1.1 | +| **`blob`** | Blob | The file to upload. | 0.1.1 | +| **`uploadPreset`** | string | The selected upload preset. | 0.1.1 | +| **`publicId`** | string | Assign a unique public identifier to the resource. | 0.1.1 | + + +#### DownloadResourceAsBlobResult + +| Prop | Type | Description | Since | +| ---------- | ----------------- | ---------------------------------- | ----- | +| **`blob`** | Blob | The downloaded resource as a blob. | 0.1.1 | + + +#### DownloadResourceOptions + +| Prop | Type | Description | Since | +| --------- | ------------------- | ------------------------------------ | ----- | +| **`url`** | string | The url of the resource to download. | 0.0.3 | + + +### Type Aliases + + +#### DownloadResourceAsBlobOptions + +DownloadResourceOptions + + +### Enums + + +#### ResourceType + +| Members | Value | Since | +| ----------- | -------------------- | ----- | +| **`Image`** | 'image' | 0.0.1 | +| **`Video`** | 'video' | 0.0.1 | +| **`Raw`** | 'raw' | 0.0.1 | + + diff --git a/package.json b/package.json index 13cb225..e2f0ce2 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "eslint": "eslint . --ext ts", "prettier": "prettier \"**/*.{css,html,ts,js,java}\"", "swiftlint": "node-swiftlint", - "docgen": "docgen --api CloudinaryPlugin --output-readme README.md --output-json dist/docs.json", + "docgen": "npm run docgen:plugin && npm run docgen:utils", + "docgen:plugin": "docgen --api CloudinaryPlugin --output-readme README.md --output-json dist/docs.json", + "docgen:utils": "docgen --api ICloudinaryUtils --output-readme docs/utils/README.md --output-json dist/utils-docs.json", "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.js", "clean": "rimraf ./dist", "watch": "tsc --watch", diff --git a/src/definitions.ts b/src/definitions.ts index 90573df..530032a 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -199,3 +199,85 @@ export enum ResourceType { */ Raw = 'raw', } + +/** + * @since 0.1.1 + */ +export interface ICloudinaryUtils { + /** + * Upload a file to Cloudinary as a blob. + * + * @since 0.1.1 + */ + uploadResourceAsBlob( + options: UploadResourceAsBlobOptions, + ): Promise; + /** + * Download a file from Cloudinary as a blob. + * + * @since 0.1.1 + */ + downloadResourceAsBlob( + options: DownloadResourceAsBlobOptions, + ): Promise; +} + +/** + * @since 0.1.1 + */ +export interface UploadResourceAsBlobOptions { + /** + * The cloud name of your app which you can find in the Cloudinary Management Console. + * + * @since 0.1.1 + */ + cloudName: string; + /** + * The resource type to upload. + * + * @since 0.1.1 + */ + resourceType: ResourceType; + /** + * The file to upload. + * + * @since 0.1.1 + */ + blob: Blob; + /** + * The selected upload preset. + * + * @since 0.1.1 + * @see https://cloudinary.com/documentation/upload_presets + */ + uploadPreset: string; + /** + * Assign a unique public identifier to the resource. + * + * @since 0.1.1 + * @see https://cloudinary.com/documentation/upload_images#public_id + */ + publicId?: string; +} + +/** + * @since 0.1.1 + */ +export type UploadResourceAsBlobResult = UploadResourceResult; + +/** + * @since 0.1.1 + */ +export type DownloadResourceAsBlobOptions = DownloadResourceOptions; + +/** + * @since 0.1.1 + */ +export interface DownloadResourceAsBlobResult { + /** + * The downloaded resource as a blob. + * + * @since 0.1.1 + */ + blob: Blob; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..4838cd6 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,104 @@ +import type { + ResourceType, + ICloudinaryUtils, + UploadResourceAsBlobOptions, + DownloadResourceAsBlobOptions, + DownloadResourceAsBlobResult, + UploadResourceAsBlobResult, +} from './definitions'; + +export class CloudinaryUtils implements ICloudinaryUtils { + public async uploadResourceAsBlob( + options: UploadResourceAsBlobOptions, + ): Promise { + const uniqueUploadId = this.generateUniqueId(); + const chunkSize = 1024 * 1024 * 10; // 10 Megabytes + const totalSize = options.blob.size; + const chunks: { start: number; end: number; blob: Blob }[] = []; + + let start = 0; + let end = Math.min(chunkSize, totalSize); + while (start < totalSize) { + const blob = options.blob.slice(start, end); + chunks.push({ start, end, blob }); + start = end; + end = Math.min(start + chunkSize, totalSize); + } + let response: any; + for (const chunk of chunks) { + const { start, end, blob } = chunk; + response = await this.uploadResourceChunk( + options, + uniqueUploadId, + start, + end - 1, + totalSize, + blob, + ); + } + return { + assetId: response.asset_id, + bytes: response.bytes, + createdAt: response.created_at, + format: response.format, + originalFilename: response.original_filename, + resourceType: response.resource_type, + publicId: response.public_id, + url: response.secure_url, + }; + } + + public async downloadResourceAsBlob( + options: DownloadResourceAsBlobOptions, + ): Promise { + const blob = await fetch(options.url).then(res => res.blob()); + return { + blob, + }; + } + + private async uploadResourceChunk( + options: { + cloudName: string; + resourceType: ResourceType; + blob: Blob; + uploadPreset: string; + publicId?: string; + }, + uniqueUploadId: string, + start: number, + end: number, + size: number, + chunk: Blob, + ): Promise { + const formData = new FormData(); + formData.append('file', chunk); + formData.append('upload_preset', options.uploadPreset); + formData.append('cloud_name', options.cloudName); + if (options.publicId) { + formData.append('public_id', options.publicId); + } + return fetch( + `https://api.cloudinary.com/v1_1/${options.cloudName}/${options.resourceType}/upload`, + { + method: 'PUT', + body: formData, + headers: { + 'X-Unique-Upload-Id': uniqueUploadId, + 'Content-Range': `bytes ${start}-${end}/${size}`, + }, + }, + ).then(async response => { + if (!response.ok) { + throw new Error( + `Request failed with status ${response.status}: ${response.statusText}`, + ); + } + return response.json(); + }); + } + + private generateUniqueId(): string { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} diff --git a/src/web.ts b/src/web.ts index 0a1bcf8..b09c489 100644 --- a/src/web.ts +++ b/src/web.ts @@ -8,10 +8,12 @@ import type { UploadResourceOptions, UploadResourceResult, } from './definitions'; +import { CloudinaryUtils } from './utils'; export class CloudinaryWeb extends WebPlugin implements CloudinaryPlugin { public static readonly ERROR_NOT_INITIALIZED = 'Plugin is not initialized.'; public static readonly ERROR_FILE_MISSING = 'blob must be provided.'; + private readonly cloudinaryUtils = new CloudinaryUtils(); private cloudName?: string; @@ -22,94 +24,22 @@ export class CloudinaryWeb extends WebPlugin implements CloudinaryPlugin { public async uploadResource( options: UploadResourceOptions, ): Promise { + if (!this.cloudName) { + throw new Error(CloudinaryWeb.ERROR_NOT_INITIALIZED); + } if (!options.blob) { throw new Error(CloudinaryWeb.ERROR_FILE_MISSING); } - const uniqueUploadId = this.generateUniqueId(); - const chunkSize = 1024 * 1024 * 10; // 10 Megabytes - const totalSize = options.blob.size; - const chunks: { start: number; end: number; blob: Blob }[] = []; - - let start = 0; - let end = Math.min(chunkSize, totalSize); - while (start < totalSize) { - const blob = options.blob.slice(start, end); - chunks.push({ start, end, blob }); - start = end; - end = Math.min(start + chunkSize, totalSize); - } - let response: any; - for (const chunk of chunks) { - const { start, end, blob } = chunk; - response = await this.uploadResourceChunk( - options, - uniqueUploadId, - start, - end - 1, - totalSize, - blob, - ); - } - return { - assetId: response.asset_id, - bytes: response.bytes, - createdAt: response.created_at, - format: response.format, - originalFilename: response.original_filename, - resourceType: response.resource_type, - publicId: response.public_id, - url: response.secure_url, - }; + return this.cloudinaryUtils.uploadResourceAsBlob({ + ...options, + cloudName: this.cloudName, + blob: options.blob, + }); } public async downloadResource( options: DownloadResourceOptions, ): Promise { - const blob = await fetch(options.url).then(res => res.blob()); - return { - blob, - }; - } - - private async uploadResourceChunk( - options: UploadResourceOptions, - uniqueUploadId: string, - start: number, - end: number, - size: number, - chunk: Blob, - ): Promise { - if (!this.cloudName) { - throw new Error(CloudinaryWeb.ERROR_NOT_INITIALIZED); - } - const formData = new FormData(); - formData.append('file', chunk); - formData.append('upload_preset', options.uploadPreset); - formData.append('cloud_name', this.cloudName); - if (options.publicId) { - formData.append('public_id', options.publicId); - } - return fetch( - `https://api.cloudinary.com/v1_1/${this.cloudName}/${options.resourceType}/upload`, - { - method: 'PUT', - body: formData, - headers: { - 'X-Unique-Upload-Id': uniqueUploadId, - 'Content-Range': `bytes ${start}-${end}/${size}`, - }, - }, - ).then(async response => { - if (!response.ok) { - throw new Error( - `Request failed with status ${response.status}: ${response.statusText}`, - ); - } - return response.json(); - }); - } - - private generateUniqueId(): string { - return Date.now().toString(36) + Math.random().toString(36).substring(2); + return this.cloudinaryUtils.downloadResourceAsBlob({ ...options }); } }