diff --git a/.size-snapshot.json b/.size-snapshot.json index 4dc5538be..28e6f8a1c 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,31 +1,31 @@ { "dist/uploadcare-upload-client.esm.js": { - "bundled": 49011, - "minified": 21736, - "gzipped": 5769, + "bundled": 59896, + "minified": 26701, + "gzipped": 6132, "treeshaked": { "rollup": { - "code": 16463, + "code": 21156, "import_statements": 49 }, "webpack": { - "code": 19935 + "code": 24345 } } }, "dist/uploadcare-upload-client.cjs.js": { - "bundled": 49099, - "minified": 21812, - "gzipped": 5794 + "bundled": 60000, + "minified": 26793, + "gzipped": 6159 }, "dist/uploadcare-upload-client.js": { - "bundled": 97142, - "minified": 30810, - "gzipped": 9310 + "bundled": 109205, + "minified": 35282, + "gzipped": 9740 }, "dist/uploadcare-upload-client.min.js": { - "bundled": 30994, - "minified": 30774, - "gzipped": 9311 + "bundled": 35445, + "minified": 35235, + "gzipped": 9755 } } diff --git a/.travis.yml b/.travis.yml index b229cea5c..170e2db98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: node_js node_js: - 'stable' + - 'lts/*' + - '8' install: - NODE_ENV=dev npm install diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f34c8277..c3c5de0c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Added -* Wrappers for group paths of Upload API (`group`, `groupInfo`) -* `onUploadProgress` for Node.js +* Wrappers for group paths of Upload API (`group`, `groupInfo`). +* The high-level function for group uploading, aka filesGroupFrom. +* Uploading progress for Node.js in `base` method. + +### Changed + +* `UploadFromInterface` was renamed to `FileUploadInterface`. +* `FileProgress` was renamed to `ProgressParams`. +* `UploadcareFile` was renamed to `UploadcareFileInterface`. [Unreleased]: https://github.com/uploadcare/uploadcare-upload-client/compare/v1.0.0-alpha.3...HEAD @@ -18,14 +25,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Added -* Support `fileFrom` 'uploaded' file (`uuid`) +* Support `fileFrom` 'uploaded' file (`uuid`). * Support of `waiting` status from `/from_url/status/` endpoint. -* Export some main types from `index.ts` file. - So you can import them now directly from `@uploadcare/upload-client` -* Throttling for `request` -* `retryThrottledMaxTimes` param to set count of max retries after throttled request (1 by default) -* `Uuid` type -* Mock server for local testing +* Export some main types from `index.ts` file. + So you can import them now directly from `@uploadcare/upload-client`. +* Throttling for `request`. +* `retryThrottledMaxTimes` param to set count of max retries after + throttled request (1 by default). +* `Uuid` type. +* Mock server for local testing. ## Fixed @@ -39,15 +47,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed * Project was moved from Flow notations to TypeScript. -* The `base` function now returns object that implements `DirectUploadInterface`. -* The `fileFrom` function now returns object that implements `UploadFromInterface`. +* The `base` function now returns object that implements + `DirectUploadInterface`. +* The `fileFrom` function now returns object that implements + `UploadFromInterface`. * The `UCFile` type renamed to `UploadcareFile`. * The progress of `fileFrom` now based on the `UploadingProgress` type. ### Added -* Low-level request wrappers for `/from_url/` and `/from_url/status/` paths of Upload API. -* Settings: the support of setting `baseCDN`, `checkForUrlDuplicates`, `saveUrlForRecurrentUploads`. +* Low-level request wrappers for `/from_url/` and `/from_url/status/` + paths of Upload API. +* Settings: the support of setting `baseCDN`, `checkForUrlDuplicates`, + `saveUrlForRecurrentUploads`. [1.0.0-alpha.2]: https://github.com/uploadcare/uploadcare-upload-client/compare/v1.0.0-alpha.1...v1.0.0-alpha.2 @@ -55,7 +67,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed -* Use the version from the `package.json` file to create Uploadcare User Agent. +* Use the version from the `package.json` file to create Uploadcare User + Agent. ### Changed diff --git a/mock-server/controllers/group.ts b/mock-server/controllers/group.ts index 2981e9031..ce9b497e6 100644 --- a/mock-server/controllers/group.ts +++ b/mock-server/controllers/group.ts @@ -5,12 +5,53 @@ import error from '../utils/error' const UUID_REGEX = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' const GROUP_ID_REGEX = `${UUID_REGEX}~[1-9][0-9]*$` +/** + * Check file UUID. + * @param {string} uuid + * @return {boolean} + */ +const isValidUuid = (uuid: string): boolean => (new RegExp(UUID_REGEX)).test(uuid) + +/** + * Check group id. + * @param {string} groupId + * @return {boolean} + */ +const isValidGroupId = (groupId: string): boolean => (new RegExp(GROUP_ID_REGEX)).test(groupId) + +/** + * Get UUID from file + * @param {string} file + * @return {string} + */ +const getFileUuid = (file: string): string => { + // If file contains CDN operations + if ((new RegExp(/\//)).test(file)) { + const array = file.split('/') + + return array[0] + } + + return file +} + +/** + * Is valid file? + * @param {string} file + * @return {boolean} + */ +const isValidFile = (file: string): boolean => { + const uuid = getFileUuid(file) + + return isValidUuid(uuid) +} + /** * '/group/' * @param {object} ctx */ const index = (ctx) => { - const files = ctx.query && ctx.query['files[]'] + let files = ctx.query && ctx.query['files[]'] const publicKey = ctx.query && ctx.query.pub_key if (!files || files.length === 0) { @@ -19,20 +60,16 @@ const index = (ctx) => { }) } - if (files && files.length > 0) { - for (let key in files) { - if (files.hasOwnProperty(key)) { - const file = files[key] - const array = file.split('/') - const uuid = array[0] - const isValidUUID = (new RegExp(UUID_REGEX)).exec(uuid) - - if (!isValidUUID) { - return error(ctx, { - statusText: `this is not valid file url: ${file}` - }) - } - } + // If `files` contains only `string` – convert in array + if (!Array.isArray(files)) { + files = [files] + } + + for (const file of files) { + if (!isValidFile(file)) { + return error(ctx, { + statusText: `this is not valid file url: ${file}` + }) } } @@ -58,9 +95,7 @@ const info = (ctx) => { }) } - const isValidGroupId = (new RegExp(GROUP_ID_REGEX)).exec(groupId) - - if (!isValidGroupId) { + if (!isValidGroupId(groupId)) { return error(ctx, { status: 404, statusText: 'group_id is invalid.' diff --git a/mock-server/middleware/auth.ts b/mock-server/middleware/auth.ts index e70861fc5..ed525586f 100644 --- a/mock-server/middleware/auth.ts +++ b/mock-server/middleware/auth.ts @@ -7,12 +7,15 @@ import error from '../utils/error' */ const protectedRoutes: Array = ROUTES .filter((route: RouteType) => { - const path = Object.keys(route).shift() + const keys = Object.keys(route) + const path = keys[0] return route[path].isProtected }) .map((route: RouteType) => { - return Object.keys(route).shift() + const keys = Object.keys(route) + + return keys[0] }) /** diff --git a/mock-server/server.ts b/mock-server/server.ts index de00df918..afd2dc4c8 100644 --- a/mock-server/server.ts +++ b/mock-server/server.ts @@ -1,3 +1,4 @@ +// @ts-ignore import Koa from 'koa' import * as router from 'koa-route' import chalk from 'chalk' @@ -5,6 +6,7 @@ import chalk from 'chalk' // Middleware import cors from '@koa/cors' import addTrailingSlashes from 'koa-add-trailing-slashes' +// @ts-ignore import koaBody from 'koa-body' import logger from './middleware/logger' @@ -29,7 +31,8 @@ app.use(auth) // Routes ROUTES.forEach((route: RouteType) => { - const path = Object.keys(route).shift() + const keys = Object.keys(route) + const path = keys[0] const method = route[path].method const fn = route[path].fn @@ -50,7 +53,8 @@ app.listen(PORT, () => { // Print all available routes ROUTES.forEach((route: RouteType) => { - const path = Object.keys(route).shift() + const keys = Object.keys(route) + const path = keys[0] const routePath = route[path] const method = routePath.method.toUpperCase() const description = routePath.description || path @@ -60,12 +64,3 @@ app.listen(PORT, () => { }) console.log() }) -// -// app.use(() => { -// -// }) -// -// app.use(async (ctx, next) => { -// process.stdout.write('\x1Bc') -// await next() -// }) diff --git a/mock-server/tsconfig.json b/mock-server/tsconfig.json index 43d3d2a35..ba3ff726a 100644 --- a/mock-server/tsconfig.json +++ b/mock-server/tsconfig.json @@ -8,7 +8,6 @@ "baseUrl": ".", "resolveJsonModule": true }, - "exclude": [ - "node_modules/**/*" - ] + "include": ["src"], + "exclude": ["node_modules"] } diff --git a/package-lock.json b/package-lock.json index 024ef8832..d1b7cfa96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -217,6 +217,14 @@ "uuid": "3.3.2", "window-size": "1.1.1", "yargs-parser": "11.0.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + } } } } @@ -1562,7 +1570,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, "requires": { @@ -1981,7 +1989,7 @@ }, "query-string": { "version": "5.1.1", - "resolved": "http://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "dev": true, "requires": { @@ -3206,7 +3214,7 @@ }, "duplexer": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, @@ -4409,25 +4417,29 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, "requires": { @@ -4437,13 +4449,15 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": false, + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "optional": true, "requires": { @@ -4453,37 +4467,43 @@ }, "chownr": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "4.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, "requires": { @@ -4492,25 +4512,29 @@ }, "deep-extend": { "version": "0.6.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": false, + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, "requires": { @@ -4519,13 +4543,15 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": false, + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, "requires": { @@ -4541,7 +4567,8 @@ }, "glob": { "version": "7.1.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, "requires": { @@ -4555,13 +4582,15 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "bundled": true, + "resolved": false, + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, "requires": { @@ -4570,7 +4599,8 @@ }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, "requires": { @@ -4579,7 +4609,8 @@ }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": false, + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, "requires": { @@ -4589,19 +4620,22 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, + "resolved": false, + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, "requires": { @@ -4610,13 +4644,15 @@ }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "optional": true, "requires": { @@ -4625,13 +4661,15 @@ }, "minimist": { "version": "0.0.8", - "bundled": true, + "resolved": false, + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true }, "minipass": { "version": "2.3.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, "optional": true, "requires": { @@ -4641,7 +4679,8 @@ }, "minizlib": { "version": "1.2.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, "requires": { @@ -4650,7 +4689,8 @@ }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, "requires": { @@ -4659,13 +4699,15 @@ }, "ms": { "version": "2.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true }, "needle": { "version": "2.3.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, "requires": { @@ -4676,7 +4718,8 @@ }, "node-pre-gyp": { "version": "0.12.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, "requires": { @@ -4694,7 +4737,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, "requires": { @@ -4704,13 +4748,15 @@ }, "npm-bundled": { "version": "1.0.6", - "bundled": true, + "resolved": false, + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, "requires": { @@ -4720,7 +4766,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, "requires": { @@ -4732,19 +4779,22 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "optional": true, "requires": { @@ -4753,19 +4803,22 @@ }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, "requires": { @@ -4775,19 +4828,22 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.8", - "bundled": true, + "resolved": false, + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, "requires": { @@ -4799,7 +4855,8 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -4807,7 +4864,8 @@ }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": false, + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, "requires": { @@ -4822,7 +4880,8 @@ }, "rimraf": { "version": "2.6.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, "requires": { @@ -4831,43 +4890,50 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.7.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, "requires": { @@ -4878,7 +4944,8 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, "requires": { @@ -4887,7 +4954,8 @@ }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "optional": true, "requires": { @@ -4896,13 +4964,15 @@ }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "tar": { "version": "4.4.8", - "bundled": true, + "resolved": false, + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, "requires": { @@ -4917,13 +4987,15 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, "requires": { @@ -4932,13 +5004,15 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true, "optional": true } @@ -5320,7 +5394,7 @@ }, "globby": { "version": "6.1.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -6955,9 +7029,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", "dev": true }, "lodash._reinterpolate": { @@ -7981,7 +8055,7 @@ }, "p-cancelable": { "version": "0.4.1", - "resolved": "http://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", "dev": true }, @@ -8962,9 +9036,9 @@ } }, "rollup": { - "version": "1.16.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.16.6.tgz", - "integrity": "sha512-oM3iKkzPCq9Da95wCnNfS8YlNZjgCD5c/TceKnJIthI9FOeJqnO3PUr/C5Suv9Kjzh0iphKL02PLeja3A5AMIA==", + "version": "1.16.7", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.16.7.tgz", + "integrity": "sha512-P3GVcbVSLLjHWFLKGerYRe3Q/yggRXmTZFx/4WZf4wzGwO6hAg5jyMAFMQKc0dts8rFID4BQngfoz6yQbI7iMQ==", "dev": true, "requires": { "@types/estree": "0.0.39", @@ -8973,9 +9047,9 @@ }, "dependencies": { "@types/node": { - "version": "12.0.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.12.tgz", - "integrity": "sha512-Uy0PN4R5vgBUXFoJrKryf5aTk3kJ8Rv3PdlHjl6UaX+Cqp1QE0yPQ68MPXGrZOfG7gZVNDIJZYyot0B9ubXUrQ==", + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.2.tgz", + "integrity": "sha512-gojym4tX0FWeV2gsW4Xmzo5wxGjXGm550oVUII7f7G5o4BV6c7DBdiG1RRQd+y1bvqRyYtPfMK85UM95vsapqQ==", "dev": true } } @@ -9013,6 +9087,14 @@ "magic-string": "0.25.2", "mkdirp": "0.5.1", "moment": "2.24.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + } } }, "rollup-plugin-node-resolve": { @@ -9113,9 +9195,9 @@ } }, "terser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.1.1.tgz", - "integrity": "sha512-Bm6xAmnJYRYg1VjDiGU4A+Y6klNzkwNrmK9ffo6H0iR7kouvTKVlZBWu0BTZ/b+5TsLscX+NC0/mzMjjc3ElKA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.1.2.tgz", + "integrity": "sha512-jvNoEQSPXJdssFwqPSgWjsOrb+ELoE+ILpHPKXC83tIxOlh2U75F1KuB2luLD/3a6/7K3Vw5pDn+hvu0C4AzSw==", "dev": true, "requires": { "commander": "^2.20.0", @@ -10381,9 +10463,9 @@ "dev": true }, "typescript": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", - "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 4cd8978df..fa294b0f9 100644 --- a/package.json +++ b/package.json @@ -56,22 +56,22 @@ "karma": "^4.1.0", "karma-chrome-launcher": "^2.2.0", "karma-jasmine": "^2.0.1", - "karma-typescript": "^4.1.0", - "lodash": "^4.17.11", + "karma-typescript": "^4.1.1", + "lodash": "^4.17.14", "npm-run-all": "^4.1.5", - "puppeteer": "^1.17.0", + "puppeteer": "^1.18.1", "rimraf": "^2.6.3", - "rollup": "^1.14.5", - "rollup-plugin-commonjs": "^10.0.0", + "rollup": "^1.16.7", + "rollup-plugin-commonjs": "^10.0.1", "rollup-plugin-json": "^4.0.0", "rollup-plugin-license": "^0.9.0", - "rollup-plugin-node-resolve": "^5.0.1", + "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-replace": "^2.2.0", "rollup-plugin-size-snapshot": "^0.9.0", - "rollup-plugin-terser": "^5.0.0", - "rollup-plugin-typescript2": "^0.21.1", - "ts-node": "^8.2.0", - "typescript": "^3.5.1" + "rollup-plugin-terser": "^5.1.1", + "rollup-plugin-typescript2": "^0.21.2", + "ts-node": "^8.3.0", + "typescript": "^3.5.3" }, "dependencies": { "axios": "^0.19.0", diff --git a/src/UploadClient.ts b/src/UploadClient.ts index ddc06d14d..12e444bbf 100644 --- a/src/UploadClient.ts +++ b/src/UploadClient.ts @@ -1,11 +1,14 @@ import defaultSettings from './defaultSettings' import UploadAPI from './api/index' -import fileFrom, {FileFrom} from './fileFrom/fileFrom' +import fileFrom from './fileFrom/fileFrom' +import {FileFrom} from './fileFrom/types' import {FileData, Settings} from './types' -import {UploadFromInterface} from './fileFrom/UploadFrom' +import {FileUploadInterface} from './fileFrom/types' import {Url} from './api/fromUrl' import {UploadAPIInterface} from './api/UploadAPI' import {Uuid} from './api/types' +import groupFrom from './groupFrom/groupFrom' +import {GroupFrom, GroupUploadInterface} from './groupFrom/types' export interface UploadClientInterface { setSettings(newSettings: Settings): void @@ -16,7 +19,9 @@ export interface UploadClientInterface { removeUpdateSettingsListener(listener: Function): void - fileFrom(from: FileFrom, data: FileData | Url | Uuid, settings?: Settings): UploadFromInterface + fileFrom(from: FileFrom, data: FileData | Url | Uuid, settings?: Settings): FileUploadInterface + + groupFrom(from: GroupFrom, data: FileData[] | Url[] | Uuid[], settings?: Settings): GroupUploadInterface } class UploadClient implements UploadClientInterface { @@ -64,12 +69,19 @@ class UploadClient implements UploadClientInterface { } } - fileFrom(from: FileFrom, data: FileData | Url | Uuid, settings: Settings = {}): UploadFromInterface { + fileFrom(from: FileFrom, data: FileData | Url | Uuid, settings: Settings = {}): FileUploadInterface { return fileFrom(from, data, { ...this.settings, ...settings, }) } + + groupFrom(from: GroupFrom, data: FileData[] | Url[] | Uuid[], settings: Settings = {}): GroupUploadInterface { + return groupFrom(from, data, { + ...this.settings, + ...settings, + }) + } } export default UploadClient diff --git a/src/UploadcareFile.ts b/src/UploadcareFile.ts new file mode 100644 index 000000000..a57977877 --- /dev/null +++ b/src/UploadcareFile.ts @@ -0,0 +1,34 @@ +import {OriginalImageInfo, Settings, UploadcareFileInterface} from './types' +import {FileInfo} from './api/types' +import prettyFileInfo from './prettyFileInfo' + +export class UploadcareFile implements UploadcareFileInterface { + private readonly fileInfo: FileInfo + + readonly uuid: string + readonly name: null | string = null + readonly size: null | number = null + readonly isStored: null | boolean = null + readonly isImage: null | boolean = null + readonly cdnUrl: null | string = null + readonly cdnUrlModifiers: null | string = null + readonly originalUrl: null | string = null + readonly originalFilename: null | string = null + readonly originalImageInfo: null | OriginalImageInfo = null + + constructor(fileInfo: FileInfo, settings: Settings) { + this.fileInfo = fileInfo + const pretty = prettyFileInfo(fileInfo, settings) + + this.uuid = pretty.uuid + this.name = pretty.name + this.size = pretty.size + this.isStored = pretty.isStored + this.isImage = pretty.isImage + this.cdnUrl = pretty.cdnUrl + this.cdnUrlModifiers = pretty.cdnUrlModifiers + this.originalUrl = pretty.originalUrl + this.originalFilename = pretty.originalFilename + this.originalImageInfo = pretty.originalImageInfo + } +} diff --git a/src/UploadcareGroup.ts b/src/UploadcareGroup.ts new file mode 100644 index 000000000..2e8b3bb8a --- /dev/null +++ b/src/UploadcareGroup.ts @@ -0,0 +1,30 @@ +import {UploadcareGroupInterface} from './types' +import {FileInfo, GroupId, GroupInfo} from './api/types' + +export class UploadcareGroup implements UploadcareGroupInterface { + private readonly groupInfo: GroupInfo + + readonly uuid: GroupId + readonly filesCount: string + readonly totalSize: number + readonly isStored: boolean + readonly isImage: boolean + readonly cdnUrl: string + readonly files: FileInfo[] + readonly createdAt: string + readonly storedAt: string | null = null + + constructor(groupInfo: GroupInfo) { + this.groupInfo = groupInfo + + this.uuid = groupInfo.id + this.filesCount = groupInfo.files_count + this.totalSize = groupInfo.files.reduce((acc, file) => acc + file.size, 0) + this.isStored = !!groupInfo.datetime_stored + this.isImage = !!groupInfo.files.filter(file => file.is_image).length + this.cdnUrl = groupInfo.cdn_url + this.files = groupInfo.files + this.createdAt = groupInfo.datetime_created + this.storedAt = groupInfo.datetime_stored + } +} diff --git a/src/api/UploadAPI.ts b/src/api/UploadAPI.ts index 725a2aecd..f564774bc 100644 --- a/src/api/UploadAPI.ts +++ b/src/api/UploadAPI.ts @@ -22,7 +22,7 @@ export interface UploadAPIInterface { fromUrlStatus(token: Token, settings?: Settings): Promise - group(files: Array, settings: Settings): Promise + group(files: Uuid[], settings: Settings): Promise groupInfo(id: GroupId, settings: Settings): Promise } @@ -65,7 +65,7 @@ class UploadAPI implements UploadAPIInterface { return fromUrlStatus(token, this.getResultSettings(settings)) } - group(files: Array, settings: Settings): Promise { + group(files: Uuid[], settings: Settings): Promise { return group(files, this.getResultSettings(settings)) } diff --git a/src/api/base.ts b/src/api/base.ts index 6ae86fe11..2141d30b1 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -39,7 +39,7 @@ class DirectUpload extends Thenable implements DirectUploadInterfa }) } - protected getRequestOptions() { + private getRequestOptions() { return { ...this.options, /* TODO Add support of progress for Node.js */ diff --git a/src/api/group.ts b/src/api/group.ts index 3c5e02d25..d6355b6ef 100644 --- a/src/api/group.ts +++ b/src/api/group.ts @@ -5,7 +5,7 @@ import {RequestOptions} from './request' export type GroupInfoResponse = GroupInfo -const getRequestQuery = (files: Array, settings: Settings) => { +const getRequestQuery = (files: Uuid[], settings: Settings) => { const query = { pub_key: settings.publicKey || '', files, @@ -24,7 +24,7 @@ const getRequestQuery = (files: Array, settings: Settings) => { return {...query} } -const getRequestOptions = (files: Array, settings: Settings): RequestOptions => { +const getRequestOptions = (files: Uuid[], settings: Settings): RequestOptions => { return prepareOptions({ method: 'POST', path: '/group/', @@ -35,11 +35,11 @@ const getRequestOptions = (files: Array, settings: Settings): RequestOptio /** * Create files group * - * @param {Array} files – A set of files you want to join in a group. + * @param {Uuid[]} files – A set of files you want to join in a group. * @param {Settings} settings * @return {Promise} */ -export default function group(files: Array, settings: Settings = {}): Promise { +export default function group(files: Uuid[], settings: Settings = {}): Promise { const options = getRequestOptions(files, settings) return request(options) diff --git a/src/api/types.ts b/src/api/types.ts index 2a58f1459..4cb0daa9b 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,3 +1,5 @@ +import {UploadcareFileInterface, UploadcareGroupInterface} from '../types' + interface StatusInterface { status: string, } @@ -70,7 +72,7 @@ export type GroupInfo = { datetime_stored: string | null, files_count: string, cdn_url: string, - files: Array, + files: FileInfo[], url: string, id: GroupId, } diff --git a/src/checkFileIsReady.ts b/src/checkFileIsReady.ts index c6b31c357..5b7dc0ad6 100644 --- a/src/checkFileIsReady.ts +++ b/src/checkFileIsReady.ts @@ -3,14 +3,14 @@ import {Settings} from './types' import poll, {DEFAULT_TIMEOUT, PollPromiseInterface} from './tools/poll' import {Uuid} from './api/types' -type CheckFileIsReadyParams = { +type FileIsReadyParams = { uuid: Uuid, timeout?: number, onProgress?: Function, settings?: Settings } -const checkFileIsReady = ({uuid, timeout = DEFAULT_TIMEOUT, onProgress, settings = {}}: CheckFileIsReadyParams): PollPromiseInterface => +const checkFileIsReady = ({uuid, timeout = DEFAULT_TIMEOUT, onProgress, settings = {}}: FileIsReadyParams): PollPromiseInterface => poll( async () => { const response = await info(uuid, settings) diff --git a/src/checkFileIsUploadedFromUrl.ts b/src/checkFileIsUploadedFromUrl.ts index c100ebe8c..65f53f7a1 100644 --- a/src/checkFileIsUploadedFromUrl.ts +++ b/src/checkFileIsUploadedFromUrl.ts @@ -3,14 +3,14 @@ import poll, {DEFAULT_TIMEOUT, PollPromiseInterface} from './tools/poll' import fromUrlStatus, {FromUrlStatusResponse, isSuccessResponse} from './api/fromUrlStatus' import {Uuid} from './api/types' -type CheckFileIsUploadedParams = { +type FileIsUploadedParams = { token: Uuid, timeout?: number, onProgress?: Function, settings?: Settings } -const checkFileIsUploadedFromUrl = ({token, timeout = DEFAULT_TIMEOUT, onProgress, settings = {}}: CheckFileIsUploadedParams): PollPromiseInterface => +const checkFileIsUploadedFromUrl = ({token, timeout = DEFAULT_TIMEOUT, onProgress, settings = {}}: FileIsUploadedParams): PollPromiseInterface => poll( async () => { const response = await fromUrlStatus(token, settings) diff --git a/src/fileFrom/FileUpload.ts b/src/fileFrom/FileUpload.ts new file mode 100644 index 000000000..3ca4efc71 --- /dev/null +++ b/src/fileFrom/FileUpload.ts @@ -0,0 +1,32 @@ +import {FileHandlerInterface, FileUploadInterface} from './types' +import {Thenable} from '../tools/Thenable' +import {UploadcareFileInterface, UploadingProgress} from '../types' +import {FileUploadLifecycleInterface} from '../lifecycle/types' +import {CancelableInterface} from '../api/types' + +export class FileUpload extends Thenable implements FileUploadInterface { + onProgress: ((progress: UploadingProgress) => void) | null = null + onUploaded: ((uuid: string) => void) | null = null + onReady: ((file: UploadcareFileInterface) => void) | null = null + onCancel: VoidFunction | null = null + + protected readonly promise: Promise + private readonly cancelable: CancelableInterface + + constructor(lifecycle: FileUploadLifecycleInterface, handler: FileHandlerInterface, cancelable: CancelableInterface) { + super() + this.cancelable = cancelable + const uploadLifecycle = lifecycle.getUploadLifecycle() + + uploadLifecycle.onProgress = this.onProgress + uploadLifecycle.onUploaded = this.onUploaded + uploadLifecycle.onReady = this.onReady + uploadLifecycle.onCancel = this.onCancel + + this.promise = handler.upload() + } + + cancel(): void { + this.cancelable.cancel() + } +} diff --git a/src/fileFrom/UploadFrom.ts b/src/fileFrom/UploadFrom.ts index 7b984b979..c8cd5e46a 100644 --- a/src/fileFrom/UploadFrom.ts +++ b/src/fileFrom/UploadFrom.ts @@ -1,48 +1,19 @@ -import {Settings, UploadcareFile} from '../types' +import {Settings, UploadcareFileInterface, UploadingProgress, ProgressState, ProgressParams} from '../types' import checkFileIsReady from '../checkFileIsReady' import prettyFileInfo from '../prettyFileInfo' import {Thenable} from '../tools/Thenable' -import {CancelableInterface, Uuid} from '../api/types' +import {Uuid} from '../api/types' import {PollPromiseInterface} from '../tools/poll' import {InfoResponse} from '../api/info' - -export enum ProgressState { - Pending = 'pending', - Uploading = 'uploading', - Uploaded = 'uploaded', - Ready = 'ready', - Canceled = 'canceled', - Error = 'error', -} - -export type FileProgress = { - total: number, - loaded: number, -} - -export type UploadingProgress = { - state: ProgressState, - uploaded: null | FileProgress, - value: number, -} +import {FileUploadInterface} from './types' /** - * Base `thenable` interface for uploading `fileFrom` (`object`, `url`, `input`, `uploaded`). - */ -export interface UploadFromInterface extends Promise, CancelableInterface { - onProgress: ((progress: UploadingProgress) => void) | null - onUploaded: ((uuid: string) => void) | null - onReady: ((file: UploadcareFile) => void) | null - onCancel: VoidFunction | null -} - -/** - * Base abstract `thenable` implementation of `UploadFromInterface`. + * Base abstract `thenable` implementation of `FileUploadInterface`. * You need to use this as base class for all uploading methods of `fileFrom`. * All that you need to implement — `promise` property and `cancel` method. */ -export abstract class UploadFrom extends Thenable implements UploadFromInterface { - protected abstract readonly promise: Promise +export abstract class UploadFrom extends Thenable implements FileUploadInterface { + protected abstract readonly promise: Promise protected isFileReadyPolling: PollPromiseInterface | null = null abstract cancel(): void @@ -51,11 +22,11 @@ export abstract class UploadFrom extends Thenable implements Upl uploaded: null, value: 0, } - protected file: UploadcareFile | null = null + protected file: UploadcareFileInterface | null = null onProgress: ((progress: UploadingProgress) => void) | null = null onUploaded: ((uuid: string) => void) | null = null - onReady: ((file: UploadcareFile) => void) | null = null + onReady: ((file: UploadcareFileInterface) => void) | null = null onCancel: VoidFunction | null = null protected constructor() { @@ -63,7 +34,7 @@ export abstract class UploadFrom extends Thenable implements Upl this.handleCancelling = this.handleCancelling.bind(this) } - protected setProgress(state: ProgressState, progress?: FileProgress) { + protected setProgress(state: ProgressState, progress?: ProgressParams) { switch (state) { case ProgressState.Pending: this.progress = { @@ -115,12 +86,12 @@ export abstract class UploadFrom extends Thenable implements Upl return this.progress } - protected setFile(file: UploadcareFile) { + protected setFile(file: UploadcareFileInterface) { this.file = file } - protected getFile(): UploadcareFile { - return this.file as UploadcareFile + protected getFile(): UploadcareFileInterface { + return this.file as UploadcareFileInterface } /** @@ -136,9 +107,9 @@ export abstract class UploadFrom extends Thenable implements Upl /** * Handle file uploading. - * @param {FileProgress} progress + * @param {ProgressParams} progress */ - protected handleUploading(progress?: FileProgress): void { + protected handleUploading(progress?: ProgressParams): void { this.setProgress(ProgressState.Uploading, progress) if (typeof this.onProgress === 'function') { @@ -151,7 +122,7 @@ export abstract class UploadFrom extends Thenable implements Upl * @param {Uuid} uuid * @param {Settings} settings */ - protected handleUploaded(uuid: Uuid, settings: Settings): Promise { + protected handleUploaded(uuid: Uuid, settings: Settings): Promise { this.setFile({ uuid, name: null, @@ -188,7 +159,7 @@ export abstract class UploadFrom extends Thenable implements Upl /** * Handle uploaded file that ready on CDN. */ - protected handleReady = (): Promise => { + protected handleReady = (): Promise => { this.setProgress(ProgressState.Ready) if (typeof this.onProgress === 'function') { @@ -207,10 +178,10 @@ export abstract class UploadFrom extends Thenable implements Upl * @param error */ protected handleError = (error) => { - this.setProgress(ProgressState.Error) - if (error.name === 'CancelError') { this.handleCancelling() + } else { + this.setProgress(ProgressState.Error) } return Promise.reject(error) diff --git a/src/fileFrom/UploadFromObject.ts b/src/fileFrom/UploadFromObject.ts index 78ed2253f..cd0123e1f 100644 --- a/src/fileFrom/UploadFromObject.ts +++ b/src/fileFrom/UploadFromObject.ts @@ -1,13 +1,13 @@ -import {FileData, Settings, UploadcareFile} from '../types' +import {FileData, Settings, UploadcareFileInterface} from '../types' import base, {DirectUploadInterface} from '../api/base' import {UploadFrom} from './UploadFrom' export class UploadFromObject extends UploadFrom { - protected readonly request: DirectUploadInterface - protected readonly promise: Promise + private readonly request: DirectUploadInterface + protected readonly promise: Promise - protected readonly data: FileData - protected readonly settings: Settings + private readonly data: FileData + private readonly settings: Settings constructor(data: FileData, settings: Settings) { super() @@ -19,7 +19,7 @@ export class UploadFromObject extends UploadFrom { this.promise = this.getFilePromise() } - private getFilePromise(): Promise { + private getFilePromise(): Promise { const fileUpload = this.request this.handleUploading() diff --git a/src/fileFrom/UploadFromUploaded.ts b/src/fileFrom/UploadFromUploaded.ts index 8a5c19296..ef80cba28 100644 --- a/src/fileFrom/UploadFromUploaded.ts +++ b/src/fileFrom/UploadFromUploaded.ts @@ -1,15 +1,15 @@ -import {Settings, UploadcareFile} from '../types' -import {ProgressState, UploadFrom} from './UploadFrom' +import {Settings, ProgressState, UploadcareFileInterface} from '../types' +import {UploadFrom} from './UploadFrom' import info, {InfoResponse} from '../api/info' import {Uuid} from '../api/types' import CancelError from '../errors/CancelError' export class UploadFromUploaded extends UploadFrom { - protected readonly promise: Promise + protected readonly promise: Promise private isCancelled: boolean = false - protected readonly data: Uuid - protected readonly settings: Settings + private readonly data: Uuid + private readonly settings: Settings constructor(data: Uuid, settings: Settings) { super() @@ -23,7 +23,7 @@ export class UploadFromUploaded extends UploadFrom { this.promise = this.getFilePromise() } - private getFilePromise = (): Promise => { + private getFilePromise = (): Promise => { this.handleUploading() return info(this.data, this.settings) diff --git a/src/fileFrom/UploadFromUrl.ts b/src/fileFrom/UploadFromUrl.ts index 9b2fbdd00..87ff03c1a 100644 --- a/src/fileFrom/UploadFromUrl.ts +++ b/src/fileFrom/UploadFromUrl.ts @@ -1,5 +1,5 @@ import fromUrl, {FromUrlResponse, isFileInfoResponse, isTokenResponse, Url} from '../api/fromUrl' -import {Settings, UploadcareFile} from '../types' +import {Settings, UploadcareFileInterface, ProgressState} from '../types' import fromUrlStatus, { FromUrlStatusResponse, isErrorResponse, @@ -8,20 +8,20 @@ import fromUrlStatus, { isUnknownResponse, isWaitingResponse, } from '../api/fromUrlStatus' -import {ProgressState, UploadFrom} from './UploadFrom' +import {UploadFrom} from './UploadFrom' import checkFileIsUploadedFromUrl from '../checkFileIsUploadedFromUrl' import {PollPromiseInterface} from '../tools/poll' import CancelError from '../errors/CancelError' import {Uuid} from '../api/types' export class UploadFromUrl extends UploadFrom { - protected readonly promise: Promise + protected readonly promise: Promise private isFileUploadedFromUrlPolling: PollPromiseInterface | null = null private isCancelled: boolean = false private unknownStatusWasTimes: number = 0 - protected readonly data: Url - protected readonly settings: Settings + private readonly data: Url + private readonly settings: Settings constructor(data: Url, settings: Settings) { super() @@ -32,7 +32,7 @@ export class UploadFromUrl extends UploadFrom { this.promise = this.getFilePromise() } - private getFilePromise = (): Promise => { + private getFilePromise = (): Promise => { this.handleUploading() return fromUrl(this.data, this.settings) diff --git a/src/fileFrom/fileFrom.ts b/src/fileFrom/fileFrom.ts index bf53d92b6..8f68855b4 100644 --- a/src/fileFrom/fileFrom.ts +++ b/src/fileFrom/fileFrom.ts @@ -3,15 +3,8 @@ import {Url} from '../api/fromUrl' import {Uuid} from '../api/types' import {UploadFromObject} from './UploadFromObject' import {UploadFromUrl} from './UploadFromUrl' -import {UploadFromInterface} from './UploadFrom' import {UploadFromUploaded} from './UploadFromUploaded' - -export enum FileFrom { - Object = 'object', - URL = 'url', - DOM = 'input', - Uploaded = 'uploaded', -} +import {FileUploadInterface, FileFrom} from './types' /** * Uploads file from provided data @@ -20,9 +13,9 @@ export enum FileFrom { * @param {FileData} data * @param {Settings} settings * @throws Error - * @returns {UploadFromInterface} + * @returns {FileUploadInterface} */ -export default function fileFrom(from: FileFrom, data: FileData | Url | Uuid, settings: Settings = {}): UploadFromInterface { +export default function fileFrom(from: FileFrom, data: FileData | Url | Uuid, settings: Settings = {}): FileUploadInterface { switch (from) { case FileFrom.Object: return new UploadFromObject(data as FileData, settings) diff --git a/src/fileFrom/types.ts b/src/fileFrom/types.ts new file mode 100644 index 000000000..e7c6ef43f --- /dev/null +++ b/src/fileFrom/types.ts @@ -0,0 +1,20 @@ +import {UploadcareFileInterface} from '../types' +import {UploadInterface} from '../lifecycle/types' + +export enum FileFrom { + Object = 'object', + URL = 'url', + DOM = 'input', + Uploaded = 'uploaded', +} + +/** + * Base `thenable` interface for uploading `fileFrom` (`object`, `url`, `input`, `uploaded`). + */ +export interface FileUploadInterface extends UploadInterface {} + +export interface HandlerInterface { + upload(): Promise +} + +export interface FileHandlerInterface extends HandlerInterface {} diff --git a/src/groupFrom/GroupUpload.ts b/src/groupFrom/GroupUpload.ts new file mode 100644 index 000000000..1ed938f2c --- /dev/null +++ b/src/groupFrom/GroupUpload.ts @@ -0,0 +1,33 @@ +import {Thenable} from '../tools/Thenable' +import {UploadcareGroupInterface, UploadingProgress} from '../types' +import {GroupHandlerInterface} from '../groupFrom/types' +import {CancelableInterface} from '../api/types' +import {GroupUploadLifecycleInterface} from '../lifecycle/types' +import {GroupUploadInterface} from './types' + +export class GroupUpload extends Thenable implements GroupUploadInterface { + onProgress: ((progress: UploadingProgress) => void) | null = null + onUploaded: ((uuid: string) => void) | null = null + onReady: ((file: UploadcareGroupInterface) => void) | null = null + onCancel: VoidFunction | null = null + + protected readonly promise: Promise + private readonly cancelable: CancelableInterface + + constructor(lifecycle: GroupUploadLifecycleInterface, handler: GroupHandlerInterface, cancelable: CancelableInterface) { + super() + this.cancelable = cancelable + const uploadLifecycle = lifecycle.getUploadLifecycle() + + uploadLifecycle.onProgress = this.onProgress + uploadLifecycle.onUploaded = this.onUploaded + uploadLifecycle.onReady = this.onReady + uploadLifecycle.onCancel = this.onCancel + + this.promise = handler.upload() + } + + cancel(): void { + this.cancelable.cancel() + } +} diff --git a/src/groupFrom/UploadFrom.ts b/src/groupFrom/UploadFrom.ts new file mode 100644 index 000000000..617776f23 --- /dev/null +++ b/src/groupFrom/UploadFrom.ts @@ -0,0 +1,164 @@ +import {Settings, UploadcareGroupInterface, UploadingProgress, ProgressState, ProgressParams} from '../types' +import {Thenable} from '../tools/Thenable' +import {GroupInfo} from '../api/types' +import {GroupUploadInterface} from './types' +import {UploadcareGroup} from '../UploadcareGroup' + +/** + * Base abstract `thenable` implementation of `GroupUploadInterface`. + * You need to use this as base class for all uploading methods of `fileFrom`. + * All that you need to implement — `promise` property and `cancel` method. + */ +export abstract class UploadFrom extends Thenable implements GroupUploadInterface { + protected abstract readonly promise: Promise + protected isCancelled: boolean = false + abstract cancel(): void + + protected progress: UploadingProgress = { + state: ProgressState.Pending, + uploaded: null, + value: 0, + } + protected group: UploadcareGroupInterface | null = null + + onProgress: ((progress: UploadingProgress) => void) | null = null + onUploaded: ((uuid: string) => void) | null = null + onReady: ((group: UploadcareGroupInterface) => void) | null = null + onCancel: VoidFunction | null = null + + protected constructor() { + super() + this.handleCancelling = this.handleCancelling.bind(this) + } + + protected setProgress(state: ProgressState, progress?: ProgressParams) { + switch (state) { + case ProgressState.Pending: + this.progress = { + state: ProgressState.Pending, + uploaded: null, + value: 0, + } + break + case ProgressState.Uploading: + this.progress = { + state: ProgressState.Uploading, + uploaded: progress || null, + // leave 1 percent for uploaded and 1 for ready on cdn + value: progress ? Math.round((progress.loaded * 98) / progress.total) : 0, + } + break + case ProgressState.Uploaded: + this.progress = { + state: ProgressState.Uploaded, + uploaded: null, + value: 99, + } + break + case ProgressState.Ready: + this.progress = { + state: ProgressState.Ready, + uploaded: null, + value: 100, + } + break + case ProgressState.Canceled: + this.progress = { + state: ProgressState.Canceled, + uploaded: null, + value: 0, + } + break + case ProgressState.Error: + this.progress = { + state: ProgressState.Error, + uploaded: null, + value: 0, + } + break + } + } + + protected getProgress(): UploadingProgress { + return this.progress + } + + protected setGroup(group: UploadcareGroupInterface) { + this.group = group + } + + protected getGroup(): UploadcareGroupInterface { + return this.group as UploadcareGroupInterface + } + + /** + * Handle cancelling of uploading file. + */ + protected handleCancelling(): void { + this.setProgress(ProgressState.Canceled) + + if (typeof this.onCancel === 'function') { + this.onCancel() + } + } + + /** + * Handle file uploading. + * @param {ProgressParams} progress + */ + protected handleUploading(progress?: ProgressParams): void { + this.setProgress(ProgressState.Uploading, progress) + + if (typeof this.onProgress === 'function') { + this.onProgress(this.getProgress()) + } + } + + /** + * Handle uploaded file. + * @param {GroupInfo} groupInfo + * @param {Settings} settings + */ + protected handleUploaded(groupInfo: GroupInfo, settings: Settings): Promise { + this.setGroup(new UploadcareGroup(groupInfo)) + + this.setProgress(ProgressState.Uploaded) + + if (typeof this.onUploaded === 'function') { + this.onUploaded(this.getGroup().uuid) + } + + return Promise.resolve(this.getGroup()) + } + + /** + * Handle uploaded file that ready on CDN. + */ + protected handleReady = (): Promise => { + this.setProgress(ProgressState.Ready) + + if (typeof this.onProgress === 'function') { + this.onProgress(this.getProgress()) + } + + if (typeof this.onReady === 'function') { + this.onReady(this.getGroup()) + } + + return Promise.resolve(this.getGroup()) + } + + /** + * Handle uploading error + * @param error + */ + protected handleError = (error) => { + if (error.name === 'CancelError') { + this.handleCancelling() + } else { + this.setProgress(ProgressState.Error) + } + + return Promise.reject(error) + } +} diff --git a/src/groupFrom/UploadFromObject.ts b/src/groupFrom/UploadFromObject.ts new file mode 100644 index 000000000..9de9bdbbc --- /dev/null +++ b/src/groupFrom/UploadFromObject.ts @@ -0,0 +1,76 @@ +import {FileData, Settings, UploadcareFileInterface, UploadcareGroupInterface} from '../types' +import {UploadFrom} from './UploadFrom' +import group, {GroupInfoResponse} from '../api/group' +import CancelError from '../errors/CancelError' +import fileFrom from '../fileFrom/fileFrom' +import {FileFrom, FileUploadInterface} from '../fileFrom/types' + +export class UploadFromObject extends UploadFrom { + protected readonly promise: Promise + + private readonly data: FileData[] + private readonly settings: Settings + private readonly uploads: FileUploadInterface[] + private readonly files: Promise + + constructor(data: FileData[], settings: Settings) { + super() + + this.data = data + this.settings = settings + + this.uploads = this.getUploadsPromises() + this.files = this.getFilesPromise() + this.promise = this.getGroupPromise() + } + + private getUploadsPromises = (): FileUploadInterface[] => { + const filesTotalCount = this.data.length + + return this.data.map((file: FileData, index: number) => { + const fileUpload = fileFrom(FileFrom.Object, file, this.settings) + const fileNumber = index + 1 + + fileUpload.onCancel = this.handleCancelling + + fileUpload.onUploaded = (() => { + this.handleUploading({ + total: filesTotalCount, + loaded: fileNumber + }) + }) + + return fileUpload + }) + } + + private getFilesPromise = (): Promise => { + return Promise.all(this.uploads) + } + + private getGroupPromise = (): Promise => { + this.handleUploading() + + return this.getFilesPromise() + .then(files => { + const uuids = files.map(file => file.uuid) + + return group(uuids, this.settings) + }) + .then(this.handleInfoResponse) + .then(this.handleReady) + .catch(this.handleError) + } + + private handleInfoResponse = (groupInfo: GroupInfoResponse) => { + if (this.isCancelled) { + return Promise.reject(new CancelError()) + } + + return this.handleUploaded(groupInfo, this.settings) + } + + cancel(): void { + this.isCancelled = true + } +} diff --git a/src/groupFrom/UploadFromUploaded.ts b/src/groupFrom/UploadFromUploaded.ts new file mode 100644 index 000000000..9b1bdbb08 --- /dev/null +++ b/src/groupFrom/UploadFromUploaded.ts @@ -0,0 +1,45 @@ +import {Settings, UploadcareGroupInterface} from '../types' +import {UploadFrom} from './UploadFrom' +import {Uuid} from '..' +import CancelError from '../errors/CancelError' +import group, {GroupInfoResponse} from '../api/group' + +export class UploadFromUploaded extends UploadFrom { + protected readonly promise: Promise + + private readonly data: Uuid[] + private readonly settings: Settings + + constructor(data: Uuid[], settings: Settings) { + super() + + this.data = data + this.settings = { + ...settings, + source: 'uploaded', + } + + this.promise = this.getGroupPromise() + } + + private getGroupPromise = (): Promise => { + this.handleUploading() + + return group(this.data, this.settings) + .then(this.handleInfoResponse) + .then(this.handleReady) + .catch(this.handleError) + } + + private handleInfoResponse = (groupInfo: GroupInfoResponse) => { + if (this.isCancelled) { + return Promise.reject(new CancelError()) + } + + return this.handleUploaded(groupInfo, this.settings) + } + + cancel(): void { + this.isCancelled = true + } +} diff --git a/src/groupFrom/UploadFromUrl.ts b/src/groupFrom/UploadFromUrl.ts new file mode 100644 index 000000000..46d6bc8bd --- /dev/null +++ b/src/groupFrom/UploadFromUrl.ts @@ -0,0 +1,77 @@ +import {Settings, UploadcareFileInterface, UploadcareGroupInterface} from '../types' +import {UploadFrom} from './UploadFrom' +import group, {GroupInfoResponse} from '../api/group' +import CancelError from '../errors/CancelError' +import fileFrom from '../fileFrom/fileFrom' +import {FileFrom, FileUploadInterface} from '../fileFrom/types' +import {Url} from '..' + +export class UploadFromUrl extends UploadFrom { + protected readonly promise: Promise + + private readonly data: Url[] + private readonly settings: Settings + private readonly uploads: FileUploadInterface[] + private readonly files: Promise + + constructor(data: Url[], settings: Settings) { + super() + + this.data = data + this.settings = settings + + this.uploads = this.getUploadsPromises() + this.files = this.getFilesPromise() + this.promise = this.getGroupPromise() + } + + private getUploadsPromises = (): FileUploadInterface[] => { + const filesTotalCount = this.data.length + + return this.data.map((sourceUrl: Url, index: number) => { + const fileUpload = fileFrom(FileFrom.URL, sourceUrl, this.settings) + const fileNumber = index + 1 + + fileUpload.onCancel = this.handleCancelling + + fileUpload.onUploaded = (() => { + this.handleUploading({ + total: filesTotalCount, + loaded: fileNumber + }) + }) + + return fileUpload + }) + } + + private getFilesPromise = (): Promise => { + return Promise.all(this.uploads) + } + + private getGroupPromise = (): Promise => { + this.handleUploading() + + return this.getFilesPromise() + .then(files => { + const uuids = files.map(file => file.uuid) + + return group(uuids, this.settings) + }) + .then(this.handleInfoResponse) + .then(this.handleReady) + .catch(this.handleError) + } + + private handleInfoResponse = (groupInfo: GroupInfoResponse) => { + if (this.isCancelled) { + return Promise.reject(new CancelError()) + } + + return this.handleUploaded(groupInfo, this.settings) + } + + cancel(): void { + this.isCancelled = true + } +} diff --git a/src/groupFrom/groupFrom.ts b/src/groupFrom/groupFrom.ts new file mode 100644 index 000000000..4ac9aab53 --- /dev/null +++ b/src/groupFrom/groupFrom.ts @@ -0,0 +1,29 @@ +import {FileData, Settings} from '../types' +import {Url} from '../api/fromUrl' +import {Uuid} from '../api/types' +import {UploadFromObject} from './UploadFromObject' +import {UploadFromUrl} from './UploadFromUrl' +import {UploadFromUploaded} from './UploadFromUploaded' +import {GroupFrom, GroupUploadInterface} from './types' + +/** + * Uploads file from provided data + * + * @param {FileFrom} from + * @param {FileData} data + * @param {Settings} settings + * @throws Error + * @returns {FileUploadInterface} + */ +export default function groupFrom(from: GroupFrom, data: FileData[] | Url[] | Uuid[], settings: Settings = {}): GroupUploadInterface { + switch (from) { + case GroupFrom.Object: + return new UploadFromObject(data as FileData[], settings) + case GroupFrom.URL: + return new UploadFromUrl(data as Url[], settings) + case GroupFrom.Uploaded: + return new UploadFromUploaded(data as Uuid[], settings) + default: + throw new Error(`Group uploading from "${from}" is not supported`) + } +} diff --git a/src/groupFrom/types.ts b/src/groupFrom/types.ts new file mode 100644 index 000000000..eb174bd58 --- /dev/null +++ b/src/groupFrom/types.ts @@ -0,0 +1,17 @@ +import {UploadcareGroupInterface} from '../types' +import {UploadInterface} from '../lifecycle/types' +import {HandlerInterface} from '../fileFrom/types' + +export enum GroupFrom { + Object = 'object', + URL = 'url', + DOM = 'input', + Uploaded = 'uploaded', +} + +/** + * Base `thenable` interface for uploading `groupFrom` (`object`, `url`, `input`, `uploaded`). + */ +export interface GroupUploadInterface extends UploadInterface {} + +export interface GroupHandlerInterface extends HandlerInterface {} diff --git a/src/index.ts b/src/index.ts index 4749fdf7a..12ec453cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ import UploadClient from './UploadClient' -export {Settings, FileData, OriginalImageInfo, UploadcareFile} from './types' +export {Settings, FileData, OriginalImageInfo, UploadcareFileInterface} from './types' export {Url} from './api/fromUrl' export {Uuid} from './api/types' -export {UploadFromInterface} from './fileFrom/UploadFrom' +export {FileUploadInterface} from './fileFrom/types' export {UploadClientInterface} from './UploadClient' export {UploadAPIInterface} from './api/UploadAPI' export {DirectUploadInterface} from './api/base' export {RequestOptions, RequestInterface} from './api/request' -export {FileFrom} from './fileFrom/fileFrom' +export {FileFrom} from './fileFrom/types' export default UploadClient diff --git a/src/lifecycle/FileUploadLifecycle.ts b/src/lifecycle/FileUploadLifecycle.ts new file mode 100644 index 000000000..9dca5ac6e --- /dev/null +++ b/src/lifecycle/FileUploadLifecycle.ts @@ -0,0 +1,55 @@ +import {FileUploadLifecycleInterface, LifecycleInterface} from './types' +import {Settings, UploadcareFileInterface} from '../types' +import {Uuid} from '..' +import checkFileIsReady from '../checkFileIsReady' +import prettyFileInfo from '../prettyFileInfo' +import {UploadedState} from './state/UploadedState' +import {PollPromiseInterface} from '../tools/poll' +import {InfoResponse} from '../api/info' + +export class FileUploadLifecycle implements FileUploadLifecycleInterface { + private readonly lifecycle: LifecycleInterface + private isFileReadyPolling: PollPromiseInterface | null = null + + constructor(lifecycle: LifecycleInterface) { + this.lifecycle = lifecycle + } + + handleUploadedFile(uuid: Uuid, settings: Settings): Promise { + this.lifecycle.updateEntity({ + uuid, + name: null, + size: null, + isStored: null, + isImage: null, + cdnUrl: null, + cdnUrlModifiers: null, + originalUrl: null, + originalFilename: null, + originalImageInfo: null, + }) + + this.lifecycle.updateState(new UploadedState()) + + if (typeof this.lifecycle.onUploaded === 'function') { + this.lifecycle.onUploaded(uuid) + } + + this.isFileReadyPolling = checkFileIsReady({ + uuid, + settings, + }) + + return this.isFileReadyPolling + .then(info => { + this.lifecycle.updateEntity(prettyFileInfo(info, settings)) + + return Promise.resolve(this.lifecycle.getEntity()) + }) + .catch(error => Promise.reject(error)) + } + + getUploadLifecycle(): LifecycleInterface { + return this.lifecycle + } +} diff --git a/src/lifecycle/GroupUploadLifecycle.ts b/src/lifecycle/GroupUploadLifecycle.ts new file mode 100644 index 000000000..607ecf324 --- /dev/null +++ b/src/lifecycle/GroupUploadLifecycle.ts @@ -0,0 +1,42 @@ +import {GroupUploadLifecycleInterface, LifecycleInterface} from './types' +import {GroupInfo} from '../api/types' +import {Settings, UploadcareGroupInterface} from '../types' +import {UploadedState} from './state/UploadedState' + +export class GroupUploadLifecycle implements GroupUploadLifecycleInterface { + private readonly lifecycle: LifecycleInterface + + constructor(lifecycle: LifecycleInterface) { + this.lifecycle = lifecycle + } + + handleUploadedGroup(groupInfo: GroupInfo, settings: Settings): Promise { + const totalSize = groupInfo.files.reduce((acc, file) => acc + file.size, 0) + const isStored = !!groupInfo.datetime_stored + const isImage = !!groupInfo.files.filter(file => file.is_image).length + + this.lifecycle.updateEntity({ + uuid: groupInfo.id, + filesCount: groupInfo.files_count, + totalSize, + isStored, + isImage, + cdnUrl: groupInfo.cdn_url, + files: groupInfo.files, + createdAt: groupInfo.datetime_created, + storedAt: groupInfo.datetime_stored, + }) + + this.lifecycle.updateState(new UploadedState()) + + if (typeof this.lifecycle.onUploaded === 'function') { + this.lifecycle.onUploaded(groupInfo.id) + } + + return Promise.resolve(this.lifecycle.getEntity()) + } + + getUploadLifecycle(): LifecycleInterface { + return this.lifecycle + } +} diff --git a/src/lifecycle/UploadLifecycle.ts b/src/lifecycle/UploadLifecycle.ts new file mode 100644 index 000000000..8fc6bb262 --- /dev/null +++ b/src/lifecycle/UploadLifecycle.ts @@ -0,0 +1,99 @@ +import {ProgressParams, UploadingProgress} from '../types' +import { + LifecycleInterface, + LifecycleStateInterface +} from './types' +import {UploadingState} from './state/UploadingState' +import {CancelledState} from './state/CancelledState' +import {ReadyState} from './state/ReadyState' +import {ErrorState} from './state/ErrorState' +import {PendingState} from './state/PendingState' + +export class UploadLifecycle implements LifecycleInterface { + protected state: LifecycleStateInterface + protected entity: T | null = null + + onProgress: ((progress: UploadingProgress) => void) | null = null + onUploaded: ((uuid: string) => void) | null = null + onReady: ((entity: T) => void) | null = null + onCancel: VoidFunction | null = null + + constructor() { + this.state = new PendingState() + this.updateState = this.updateState.bind(this) + this.getProgress = this.getProgress.bind(this) + this.updateEntity = this.updateEntity.bind(this) + this.getEntity = this.getEntity.bind(this) + this.handleUploading = this.handleUploading.bind(this) + this.handleCancelling = this.handleCancelling.bind(this) + this.handleReady = this.handleReady.bind(this) + this.handleError = this.handleError.bind(this) + } + + updateState(state: LifecycleStateInterface): void { + if (this.state.isCanBeChangedTo(state)) { + this.state = state + } else { + // TODO: Make a new Exception + throw new Error(`${this.state.progress.state} can't be changed to ${state.progress.state}`) + } + } + + getProgress(): UploadingProgress { + return this.state.progress + } + + updateEntity(entity: T): void { + this.entity = entity + } + + getEntity(): T { + if (this.entity === null) { + throw new Error('Entity is not ready yet.') + } + + return this.entity + } + + handleUploading(progress?: ProgressParams): void { + if (progress) { + this.updateState(new UploadingState(progress)) + } + + if (typeof this.onProgress === 'function') { + this.onProgress(this.getProgress()) + } + } + + handleCancelling(): void { + this.updateState(new CancelledState()) + + if (typeof this.onCancel === 'function') { + this.onCancel() + } + } + + handleReady(): Promise { + this.updateState(new ReadyState()) + + if (typeof this.onProgress === 'function') { + this.onProgress(this.getProgress()) + } + + if (typeof this.onReady === 'function') { + this.onReady(this.getEntity()) + } + + return Promise.resolve(this.getEntity()) + } + + handleError(error: Error) { + if (error.name === 'CancelError') { + this.handleCancelling() + } else { + this.updateState(new ErrorState()) + } + + return Promise.reject(error) + } +} diff --git a/src/lifecycle/state/AbstractState.ts b/src/lifecycle/state/AbstractState.ts new file mode 100644 index 000000000..19fae6439 --- /dev/null +++ b/src/lifecycle/state/AbstractState.ts @@ -0,0 +1,11 @@ +import {LifecycleStateInterface} from '../types' +import {ProgressState, UploadingProgress} from '../../types' + +export abstract class AbstractState implements LifecycleStateInterface { + abstract readonly progress: UploadingProgress + protected abstract nextPossibleState: ProgressState[] = [] + + isCanBeChangedTo(state: LifecycleStateInterface): boolean { + return this.nextPossibleState.includes(state.progress.state) + } +} diff --git a/src/lifecycle/state/CancelledState.ts b/src/lifecycle/state/CancelledState.ts new file mode 100644 index 000000000..69bc5ffa7 --- /dev/null +++ b/src/lifecycle/state/CancelledState.ts @@ -0,0 +1,12 @@ +import {AbstractState} from './AbstractState' +import {LifecycleStateInterface} from '../types' +import {ProgressState, UploadingProgress} from '../../types' + +export class CancelledState extends AbstractState implements LifecycleStateInterface { + readonly progress: UploadingProgress = { + state: ProgressState.Canceled, + uploaded: null, + value: 0, + } + protected nextPossibleState: ProgressState[] = [] +} diff --git a/src/lifecycle/state/ErrorState.ts b/src/lifecycle/state/ErrorState.ts new file mode 100644 index 000000000..94896c840 --- /dev/null +++ b/src/lifecycle/state/ErrorState.ts @@ -0,0 +1,12 @@ +import {AbstractState} from './AbstractState' +import {LifecycleStateInterface} from '../types' +import {ProgressState, UploadingProgress} from '../../types' + +export class ErrorState extends AbstractState implements LifecycleStateInterface { + readonly progress: UploadingProgress = { + state: ProgressState.Error, + uploaded: null, + value: 0, + } + protected nextPossibleState: ProgressState[] = [] +} diff --git a/src/lifecycle/state/PendingState.ts b/src/lifecycle/state/PendingState.ts new file mode 100644 index 000000000..de1c10b18 --- /dev/null +++ b/src/lifecycle/state/PendingState.ts @@ -0,0 +1,18 @@ +import {AbstractState} from './AbstractState' +import {LifecycleStateInterface} from '../types' +import {ProgressState, UploadingProgress} from '../../types' + +export class PendingState extends AbstractState implements LifecycleStateInterface { + readonly progress: UploadingProgress = { + state: ProgressState.Pending, + uploaded: null, + value: 0, + } + protected nextPossibleState: ProgressState[] = [ + ProgressState.Uploading, + ProgressState.Uploaded, + ProgressState.Ready, + ProgressState.Canceled, + ProgressState.Error, + ] +} diff --git a/src/lifecycle/state/ReadyState.ts b/src/lifecycle/state/ReadyState.ts new file mode 100644 index 000000000..8e1cd4837 --- /dev/null +++ b/src/lifecycle/state/ReadyState.ts @@ -0,0 +1,15 @@ +import {AbstractState} from './AbstractState' +import {LifecycleStateInterface} from '../types' +import {ProgressState, UploadingProgress} from '../../types' + +export class ReadyState extends AbstractState implements LifecycleStateInterface { + readonly progress: UploadingProgress = { + state: ProgressState.Ready, + uploaded: null, + value: 100, + } + protected nextPossibleState: ProgressState[] = [ + ProgressState.Canceled, + ProgressState.Error, + ] +} diff --git a/src/lifecycle/state/UploadedState.ts b/src/lifecycle/state/UploadedState.ts new file mode 100644 index 000000000..c05051c29 --- /dev/null +++ b/src/lifecycle/state/UploadedState.ts @@ -0,0 +1,16 @@ +import {AbstractState} from './AbstractState' +import {LifecycleStateInterface} from '../types' +import {ProgressState, UploadingProgress} from '../../types' + +export class UploadedState extends AbstractState implements LifecycleStateInterface { + readonly progress: UploadingProgress = { + state: ProgressState.Uploaded, + uploaded: null, + value: 99, + } + protected nextPossibleState: ProgressState[] = [ + ProgressState.Ready, + ProgressState.Canceled, + ProgressState.Error, + ] +} diff --git a/src/lifecycle/state/UploadingState.ts b/src/lifecycle/state/UploadingState.ts new file mode 100644 index 000000000..3a90c6061 --- /dev/null +++ b/src/lifecycle/state/UploadingState.ts @@ -0,0 +1,23 @@ +import {AbstractState} from './AbstractState' +import {LifecycleStateInterface} from '../types' +import {ProgressParams, ProgressState, UploadingProgress} from '../../types' + +export class UploadingState extends AbstractState implements LifecycleStateInterface { + readonly progress: UploadingProgress + protected nextPossibleState: ProgressState[] = [ + ProgressState.Uploaded, + ProgressState.Ready, + ProgressState.Canceled, + ProgressState.Error, + ] + + constructor(params: ProgressParams) { + super() + this.progress = { + state: ProgressState.Uploading, + uploaded: params, + // leave 1 percent for uploaded and 1 for ready on cdn + value: params ? Math.round((params.loaded * 98) / params.total) : 0, + } + } +} diff --git a/src/lifecycle/types.ts b/src/lifecycle/types.ts new file mode 100644 index 000000000..1168f9a61 --- /dev/null +++ b/src/lifecycle/types.ts @@ -0,0 +1,42 @@ +import {ProgressParams, Settings, UploadcareFileInterface, UploadcareGroupInterface, UploadingProgress} from '../types' +import {Uuid} from '..' +import {CancelableInterface, GroupInfo} from '../api/types' + +export interface HooksInterface { + onProgress: ((progress: UploadingProgress) => void) | null + onUploaded: ((uuid: string) => void) | null + onReady: ((entity: T) => void) | null + onCancel: VoidFunction | null +} + +export interface UploadInterface extends + HooksInterface, + Promise, + CancelableInterface { +} + +export interface LifecycleStateInterface { + readonly progress: UploadingProgress + isCanBeChangedTo(state: LifecycleStateInterface): boolean +} + +export interface LifecycleInterface extends HooksInterface { + updateState(state: LifecycleStateInterface): void + getProgress(): UploadingProgress + updateEntity(entity: T): void + getEntity(): T + handleUploading(progress?: ProgressParams): void + handleCancelling(): void + handleReady(): Promise + handleError(error: Error) +} + +export interface FileUploadLifecycleInterface { + getUploadLifecycle(): LifecycleInterface + handleUploadedFile(uuid: Uuid, settings: Settings): Promise +} + +export interface GroupUploadLifecycleInterface { + getUploadLifecycle(): LifecycleInterface + handleUploadedGroup(groupInfo: GroupInfo, settings: Settings): Promise +} diff --git a/src/prettyFileInfo.ts b/src/prettyFileInfo.ts index 199f16923..97b402b0f 100644 --- a/src/prettyFileInfo.ts +++ b/src/prettyFileInfo.ts @@ -1,16 +1,16 @@ import defaultSettings from './defaultSettings' import camelizeKeys from './tools/camelizeKeys' import {InfoResponse} from './api/info' -import {Settings, UploadcareFile} from './types' +import {Settings, UploadcareFileInterface} from './types' /** * Transforms file info getting from Upload API to pretty info * * @param {InfoResponse} info * @param {Settings} settings - * @returns {UploadcareFile} + * @returns {UploadcareFileInterface} */ -export default function prettyFileInfo(info: InfoResponse, settings: Settings = {}): UploadcareFile { +export default function prettyFileInfo(info: InfoResponse, settings: Settings = {}): UploadcareFileInterface { const { uuid, filename, diff --git a/src/types.ts b/src/types.ts index b4c441aab..8522468d6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import {FileInfo, GroupId} from './api/types' + export type Settings = { baseCDN?: string, baseURL?: string, @@ -31,15 +33,49 @@ export type OriginalImageInfo = { } /* TODO Add sourceInfo */ -export type UploadcareFile = { - uuid: string, - name: null | string, - size: null | number, - isStored: null | boolean, - isImage: null | boolean, - cdnUrl: null | string, - cdnUrlModifiers: null | string, - originalUrl: null | string, - originalFilename: null | string, - originalImageInfo: null | OriginalImageInfo, +export interface UploadcareFileInterface { + readonly uuid: string, + readonly name: null | string, + readonly size: null | number, + readonly isStored: null | boolean, + readonly isImage: null | boolean, + readonly cdnUrl: null | string, + readonly cdnUrlModifiers: null | string, + readonly originalUrl: null | string, + readonly originalFilename: null | string, + readonly originalImageInfo: null | OriginalImageInfo, +} + +export type UploadcareFiles = UploadcareFileInterface[] + +export interface UploadcareGroupInterface { + readonly uuid: GroupId, + readonly filesCount: string, + readonly totalSize: number, + readonly isStored: boolean, + readonly isImage: boolean, + readonly cdnUrl: string, + readonly files: FileInfo[], + readonly createdAt: string, + readonly storedAt: string | null, +} + +export enum ProgressState { + Pending = 'pending', + Uploading = 'uploading', + Uploaded = 'uploaded', + Ready = 'ready', + Canceled = 'canceled', + Error = 'error', +} + +export type ProgressParams = { + total: number, + loaded: number, +} + +export type UploadingProgress = { + state: ProgressState, + uploaded: null | ProgressParams, + value: number, } diff --git a/test/_fixtureFactory.ts b/test/_fixtureFactory.ts index 2727d96a9..8ec92313d 100644 --- a/test/_fixtureFactory.ts +++ b/test/_fixtureFactory.ts @@ -106,10 +106,6 @@ export function publicKey(id: string): string { return publicKey } -export function linkTo(uuid: string): string { - return `${settings.baseCDN}/${uuid}/` -} - export function imageUrl(id: string): string { const images = { valid: `${settings.baseCDN}/d3275f8b-686d-4980-916a-53a1fc17450b/1findfacecropgrayscale.jpg`, diff --git a/test/checkFileIsUploadedFromUrl.spec.ts b/test/checkFileIsUploadedFromUrl.spec.ts index 5c83ddf7a..0fc29f4b0 100644 --- a/test/checkFileIsUploadedFromUrl.spec.ts +++ b/test/checkFileIsUploadedFromUrl.spec.ts @@ -2,23 +2,31 @@ import * as factory from './_fixtureFactory' import checkFileIsUploadedFromUrl from '../src/checkFileIsUploadedFromUrl' import {StatusEnum} from '../src/api/fromUrlStatus' import {getSettingsForTesting} from './_helpers' +import fromUrl from '../src/api/fromUrl' describe('checkFileIsUploadedFromUrl', () => { + const sourceUrl = factory.imageUrl('valid') const settings = getSettingsForTesting({ - publicKey: factory.publicKey('token'), + publicKey: factory.publicKey('demo') }) it('should be resolved if file is uploaded', async() => { + const data = await fromUrl(sourceUrl, settings) + // @ts-ignore + const {token} = data const info = await checkFileIsUploadedFromUrl({ - token: factory.token('valid'), + token, settings, }) expect(info.status).toBe(StatusEnum.Success) }) - it('should be cancelable', (done) => { + it('should be cancelable', async(done) => { + const data = await fromUrl(sourceUrl, settings) + // @ts-ignore + const {token} = data const polling = checkFileIsUploadedFromUrl({ - token: factory.token('valid'), + token, settings, }) diff --git a/test/fileFrom/fileFromObject.spec.ts b/test/fileFrom/fileFromObject.spec.ts index f78544dcf..664322162 100644 --- a/test/fileFrom/fileFromObject.spec.ts +++ b/test/fileFrom/fileFromObject.spec.ts @@ -1,6 +1,7 @@ import * as factory from '../_fixtureFactory' -import fileFrom, {FileFrom} from '../../src/fileFrom/fileFrom' -import {getSettingsForTesting, sleep} from '../_helpers' +import fileFrom from '../../src/fileFrom/fileFrom' +import {FileFrom} from '../../src/fileFrom/types' +import {getSettingsForTesting} from '../_helpers' describe('fileFrom', () => { describe('Object', () => { diff --git a/test/fileFrom/fileFromUploaded.spec.ts b/test/fileFrom/fileFromUploaded.spec.ts index de29f4dab..3f6cca2a8 100644 --- a/test/fileFrom/fileFromUploaded.spec.ts +++ b/test/fileFrom/fileFromUploaded.spec.ts @@ -1,6 +1,7 @@ import * as factory from '../_fixtureFactory' -import fileFrom, {FileFrom} from '../../src/fileFrom/fileFrom' -import {getSettingsForTesting, sleep} from '../_helpers' +import fileFrom from '../../src/fileFrom/fileFrom' +import {FileFrom} from '../../src/fileFrom/types' +import {getSettingsForTesting} from '../_helpers' describe('fileFrom', () => { describe('Uploaded', () => { diff --git a/test/fileFrom/fileFromUrl.spec.ts b/test/fileFrom/fileFromUrl.spec.ts index dc0e4ecb6..c640434bf 100644 --- a/test/fileFrom/fileFromUrl.spec.ts +++ b/test/fileFrom/fileFromUrl.spec.ts @@ -1,6 +1,7 @@ import * as factory from '../_fixtureFactory' -import fileFrom, {FileFrom} from '../../src/fileFrom/fileFrom' -import {getSettingsForTesting, sleep} from '../_helpers' +import fileFrom from '../../src/fileFrom/fileFrom' +import {FileFrom} from '../../src/fileFrom/types' +import {getSettingsForTesting} from '../_helpers' describe('fileFrom', () => { describe('URL', () => { diff --git a/test/groupFrom/groupFromObject.spec.ts b/test/groupFrom/groupFromObject.spec.ts new file mode 100644 index 000000000..af4055ae1 --- /dev/null +++ b/test/groupFrom/groupFromObject.spec.ts @@ -0,0 +1,124 @@ +import * as factory from '../_fixtureFactory' +import groupFrom from '../../src/groupFrom/groupFrom' +import {GroupFrom} from '../../src/groupFrom/types' +import {getSettingsForTesting} from '../_helpers' + +describe('groupFrom', () => { + describe('Object[]', () => { + const fileToUpload = factory.image('blackSquare') + + it('should resolves when file is ready on CDN', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Object, [fileToUpload.data], settings) + + groupPromise + .then(group => { + expect(group.cdnUrl).toBeTruthy() + done() + }) + }) + + it('should accept doNotStore setting', async() => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + doNotStore: true, + }) + const groupPromise = groupFrom(GroupFrom.Object, [fileToUpload.data], settings) + const group = await groupPromise + + expect(group.isStored).toBeFalsy() + }) + + it('should be able to cancel uploading', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Object, [fileToUpload.data], settings) + + setTimeout(() => { + groupPromise.cancel() + }, 1) + + groupPromise + .then(() => done.fail('Promise should not to be resolved')) + .catch((error) => error.name === 'CancelError' ? done() : done.fail(error)) + }) + + describe('should be able to handle', () => { + it('cancel uploading', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Object, [fileToUpload.data], settings) + + setTimeout(() => { + groupPromise.cancel() + }, 1) + + groupPromise.onCancel = () => { + done() + } + + groupPromise + .then(() => done.fail('Promise should not to be resolved')) + .catch((error) => { + if (error.name !== 'CancelError') { + done.fail(error) + } + }) + }) + + it('progress', (done) => { + let progressValue = 0 + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Object, [fileToUpload.data], settings) + + groupPromise.onProgress = (progress) => { + const {value} = progress + + progressValue = value + } + + groupPromise + .then(() => + progressValue > 0 + ? done() + : done.fail() + ) + .catch(error => done.fail(error)) + }) + + it('uploaded', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Object, [fileToUpload.data], settings) + + groupPromise.onUploaded = () => { + done() + } + + groupPromise + .catch(error => done.fail(error)) + }) + + it('ready', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Object, [fileToUpload.data], settings) + + groupPromise.onReady = () => { + done() + } + + groupPromise + .catch(error => done.fail(error)) + }) + }) + }) +}) diff --git a/test/groupFrom/groupFromUploaded.spec.ts b/test/groupFrom/groupFromUploaded.spec.ts new file mode 100644 index 000000000..3598bc27b --- /dev/null +++ b/test/groupFrom/groupFromUploaded.spec.ts @@ -0,0 +1,124 @@ +import * as factory from '../_fixtureFactory' +import {getSettingsForTesting} from '../_helpers' +import groupFrom from '../../src/groupFrom/groupFrom' +import {GroupFrom} from '../../src/groupFrom/types' + +describe('groupFrom', () => { + describe('Uploaded[]', () => { + const uuid = factory.uuid('image') + + it('should resolves when file is ready on CDN', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Uploaded, [uuid], settings) + + groupPromise + .then(group => { + expect(group.cdnUrl).toBeTruthy() + done() + }) + }) + + it('should accept doNotStore setting', async() => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + doNotStore: true, + }) + const groupPromise = groupFrom(GroupFrom.Uploaded, [uuid], settings) + const group = await groupPromise + + expect(group.isStored).toBeFalsy() + }) + + it('should be able to cancel uploading', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Uploaded, [uuid], settings) + + setTimeout(() => { + groupPromise.cancel() + }, 1) + + groupPromise + .then(() => done.fail('Promise should not to be resolved')) + .catch((error) => error.name === 'CancelError' ? done() : done.fail(error)) + }) + + describe('should be able to handle', () => { + it('cancel uploading', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Uploaded, [uuid], settings) + + setTimeout(() => { + groupPromise.cancel() + }, 1) + + groupPromise.onCancel = () => { + done() + } + + groupPromise + .then(() => done.fail('Promise should not to be resolved')) + .catch((error) => { + if (error.name !== 'CancelError') { + done.fail(error) + } + }) + }) + + it('progress', (done) => { + let progressValue = 0 + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Uploaded, [uuid], settings) + + groupPromise.onProgress = (progress) => { + const {value} = progress + + progressValue = value + } + + groupPromise + .then(() => + progressValue > 0 + ? done() + : done.fail() + ) + .catch(error => done.fail(error)) + }) + + it('uploaded', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Uploaded, [uuid], settings) + + groupPromise.onUploaded = () => { + done() + } + + groupPromise + .catch(error => done.fail(error)) + }) + + it('ready', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.Uploaded, [uuid], settings) + + groupPromise.onReady = () => { + done() + } + + groupPromise + .catch(error => done.fail(error)) + }) + }) + }) +}) diff --git a/test/groupFrom/groupFromUrl.spec.ts b/test/groupFrom/groupFromUrl.spec.ts new file mode 100644 index 000000000..3824b8f11 --- /dev/null +++ b/test/groupFrom/groupFromUrl.spec.ts @@ -0,0 +1,124 @@ +import * as factory from '../_fixtureFactory' +import {getSettingsForTesting} from '../_helpers' +import groupFrom from '../../src/groupFrom/groupFrom' +import {GroupFrom} from '../../src/groupFrom/types' + +describe('groupFrom', () => { + describe('Url[]', () => { + const sourceUrl = factory.imageUrl('valid') + + it('should resolves when file is ready on CDN', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.URL, [sourceUrl, sourceUrl], settings) + + groupPromise + .then(group => { + expect(group.cdnUrl).toBeTruthy() + done() + }) + }) + + it('should accept doNotStore setting', async() => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + doNotStore: true, + }) + const groupPromise = groupFrom(GroupFrom.URL, [sourceUrl, sourceUrl], settings) + const group = await groupPromise + + expect(group.isStored).toBeFalsy() + }) + + it('should be able to cancel uploading', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.URL, [sourceUrl, sourceUrl], settings) + + setTimeout(() => { + groupPromise.cancel() + }, 1) + + groupPromise + .then(() => done.fail('Promise should not to be resolved')) + .catch((error) => error.name === 'CancelError' ? done() : done.fail(error)) + }) + + describe('should be able to handle', () => { + it('cancel uploading', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.URL, [sourceUrl, sourceUrl], settings) + + setTimeout(() => { + groupPromise.cancel() + }, 1) + + groupPromise.onCancel = () => { + done() + } + + groupPromise + .then(() => done.fail('Promise should not to be resolved')) + .catch((error) => { + if (error.name !== 'CancelError') { + done.fail(error) + } + }) + }) + + it('progress', (done) => { + let progressValue = 0 + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.URL, [sourceUrl, sourceUrl], settings) + + groupPromise.onProgress = (progress) => { + const {value} = progress + + progressValue = value + } + + groupPromise + .then(() => + progressValue > 0 + ? done() + : done.fail() + ) + .catch(error => done.fail(error)) + }) + + it('uploaded', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.URL, [sourceUrl, sourceUrl], settings) + + groupPromise.onUploaded = () => { + done() + } + + groupPromise + .catch(error => done.fail(error)) + }) + + it('ready', (done) => { + const settings = getSettingsForTesting({ + publicKey: factory.publicKey('image'), + }) + const groupPromise = groupFrom(GroupFrom.URL, [sourceUrl, sourceUrl], settings) + + groupPromise.onReady = () => { + done() + } + + groupPromise + .catch(error => done.fail(error)) + }) + }) + }) +})