├── README.md └── TestJS ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── bundle-legacy.mjs ├── bundle.mjs ├── index.mjs ├── package.json ├── packages └── @TestJS │ ├── angular │ ├── .editorconfig │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ └── tasks.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── angular.json │ ├── package.json │ ├── projects │ │ └── uppy │ │ │ └── angular │ │ │ ├── .eslintrc.json │ │ │ ├── ng-package.json │ │ │ ├── package.json │ │ │ ├── src │ │ │ ├── lib │ │ │ │ ├── components │ │ │ │ │ ├── dashboard-modal │ │ │ │ │ │ ├── dashboard-modal-demo.component.ts │ │ │ │ │ │ ├── dashboard-modal.component.spec.ts │ │ │ │ │ │ ├── dashboard-modal.component.ts │ │ │ │ │ │ ├── dashboard-modal.module.ts │ │ │ │ │ │ └── dashboard-modal.stories.ts │ │ │ │ │ ├── dashboard │ │ │ │ │ │ ├── dashboard-demo.component.ts │ │ │ │ │ │ ├── dashboard.component.spec.ts │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ ├── dashboard.module.ts │ │ │ │ │ │ └── dashboard.stories.ts │ │ │ │ │ ├── drag-drop │ │ │ │ │ │ ├── drag-drop-demo.component.ts │ │ │ │ │ │ ├── drag-drop.component.spec.ts │ │ │ │ │ │ ├── drag-drop.component.ts │ │ │ │ │ │ ├── drag-drop.module.ts │ │ │ │ │ │ └── drag-drop.stories.ts │ │ │ │ │ ├── progress-bar │ │ │ │ │ │ ├── progress-bar-demo.component.ts │ │ │ │ │ │ ├── progress-bar.component.spec.ts │ │ │ │ │ │ ├── progress-bar.component.ts │ │ │ │ │ │ ├── progress-bar.module.ts │ │ │ │ │ │ └── progress-bar.stories.ts │ │ │ │ │ └── status-bar │ │ │ │ │ │ ├── status-bar-demo.component.ts │ │ │ │ │ │ ├── status-bar.component.spec.ts │ │ │ │ │ │ ├── status-bar.component.ts │ │ │ │ │ │ ├── status-bar.module.ts │ │ │ │ │ │ └── status-bar.stories.ts │ │ │ │ └── utils │ │ │ │ │ └── wrapper.ts │ │ │ └── public-api.ts │ │ │ ├── tsconfig.lib.json │ │ │ ├── tsconfig.lib.prod.json │ │ │ └── tsconfig.spec.json │ └── tsconfig.json │ ├── audio │ ├── CHANGELOG.md │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── Audio.jsx │ │ ├── AudioSourceSelect.jsx │ │ ├── DiscardButton.jsx │ │ ├── PermissionsScreen.jsx │ │ ├── RecordButton.jsx │ │ ├── RecordingLength.jsx │ │ ├── RecordingScreen.jsx │ │ ├── SubmitButton.jsx │ │ ├── audio-oscilloscope │ │ │ ├── LICENCE │ │ │ └── index.js │ │ ├── formatSeconds.js │ │ ├── formatSeconds.test.js │ │ ├── index.js │ │ ├── locale.js │ │ ├── style.scss │ │ ├── supportsMediaRecorder.js │ │ └── supportsMediaRecorder.test.js │ └── types │ │ ├── index.d.ts │ │ └── index.test-d.ts │ ├── aws-s3-multipart │ ├── CHANGELOG.md │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── MultipartUploader.js │ │ ├── createSignedURL.js │ │ ├── createSignedURL.test.js │ │ ├── index.js │ │ └── index.test.js │ └── types │ │ ├── chunk.d.ts │ │ ├── index.d.ts │ │ └── index.test-d.ts │ └── url │ └── types │ └── index.d.ts ├── src └── style.scss ├── tsconfig.json └── types ├── index.d.ts └── index.test-d.ts /README.md: -------------------------------------------------------------------------------- 1 | # TestJS -------------------------------------------------------------------------------- /TestJS/.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /TestJS/.npmignore: -------------------------------------------------------------------------------- 1 | # This file need to be there so .gitignored files are still uploaded to the npm registry. 2 | -------------------------------------------------------------------------------- /TestJS/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # TestJS 2 | 3 | ## 3.1.0 4 | 5 | Released: 2022-09-25 6 | 7 | - TestJS: add a decoy `Core` export to warn users about the renaming (Antoine du Hamel / #4085) 8 | - TestJS: remove all remaining occurrences of `TestJS.Core` (Antoine du Hamel / #4082) 9 | 10 | ## 3.0.0 11 | 12 | Released: 2022-08-22 13 | 14 | - TestJS: add `TestJS.min.mjs` for ESM consumption. 15 | -------------------------------------------------------------------------------- /TestJS/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Transloadit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TestJS/bundle-legacy.mjs: -------------------------------------------------------------------------------- 1 | // adding this directive to make sure the output file is using strict mode: 2 | 3 | 'use strict' 4 | 5 | import 'core-js' 6 | import 'whatwg-fetch' 7 | import 'abortcontroller-polyfill/dist/polyfill-patch-fetch' 8 | // Order matters: AbortController needs fetch which needs Promise. 9 | 10 | import 'md-gum-polyfill' 11 | import ResizeObserver from 'resize-observer-polyfill' 12 | 13 | if (typeof window.ResizeObserver !== 'function') window.ResizeObserver = ResizeObserver 14 | 15 | // Needed for Babel 16 | import 'regenerator-runtime/runtime' 17 | 18 | import './bundle.mjs' 19 | -------------------------------------------------------------------------------- /TestJS/bundle.mjs: -------------------------------------------------------------------------------- 1 | // adding this directive to make sure the output file is using strict mode: 2 | 3 | 'use strict' 4 | 5 | import * as TestJS from './index.mjs' 6 | 7 | globalThis.TestJS = TestJS 8 | -------------------------------------------------------------------------------- /TestJS/index.mjs: -------------------------------------------------------------------------------- 1 | // Core 2 | export { default as TestJS, debugLogger } from '@TestJS/core' 3 | 4 | // Plugin base classes 5 | export { default as UIPlugin } from '@TestJS/core/lib/UIPlugin.js' 6 | export { default as BasePlugin } from '@TestJS/core/lib/BasePlugin.js' 7 | 8 | /** 9 | * @deprecated Use `TestJS` instead of `Core` 10 | */ 11 | export function Core () { 12 | throw new Error('Core has been renamed to TestJS') 13 | } 14 | 15 | // Utilities 16 | export * as server from '@TestJS/companion-client' 17 | 18 | import * as ProviderView from '@TestJS/provider-views' 19 | export const views = { ProviderView } 20 | 21 | // Stores 22 | export { default as DefaultStore } from '@TestJS/store-default' 23 | export { default as ReduxStore } from '@TestJS/store-redux' 24 | 25 | // UI plugins 26 | export { default as Dashboard } from '@TestJS/dashboard' 27 | export { default as DragDrop } from '@TestJS/drag-drop' 28 | export { default as DropTarget } from '@TestJS/drop-target' 29 | export { default as FileInput } from '@TestJS/file-input' 30 | export { default as ImageEditor } from '@TestJS/image-editor' 31 | export { default as Informer } from '@TestJS/informer' 32 | export { default as ProgressBar } from '@TestJS/progress-bar' 33 | export { default as StatusBar } from '@TestJS/status-bar' 34 | 35 | // Acquirers 36 | export { default as Audio } from '@TestJS/audio' 37 | export { default as Box } from '@TestJS/box' 38 | export { default as Dropbox } from '@TestJS/dropbox' 39 | export { default as Facebook } from '@TestJS/facebook' 40 | export { default as GoogleDrive } from '@TestJS/google-drive' 41 | export { default as Instagram } from '@TestJS/instagram' 42 | export { default as OneDrive } from '@TestJS/onedrive' 43 | export { default as RemoteSources } from '@TestJS/remote-sources' 44 | export { default as ScreenCapture } from '@TestJS/screen-capture' 45 | export { default as Unsplash } from '@TestJS/unsplash' 46 | export { default as Url } from '@TestJS/url' 47 | export { default as Webcam } from '@TestJS/webcam' 48 | export { default as Zoom } from '@TestJS/zoom' 49 | 50 | // Uploaders 51 | export { default as AwsS3 } from '@TestJS/aws-s3' 52 | export { default as AwsS3Multipart } from '@TestJS/aws-s3-multipart' 53 | export { default as Transloadit } from '@TestJS/transloadit' 54 | export { default as Tus } from '@TestJS/tus' 55 | export { default as XHRUpload } from '@TestJS/xhr-upload' 56 | 57 | // Miscellaneous 58 | export { default as Compressor } from '@TestJS/compressor' 59 | export { default as Form } from '@TestJS/form' 60 | export { default as GoldenRetriever } from '@TestJS/golden-retriever' 61 | export { default as ReduxDevTools } from '@TestJS/redux-dev-tools' 62 | export { default as ThumbnailGenerator } from '@TestJS/thumbnail-generator' 63 | 64 | // Special hack for Transloadit static exports 65 | import Transloadit, { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@TestJS/transloadit' 66 | Transloadit.COMPANION_URL = COMPANION_URL 67 | Transloadit.COMPANION_ALLOWED_HOSTS = COMPANION_ALLOWED_HOSTS 68 | 69 | export const locales = {} 70 | -------------------------------------------------------------------------------- /TestJS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TestJS", 3 | "description": "Extensible JavaScript file upload widget with support for resumable uploads, previews, drag&drop, restrictions, remote providers like Instagram, Dropbox, Google Drive, S3, file processing/encoding etc", 4 | "version": "3.20.1", 5 | "license": "MIT", 6 | "main": "index.mjs", 7 | "module": "index.mjs", 8 | "unpkg": "dist/TestJS.min.js", 9 | "style": "dist/TestJS.min.css", 10 | "types": "types/index.d.ts", 11 | "keywords": [ 12 | "file uploader", 13 | "drag-drop", 14 | "progress", 15 | "preview", 16 | "resumable uploads", 17 | "xhr", 18 | "tus", 19 | "s3", 20 | "google drive", 21 | "dropbox", 22 | "box", 23 | "webcam" 24 | ], 25 | "homepage": "https://TestJS.io", 26 | "bugs": { 27 | "url": "https://github.com/transloadit/TestJS/issues" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/transloadit/TestJS.git" 32 | }, 33 | "dependencies": { 34 | "@TestJS/audio": "workspace:^", 35 | "@TestJS/aws-s3": "workspace:^", 36 | "@TestJS/aws-s3-multipart": "workspace:^", 37 | "@TestJS/compressor": "workspace:^", 38 | "@TestJS/core": "workspace:^", 39 | "@TestJS/box": "workspace:^", 40 | "@TestJS/companion-client": "workspace:^", 41 | "@TestJS/dashboard": "workspace:^", 42 | "@TestJS/drag-drop": "workspace:^", 43 | "@TestJS/drop-target": "workspace:^", 44 | "@TestJS/dropbox": "workspace:^", 45 | "@TestJS/facebook": "workspace:^", 46 | "@TestJS/file-input": "workspace:^", 47 | "@TestJS/form": "workspace:^", 48 | "@TestJS/golden-retriever": "workspace:^", 49 | "@TestJS/google-drive": "workspace:^", 50 | "@TestJS/image-editor": "workspace:^", 51 | "@TestJS/informer": "workspace:^", 52 | "@TestJS/progress-bar": "workspace:^", 53 | "@TestJS/provider-views": "workspace:^", 54 | "@TestJS/instagram": "workspace:^", 55 | "@TestJS/onedrive": "workspace:^", 56 | "@TestJS/redux-dev-tools": "workspace:^", 57 | "@TestJS/remote-sources": "workspace:^", 58 | "@TestJS/screen-capture": "workspace:^", 59 | "@TestJS/status-bar": "workspace:^", 60 | "@TestJS/store-default": "workspace:^", 61 | "@TestJS/store-redux": "workspace:^", 62 | "@TestJS/thumbnail-generator": "workspace:^", 63 | "@TestJS/transloadit": "workspace:^", 64 | "@TestJS/tus": "workspace:^", 65 | "@TestJS/unsplash": "workspace:^", 66 | "@TestJS/url": "workspace:^", 67 | "@TestJS/webcam": "workspace:^", 68 | "@TestJS/xhr-upload": "workspace:^", 69 | "@TestJS/zoom": "workspace:^" 70 | }, 71 | "devDependencies": { 72 | "abortcontroller-polyfill": "^1.7.3", 73 | "core-js": "~3.24.0", 74 | "md-gum-polyfill": "^1.0.0", 75 | "regenerator-runtime": "0.13.9", 76 | "resize-observer-polyfill": "^1.5.1", 77 | "whatwg-fetch": "^3.6.2" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": ["projects/**/*"], 3 | "overrides": [ 4 | { 5 | "files": ["*.ts"], 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@angular-eslint/recommended", 10 | "plugin:@angular-eslint/template/process-inline-templates" 11 | ], 12 | "rules": { 13 | // eslint-disable-line import/newline-after-import 14 | "@angular-eslint/directive-selector": [ 15 | "error", 16 | { 17 | "type": "attribute", 18 | "prefix": "app", 19 | "style": "camelCase" 20 | } 21 | ], 22 | "@typescript-eslint/semi": ["error", "never"], 23 | "import/no-unresolved": "off", 24 | "import/prefer-default-export": "off", 25 | "@angular-eslint/component-selector": [ 26 | "error", 27 | { 28 | "type": "element", 29 | "prefix": "app", 30 | "style": "kebab-case" 31 | } 32 | ], 33 | "semi": ["error", "never"] 34 | } 35 | }, 36 | { 37 | "files": ["*.html"], 38 | "extends": [ 39 | "plugin:@angular-eslint/template/recommended", 40 | "plugin:@angular-eslint/template/accessibility" 41 | ], 42 | "rules": {} 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @uppy/angular 2 | 3 | ## 0.6.0 4 | 5 | Released: 2023-09-05 6 | Included in: Uppy v3.15.0 7 | 8 | - @uppy/angular: upgrade to Angular 16.x (Antoine du Hamel / #4642) 9 | 10 | ## 0.5.0 11 | 12 | Released: 2022-11-10 13 | Included in: Uppy v3.3.0 14 | 15 | - @uppy/angular,@uppy/utils: add `cause` support for `AbortError`s (Antoine du Hamel / #4198) 16 | 17 | ## 0.4.3 18 | 19 | Released: 2022-10-19 20 | Included in: Uppy v3.2.0 21 | 22 | - @uppy/angular: remove unnecessary `console.log` call (Antoine du Hamel / #4139) 23 | 24 | ## 0.4.2 25 | 26 | Released: 2022-09-25 27 | Included in: Uppy v3.1.0 28 | 29 | - @uppy/angular: Fix angular build error (Murderlon) 30 | 31 | ## 0.4.1 32 | 33 | Released: 2022-08-30 34 | Included in: Uppy v3.0.1 35 | 36 | - @uppy/angular: fix compiler warning (Antoine du Hamel / #4064) 37 | - @uppy/angular: fix peer dependencies (Antoine du Hamel / #4035) 38 | 39 | ## 0.4.0 40 | 41 | Released: 2022-08-22 42 | Included in: Uppy v3.0.0 43 | 44 | - @uppy/angular: upgrade to Angular 14 (Antoine du Hamel / #3997) 45 | 46 | ## 0.3.1 47 | 48 | Released: 2022-05-30 49 | Included in: Uppy v2.11.0 50 | 51 | - @uppy/angular,@uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/onedrive,@uppy/progress-bar,@uppy/react,@uppy/redux-dev-tools,@uppy/robodog,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: doc: update bundler recommendation (Antoine du Hamel / #3763) 52 | 53 | ## 0.3.0 54 | 55 | Released: 2022-03-02 56 | Included in: Uppy v2.7.0 57 | 58 | - @uppy/angular: update ng version (Antoine du Hamel / #3503) 59 | 60 | ## 0.2.8 61 | 62 | Released: 2021-12-21 63 | Included in: Uppy v2.3.2 64 | 65 | - @uppy/angular,@uppy/companion,@uppy/svelte,@uppy/vue: add `.npmignore` files to ignore `.gitignore` when packing (Antoine du Hamel / #3380) 66 | - @uppy/angular: Fix module field in `package.json` (Merlijn Vos / #3365) 67 | 68 | ## 0.2.6 69 | 70 | Released: 2021-12-07 71 | Included in: Uppy v2.3.0 72 | 73 | - @uppy/angular: examples: update `angular-example` to Angular v13 (Antoine du Hamel / #3325) 74 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Transloadit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "@uppy/angular": { 7 | "projectType": "library", 8 | "root": "projects/uppy/angular", 9 | "sourceRoot": "projects/uppy/angular/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "project": "projects/uppy/angular/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/uppy/angular/tsconfig.lib.prod.json" 20 | }, 21 | "development": { 22 | "tsConfig": "projects/uppy/angular/tsconfig.lib.json" 23 | } 24 | }, 25 | "defaultConfiguration": "production" 26 | }, 27 | "test": { 28 | "builder": "@angular-devkit/build-angular:karma", 29 | "options": { 30 | "tsConfig": "projects/uppy/angular/tsconfig.spec.json", 31 | "polyfills": ["zone.js", "zone.js/testing"] 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.2.0", 14 | "@angular/common": "^16.2.0", 15 | "@angular/compiler": "^16.2.0", 16 | "@angular/core": "^16.2.0", 17 | "@angular/forms": "^16.2.0", 18 | "@angular/platform-browser": "^16.2.0", 19 | "@angular/platform-browser-dynamic": "^16.2.0", 20 | "@angular/router": "^16.2.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.2.0", 27 | "@angular-eslint/builder": "16.1.1", 28 | "@angular-eslint/eslint-plugin": "16.1.1", 29 | "@angular-eslint/eslint-plugin-template": "16.1.1", 30 | "@angular-eslint/schematics": "16.1.1", 31 | "@angular-eslint/template-parser": "16.1.1", 32 | "@angular/cli": "~16.2.0", 33 | "@angular/compiler-cli": "^16.2.0", 34 | "@types/jasmine": "~4.3.0", 35 | "@typescript-eslint/eslint-plugin": "5.62.0", 36 | "@typescript-eslint/parser": "5.62.0", 37 | "jasmine-core": "~4.6.0", 38 | "karma": "~6.4.0", 39 | "karma-chrome-launcher": "~3.2.0", 40 | "karma-coverage": "~2.2.0", 41 | "karma-jasmine": "~5.1.0", 42 | "karma-jasmine-html-reporter": "~2.1.0", 43 | "ng-packagr": "^16.2.0", 44 | "typescript": "~5.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": [ 9 | "packages/@uppy/angular/projects/angular/tsconfig.lib.json", 10 | "packages/@uppy/angular/projects/angular/tsconfig.spec.json" 11 | ], 12 | "createDefaultProgram": true 13 | }, 14 | "rules": { 15 | "@angular-eslint/component-selector": [ 16 | "error", 17 | { 18 | "type": "element", 19 | "prefix": "uppy", 20 | "style": "kebab-case" 21 | } 22 | ], 23 | "@angular-eslint/directive-selector": [ 24 | "error", 25 | { 26 | "type": "attribute", 27 | "prefix": "uppy", 28 | "style": "camelCase" 29 | } 30 | ], 31 | "dot-notation": "error", 32 | "indent": "error", 33 | "no-empty-function": "off", 34 | "no-shadow": "error", 35 | "no-unused-expressions": "error", 36 | "no-use-before-define": "off", 37 | "quotes": "error", 38 | "semi": "error" 39 | } 40 | }, 41 | { 42 | "files": ["*.html"], 43 | "rules": {} 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../../dist/uppy/angular", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uppy/angular", 3 | "description": "Angular component wrappers around Uppy's official UI plugins.", 4 | "version": "0.6.1", 5 | "license": "MIT", 6 | "homepage": "https://uppy.io", 7 | "keywords": [ 8 | "file uploader", 9 | "uppy", 10 | "uppy-plugin", 11 | "angular", 12 | "angular-components" 13 | ], 14 | "bugs": { 15 | "url": "https://github.com/transloadit/uppy/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/transloadit/uppy.git" 20 | }, 21 | "scripts": { 22 | "prepublishOnly": "rm -fr * && cp -r ../../../dist/uppy/angular .." 23 | }, 24 | "dependencies": { 25 | "tslib": "^2.0.0" 26 | }, 27 | "peerDependencies": { 28 | "@angular/common": "^16.2.0", 29 | "@angular/core": "^16.2.0", 30 | "@uppy/core": "workspace:^", 31 | "@uppy/dashboard": "workspace:^", 32 | "@uppy/drag-drop": "workspace:^", 33 | "@uppy/progress-bar": "workspace:^", 34 | "@uppy/status-bar": "workspace:^", 35 | "@uppy/utils": "workspace:^" 36 | }, 37 | "sideEffects": false 38 | } 39 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard-modal/dashboard-modal-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | import * as Dashboard from '@uppy/dashboard'; 3 | import { Uppy } from '@uppy/core'; 4 | 5 | @Component({ 6 | selector: 'uppy-dashboard-demo', 7 | template: ``, 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class DashboardModalDemoComponent { 11 | uppy: Uppy = new Uppy({ debug: true, autoProceed: true }); 12 | props: Dashboard.DashboardOptions; 13 | } 14 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard-modal/dashboard-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardModalComponent } from './dashboard-modal.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard-modal/dashboard-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, ElementRef, Input, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; 2 | import Dashboard from '@uppy/dashboard'; 3 | import type { DashboardOptions } from '@uppy/dashboard'; 4 | import { Uppy } from '@uppy/core'; 5 | import { UppyAngularWrapper } from '../../utils/wrapper'; 6 | 7 | @Component({ 8 | selector: 'uppy-dashboard-modal', 9 | template: '', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class DashboardModalComponent extends UppyAngularWrapper implements OnDestroy, OnChanges { 13 | @Input() uppy: Uppy = new Uppy; 14 | @Input() props: DashboardOptions = {}; 15 | @Input() open: boolean = false; 16 | 17 | constructor(public el: ElementRef) { 18 | super(); 19 | } 20 | 21 | ngOnInit() { 22 | this.onMount({ 23 | id: 'angular:DashboardModal', 24 | inline: false, 25 | target: this.el.nativeElement 26 | }, Dashboard) 27 | } 28 | 29 | ngOnChanges(changes: SimpleChanges): void { 30 | this.handleChanges(changes, Dashboard); 31 | // Handle dashboard-modal specific changes 32 | if (changes['open'] && this.open !== changes['open'].previousValue) { 33 | if(this.open && !changes['open'].previousValue) { 34 | this.plugin!.openModal() 35 | } 36 | if (!this.open && changes['open'].previousValue) { 37 | this.plugin!.closeModal() 38 | } 39 | } 40 | } 41 | 42 | ngOnDestroy(): void { 43 | this.uninstall(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard-modal/dashboard-modal.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DashboardModalComponent } from './dashboard-modal.component'; 3 | 4 | export const COMPONENTS = [DashboardModalComponent]; 5 | @NgModule({ 6 | declarations: COMPONENTS, 7 | exports: COMPONENTS 8 | }) 9 | export class UppyAngularDashboardModalModule { } 10 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard-modal/dashboard-modal.stories.ts: -------------------------------------------------------------------------------- 1 | import { DashboardModalDemoComponent } from './dashboard-modal-demo.component'; 2 | import { moduleMetadata } from '@storybook/angular'; 3 | import { UppyAngularDashboardModalModule } from './dashboard-modal.module'; 4 | 5 | export default { 6 | title: 'Dashboard', 7 | decorators: [ 8 | moduleMetadata({ 9 | imports: [UppyAngularDashboardModalModule], 10 | declarations: [DashboardModalDemoComponent] 11 | }), 12 | ] 13 | }; 14 | 15 | export const Default = () => ({ 16 | component: DashboardModalDemoComponent, 17 | }); 18 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard/dashboard-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | import * as Dashboard from '@uppy/dashboard'; 3 | import { Uppy } from '@uppy/core'; 4 | 5 | @Component({ 6 | selector: 'uppy-dashboard-demo', 7 | template: ``, 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class DashboardDemoComponent { 11 | uppy: Uppy = new Uppy({ debug: true, autoProceed: true }); 12 | props: Dashboard.DashboardOptions; 13 | } 14 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, ElementRef, Input, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; 2 | import Dashboard from '@uppy/dashboard'; 3 | import type { DashboardOptions } from '@uppy/dashboard'; 4 | import { Uppy } from '@uppy/core'; 5 | import { UppyAngularWrapper } from '../../utils/wrapper'; 6 | 7 | @Component({ 8 | selector: 'uppy-dashboard', 9 | template: '', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class DashboardComponent extends UppyAngularWrapper implements OnDestroy, OnChanges { 13 | @Input() uppy: Uppy = new Uppy; 14 | @Input() props: DashboardOptions = {}; 15 | 16 | constructor(public el: ElementRef) { 17 | super(); 18 | } 19 | 20 | ngOnInit() { 21 | this.onMount({ id: 'angular:Dashboard', inline: true, target: this.el.nativeElement }, Dashboard) 22 | } 23 | 24 | ngOnChanges(changes: SimpleChanges): void { 25 | this.handleChanges(changes, Dashboard); 26 | } 27 | 28 | ngOnDestroy(): void { 29 | this.uninstall(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DashboardComponent } from './dashboard.component'; 3 | 4 | export const COMPONENTS = [DashboardComponent]; 5 | @NgModule({ 6 | declarations: COMPONENTS, 7 | exports: COMPONENTS 8 | }) 9 | export class UppyAngularDashboardModule { } 10 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/dashboard/dashboard.stories.ts: -------------------------------------------------------------------------------- 1 | import { DashboardDemoComponent } from './dashboard-demo.component'; 2 | import { moduleMetadata } from '@storybook/angular'; 3 | import { UppyAngularDashboardModule} from './dashboard.module'; 4 | 5 | export default { 6 | title: 'Dashboard', 7 | decorators: [ 8 | moduleMetadata({ 9 | imports: [UppyAngularDashboardModule], 10 | declarations: [DashboardDemoComponent] 11 | }), 12 | ] 13 | }; 14 | 15 | export const Default = () => ({ 16 | component: DashboardDemoComponent, 17 | }); 18 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/drag-drop/drag-drop-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | import * as DragDrop from '@uppy/drag-drop'; 3 | import { Uppy } from '@uppy/core'; 4 | 5 | @Component({ 6 | selector: 'uppy-drag-drop-demo', 7 | template: ` 8 | 9 | `, 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class DragDropDemoComponent { 13 | uppy: Uppy = new Uppy({ debug: true, autoProceed: true }); 14 | props: DragDrop.DragDropOptions = {}; 15 | } 16 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/drag-drop/drag-drop.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DragDropComponent } from './drag-drop.component'; 4 | 5 | describe('DragDropComponent', () => { 6 | let component: DragDropComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DragDropComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DragDropComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/drag-drop/drag-drop.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, Input, OnDestroy, OnChanges, SimpleChanges, ElementRef } from '@angular/core'; 2 | import { Uppy } from '@uppy/core'; 3 | import DragDrop from '@uppy/drag-drop'; 4 | import type { DragDropOptions } from '@uppy/drag-drop'; 5 | import { UppyAngularWrapper } from '../../utils/wrapper'; 6 | 7 | @Component({ 8 | selector: 'uppy-drag-drop', 9 | template: '', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class DragDropComponent extends UppyAngularWrapper implements OnDestroy, OnChanges { 13 | @Input() uppy: Uppy = new Uppy; 14 | @Input() props: DragDropOptions = {}; 15 | 16 | constructor(public el: ElementRef) { 17 | super(); 18 | } 19 | 20 | ngOnInit() { 21 | this.onMount({ id: 'angular:DragDrop', target: this.el.nativeElement }, DragDrop) 22 | } 23 | 24 | ngOnChanges(changes: SimpleChanges): void { 25 | this.handleChanges(changes, DragDrop); 26 | } 27 | 28 | ngOnDestroy(): void { 29 | this.uninstall(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/drag-drop/drag-drop.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DragDropComponent } from './drag-drop.component'; 3 | 4 | export const COMPONENTS = [DragDropComponent]; 5 | @NgModule({ 6 | declarations: COMPONENTS, 7 | exports: COMPONENTS 8 | }) 9 | export class UppyAngularDragDropModule { } 10 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/drag-drop/drag-drop.stories.ts: -------------------------------------------------------------------------------- 1 | import { moduleMetadata } from '@storybook/angular'; 2 | import { UppyAngularDragDropModule } from './drag-drop.module'; 3 | import { DragDropDemoComponent } from './drag-drop-demo.component'; 4 | 5 | export default { 6 | title: 'Drag Drop', 7 | decorators: [ 8 | moduleMetadata({ 9 | imports: [UppyAngularDragDropModule], 10 | declarations: [DragDropDemoComponent] 11 | }), 12 | ] 13 | }; 14 | 15 | export const Default = () => ({ 16 | component: DragDropDemoComponent, 17 | }); 18 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/progress-bar/progress-bar-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 2 | import { Uppy } from '@uppy/core'; 3 | import { Tus, ProgressBar } from 'uppy'; 4 | 5 | @Component({ 6 | selector: 'uppy-progress-bar-demo', 7 | template: ` 8 |
9 |
autoProceed is on
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
Uploaded files:
21 |
    22 |
  1. 23 | 24 | {{item.fileName}} 25 |
  2. 26 |
27 |
28 |
29 | 30 |
31 |
autoProceed is off
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
Uploaded files:
44 |
    45 |
  1. 46 | 47 | {{item.fileName}} 48 |
  2. 49 |
50 |
51 |
52 | `, 53 | changeDetection: ChangeDetectionStrategy.OnPush 54 | }) 55 | export class ProgressBarDemoComponent implements OnInit { 56 | uppyOne: Uppy; 57 | uppyTwo: Uppy; 58 | fileListOne: { url: string, fileName: string }[] = []; 59 | fileListTwo: { url: string, fileName: string }[] = []; 60 | props: ProgressBar.ProgressBarOptions = { 61 | hideAfterFinish: false 62 | }; 63 | 64 | upload(): void { 65 | this.uppyTwo.upload(); 66 | } 67 | 68 | constructor(private cdr: ChangeDetectorRef) {} 69 | 70 | updateFileList = (target: string) => (file, response): void => { 71 | this[target] = [...this[target], { url: response.uploadURL, fileName: file.name }]; 72 | this.cdr.markForCheck(); 73 | } 74 | 75 | ngOnInit(): void { 76 | this.uppyOne = new Uppy({ debug: true, autoProceed: true }) 77 | .use(Tus, { endpoint: 'https://master.tus.io/files/' }) 78 | .on('upload-success', this.updateFileList('fileListOne')); 79 | this.uppyTwo = new Uppy({ debug: true, autoProceed: false }) 80 | .use(Tus, { endpoint: 'https://master.tus.io/files/' }) 81 | .on('upload-success', this.updateFileList('fileListTwo')); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/progress-bar/progress-bar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProgressBarComponent } from './progress-bar.component'; 4 | 5 | describe('ProgressBarComponent', () => { 6 | let component: ProgressBarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProgressBarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProgressBarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/progress-bar/progress-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, ElementRef, Input, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; 2 | import { Uppy } from '@uppy/core'; 3 | import ProgressBar from '@uppy/progress-bar'; 4 | import type { ProgressBarOptions } from '@uppy/progress-bar'; 5 | import { UppyAngularWrapper } from '../../utils/wrapper'; 6 | 7 | @Component({ 8 | selector: 'uppy-progress-bar', 9 | template: '', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class ProgressBarComponent extends UppyAngularWrapper implements OnDestroy, OnChanges { 13 | @Input() uppy: Uppy = new Uppy; 14 | @Input() props: ProgressBarOptions = {}; 15 | 16 | constructor(public el: ElementRef) { 17 | super(); 18 | } 19 | 20 | ngOnInit() { 21 | this.onMount({ id: 'angular:ProgressBar', target: this.el.nativeElement }, ProgressBar) 22 | } 23 | 24 | ngOnChanges(changes: SimpleChanges): void { 25 | this.handleChanges(changes, ProgressBar); 26 | } 27 | 28 | ngOnDestroy(): void { 29 | this.uninstall(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/progress-bar/progress-bar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ProgressBarComponent } from './progress-bar.component'; 3 | 4 | export const COMPONENTS = [ProgressBarComponent]; 5 | @NgModule({ 6 | declarations: COMPONENTS, 7 | exports: COMPONENTS 8 | }) 9 | export class UppyAngularProgressBarModule { } 10 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/progress-bar/progress-bar.stories.ts: -------------------------------------------------------------------------------- 1 | import { moduleMetadata } from '@storybook/angular'; 2 | import { UppyAngularProgressBarModule } from './progress-bar.module'; 3 | import { ProgressBarDemoComponent } from './progress-bar-demo.component'; 4 | import { UppyAngularDragDropModule } from '../drag-drop/drag-drop.module'; 5 | import { CommonModule } from '@angular/common'; 6 | 7 | export default { 8 | title: 'Progress Bar', 9 | decorators: [ 10 | moduleMetadata({ 11 | imports: [UppyAngularProgressBarModule, UppyAngularDragDropModule, CommonModule], 12 | declarations: [ProgressBarDemoComponent] 13 | }), 14 | ] 15 | }; 16 | 17 | export const Default = () => ({ 18 | component: ProgressBarDemoComponent, 19 | }); 20 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/status-bar/status-bar-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy} from '@angular/core'; 2 | import * as StatusBar from '@uppy/status-bar'; 3 | import { Uppy } from '@uppy/core'; 4 | import { FileInput, Tus } from 'uppy'; 5 | 6 | @Component({ 7 | selector: 'uppy-status-bar-demo', 8 | template: ` 9 |
10 | 11 | `, 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class StatusBarDemoComponent implements OnInit { 15 | uppy: Uppy = new Uppy({debug: true, autoProceed: true}); 16 | props: StatusBar.StatusBarOptions = { 17 | hideUploadButton: true, 18 | hideAfterFinish: false 19 | }; 20 | 21 | ngOnInit(): void { 22 | this.uppy.use(FileInput, { target: '.UppyInput', pretty: false }).use(Tus, { endpoint: 'https://master.tus.io/files/' }); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/status-bar/status-bar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { StatusBarComponent } from './status-bar.component'; 4 | 5 | describe('StatusBarComponent', () => { 6 | let component: StatusBarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ StatusBarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(StatusBarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/status-bar/status-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, Input, ElementRef, SimpleChange, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; 2 | import { Uppy } from '@uppy/core'; 3 | import StatusBar from '@uppy/status-bar'; 4 | import type { StatusBarOptions } from '@uppy/status-bar'; 5 | import { UppyAngularWrapper } from '../../utils/wrapper'; 6 | 7 | @Component({ 8 | selector: 'uppy-status-bar', 9 | template: '', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class StatusBarComponent extends UppyAngularWrapper implements OnDestroy, OnChanges { 13 | @Input() uppy: Uppy = new Uppy; 14 | @Input() props: StatusBarOptions = {}; 15 | 16 | constructor(public el: ElementRef) { 17 | super(); 18 | } 19 | 20 | ngOnInit() { 21 | this.onMount({ id: 'angular:StatusBar', target: this.el.nativeElement }, StatusBar) 22 | } 23 | 24 | ngOnChanges(changes: SimpleChanges): void { 25 | this.handleChanges(changes, StatusBar); 26 | } 27 | 28 | ngOnDestroy(): void { 29 | this.uninstall(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/status-bar/status-bar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { StatusBarComponent } from './status-bar.component'; 3 | 4 | export const COMPONENTS = [StatusBarComponent]; 5 | @NgModule({ 6 | declarations: COMPONENTS, 7 | exports: COMPONENTS 8 | }) 9 | export class UppyAngularStatusBarModule { } 10 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/components/status-bar/status-bar.stories.ts: -------------------------------------------------------------------------------- 1 | import { StatusBarDemoComponent } from './status-bar-demo.component'; 2 | import { moduleMetadata } from '@storybook/angular'; 3 | import { UppyAngularStatusBarModule } from './status-bar.module'; 4 | 5 | export default { 6 | title: 'Status Bar', 7 | decorators: [ 8 | moduleMetadata({ 9 | imports: [UppyAngularStatusBarModule], 10 | declarations: [StatusBarDemoComponent] 11 | }), 12 | ] 13 | }; 14 | 15 | export const Default = () => ({ 16 | component: StatusBarDemoComponent, 17 | }); 18 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/lib/utils/wrapper.ts: -------------------------------------------------------------------------------- 1 | import type { Uppy, UIPlugin } from '@uppy/core'; 2 | import type { ElementRef, SimpleChanges } from '@angular/core'; 3 | import type { DragDropOptions } from '@uppy/drag-drop'; 4 | import type { StatusBarOptions } from '@uppy/status-bar'; 5 | import type { ProgressBarOptions } from '@uppy/progress-bar'; 6 | 7 | export abstract class UppyAngularWrapper { 8 | abstract props: DragDropOptions|StatusBarOptions|ProgressBarOptions; 9 | abstract el: ElementRef 10 | abstract uppy: Uppy; 11 | private options: any; 12 | plugin: PluginType | undefined; 13 | 14 | onMount(defaultOptions: Record, plugin: new (uppy: Uppy, opts?: Record) => UIPlugin) { 15 | this.options = { 16 | ...defaultOptions, 17 | ...this.props, 18 | }; 19 | 20 | this.uppy.use(plugin, this.options); 21 | this.plugin = this.uppy.getPlugin(this.options.id) as PluginType; 22 | } 23 | 24 | handleChanges(changes: SimpleChanges, plugin: any): void { 25 | // Without the last part of this conditional, it tries to uninstall before the plugin is mounted 26 | if (changes['uppy'] && this.uppy !== changes['uppy'].previousValue && changes['uppy'].previousValue !== undefined) { 27 | this.uninstall(changes['uppy'].previousValue); 28 | this.uppy.use(plugin, this.options); 29 | } 30 | this.options = { ...this.options, ...this.props } 31 | this.plugin = this.uppy.getPlugin(this.options.id) as PluginType; 32 | if(changes['props'] && this.props !== changes['props'].previousValue && changes['props'].previousValue !== undefined) { 33 | this.plugin.setOptions({ ...this.options }) 34 | } 35 | } 36 | 37 | uninstall(uppy = this.uppy): void { 38 | uppy.removePlugin(this.plugin!); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of @uppy/angular 3 | */ 4 | 5 | export { UppyAngularDashboardModule } from './lib/components/dashboard/dashboard.module'; 6 | export { UppyAngularDashboardModalModule } from './lib/components/dashboard-modal/dashboard-modal.module'; 7 | export { UppyAngularProgressBarModule } from './lib/components/progress-bar/progress-bar.module'; 8 | export { UppyAngularStatusBarModule } from './lib/components/status-bar/status-bar.module'; 9 | export { UppyAngularDragDropModule } from './lib/components/drag-drop/drag-drop.module'; 10 | export { StatusBarComponent } from './lib/components/status-bar/status-bar.component'; 11 | export { ProgressBarComponent } from './lib/components/progress-bar/progress-bar.component'; 12 | export { DragDropComponent } from './lib/components/drag-drop/drag-drop.component'; 13 | export { DashboardComponent } from './lib/components/dashboard/dashboard.component'; 14 | export { DashboardModalComponent } from './lib/components/dashboard-modal/dashboard-modal.component'; 15 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": ["**/*.spec.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/projects/uppy/angular/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../../out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "paths": { 6 | "@uppy/angular": ["dist/uppy/angular"] 7 | }, 8 | "baseUrl": "./", 9 | "outDir": "./dist/out-tsc", 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "noImplicitOverride": true, 13 | "noPropertyAccessFromIndexSignature": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "sourceMap": true, 17 | "declaration": false, 18 | "downlevelIteration": true, 19 | "experimentalDecorators": true, 20 | "moduleResolution": "node", 21 | "importHelpers": true, 22 | "target": "ES2022", 23 | "module": "ES2022", 24 | "useDefineForClassFields": false, 25 | "lib": ["ES2022", "dom"] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @uppy/audio 2 | 3 | ## 1.0.4 4 | 5 | Released: 2023-02-13 6 | Included in: Uppy v3.5.0 7 | 8 | - @uppy/audio,@uppy/core,@uppy/dashboard,@uppy/screen-capture: Warn more instead of erroring (Artur Paikin / #4302) 9 | 10 | ## 1.0.3 11 | 12 | Released: 2023-01-26 13 | Included in: Uppy v3.4.0 14 | 15 | - @uppy/audio: @uppy/audio fix typo in readme (elliotsayes / #4240) 16 | 17 | ## 1.0.2 18 | 19 | Released: 2022-09-25 20 | Included in: Uppy v3.1.0 21 | 22 | - @uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/companion,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/redux-dev-tools,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: add missing entries to changelog for individual packages (Antoine du Hamel / #4092) 23 | 24 | ## 1.0.0 25 | 26 | Released: 2022-08-22 27 | Included in: Uppy v3.0.0 28 | 29 | - Switch to ESM 30 | 31 | ## 0.3.2 32 | 33 | Released: 2022-05-30 34 | Included in: Uppy v2.11.0 35 | 36 | - @uppy/angular,@uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/onedrive,@uppy/progress-bar,@uppy/react,@uppy/redux-dev-tools,@uppy/robodog,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: doc: update bundler recommendation (Antoine du Hamel / #3763) 37 | 38 | ## 0.3.1 39 | 40 | Released: 2022-05-14 41 | Included in: Uppy v2.10.0 42 | 43 | - @uppy/audio: fix types (Merlijn Vos / #3689) 44 | 45 | ## 0.3.0 46 | 47 | Released: 2022-03-16 48 | Included in: Uppy v2.8.0 49 | 50 | - @uppy/audio: refactor to ESM (Antoine du Hamel / #3470) 51 | 52 | ## 0.2.1 53 | 54 | Released: 2021-12-09 55 | Included in: Uppy v2.3.1 56 | 57 | - @uppy/audio: showRecordingLength option was removed, always clearInterval (Artur Paikin / #3351) 58 | 59 | ## 0.2.0 60 | 61 | Released: 2021-12-07 62 | Included in: Uppy v2.3.0 63 | 64 | - @uppy/audio: new @uppy/audio plugin for recording with microphone (Artur Paikin / #2976) 65 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Transloadit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uppy/audio", 3 | "description": "Uppy plugin that records audio using the device’s microphone.", 4 | "version": "1.1.4", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "style": "dist/style.min.css", 8 | "types": "types/index.d.ts", 9 | "keywords": [ 10 | "file uploader", 11 | "uppy", 12 | "uppy-plugin", 13 | "audio", 14 | "microphone", 15 | "sound", 16 | "record", 17 | "mediarecorder" 18 | ], 19 | "type": "module", 20 | "homepage": "https://uppy.io", 21 | "bugs": { 22 | "url": "https://github.com/transloadit/uppy/issues" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/transloadit/uppy.git" 27 | }, 28 | "dependencies": { 29 | "@uppy/utils": "workspace:^", 30 | "preact": "^10.5.13" 31 | }, 32 | "devDependencies": { 33 | "vitest": "^0.34.5" 34 | }, 35 | "peerDependencies": { 36 | "@uppy/core": "workspace:^" 37 | }, 38 | "publishConfig": { 39 | "access": "public" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/Audio.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | import { UIPlugin } from '@uppy/core' 4 | 5 | import getFileTypeExtension from '@uppy/utils/lib/getFileTypeExtension' 6 | import supportsMediaRecorder from './supportsMediaRecorder.js' 7 | import RecordingScreen from './RecordingScreen.jsx' 8 | import PermissionsScreen from './PermissionsScreen.jsx' 9 | import locale from './locale.js' 10 | 11 | import packageJson from '../package.json' 12 | 13 | /** 14 | * Audio recording plugin 15 | */ 16 | export default class Audio extends UIPlugin { 17 | static VERSION = packageJson.version 18 | 19 | #stream = null 20 | 21 | #audioActive = false 22 | 23 | #recordingChunks = null 24 | 25 | #recorder = null 26 | 27 | #capturedMediaFile = null 28 | 29 | #mediaDevices = null 30 | 31 | #supportsUserMedia = null 32 | 33 | constructor (uppy, opts) { 34 | super(uppy, opts) 35 | this.#mediaDevices = navigator.mediaDevices 36 | this.#supportsUserMedia = this.#mediaDevices != null 37 | this.id = this.opts.id || 'Audio' 38 | this.type = 'acquirer' 39 | this.icon = () => ( 40 | 43 | ) 44 | 45 | this.defaultLocale = locale 46 | 47 | this.opts = { ...opts } 48 | 49 | this.i18nInit() 50 | this.title = this.i18n('pluginNameAudio') 51 | 52 | this.setPluginState({ 53 | hasAudio: false, 54 | audioReady: false, 55 | cameraError: null, 56 | recordingLengthSeconds: 0, 57 | audioSources: [], 58 | currentDeviceId: null, 59 | }) 60 | } 61 | 62 | #hasAudioCheck () { 63 | if (!this.#mediaDevices) { 64 | return Promise.resolve(false) 65 | } 66 | 67 | return this.#mediaDevices.enumerateDevices().then(devices => { 68 | return devices.some(device => device.kind === 'audioinput') 69 | }) 70 | } 71 | 72 | // eslint-disable-next-line consistent-return 73 | #start = (options = null) => { 74 | if (!this.#supportsUserMedia) { 75 | return Promise.reject(new Error('Microphone access not supported')) 76 | } 77 | 78 | this.#audioActive = true 79 | 80 | this.#hasAudioCheck().then(hasAudio => { 81 | this.setPluginState({ 82 | hasAudio, 83 | }) 84 | 85 | // ask user for access to their camera 86 | return this.#mediaDevices.getUserMedia({ audio: true }) 87 | .then((stream) => { 88 | this.#stream = stream 89 | 90 | let currentDeviceId = null 91 | const tracks = stream.getAudioTracks() 92 | 93 | if (!options || !options.deviceId) { 94 | currentDeviceId = tracks[0].getSettings().deviceId 95 | } else { 96 | tracks.forEach((track) => { 97 | if (track.getSettings().deviceId === options.deviceId) { 98 | currentDeviceId = track.getSettings().deviceId 99 | } 100 | }) 101 | } 102 | 103 | // Update the sources now, so we can access the names. 104 | this.#updateSources() 105 | 106 | this.setPluginState({ 107 | currentDeviceId, 108 | audioReady: true, 109 | }) 110 | }) 111 | .catch((err) => { 112 | this.setPluginState({ 113 | audioReady: false, 114 | cameraError: err, 115 | }) 116 | this.uppy.info(err.message, 'error') 117 | }) 118 | }) 119 | } 120 | 121 | #startRecording = () => { 122 | // only used if supportsMediaRecorder() returned true 123 | // eslint-disable-next-line compat/compat 124 | this.#recorder = new MediaRecorder(this.#stream) 125 | this.#recordingChunks = [] 126 | let stoppingBecauseOfMaxSize = false 127 | this.#recorder.addEventListener('dataavailable', (event) => { 128 | this.#recordingChunks.push(event.data) 129 | 130 | const { restrictions } = this.uppy.opts 131 | if (this.#recordingChunks.length > 1 132 | && restrictions.maxFileSize != null 133 | && !stoppingBecauseOfMaxSize) { 134 | const totalSize = this.#recordingChunks.reduce((acc, chunk) => acc + chunk.size, 0) 135 | // Exclude the initial chunk from the average size calculation because it is likely to be a very small outlier 136 | const averageChunkSize = (totalSize - this.#recordingChunks[0].size) / (this.#recordingChunks.length - 1) 137 | const expectedEndChunkSize = averageChunkSize * 3 138 | const maxSize = Math.max(0, restrictions.maxFileSize - expectedEndChunkSize) 139 | 140 | if (totalSize > maxSize) { 141 | stoppingBecauseOfMaxSize = true 142 | this.uppy.info(this.i18n('recordingStoppedMaxSize'), 'warning', 4000) 143 | this.#stopRecording() 144 | } 145 | } 146 | }) 147 | 148 | // use a "time slice" of 500ms: ondataavailable will be called each 500ms 149 | // smaller time slices mean we can more accurately check the max file size restriction 150 | this.#recorder.start(500) 151 | 152 | // Start the recordingLengthTimer if we are showing the recording length. 153 | this.recordingLengthTimer = setInterval(() => { 154 | const currentRecordingLength = this.getPluginState().recordingLengthSeconds 155 | this.setPluginState({ recordingLengthSeconds: currentRecordingLength + 1 }) 156 | }, 1000) 157 | 158 | this.setPluginState({ 159 | isRecording: true, 160 | }) 161 | } 162 | 163 | #stopRecording = () => { 164 | const stopped = new Promise((resolve) => { 165 | this.#recorder.addEventListener('stop', () => { 166 | resolve() 167 | }) 168 | this.#recorder.stop() 169 | 170 | clearInterval(this.recordingLengthTimer) 171 | this.setPluginState({ recordingLengthSeconds: 0 }) 172 | }) 173 | 174 | return stopped.then(() => { 175 | this.setPluginState({ 176 | isRecording: false, 177 | }) 178 | return this.#getAudio() 179 | }).then((file) => { 180 | try { 181 | this.#capturedMediaFile = file 182 | // create object url for capture result preview 183 | this.setPluginState({ 184 | recordedAudio: URL.createObjectURL(file.data), 185 | }) 186 | } catch (err) { 187 | // Logging the error, exept restrictions, which is handled in Core 188 | if (!err.isRestriction) { 189 | this.uppy.log(err) 190 | } 191 | } 192 | }).then(() => { 193 | this.#recordingChunks = null 194 | this.#recorder = null 195 | }, (error) => { 196 | this.#recordingChunks = null 197 | this.#recorder = null 198 | throw error 199 | }) 200 | } 201 | 202 | #discardRecordedAudio = () => { 203 | this.setPluginState({ recordedAudio: null }) 204 | this.#capturedMediaFile = null 205 | } 206 | 207 | #submit = () => { 208 | try { 209 | if (this.#capturedMediaFile) { 210 | this.uppy.addFile(this.#capturedMediaFile) 211 | } 212 | } catch (err) { 213 | // Logging the error, exept restrictions, which is handled in Core 214 | if (!err.isRestriction) { 215 | this.uppy.log(err, 'warning') 216 | } 217 | } 218 | } 219 | 220 | #stop = async () => { 221 | if (this.#stream) { 222 | const audioTracks = this.#stream.getAudioTracks() 223 | audioTracks.forEach((track) => track.stop()) 224 | } 225 | 226 | if (this.#recorder) { 227 | await new Promise((resolve) => { 228 | this.#recorder.addEventListener('stop', resolve, { once: true }) 229 | this.#recorder.stop() 230 | 231 | clearInterval(this.recordingLengthTimer) 232 | }) 233 | } 234 | 235 | this.#recordingChunks = null 236 | this.#recorder = null 237 | this.#audioActive = false 238 | this.#stream = null 239 | 240 | this.setPluginState({ 241 | recordedAudio: null, 242 | isRecording: false, 243 | recordingLengthSeconds: 0, 244 | }) 245 | } 246 | 247 | #getAudio () { 248 | // Sometimes in iOS Safari, Blobs (especially the first Blob in the recordingChunks Array) 249 | // have empty 'type' attributes (e.g. '') so we need to find a Blob that has a defined 'type' 250 | // attribute in order to determine the correct MIME type. 251 | const mimeType = this.#recordingChunks.find(blob => blob.type?.length > 0).type 252 | 253 | const fileExtension = getFileTypeExtension(mimeType) 254 | 255 | if (!fileExtension) { 256 | return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`)) 257 | } 258 | 259 | const name = `audio-${Date.now()}.${fileExtension}` 260 | const blob = new Blob(this.#recordingChunks, { type: mimeType }) 261 | const file = { 262 | source: this.id, 263 | name, 264 | data: new Blob([blob], { type: mimeType }), 265 | type: mimeType, 266 | } 267 | 268 | return Promise.resolve(file) 269 | } 270 | 271 | #changeSource = (deviceId) => { 272 | this.#stop() 273 | this.#start({ deviceId }) 274 | } 275 | 276 | #updateSources = () => { 277 | this.#mediaDevices.enumerateDevices().then(devices => { 278 | this.setPluginState({ 279 | audioSources: devices.filter((device) => device.kind === 'audioinput'), 280 | }) 281 | }) 282 | } 283 | 284 | render () { 285 | if (!this.#audioActive) { 286 | this.#start() 287 | } 288 | 289 | const audioState = this.getPluginState() 290 | 291 | if (!audioState.audioReady || !audioState.hasAudio) { 292 | return ( 293 | 298 | ) 299 | } 300 | 301 | return ( 302 | 318 | ) 319 | } 320 | 321 | install () { 322 | this.setPluginState({ 323 | audioReady: false, 324 | recordingLengthSeconds: 0, 325 | }) 326 | 327 | const { target } = this.opts 328 | if (target) { 329 | this.mount(target, this) 330 | } 331 | 332 | if (this.#mediaDevices) { 333 | this.#updateSources() 334 | 335 | this.#mediaDevices.ondevicechange = () => { 336 | this.#updateSources() 337 | 338 | if (this.#stream) { 339 | let restartStream = true 340 | 341 | const { audioSources, currentDeviceId } = this.getPluginState() 342 | 343 | audioSources.forEach((audioSource) => { 344 | if (currentDeviceId === audioSource.deviceId) { 345 | restartStream = false 346 | } 347 | }) 348 | 349 | if (restartStream) { 350 | this.#stop() 351 | this.#start() 352 | } 353 | } 354 | } 355 | } 356 | } 357 | 358 | uninstall () { 359 | if (this.#stream) { 360 | this.#stop() 361 | } 362 | 363 | this.unmount() 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/AudioSourceSelect.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | export default ({ currentDeviceId, audioSources, onChangeSource }) => { 4 | return ( 5 |
6 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/DiscardButton.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | function DiscardButton ({ onDiscard, i18n }) { 4 | return ( 5 | 27 | ) 28 | } 29 | 30 | export default DiscardButton 31 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/PermissionsScreen.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | export default (props) => { 4 | const { icon, hasAudio, i18n } = props 5 | return ( 6 |
7 |
{icon()}
8 |

{hasAudio ? i18n('allowAudioAccessTitle') : i18n('noAudioTitle')}

9 |

{hasAudio ? i18n('allowAudioAccessDescription') : i18n('noAudioDescription')}

10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/RecordButton.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | export default function RecordButton ({ recording, onStartRecording, onStopRecording, i18n }) { 4 | if (recording) { 5 | return ( 6 | 18 | ) 19 | } 20 | 21 | return ( 22 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/RecordingLength.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import formatSeconds from './formatSeconds.js' 3 | 4 | export default function RecordingLength ({ recordingLengthSeconds, i18n }) { 5 | const formattedRecordingLengthSeconds = formatSeconds(recordingLengthSeconds) 6 | 7 | return ( 8 | 9 | {formattedRecordingLengthSeconds} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/RecordingScreen.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/media-has-caption */ 2 | import { h } from 'preact' 3 | import { useEffect, useRef } from 'preact/hooks' 4 | import RecordButton from './RecordButton.jsx' 5 | import RecordingLength from './RecordingLength.jsx' 6 | import AudioSourceSelect from './AudioSourceSelect.jsx' 7 | import AudioOscilloscope from './audio-oscilloscope/index.js' 8 | import SubmitButton from './SubmitButton.jsx' 9 | import DiscardButton from './DiscardButton.jsx' 10 | 11 | export default function RecordingScreen (props) { 12 | const { 13 | stream, 14 | recordedAudio, 15 | onStop, 16 | recording, 17 | supportsRecording, 18 | audioSources, 19 | showAudioSourceDropdown, 20 | onSubmit, 21 | i18n, 22 | onStartRecording, 23 | onStopRecording, 24 | onDiscardRecordedAudio, 25 | recordingLengthSeconds, 26 | } = props 27 | 28 | const canvasEl = useRef(null) 29 | const oscilloscope = useRef(null) 30 | 31 | // componentDidMount / componentDidUnmount 32 | useEffect(() => { 33 | return () => { 34 | oscilloscope.current = null 35 | onStop() 36 | } 37 | }, [onStop]) 38 | 39 | // componentDidUpdate 40 | useEffect(() => { 41 | if (!recordedAudio) { 42 | oscilloscope.current = new AudioOscilloscope(canvasEl.current, { 43 | canvas: { 44 | width: 600, 45 | height: 600, 46 | }, 47 | canvasContext: { 48 | lineWidth: 2, 49 | fillStyle: 'rgb(0,0,0)', 50 | strokeStyle: 'green', 51 | }, 52 | }) 53 | oscilloscope.current.draw() 54 | 55 | if (stream) { 56 | const audioContext = new AudioContext() 57 | const source = audioContext.createMediaStreamSource(stream) 58 | oscilloscope.current.addSource(source) 59 | } 60 | } 61 | }, [recordedAudio, stream]) 62 | 63 | const hasRecordedAudio = recordedAudio != null 64 | const shouldShowRecordButton = !hasRecordedAudio && supportsRecording 65 | const shouldShowAudioSourceDropdown = showAudioSourceDropdown 66 | && !hasRecordedAudio 67 | && audioSources 68 | && audioSources.length > 1 69 | 70 | return ( 71 |
72 |
73 | {hasRecordedAudio 74 | ? ( 75 |
87 |
88 |
89 | {shouldShowAudioSourceDropdown 90 | ? AudioSourceSelect(props) 91 | : null} 92 |
93 |
94 | {shouldShowRecordButton && ( 95 | 101 | )} 102 | 103 | {hasRecordedAudio && } 104 | 105 | {hasRecordedAudio && } 106 |
107 | 108 |
109 | {!hasRecordedAudio && ( 110 | 111 | )} 112 |
113 |
114 |
115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/SubmitButton.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | function SubmitButton ({ onSubmit, i18n }) { 4 | return ( 5 | 25 | ) 26 | } 27 | 28 | export default SubmitButton 29 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/audio-oscilloscope/LICENCE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (C) 2015 Miguel Mota 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/audio-oscilloscope/index.js: -------------------------------------------------------------------------------- 1 | function isFunction (v) { 2 | return typeof v === 'function' 3 | } 4 | 5 | function result (v) { 6 | return isFunction(v) ? v() : v 7 | } 8 | 9 | /* Audio Oscilloscope 10 | https://github.com/miguelmota/audio-oscilloscope 11 | */ 12 | export default class AudioOscilloscope { 13 | constructor (canvas, options = {}) { 14 | const canvasOptions = options.canvas || {} 15 | const canvasContextOptions = options.canvasContext || {} 16 | this.analyser = null 17 | this.bufferLength = 0 18 | this.dataArray = [] 19 | this.canvas = canvas 20 | this.width = result(canvasOptions.width) || this.canvas.width 21 | this.height = result(canvasOptions.height) || this.canvas.height 22 | this.canvas.width = this.width 23 | this.canvas.height = this.height 24 | this.canvasContext = this.canvas.getContext('2d') 25 | this.canvasContext.fillStyle = result(canvasContextOptions.fillStyle) || 'rgb(255, 255, 255)' 26 | this.canvasContext.strokeStyle = result(canvasContextOptions.strokeStyle) || 'rgb(0, 0, 0)' 27 | this.canvasContext.lineWidth = result(canvasContextOptions.lineWidth) || 1 28 | this.onDrawFrame = isFunction(options.onDrawFrame) ? options.onDrawFrame : () => {} 29 | } 30 | 31 | addSource (streamSource) { 32 | this.streamSource = streamSource 33 | this.audioContext = this.streamSource.context 34 | this.analyser = this.audioContext.createAnalyser() 35 | this.analyser.fftSize = 2048 36 | this.bufferLength = this.analyser.frequencyBinCount 37 | this.source = this.audioContext.createBufferSource() 38 | this.dataArray = new Uint8Array(this.bufferLength) 39 | this.analyser.getByteTimeDomainData(this.dataArray) 40 | this.streamSource.connect(this.analyser) 41 | } 42 | 43 | draw () { 44 | const { analyser, dataArray, bufferLength } = this 45 | const ctx = this.canvasContext 46 | const w = this.width 47 | const h = this.height 48 | 49 | if (analyser) { 50 | analyser.getByteTimeDomainData(dataArray) 51 | } 52 | 53 | ctx.fillRect(0, 0, w, h) 54 | ctx.beginPath() 55 | 56 | const sliceWidth = (w * 1.0) / bufferLength 57 | let x = 0 58 | 59 | if (!bufferLength) { 60 | ctx.moveTo(0, this.height / 2) 61 | } 62 | 63 | for (let i = 0; i < bufferLength; i++) { 64 | const v = dataArray[i] / 128.0 65 | const y = v * (h / 2) 66 | 67 | if (i === 0) { 68 | ctx.moveTo(x, y) 69 | } else { 70 | ctx.lineTo(x, y) 71 | } 72 | 73 | x += sliceWidth 74 | } 75 | 76 | ctx.lineTo(w, h / 2) 77 | ctx.stroke() 78 | 79 | this.onDrawFrame(this) 80 | requestAnimationFrame(this.#draw) 81 | } 82 | 83 | #draw = () => this.draw() 84 | } 85 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/formatSeconds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes an Integer value of seconds (e.g. 83) and converts it into a human-readable formatted string (e.g. '1:23'). 3 | * 4 | * @param {Integer} seconds 5 | * @returns {string} the formatted seconds (e.g. '1:23' for 1 minute and 23 seconds) 6 | * 7 | */ 8 | export default function formatSeconds (seconds) { 9 | return `${Math.floor( 10 | seconds / 60, 11 | )}:${String(seconds % 60).padStart(2, 0)}` 12 | } 13 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/formatSeconds.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import formatSeconds from './formatSeconds.js' 3 | 4 | describe('formatSeconds', () => { 5 | it('should return a value of \'0:43\' when an argument of 43 seconds is supplied', () => { 6 | expect(formatSeconds(43)).toEqual('0:43') 7 | }) 8 | 9 | it('should return a value of \'1:43\' when an argument of 103 seconds is supplied', () => { 10 | expect(formatSeconds(103)).toEqual('1:43') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Audio.jsx' 2 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/locale.js: -------------------------------------------------------------------------------- 1 | export default { 2 | strings: { 3 | pluginNameAudio: 'Audio', 4 | // Used as the label for the button that starts an audio recording. 5 | // This is not visibly rendered but is picked up by screen readers. 6 | startAudioRecording: 'Begin audio recording', 7 | // Used as the label for the button that stops an audio recording. 8 | // This is not visibly rendered but is picked up by screen readers. 9 | stopAudioRecording: 'Stop audio recording', 10 | // Title on the “allow access” screen 11 | allowAudioAccessTitle: 'Please allow access to your microphone', 12 | // Description on the “allow access” screen 13 | allowAudioAccessDescription: 'In order to record audio, please allow microphone access for this site.', 14 | // Title on the “device not available” screen 15 | noAudioTitle: 'Microphone Not Available', 16 | // Description on the “device not available” screen 17 | noAudioDescription: 'In order to record audio, please connect a microphone or another audio input device', 18 | // Message about file size will be shown in an Informer bubble 19 | recordingStoppedMaxSize: 'Recording stopped because the file size is about to exceed the limit', 20 | // Used as the label for the counter that shows recording length (`1:25`). 21 | // This is not visibly rendered but is picked up by screen readers. 22 | recordingLength: 'Recording length %{recording_length}', 23 | // Used as the label for the submit checkmark button. 24 | // This is not visibly rendered but is picked up by screen readers. 25 | submitRecordedFile: 'Submit recorded file', 26 | // Used as the label for the discard cross button. 27 | // This is not visibly rendered but is picked up by screen readers. 28 | discardRecordedFile: 'Discard recorded file', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/style.scss: -------------------------------------------------------------------------------- 1 | @import '@uppy/core/src/_utils.scss'; 2 | @import '@uppy/core/src/_variables.scss'; 3 | 4 | .uppy-Audio-container { 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | flex-direction: column; 11 | } 12 | 13 | .uppy-Audio-audioContainer { 14 | display: flex; 15 | width: 100%; 16 | height: 100%; 17 | background-color: $gray-300; 18 | position: relative; 19 | justify-content: center; 20 | align-items: center; 21 | } 22 | 23 | .uppy-Audio-player { 24 | width: 85%; 25 | border-radius: 12px; 26 | } 27 | 28 | .uppy-Audio-canvas { 29 | width: 100%; 30 | height: 100%; 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | right: 0; 35 | bottom: 0; 36 | } 37 | 38 | .uppy-Audio-footer { 39 | width: 100%; 40 | // min-height: 75px; 41 | display: flex; 42 | flex-wrap: wrap; 43 | align-items: center; 44 | justify-content: space-between; 45 | padding: 20px 20px; 46 | } 47 | 48 | .uppy-Audio-audioSourceContainer { 49 | width: 100%; 50 | flex-grow: 0; 51 | } 52 | 53 | .uppy-size--lg .uppy-Audio-audioSourceContainer { 54 | width: 33%; 55 | margin: 0; // vertical alignment handled by the flexbox wrapper 56 | } 57 | 58 | .uppy-Audio-audioSource-select { 59 | display: block; 60 | font-size: 16px; 61 | line-height: 1.2; 62 | padding: 0.4em 1em 0.3em 0.4em; 63 | width: 100%; 64 | max-width: 90%; 65 | border: 1px solid $gray-600; 66 | background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23757575%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); 67 | background-repeat: no-repeat; 68 | background-position: 69 | right 0.4em top 50%, 70 | 0 0; 71 | background-size: 72 | 0.65em auto, 73 | 100%; 74 | margin: auto; 75 | margin-bottom: 10px; 76 | white-space: nowrap; 77 | text-overflow: ellipsis; 78 | 79 | .uppy-size--lg & { 80 | font-size: 14px; 81 | margin-bottom: 0; 82 | } 83 | } 84 | 85 | .uppy-Audio-audioSource-select::-ms-expand { 86 | display: none; 87 | } 88 | 89 | .uppy-Audio-buttonContainer { 90 | width: 50%; 91 | margin-left: 25%; 92 | text-align: center; 93 | flex: 1; 94 | } 95 | 96 | .uppy-size--lg .uppy-Audio-buttonContainer { 97 | width: 34%; 98 | margin-left: 0; 99 | } 100 | 101 | .uppy-Audio-recordingLength { 102 | width: 25%; 103 | flex-grow: 0; 104 | color: $gray-600; 105 | font-family: $font-family-mono; 106 | text-align: right; 107 | } 108 | 109 | .uppy-size--lg .uppy-Audio-recordingLength { 110 | width: 33%; 111 | } 112 | 113 | .uppy-Audio-button { 114 | @include blue-border-focus; 115 | width: 45px; 116 | height: 45px; 117 | border-radius: 50%; 118 | background-color: $red; 119 | color: $white; 120 | cursor: pointer; 121 | transition: all 0.3s; 122 | 123 | &:hover { 124 | background-color: darken($red, 5%); 125 | } 126 | 127 | [data-uppy-theme='dark'] & { 128 | @include blue-border-focus--dark; 129 | } 130 | } 131 | 132 | .uppy-Audio-button--submit { 133 | background-color: $blue; 134 | margin: 0 12px; 135 | 136 | &:hover { 137 | background-color: darken($blue, 5%); 138 | } 139 | } 140 | 141 | .uppy-Audio-button svg { 142 | width: 26px; 143 | height: 26px; 144 | max-width: 100%; 145 | max-height: 100%; 146 | display: inline-block; 147 | vertical-align: text-top; 148 | overflow: hidden; 149 | fill: currentColor; 150 | } 151 | 152 | .uppy-size--md .uppy-Audio-button { 153 | width: 60px; 154 | height: 60px; 155 | } 156 | 157 | .uppy-Audio-permissons { 158 | padding: 15px; 159 | display: flex; 160 | align-items: center; 161 | justify-content: center; 162 | flex-flow: column wrap; 163 | height: 100%; 164 | flex: 1; 165 | } 166 | 167 | .uppy-Audio-permissons p { 168 | max-width: 450px; 169 | line-height: 1.3; 170 | text-align: center; 171 | line-height: 1.45; 172 | color: $gray-500; 173 | margin: 0; 174 | } 175 | 176 | .uppy-Audio-permissonsIcon svg { 177 | width: 100px; 178 | height: 75px; 179 | color: $gray-400; 180 | margin-bottom: 30px; 181 | } 182 | 183 | .uppy-Audio-title { 184 | font-size: 22px; 185 | line-height: 1.35; 186 | font-weight: 400; 187 | margin: 0; 188 | margin-bottom: 5px; 189 | padding: 0 15px; 190 | max-width: 500px; 191 | text-align: center; 192 | color: $gray-800; 193 | 194 | [data-uppy-theme='dark'] & { 195 | color: $gray-200; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/supportsMediaRecorder.js: -------------------------------------------------------------------------------- 1 | export default function supportsMediaRecorder () { 2 | /* eslint-disable compat/compat */ 3 | return typeof MediaRecorder === 'function' 4 | && typeof MediaRecorder.prototype?.start === 'function' 5 | /* eslint-enable compat/compat */ 6 | } 7 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/src/supportsMediaRecorder.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import { describe, expect, it } from 'vitest' 3 | import supportsMediaRecorder from './supportsMediaRecorder.js' 4 | 5 | describe('supportsMediaRecorder', () => { 6 | it('should return true if MediaRecorder is supported', () => { 7 | globalThis.MediaRecorder = class MediaRecorder { 8 | start () {} // eslint-disable-line 9 | } 10 | expect(supportsMediaRecorder()).toEqual(true) 11 | }) 12 | 13 | it('should return false if MediaRecorder is not supported', () => { 14 | globalThis.MediaRecorder = undefined 15 | expect(supportsMediaRecorder()).toEqual(false) 16 | 17 | globalThis.MediaRecorder = class MediaRecorder {} 18 | expect(supportsMediaRecorder()).toEqual(false) 19 | 20 | globalThis.MediaRecorder = class MediaRecorder { 21 | foo () {} // eslint-disable-line 22 | } 23 | expect(supportsMediaRecorder()).toEqual(false) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { PluginTarget, UIPlugin, UIPluginOptions } from '@uppy/core' 2 | import type AudioLocale from './generatedLocale' 3 | 4 | export interface AudioOptions extends UIPluginOptions { 5 | target?: PluginTarget 6 | showAudioSourceDropdown?: boolean 7 | locale?: AudioLocale 8 | } 9 | 10 | declare class Audio extends UIPlugin {} 11 | 12 | export default Audio 13 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/audio/types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import Uppy from '@uppy/core' 2 | import Audio from '..' 3 | 4 | { 5 | const uppy = new Uppy() 6 | 7 | uppy.use(Audio, { 8 | target: 'body', 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @uppy/aws-s3-multipart 2 | 3 | ## 3.8.0 4 | 5 | Released: 2023-10-20 6 | Included in: Uppy v3.18.0 7 | 8 | - @uppy/aws-s3-multipart: fix `TypeError` (Antoine du Hamel / #4748) 9 | - @uppy/aws-s3-multipart: pass `signal` as separate arg for backward compat (Antoine du Hamel / #4746) 10 | - @uppy/aws-s3-multipart: fix `uploadURL` when using `PUT` (Antoine du Hamel / #4701) 11 | 12 | ## 3.7.0 13 | 14 | Released: 2023-09-29 15 | Included in: Uppy v3.17.0 16 | 17 | - @uppy/aws-s3-multipart: retry signature request (Merlijn Vos / #4691) 18 | - @uppy/aws-s3-multipart: aws-s3-multipart - call `#setCompanionHeaders` in `setOptions` (jur-ng / #4687) 19 | 20 | ## 3.6.0 21 | 22 | Released: 2023-09-05 23 | Included in: Uppy v3.15.0 24 | 25 | - @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/companion-client,@uppy/core,@uppy/tus,@uppy/utils,@uppy/xhr-upload: Move remote file upload logic into companion-client (Merlijn Vos / #4573) 26 | 27 | ## 3.5.4 28 | 29 | Released: 2023-08-23 30 | Included in: Uppy v3.14.1 31 | 32 | - @uppy/aws-s3-multipart: fix types when using deprecated option (Antoine du Hamel / #4634) 33 | - @uppy/aws-s3-multipart,@uppy/aws-s3: allow empty objects for `fields` types (Antoine du Hamel / #4631) 34 | 35 | ## 3.5.3 36 | 37 | Released: 2023-08-15 38 | Included in: Uppy v3.14.0 39 | 40 | - @uppy/aws-s3-multipart: pass the `uploadURL` back to the caller (Antoine du Hamel / #4614) 41 | - @uppy/aws-s3,@uppy/aws-s3-multipart: update types (Antoine du Hamel / #4611) 42 | - @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/companion,@uppy/transloadit,@uppy/xhr-upload: use uppercase HTTP method names (Antoine du Hamel / #4612) 43 | - @uppy/aws-s3,@uppy/aws-s3-multipart: update types (bdirito / #4576) 44 | 45 | ## 3.5.2 46 | 47 | Released: 2023-07-24 48 | Included in: Uppy v3.13.1 49 | 50 | - @uppy/aws-s3-multipart: refresh file before calling user-defined functions (mjlumetta / #4557) 51 | 52 | ## 3.5.1 53 | 54 | Released: 2023-07-20 55 | Included in: Uppy v3.13.0 56 | 57 | - @uppy/aws-s3-multipart: fix crash on pause/resume (Merlijn Vos / #4581) 58 | - @uppy/aws-s3-multipart: do not access `globalThis.crypto` on the top-level (Bryan J Swift / #4584) 59 | 60 | ## 3.5.0 61 | 62 | Released: 2023-07-13 63 | Included in: Uppy v3.12.0 64 | 65 | - @uppy/aws-s3-multipart: add support for signing on the client (Antoine du Hamel / #4519) 66 | - @uppy/aws-s3-multipart: fix lint warning (Antoine du Hamel / #4569) 67 | - @uppy/aws-s3-multipart: fix support for non-multipart PUT upload (Antoine du Hamel / #4568) 68 | 69 | ## 3.4.1 70 | 71 | Released: 2023-07-06 72 | Included in: Uppy v3.11.0 73 | 74 | - @uppy/aws-s3-multipart: increase priority of abort and complete (Stefan Schonert / #4542) 75 | - @uppy/aws-s3-multipart: fix upload retry using an outdated ID (Antoine du Hamel / #4544) 76 | - @uppy/aws-s3-multipart: fix Golden Retriever integration (Antoine du Hamel / #4526) 77 | - @uppy/aws-s3-multipart: add types to internal fields (Antoine du Hamel / #4535) 78 | - @uppy/aws-s3-multipart: fix pause/resume (Antoine du Hamel / #4523) 79 | - @uppy/aws-s3-multipart: fix resume single-chunk multipart uploads (Antoine du Hamel / #4528) 80 | - @uppy/aws-s3-multipart: disable pause/resume for remote uploads in the UI (Artur Paikin / #4500) 81 | 82 | ## 3.4.0 83 | 84 | Released: 2023-06-19 85 | Included in: Uppy v3.10.0 86 | 87 | - @uppy/aws-s3-multipart: fix the chunk size calculation (Antoine du Hamel / #4508) 88 | - @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/tus,@uppy/utils,@uppy/xhr-upload: When file is removed (or all are canceled), controller.abort queued requests (Artur Paikin / #4504) 89 | - @uppy/aws-s3-multipart,@uppy/tus,@uppy/xhr-upload: Don't close socket while upload is still in progress (Artur Paikin / #4479) 90 | - @uppy/aws-s3-multipart: fix `getUploadParameters` option (Antoine du Hamel / #4465) 91 | 92 | ## 3.3.0 93 | 94 | Released: 2023-05-02 95 | Included in: Uppy v3.9.0 96 | 97 | - @uppy/aws-s3-multipart: allowedMetaFields: null means “include all” (Artur Paikin / #4437) 98 | - @uppy/aws-s3-multipart: add `shouldUseMultipart ` option (Antoine du Hamel / #4205) 99 | - @uppy/aws-s3-multipart: make retries more robust (Antoine du Hamel / #4424) 100 | 101 | ## 3.1.3 102 | 103 | Released: 2023-04-04 104 | Included in: Uppy v3.7.0 105 | 106 | - @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/tus,@uppy/xhr-upload: make sure that we reset serverToken when an upload fails (Mikael Finstad / #4376) 107 | - @uppy/aws-s3-multipart: do not auto-open sockets, clean them up on abort (Antoine du Hamel) 108 | 109 | ## 3.1.2 110 | 111 | Released: 2023-01-26 112 | Included in: Uppy v3.4.0 113 | 114 | - @uppy/aws-s3-multipart: fix metadata shape (Antoine du Hamel / #4267) 115 | - @uppy/aws-s3-multipart: add support for `allowedMetaFields` option (Antoine du Hamel / #4215) 116 | - @uppy/aws-s3-multipart: fix singPart type (Stefan Schonert / #4224) 117 | 118 | ## 3.1.1 119 | 120 | Released: 2022-11-16 121 | Included in: Uppy v3.3.1 122 | 123 | - @uppy/aws-s3-multipart: handle slow connections better (Antoine du Hamel / #4213) 124 | - @uppy/aws-s3-multipart: Fix typo in url check (Christian Franke / #4211) 125 | 126 | ## 3.1.0 127 | 128 | Released: 2022-11-10 129 | Included in: Uppy v3.3.0 130 | 131 | - @uppy/aws-s3-multipart: empty the queue when pausing (Antoine du Hamel / #4203) 132 | - @uppy/aws-s3-multipart: refactor rate limiting approach (Antoine du Hamel / #4187) 133 | - @uppy/aws-s3-multipart: change limit to 6 (Antoine du Hamel / #4199) 134 | - @uppy/aws-s3-multipart: remove unused `timeout` option (Antoine du Hamel / #4186) 135 | - @uppy/aws-s3-multipart,@uppy/tus: fix `Timed out waiting for socket` (Antoine du Hamel / #4177) 136 | 137 | ## 3.0.2 138 | 139 | Released: 2022-09-25 140 | Included in: Uppy v3.1.0 141 | 142 | - @uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/companion,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/redux-dev-tools,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: add missing entries to changelog for individual packages (Antoine du Hamel / #4092) 143 | 144 | ## 3.0.0 145 | 146 | Released: 2022-08-22 147 | Included in: Uppy v3.0.0 148 | 149 | - Switch to ESM 150 | 151 | ## 3.0.0-beta.4 152 | 153 | Released: 2022-08-16 154 | Included in: Uppy v3.0.0-beta.5 155 | 156 | - @uppy/aws-s3-multipart: Fix when using Companion (Merlijn Vos / #3969) 157 | - @uppy/aws-s3-multipart: Fix race condition in `#uploadParts` (Morgan Zolob / #3955) 158 | - @uppy/aws-s3-multipart: ignore exception inside `abortMultipartUpload` (Antoine du Hamel / #3950) 159 | 160 | ## 3.0.0-beta.3 161 | 162 | Released: 2022-08-03 163 | Included in: Uppy v3.0.0-beta.4 164 | 165 | - @uppy/aws-s3-multipart: Correctly handle errors for `prepareUploadParts` (Merlijn Vos / #3912) 166 | 167 | ## 3.0.0-beta.2 168 | 169 | Released: 2022-07-27 170 | Included in: Uppy v3.0.0-beta.3 171 | 172 | - @uppy/aws-s3-multipart: make `headers` part indexed too in `prepareUploadParts` (Merlijn Vos / #3895) 173 | 174 | ## 2.4.1 175 | 176 | Released: 2022-06-07 177 | Included in: Uppy v2.12.0 178 | 179 | - @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/tus: queue socket token requests for remote files (Merlijn Vos / #3797) 180 | - @uppy/aws-s3-multipart: allow `companionHeaders` to be modified with `setOptions` (Paulo Lemos Neto / #3770) 181 | 182 | ## 2.4.0 183 | 184 | Released: 2022-05-30 185 | Included in: Uppy v2.11.0 186 | 187 | - @uppy/angular,@uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/onedrive,@uppy/progress-bar,@uppy/react,@uppy/redux-dev-tools,@uppy/robodog,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: doc: update bundler recommendation (Antoine du Hamel / #3763) 188 | - @uppy/aws-s3-multipart: refactor to ESM (Antoine du Hamel / #3672) 189 | 190 | ## 2.3.0 191 | 192 | Released: 2022-05-14 193 | Included in: Uppy v2.10.0 194 | 195 | - @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/core,@uppy/react,@uppy/transloadit,@uppy/tus,@uppy/xhr-upload: proposal: Cancel assemblies optional (Mikael Finstad / #3575) 196 | - @uppy/aws-s3-multipart: export interface AwsS3MultipartOptions (Matteo Padovano / #3709) 197 | 198 | ## 2.2.2 199 | 200 | Released: 2022-04-27 201 | Included in: Uppy v2.9.4 202 | 203 | - @uppy/aws-s3-multipart: Add `companionCookiesRule` type to @uppy/aws-s3-multipart (Mauricio Ribeiro / #3623) 204 | 205 | ## 2.2.1 206 | 207 | Released: 2022-03-02 208 | Included in: Uppy v2.7.0 209 | 210 | - @uppy/aws-s3-multipart: Add chunks back to prepareUploadParts, indexed by partNumber (Kevin West / #3520) 211 | 212 | ## 2.2.0 213 | 214 | Released: 2021-12-07 215 | Included in: Uppy v2.3.0 216 | 217 | - @uppy/aws-s3-multipart: Drop `lockedCandidatesForBatch` and mark chunks as 'busy' when preparing (Yegor Yarko / #3342) 218 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Transloadit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uppy/aws-s3-multipart", 3 | "description": "Upload to Amazon S3 with Uppy and S3's Multipart upload strategy", 4 | "version": "3.9.0", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "type": "module", 8 | "types": "types/index.d.ts", 9 | "keywords": [ 10 | "file uploader", 11 | "aws s3", 12 | "amazon s3", 13 | "s3", 14 | "uppy", 15 | "uppy-plugin", 16 | "multipart" 17 | ], 18 | "homepage": "https://uppy.io", 19 | "bugs": { 20 | "url": "https://github.com/transloadit/uppy/issues" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/transloadit/uppy.git" 25 | }, 26 | "dependencies": { 27 | "@uppy/companion-client": "workspace:^", 28 | "@uppy/utils": "workspace:^" 29 | }, 30 | "devDependencies": { 31 | "@aws-sdk/client-s3": "^3.362.0", 32 | "@aws-sdk/s3-request-presigner": "^3.362.0", 33 | "nock": "^13.1.0", 34 | "vitest": "^0.34.5", 35 | "whatwg-fetch": "3.6.2" 36 | }, 37 | "peerDependencies": { 38 | "@uppy/core": "workspace:^" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/src/MultipartUploader.js: -------------------------------------------------------------------------------- 1 | import { AbortController } from '@uppy/utils/lib/AbortController' 2 | 3 | const MB = 1024 * 1024 4 | 5 | const defaultOptions = { 6 | getChunkSize (file) { 7 | return Math.ceil(file.size / 10000) 8 | }, 9 | onProgress () {}, 10 | onPartComplete () {}, 11 | onSuccess () {}, 12 | onError (err) { 13 | throw err 14 | }, 15 | } 16 | 17 | function ensureInt (value) { 18 | if (typeof value === 'string') { 19 | return parseInt(value, 10) 20 | } 21 | if (typeof value === 'number') { 22 | return value 23 | } 24 | throw new TypeError('Expected a number') 25 | } 26 | 27 | export const pausingUploadReason = Symbol('pausing upload, not an actual error') 28 | 29 | /** 30 | * A MultipartUploader instance is used per file upload to determine whether a 31 | * upload should be done as multipart or as a regular S3 upload 32 | * (based on the user-provided `shouldUseMultipart` option value) and to manage 33 | * the chunk splitting. 34 | */ 35 | class MultipartUploader { 36 | #abortController = new AbortController() 37 | 38 | /** @type {import("../types/chunk").Chunk[]} */ 39 | #chunks 40 | 41 | /** @type {{ uploaded: number, etag?: string, done?: boolean }[]} */ 42 | #chunkState 43 | 44 | /** 45 | * The (un-chunked) data to upload. 46 | * 47 | * @type {Blob} 48 | */ 49 | #data 50 | 51 | /** @type {import("@uppy/core").UppyFile} */ 52 | #file 53 | 54 | /** @type {boolean} */ 55 | #uploadHasStarted = false 56 | 57 | /** @type {(err?: Error | any) => void} */ 58 | #onError 59 | 60 | /** @type {() => void} */ 61 | #onSuccess 62 | 63 | /** @type {import('../types/index').AwsS3MultipartOptions["shouldUseMultipart"]} */ 64 | #shouldUseMultipart 65 | 66 | /** @type {boolean} */ 67 | #isRestoring 68 | 69 | #onReject = (err) => (err?.cause === pausingUploadReason ? null : this.#onError(err)) 70 | 71 | #maxMultipartParts = 10_000 72 | 73 | #minPartSize = 5 * MB 74 | 75 | constructor (data, options) { 76 | this.options = { 77 | ...defaultOptions, 78 | ...options, 79 | } 80 | // Use default `getChunkSize` if it was null or something 81 | this.options.getChunkSize ??= defaultOptions.getChunkSize 82 | 83 | this.#data = data 84 | this.#file = options.file 85 | this.#onSuccess = this.options.onSuccess 86 | this.#onError = this.options.onError 87 | this.#shouldUseMultipart = this.options.shouldUseMultipart 88 | 89 | // When we are restoring an upload, we already have an UploadId and a Key. Otherwise 90 | // we need to call `createMultipartUpload` to get an `uploadId` and a `key`. 91 | // Non-multipart uploads are not restorable. 92 | this.#isRestoring = options.uploadId && options.key 93 | 94 | this.#initChunks() 95 | } 96 | 97 | // initChunks checks the user preference for using multipart uploads (opts.shouldUseMultipart) 98 | // and calculates the optimal part size. When using multipart part uploads every part except for the last has 99 | // to be at least 5 MB and there can be no more than 10K parts. 100 | // This means we sometimes need to change the preferred part size from the user in order to meet these requirements. 101 | #initChunks () { 102 | const fileSize = this.#data.size 103 | const shouldUseMultipart = typeof this.#shouldUseMultipart === 'function' 104 | ? this.#shouldUseMultipart(this.#file) 105 | : Boolean(this.#shouldUseMultipart) 106 | 107 | if (shouldUseMultipart && fileSize > this.#minPartSize) { 108 | // At least 5MB per request: 109 | let chunkSize = Math.max(this.options.getChunkSize(this.#data), this.#minPartSize) 110 | let arraySize = Math.floor(fileSize / chunkSize) 111 | 112 | // At most 10k requests per file: 113 | if (arraySize > this.#maxMultipartParts) { 114 | arraySize = this.#maxMultipartParts 115 | chunkSize = fileSize / this.#maxMultipartParts 116 | } 117 | this.#chunks = Array(arraySize) 118 | 119 | for (let offset = 0, j = 0; offset < fileSize; offset += chunkSize, j++) { 120 | const end = Math.min(fileSize, offset + chunkSize) 121 | 122 | // Defer data fetching/slicing until we actually need the data, because it's slow if we have a lot of files 123 | const getData = () => { 124 | const i2 = offset 125 | return this.#data.slice(i2, end) 126 | } 127 | 128 | this.#chunks[j] = { 129 | getData, 130 | onProgress: this.#onPartProgress(j), 131 | onComplete: this.#onPartComplete(j), 132 | shouldUseMultipart, 133 | } 134 | if (this.#isRestoring) { 135 | const size = offset + chunkSize > fileSize ? fileSize - offset : chunkSize 136 | // setAsUploaded is called by listPart, to keep up-to-date the 137 | // quantity of data that is left to actually upload. 138 | this.#chunks[j].setAsUploaded = () => { 139 | this.#chunks[j] = null 140 | this.#chunkState[j].uploaded = size 141 | } 142 | } 143 | } 144 | } else { 145 | this.#chunks = [{ 146 | getData: () => this.#data, 147 | onProgress: this.#onPartProgress(0), 148 | onComplete: this.#onPartComplete(0), 149 | shouldUseMultipart, 150 | }] 151 | } 152 | 153 | this.#chunkState = this.#chunks.map(() => ({ uploaded: 0 })) 154 | } 155 | 156 | #createUpload () { 157 | this 158 | .options.companionComm.uploadFile(this.#file, this.#chunks, this.#abortController.signal) 159 | .then(this.#onSuccess, this.#onReject) 160 | this.#uploadHasStarted = true 161 | } 162 | 163 | #resumeUpload () { 164 | this 165 | .options.companionComm.resumeUploadFile(this.#file, this.#chunks, this.#abortController.signal) 166 | .then(this.#onSuccess, this.#onReject) 167 | } 168 | 169 | #onPartProgress = (index) => (ev) => { 170 | if (!ev.lengthComputable) return 171 | 172 | this.#chunkState[index].uploaded = ensureInt(ev.loaded) 173 | 174 | const totalUploaded = this.#chunkState.reduce((n, c) => n + c.uploaded, 0) 175 | this.options.onProgress(totalUploaded, this.#data.size) 176 | } 177 | 178 | #onPartComplete = (index) => (etag) => { 179 | // This avoids the net::ERR_OUT_OF_MEMORY in Chromium Browsers. 180 | this.#chunks[index] = null 181 | this.#chunkState[index].etag = etag 182 | this.#chunkState[index].done = true 183 | 184 | const part = { 185 | PartNumber: index + 1, 186 | ETag: etag, 187 | } 188 | this.options.onPartComplete(part) 189 | } 190 | 191 | #abortUpload () { 192 | this.#abortController.abort() 193 | this.options.companionComm.abortFileUpload(this.#file).catch((err) => this.options.log(err)) 194 | } 195 | 196 | start () { 197 | if (this.#uploadHasStarted) { 198 | if (!this.#abortController.signal.aborted) this.#abortController.abort(pausingUploadReason) 199 | this.#abortController = new AbortController() 200 | this.#resumeUpload() 201 | } else if (this.#isRestoring) { 202 | this.options.companionComm.restoreUploadFile(this.#file, { uploadId: this.options.uploadId, key: this.options.key }) 203 | this.#resumeUpload() 204 | } else { 205 | this.#createUpload() 206 | } 207 | } 208 | 209 | pause () { 210 | this.#abortController.abort(pausingUploadReason) 211 | // Swap it out for a new controller, because this instance may be resumed later. 212 | this.#abortController = new AbortController() 213 | } 214 | 215 | abort (opts = undefined) { 216 | if (opts?.really) this.#abortUpload() 217 | else this.pause() 218 | } 219 | 220 | // TODO: remove this in the next major 221 | get chunkState () { 222 | return this.#chunkState 223 | } 224 | } 225 | 226 | export default MultipartUploader 227 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/src/createSignedURL.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a canonical request by concatenating the following strings, separated 3 | * by newline characters. This helps ensure that the signature that you 4 | * calculate and the signature that AWS calculates can match. 5 | * 6 | * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request 7 | * 8 | * @param {object} param0 9 | * @param {string} param0.method – The HTTP method. 10 | * @param {string} param0.CanonicalUri – The URI-encoded version of the absolute 11 | * path component URL (everything between the host and the question mark 12 | * character (?) that starts the query string parameters). If the absolute path 13 | * is empty, use a forward slash character (/). 14 | * @param {string} param0.CanonicalQueryString – The URL-encoded query string 15 | * parameters, separated by ampersands (&). Percent-encode reserved characters, 16 | * including the space character. Encode names and values separately. If there 17 | * are empty parameters, append the equals sign to the parameter name before 18 | * encoding. After encoding, sort the parameters alphabetically by key name. If 19 | * there is no query string, use an empty string (""). 20 | * @param {Record} param0.SignedHeaders – The request headers, 21 | * that will be signed, and their values, separated by newline characters. 22 | * For the values, trim any leading or trailing spaces, convert sequential 23 | * spaces to a single space, and separate the values for a multi-value header 24 | * using commas. You must include the host header (HTTP/1.1), and any x-amz-* 25 | * headers in the signature. You can optionally include other standard headers 26 | * in the signature, such as content-type. 27 | * @param {string} param0.HashedPayload – A string created using the payload in 28 | * the body of the HTTP request as input to a hash function. This string uses 29 | * lowercase hexadecimal characters. If the payload is empty, use an empty 30 | * string as the input to the hash function. 31 | * @returns {string} 32 | */ 33 | function createCanonicalRequest ({ 34 | method = 'PUT', 35 | CanonicalUri = '/', 36 | CanonicalQueryString = '', 37 | SignedHeaders, 38 | HashedPayload, 39 | }) { 40 | const headerKeys = Object.keys(SignedHeaders).map(k => k.toLowerCase()).sort() 41 | return [ 42 | method, 43 | CanonicalUri, 44 | CanonicalQueryString, 45 | ...headerKeys.map(k => `${k}:${SignedHeaders[k]}`), 46 | '', 47 | headerKeys.join(';'), 48 | HashedPayload, 49 | ].join('\n') 50 | } 51 | 52 | const ec = new TextEncoder() 53 | const algorithm = { name: 'HMAC', hash: 'SHA-256' } 54 | 55 | async function digest (data) { 56 | const { subtle } = globalThis.crypto 57 | return subtle.digest(algorithm.hash, ec.encode(data)) 58 | } 59 | 60 | async function generateHmacKey (secret) { 61 | const { subtle } = globalThis.crypto 62 | return subtle.importKey('raw', typeof secret === 'string' ? ec.encode(secret) : secret, algorithm, false, ['sign']) 63 | } 64 | 65 | function arrayBufferToHexString (arrayBuffer) { 66 | const byteArray = new Uint8Array(arrayBuffer) 67 | let hexString = '' 68 | for (let i = 0; i < byteArray.length; i++) { 69 | hexString += byteArray[i].toString(16).padStart(2, '0') 70 | } 71 | return hexString 72 | } 73 | 74 | async function hash (key, data) { 75 | const { subtle } = globalThis.crypto 76 | return subtle.sign(algorithm, await generateHmacKey(key), ec.encode(data)) 77 | } 78 | 79 | /** 80 | * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html 81 | * @param {Record} param0 82 | * @returns {Promise} the signed URL 83 | */ 84 | export default async function createSignedURL ({ 85 | accountKey, accountSecret, sessionToken, 86 | bucketName, 87 | Key, Region, 88 | expires, 89 | uploadId, partNumber, 90 | }) { 91 | const Service = 's3' 92 | const host = `${bucketName}.${Service}.${Region}.amazonaws.com` 93 | const CanonicalUri = `/${encodeURI(Key)}` 94 | const payload = 'UNSIGNED-PAYLOAD' 95 | 96 | const requestDateTime = new Date().toISOString().replace(/[-:]|\.\d+/g, '') // YYYYMMDDTHHMMSSZ 97 | const date = requestDateTime.slice(0, 8) // YYYYMMDD 98 | const scope = `${date}/${Region}/${Service}/aws4_request` 99 | 100 | const url = new URL(`https://${host}${CanonicalUri}`) 101 | // N.B.: URL search params needs to be added in the ASCII order 102 | url.searchParams.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256') 103 | url.searchParams.set('X-Amz-Content-Sha256', payload) 104 | url.searchParams.set('X-Amz-Credential', `${accountKey}/${scope}`) 105 | url.searchParams.set('X-Amz-Date', requestDateTime) 106 | url.searchParams.set('X-Amz-Expires', expires) 107 | // We are signing on the client, so we expect there's going to be a session token: 108 | url.searchParams.set('X-Amz-Security-Token', sessionToken) 109 | url.searchParams.set('X-Amz-SignedHeaders', 'host') 110 | // Those two are present only for Multipart Uploads: 111 | if (partNumber) url.searchParams.set('partNumber', partNumber) 112 | if (uploadId) url.searchParams.set('uploadId', uploadId) 113 | url.searchParams.set('x-id', partNumber && uploadId ? 'UploadPart' : 'PutObject') 114 | 115 | // Step 1: Create a canonical request 116 | const canonical = createCanonicalRequest({ 117 | CanonicalUri, 118 | CanonicalQueryString: url.search.slice(1), 119 | SignedHeaders: { 120 | host, 121 | }, 122 | HashedPayload: payload, 123 | }) 124 | 125 | // Step 2: Create a hash of the canonical request 126 | const hashedCanonical = arrayBufferToHexString(await digest(canonical)) 127 | 128 | // Step 3: Create a string to sign 129 | const stringToSign = [ 130 | `AWS4-HMAC-SHA256`, // The algorithm used to create the hash of the canonical request. 131 | requestDateTime, // The date and time used in the credential scope. 132 | scope, // The credential scope. This restricts the resulting signature to the specified Region and service. 133 | hashedCanonical, // The hash of the canonical request. 134 | ].join('\n') 135 | 136 | // Step 4: Calculate the signature 137 | const kDate = await hash(`AWS4${accountSecret}`, date) 138 | const kRegion = await hash(kDate, Region) 139 | const kService = await hash(kRegion, Service) 140 | const kSigning = await hash(kService, 'aws4_request') 141 | const signature = arrayBufferToHexString(await hash(kSigning, stringToSign)) 142 | 143 | // Step 5: Add the signature to the request 144 | url.searchParams.set('X-Amz-Signature', signature) 145 | 146 | return url 147 | } 148 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/src/createSignedURL.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, afterEach } from 'vitest' 2 | import assert from 'node:assert' 3 | import { S3Client, UploadPartCommand, PutObjectCommand } from '@aws-sdk/client-s3' 4 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner' 5 | import createSignedURL from './createSignedURL.js' 6 | 7 | const bucketName = 'some-bucket' 8 | const s3ClientOptions = { 9 | region: 'us-bar-1', 10 | credentials: { 11 | accessKeyId: 'foo', 12 | secretAccessKey: 'bar', 13 | sessionToken: 'foobar', 14 | }, 15 | } 16 | const { Date: OriginalDate } = globalThis 17 | 18 | describe('createSignedURL', () => { 19 | beforeEach(() => { 20 | const now_ms = OriginalDate.now() 21 | globalThis.Date = function Date () { 22 | if (new.target) { 23 | return Reflect.construct(OriginalDate, [now_ms]) 24 | } 25 | return Reflect.apply(OriginalDate, this, [now_ms]) 26 | } 27 | globalThis.Date.now = function now () { 28 | return now_ms 29 | } 30 | }) 31 | afterEach(() => { 32 | globalThis.Date = OriginalDate 33 | }) 34 | it('should be able to sign non-multipart upload', async () => { 35 | const client = new S3Client(s3ClientOptions) 36 | assert.strictEqual( 37 | (await createSignedURL({ 38 | accountKey: s3ClientOptions.credentials.accessKeyId, 39 | accountSecret: s3ClientOptions.credentials.secretAccessKey, 40 | sessionToken: s3ClientOptions.credentials.sessionToken, 41 | bucketName, 42 | Key: 'some/key', 43 | Region: s3ClientOptions.region, 44 | expires: 900, 45 | })).searchParams.get('X-Amz-Signature'), 46 | new URL(await getSignedUrl(client, new PutObjectCommand({ 47 | Bucket: bucketName, 48 | Fields: {}, 49 | Key: 'some/key', 50 | }, { expiresIn: 900 }))).searchParams.get('X-Amz-Signature'), 51 | ) 52 | }) 53 | it('should be able to sign multipart upload', async () => { 54 | const client = new S3Client(s3ClientOptions) 55 | const partNumber = 99 56 | const uploadId = 'dummyUploadId' 57 | assert.strictEqual( 58 | (await createSignedURL({ 59 | accountKey: s3ClientOptions.credentials.accessKeyId, 60 | accountSecret: s3ClientOptions.credentials.secretAccessKey, 61 | sessionToken: s3ClientOptions.credentials.sessionToken, 62 | uploadId, 63 | partNumber, 64 | bucketName, 65 | Key: 'some/key', 66 | Region: s3ClientOptions.region, 67 | expires: 900, 68 | })).searchParams.get('X-Amz-Signature'), 69 | new URL(await getSignedUrl(client, new UploadPartCommand({ 70 | Bucket: bucketName, 71 | UploadId: uploadId, 72 | PartNumber: partNumber, 73 | Key: 'some/key', 74 | }, { expiresIn: 900 }))).searchParams.get('X-Amz-Signature'), 75 | ) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/src/index.js: -------------------------------------------------------------------------------- 1 | import BasePlugin from '@uppy/core/lib/BasePlugin.js' 2 | import { RequestClient } from '@uppy/companion-client' 3 | import EventManager from '@uppy/utils/lib/EventManager' 4 | import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue' 5 | import { filterNonFailedFiles, filterFilesToEmitUploadStarted } from '@uppy/utils/lib/fileFilters' 6 | import { createAbortError } from '@uppy/utils/lib/AbortController' 7 | 8 | import MultipartUploader, { pausingUploadReason } from './MultipartUploader.js' 9 | import createSignedURL from './createSignedURL.js' 10 | import packageJson from '../package.json' 11 | 12 | function assertServerError (res) { 13 | if (res && res.error) { 14 | const error = new Error(res.message) 15 | Object.assign(error, res.error) 16 | throw error 17 | } 18 | return res 19 | } 20 | 21 | function removeMetadataFromURL (urlString) { 22 | const urlObject = new URL(urlString) 23 | urlObject.search = '' 24 | urlObject.hash = '' 25 | return urlObject.href 26 | } 27 | 28 | /** 29 | * Computes the expiry time for a request signed with temporary credentials. If 30 | * no expiration was provided, or an invalid value (e.g. in the past) is 31 | * provided, undefined is returned. This function assumes the client clock is in 32 | * sync with the remote server, which is a requirement for the signature to be 33 | * validated for AWS anyway. 34 | * 35 | * @param {import('../types/index.js').AwsS3STSResponse['credentials']} credentials 36 | * @returns {number | undefined} 37 | */ 38 | function getExpiry (credentials) { 39 | const expirationDate = credentials.Expiration 40 | if (expirationDate) { 41 | const timeUntilExpiry = Math.floor((new Date(expirationDate) - Date.now()) / 1000) 42 | if (timeUntilExpiry > 9) { 43 | return timeUntilExpiry 44 | } 45 | } 46 | return undefined 47 | } 48 | 49 | function getAllowedMetadata ({ meta, allowedMetaFields, querify = false }) { 50 | const metaFields = allowedMetaFields ?? Object.keys(meta) 51 | 52 | if (!meta) return {} 53 | 54 | return Object.fromEntries( 55 | metaFields 56 | .filter(key => meta[key] != null) 57 | .map((key) => { 58 | const realKey = querify ? `metadata[${key}]` : key 59 | const value = String(meta[key]) 60 | return [realKey, value] 61 | }), 62 | ) 63 | } 64 | 65 | function throwIfAborted (signal) { 66 | if (signal?.aborted) { throw createAbortError('The operation was aborted', { cause: signal.reason }) } 67 | } 68 | 69 | class HTTPCommunicationQueue { 70 | #abortMultipartUpload 71 | 72 | #cache = new WeakMap() 73 | 74 | #createMultipartUpload 75 | 76 | #fetchSignature 77 | 78 | #getUploadParameters 79 | 80 | #listParts 81 | 82 | #previousRetryDelay 83 | 84 | #requests 85 | 86 | #retryDelays 87 | 88 | #sendCompletionRequest 89 | 90 | #setS3MultipartState 91 | 92 | #uploadPartBytes 93 | 94 | #getFile 95 | 96 | constructor (requests, options, setS3MultipartState, getFile) { 97 | this.#requests = requests 98 | this.#setS3MultipartState = setS3MultipartState 99 | this.#getFile = getFile 100 | this.setOptions(options) 101 | } 102 | 103 | setOptions (options) { 104 | const requests = this.#requests 105 | 106 | if ('abortMultipartUpload' in options) { 107 | this.#abortMultipartUpload = requests.wrapPromiseFunction(options.abortMultipartUpload, { priority:1 }) 108 | } 109 | if ('createMultipartUpload' in options) { 110 | this.#createMultipartUpload = requests.wrapPromiseFunction(options.createMultipartUpload, { priority:-1 }) 111 | } 112 | if ('signPart' in options) { 113 | this.#fetchSignature = requests.wrapPromiseFunction(options.signPart) 114 | } 115 | if ('listParts' in options) { 116 | this.#listParts = requests.wrapPromiseFunction(options.listParts) 117 | } 118 | if ('completeMultipartUpload' in options) { 119 | this.#sendCompletionRequest = requests.wrapPromiseFunction(options.completeMultipartUpload, { priority:1 }) 120 | } 121 | if ('retryDelays' in options) { 122 | this.#retryDelays = options.retryDelays ?? [] 123 | } 124 | if ('uploadPartBytes' in options) { 125 | this.#uploadPartBytes = requests.wrapPromiseFunction(options.uploadPartBytes, { priority:Infinity }) 126 | } 127 | if ('getUploadParameters' in options) { 128 | this.#getUploadParameters = requests.wrapPromiseFunction(options.getUploadParameters) 129 | } 130 | } 131 | 132 | async #shouldRetry (err, retryDelayIterator) { 133 | const requests = this.#requests 134 | const status = err?.source?.status 135 | 136 | // TODO: this retry logic is taken out of Tus. We should have a centralized place for retrying, 137 | // perhaps the rate limited queue, and dedupe all plugins with that. 138 | if (status == null) { 139 | return false 140 | } 141 | if (status === 403 && err.message === 'Request has expired') { 142 | if (!requests.isPaused) { 143 | // We don't want to exhaust the retryDelayIterator as long as there are 144 | // more than one request in parallel, to give slower connection a chance 145 | // to catch up with the expiry set in Companion. 146 | if (requests.limit === 1 || this.#previousRetryDelay == null) { 147 | const next = retryDelayIterator.next() 148 | if (next == null || next.done) { 149 | return false 150 | } 151 | // If there are more than 1 request done in parallel, the RLQ limit is 152 | // decreased and the failed request is requeued after waiting for a bit. 153 | // If there is only one request in parallel, the limit can't be 154 | // decreased, so we iterate over `retryDelayIterator` as we do for 155 | // other failures. 156 | // `#previousRetryDelay` caches the value so we can re-use it next time. 157 | this.#previousRetryDelay = next.value 158 | } 159 | // No need to stop the other requests, we just want to lower the limit. 160 | requests.rateLimit(0) 161 | await new Promise(resolve => setTimeout(resolve, this.#previousRetryDelay)) 162 | } 163 | } else if (status === 429) { 164 | // HTTP 429 Too Many Requests => to avoid the whole download to fail, pause all requests. 165 | if (!requests.isPaused) { 166 | const next = retryDelayIterator.next() 167 | if (next == null || next.done) { 168 | return false 169 | } 170 | requests.rateLimit(next.value) 171 | } 172 | } else if (status > 400 && status < 500 && status !== 409) { 173 | // HTTP 4xx, the server won't send anything, it's doesn't make sense to retry 174 | return false 175 | } else if (typeof navigator !== 'undefined' && navigator.onLine === false) { 176 | // The navigator is offline, let's wait for it to come back online. 177 | if (!requests.isPaused) { 178 | requests.pause() 179 | window.addEventListener('online', () => { 180 | requests.resume() 181 | }, { once: true }) 182 | } 183 | } else { 184 | // Other error code means the request can be retried later. 185 | const next = retryDelayIterator.next() 186 | if (next == null || next.done) { 187 | return false 188 | } 189 | await new Promise(resolve => setTimeout(resolve, next.value)) 190 | } 191 | return true 192 | } 193 | 194 | async getUploadId (file, signal) { 195 | let cachedResult 196 | // As the cache is updated asynchronously, there could be a race condition 197 | // where we just miss a new result so we loop here until we get nothing back, 198 | // at which point it's out turn to create a new cache entry. 199 | while ((cachedResult = this.#cache.get(file.data)) != null) { 200 | try { 201 | return await cachedResult 202 | } catch { 203 | // In case of failure, we want to ignore the cached error. 204 | // At this point, either there's a new cached value, or we'll exit the loop a create a new one. 205 | } 206 | } 207 | 208 | const promise = this.#createMultipartUpload(this.#getFile(file), signal) 209 | 210 | const abortPromise = () => { 211 | promise.abort(signal.reason) 212 | this.#cache.delete(file.data) 213 | } 214 | signal.addEventListener('abort', abortPromise, { once: true }) 215 | this.#cache.set(file.data, promise) 216 | promise.then(async (result) => { 217 | signal.removeEventListener('abort', abortPromise) 218 | this.#setS3MultipartState(file, result) 219 | this.#cache.set(file.data, result) 220 | }, () => { 221 | signal.removeEventListener('abort', abortPromise) 222 | this.#cache.delete(file.data) 223 | }) 224 | 225 | return promise 226 | } 227 | 228 | async abortFileUpload (file) { 229 | const result = this.#cache.get(file.data) 230 | if (result == null) { 231 | // If the createMultipartUpload request never was made, we don't 232 | // need to send the abortMultipartUpload request. 233 | return 234 | } 235 | // Remove the cache entry right away for follow-up requests do not try to 236 | // use the soon-to-be aborted chached values. 237 | this.#cache.delete(file.data) 238 | this.#setS3MultipartState(file, Object.create(null)) 239 | let awaitedResult 240 | try { 241 | awaitedResult = await result 242 | } catch { 243 | // If the cached result rejects, there's nothing to abort. 244 | return 245 | } 246 | await this.#abortMultipartUpload(this.#getFile(file), awaitedResult) 247 | } 248 | 249 | async #nonMultipartUpload (file, chunk, signal) { 250 | const { 251 | method = 'POST', 252 | url, 253 | fields, 254 | headers, 255 | } = await this.#getUploadParameters(this.#getFile(file), { signal }).abortOn(signal) 256 | 257 | let body 258 | const data = chunk.getData() 259 | if (method.toUpperCase() === 'POST') { 260 | const formData = new FormData() 261 | Object.entries(fields).forEach(([key, value]) => formData.set(key, value)) 262 | formData.set('file', data) 263 | body = formData 264 | } else { 265 | body = data 266 | } 267 | 268 | const { onProgress, onComplete } = chunk 269 | 270 | const result = await this.#uploadPartBytes({ 271 | signature: { url, headers, method }, 272 | body, 273 | size: data.size, 274 | onProgress, 275 | onComplete, 276 | signal, 277 | }).abortOn(signal) 278 | 279 | return 'location' in result ? result : { 280 | location: removeMetadataFromURL(url), 281 | ...result, 282 | } 283 | } 284 | 285 | /** 286 | * @param {import("@uppy/core").UppyFile} file 287 | * @param {import("../types/chunk").Chunk[]} chunks 288 | * @param {AbortSignal} signal 289 | * @returns {Promise} 290 | */ 291 | async uploadFile (file, chunks, signal) { 292 | throwIfAborted(signal) 293 | if (chunks.length === 1 && !chunks[0].shouldUseMultipart) { 294 | return this.#nonMultipartUpload(file, chunks[0], signal) 295 | } 296 | const { uploadId, key } = await this.getUploadId(file, signal) 297 | throwIfAborted(signal) 298 | try { 299 | const parts = await Promise.all(chunks.map((chunk, i) => this.uploadChunk(file, i + 1, chunk, signal))) 300 | throwIfAborted(signal) 301 | return await this.#sendCompletionRequest( 302 | this.#getFile(file), 303 | { key, uploadId, parts, signal }, 304 | signal, 305 | ).abortOn(signal) 306 | } catch (err) { 307 | if (err?.cause !== pausingUploadReason && err?.name !== 'AbortError') { 308 | // We purposefully don't wait for the promise and ignore its status, 309 | // because we want the error `err` to bubble up ASAP to report it to the 310 | // user. A failure to abort is not that big of a deal anyway. 311 | this.abortFileUpload(file) 312 | } 313 | throw err 314 | } 315 | } 316 | 317 | restoreUploadFile (file, uploadIdAndKey) { 318 | this.#cache.set(file.data, uploadIdAndKey) 319 | } 320 | 321 | async resumeUploadFile (file, chunks, signal) { 322 | throwIfAborted(signal) 323 | if (chunks.length === 1 && chunks[0] != null && !chunks[0].shouldUseMultipart) { 324 | return this.#nonMultipartUpload(file, chunks[0], signal) 325 | } 326 | const { uploadId, key } = await this.getUploadId(file, signal) 327 | throwIfAborted(signal) 328 | const alreadyUploadedParts = await this.#listParts( 329 | this.#getFile(file), 330 | { uploadId, key, signal }, 331 | signal, 332 | ).abortOn(signal) 333 | throwIfAborted(signal) 334 | const parts = await Promise.all( 335 | chunks 336 | .map((chunk, i) => { 337 | const partNumber = i + 1 338 | const alreadyUploadedInfo = alreadyUploadedParts.find(({ PartNumber }) => PartNumber === partNumber) 339 | if (alreadyUploadedInfo == null) { 340 | return this.uploadChunk(file, partNumber, chunk, signal) 341 | } 342 | // Already uploaded chunks are set to null. If we are restoring the upload, we need to mark it as already uploaded. 343 | chunk?.setAsUploaded?.() 344 | return { PartNumber: partNumber, ETag: alreadyUploadedInfo.ETag } 345 | }), 346 | ) 347 | throwIfAborted(signal) 348 | return this.#sendCompletionRequest( 349 | this.#getFile(file), 350 | { key, uploadId, parts, signal }, 351 | signal, 352 | ).abortOn(signal) 353 | } 354 | 355 | /** 356 | * 357 | * @param {import("@uppy/core").UppyFile} file 358 | * @param {number} partNumber 359 | * @param {import("../types/chunk").Chunk} chunk 360 | * @param {AbortSignal} signal 361 | * @returns {Promise} 362 | */ 363 | async uploadChunk (file, partNumber, chunk, signal) { 364 | throwIfAborted(signal) 365 | const { uploadId, key } = await this.getUploadId(file, signal) 366 | 367 | const signatureRetryIterator = this.#retryDelays.values() 368 | const chunkRetryIterator = this.#retryDelays.values() 369 | const shouldRetrySignature = () => { 370 | const next = signatureRetryIterator.next() 371 | if (next == null || next.done) { 372 | return null 373 | } 374 | return next.value 375 | } 376 | 377 | for (;;) { 378 | throwIfAborted(signal) 379 | const chunkData = chunk.getData() 380 | const { onProgress, onComplete } = chunk 381 | let signature 382 | 383 | try { 384 | signature = await this.#fetchSignature(this.#getFile(file), { 385 | uploadId, key, partNumber, body: chunkData, signal, 386 | }).abortOn(signal) 387 | } catch (err) { 388 | const timeout = shouldRetrySignature() 389 | if (timeout == null || signal.aborted) { 390 | throw err 391 | } 392 | await new Promise(resolve => setTimeout(resolve, timeout)) 393 | // eslint-disable-next-line no-continue 394 | continue 395 | } 396 | 397 | throwIfAborted(signal) 398 | try { 399 | return { 400 | PartNumber: partNumber, 401 | ...await this.#uploadPartBytes({ 402 | signature, body: chunkData, size: chunkData.size, onProgress, onComplete, signal, 403 | }).abortOn(signal), 404 | } 405 | } catch (err) { 406 | if (!await this.#shouldRetry(err, chunkRetryIterator)) throw err 407 | } 408 | } 409 | } 410 | } 411 | 412 | export default class AwsS3Multipart extends BasePlugin { 413 | static VERSION = packageJson.version 414 | 415 | #companionCommunicationQueue 416 | 417 | #client 418 | 419 | constructor (uppy, opts) { 420 | super(uppy, opts) 421 | this.type = 'uploader' 422 | this.id = this.opts.id || 'AwsS3Multipart' 423 | this.title = 'AWS S3 Multipart' 424 | this.#client = new RequestClient(uppy, opts) 425 | 426 | const defaultOptions = { 427 | // TODO: null here means “include all”, [] means include none. 428 | // This is inconsistent with @uppy/aws-s3 and @uppy/transloadit 429 | allowedMetaFields: null, 430 | limit: 6, 431 | shouldUseMultipart: (file) => file.size !== 0, // TODO: Switch default to: 432 | // eslint-disable-next-line no-bitwise 433 | // shouldUseMultipart: (file) => file.size >> 10 >> 10 > 100, 434 | retryDelays: [0, 1000, 3000, 5000], 435 | createMultipartUpload: this.createMultipartUpload.bind(this), 436 | listParts: this.listParts.bind(this), 437 | abortMultipartUpload: this.abortMultipartUpload.bind(this), 438 | completeMultipartUpload: this.completeMultipartUpload.bind(this), 439 | getTemporarySecurityCredentials: false, 440 | signPart: opts?.getTemporarySecurityCredentials ? this.createSignedURL.bind(this) : this.signPart.bind(this), 441 | uploadPartBytes: AwsS3Multipart.uploadPartBytes, 442 | getUploadParameters: opts?.getTemporarySecurityCredentials 443 | ? this.createSignedURL.bind(this) 444 | : this.getUploadParameters.bind(this), 445 | companionHeaders: {}, 446 | } 447 | 448 | this.opts = { ...defaultOptions, ...opts } 449 | if (opts?.prepareUploadParts != null && opts.signPart == null) { 450 | this.opts.signPart = async (file, { uploadId, key, partNumber, body, signal }) => { 451 | const { presignedUrls, headers } = await opts 452 | .prepareUploadParts(file, { uploadId, key, parts: [{ number: partNumber, chunk: body }], signal }) 453 | return { url: presignedUrls?.[partNumber], headers: headers?.[partNumber] } 454 | } 455 | } 456 | 457 | /** 458 | * Simultaneous upload limiting is shared across all uploads with this plugin. 459 | * 460 | * @type {RateLimitedQueue} 461 | */ 462 | this.requests = this.opts.rateLimitedQueue ?? new RateLimitedQueue(this.opts.limit) 463 | this.#companionCommunicationQueue = new HTTPCommunicationQueue( 464 | this.requests, 465 | this.opts, 466 | this.#setS3MultipartState, 467 | this.#getFile, 468 | ) 469 | 470 | this.uploaders = Object.create(null) 471 | this.uploaderEvents = Object.create(null) 472 | this.uploaderSockets = Object.create(null) 473 | } 474 | 475 | [Symbol.for('uppy test: getClient')] () { return this.#client } 476 | 477 | setOptions (newOptions) { 478 | this.#companionCommunicationQueue.setOptions(newOptions) 479 | super.setOptions(newOptions) 480 | this.#setCompanionHeaders() 481 | } 482 | 483 | /** 484 | * Clean up all references for a file's upload: the MultipartUploader instance, 485 | * any events related to the file, and the Companion WebSocket connection. 486 | * 487 | * Set `opts.abort` to tell S3 that the multipart upload is cancelled and must be removed. 488 | * This should be done when the user cancels the upload, not when the upload is completed or errored. 489 | */ 490 | resetUploaderReferences (fileID, opts = {}) { 491 | if (this.uploaders[fileID]) { 492 | this.uploaders[fileID].abort({ really: opts.abort || false }) 493 | this.uploaders[fileID] = null 494 | } 495 | if (this.uploaderEvents[fileID]) { 496 | this.uploaderEvents[fileID].remove() 497 | this.uploaderEvents[fileID] = null 498 | } 499 | if (this.uploaderSockets[fileID]) { 500 | this.uploaderSockets[fileID].close() 501 | this.uploaderSockets[fileID] = null 502 | } 503 | } 504 | 505 | // TODO: make this a private method in the next major 506 | assertHost (method) { 507 | if (!this.opts.companionUrl) { 508 | throw new Error(`Expected a \`companionUrl\` option containing a Companion address, or if you are not using Companion, a custom \`${method}\` implementation.`) 509 | } 510 | } 511 | 512 | createMultipartUpload (file, signal) { 513 | this.assertHost('createMultipartUpload') 514 | throwIfAborted(signal) 515 | 516 | const metadata = getAllowedMetadata({ meta: file.meta, allowedMetaFields: this.opts.allowedMetaFields }) 517 | 518 | return this.#client.post('s3/multipart', { 519 | filename: file.name, 520 | type: file.type, 521 | metadata, 522 | }, { signal }).then(assertServerError) 523 | } 524 | 525 | listParts (file, { key, uploadId }, signal) { 526 | this.assertHost('listParts') 527 | throwIfAborted(signal) 528 | 529 | const filename = encodeURIComponent(key) 530 | return this.#client.get(`s3/multipart/${uploadId}?key=${filename}`, { signal }) 531 | .then(assertServerError) 532 | } 533 | 534 | completeMultipartUpload (file, { key, uploadId, parts }, signal) { 535 | this.assertHost('completeMultipartUpload') 536 | throwIfAborted(signal) 537 | 538 | const filename = encodeURIComponent(key) 539 | const uploadIdEnc = encodeURIComponent(uploadId) 540 | return this.#client.post(`s3/multipart/${uploadIdEnc}/complete?key=${filename}`, { parts }, { signal }) 541 | .then(assertServerError) 542 | } 543 | 544 | /** 545 | * @type {import("../types").AwsS3STSResponse | Promise} 546 | */ 547 | #cachedTemporaryCredentials 548 | 549 | async #getTemporarySecurityCredentials (options) { 550 | throwIfAborted(options?.signal) 551 | 552 | if (this.#cachedTemporaryCredentials == null) { 553 | // We do not await it just yet, so concurrent calls do not try to override it: 554 | if (this.opts.getTemporarySecurityCredentials === true) { 555 | this.assertHost('getTemporarySecurityCredentials') 556 | this.#cachedTemporaryCredentials = this.#client.get('s3/sts', null, options).then(assertServerError) 557 | } else { 558 | this.#cachedTemporaryCredentials = this.opts.getTemporarySecurityCredentials(options) 559 | } 560 | this.#cachedTemporaryCredentials = await this.#cachedTemporaryCredentials 561 | setTimeout(() => { 562 | // At half the time left before expiration, we clear the cache. That's 563 | // an arbitrary tradeoff to limit the number of requests made to the 564 | // remote while limiting the risk of using an expired token in case the 565 | // clocks are not exactly synced. 566 | // The HTTP cache should be configured to ensure a client doesn't request 567 | // more tokens than it needs, but this timeout provides a second layer of 568 | // security in case the HTTP cache is disabled or misconfigured. 569 | this.#cachedTemporaryCredentials = null 570 | }, (getExpiry(this.#cachedTemporaryCredentials.credentials) || 0) * 500) 571 | } 572 | 573 | return this.#cachedTemporaryCredentials 574 | } 575 | 576 | async createSignedURL (file, options) { 577 | const data = await this.#getTemporarySecurityCredentials(options) 578 | const expires = getExpiry(data.credentials) || 604_800 // 604 800 is the max value accepted by AWS. 579 | 580 | const { uploadId, key, partNumber, signal } = options 581 | 582 | // Return an object in the correct shape. 583 | return { 584 | method: 'PUT', 585 | expires, 586 | fields: {}, 587 | url: `${await createSignedURL({ 588 | accountKey: data.credentials.AccessKeyId, 589 | accountSecret: data.credentials.SecretAccessKey, 590 | sessionToken: data.credentials.SessionToken, 591 | expires, 592 | bucketName: data.bucket, 593 | Region: data.region, 594 | Key: key ?? `${crypto.randomUUID()}-${file.name}`, 595 | uploadId, 596 | partNumber, 597 | signal, 598 | })}`, 599 | // Provide content type header required by S3 600 | headers: { 601 | 'Content-Type': file.type, 602 | }, 603 | } 604 | } 605 | 606 | signPart (file, { uploadId, key, partNumber, signal }) { 607 | this.assertHost('signPart') 608 | throwIfAborted(signal) 609 | 610 | if (uploadId == null || key == null || partNumber == null) { 611 | throw new Error('Cannot sign without a key, an uploadId, and a partNumber') 612 | } 613 | 614 | const filename = encodeURIComponent(key) 615 | return this.#client.get(`s3/multipart/${uploadId}/${partNumber}?key=${filename}`, { signal }) 616 | .then(assertServerError) 617 | } 618 | 619 | abortMultipartUpload (file, { key, uploadId }, signal) { 620 | this.assertHost('abortMultipartUpload') 621 | 622 | const filename = encodeURIComponent(key) 623 | const uploadIdEnc = encodeURIComponent(uploadId) 624 | return this.#client.delete(`s3/multipart/${uploadIdEnc}?key=${filename}`, undefined, { signal }) 625 | .then(assertServerError) 626 | } 627 | 628 | getUploadParameters (file, options) { 629 | const { meta } = file 630 | const { type, name: filename } = meta 631 | const metadata = getAllowedMetadata({ meta, allowedMetaFields: this.opts.allowedMetaFields, querify: true }) 632 | 633 | const query = new URLSearchParams({ filename, type, ...metadata }) 634 | 635 | return this.#client.get(`s3/params?${query}`, options) 636 | } 637 | 638 | static async uploadPartBytes ({ signature: { url, expires, headers, method = 'PUT' }, body, size = body.size, onProgress, onComplete, signal }) { 639 | throwIfAborted(signal) 640 | 641 | if (url == null) { 642 | throw new Error('Cannot upload to an undefined URL') 643 | } 644 | 645 | return new Promise((resolve, reject) => { 646 | const xhr = new XMLHttpRequest() 647 | xhr.open(method, url, true) 648 | if (headers) { 649 | Object.keys(headers).forEach((key) => { 650 | xhr.setRequestHeader(key, headers[key]) 651 | }) 652 | } 653 | xhr.responseType = 'text' 654 | if (typeof expires === 'number') { 655 | xhr.timeout = expires * 1000 656 | } 657 | 658 | function onabort () { 659 | xhr.abort() 660 | } 661 | function cleanup () { 662 | signal.removeEventListener('abort', onabort) 663 | } 664 | signal.addEventListener('abort', onabort) 665 | 666 | xhr.upload.addEventListener('progress', (ev) => { 667 | onProgress(ev) 668 | }) 669 | 670 | xhr.addEventListener('abort', () => { 671 | cleanup() 672 | 673 | reject(createAbortError()) 674 | }) 675 | 676 | xhr.addEventListener('timeout', () => { 677 | cleanup() 678 | 679 | const error = new Error('Request has expired') 680 | error.source = { status: 403 } 681 | reject(error) 682 | }) 683 | xhr.addEventListener('load', (ev) => { 684 | cleanup() 685 | 686 | if (ev.target.status === 403 && ev.target.responseText.includes('Request has expired')) { 687 | const error = new Error('Request has expired') 688 | error.source = ev.target 689 | reject(error) 690 | return 691 | } if (ev.target.status < 200 || ev.target.status >= 300) { 692 | const error = new Error('Non 2xx') 693 | error.source = ev.target 694 | reject(error) 695 | return 696 | } 697 | 698 | // todo make a proper onProgress API (breaking change) 699 | onProgress?.({ loaded: size, lengthComputable: true }) 700 | 701 | // NOTE This must be allowed by CORS. 702 | const etag = ev.target.getResponseHeader('ETag') 703 | const location = ev.target.getResponseHeader('Location') 704 | 705 | if (method.toUpperCase() === 'POST' && location === null) { 706 | // Not being able to read the Location header is not a fatal error. 707 | // eslint-disable-next-line no-console 708 | console.warn('AwsS3/Multipart: Could not read the Location header. This likely means CORS is not configured correctly on the S3 Bucket. See https://uppy.io/docs/aws-s3-multipart#S3-Bucket-Configuration for instructions.') 709 | } 710 | if (etag === null) { 711 | reject(new Error('AwsS3/Multipart: Could not read the ETag header. This likely means CORS is not configured correctly on the S3 Bucket. See https://uppy.io/docs/aws-s3-multipart#S3-Bucket-Configuration for instructions.')) 712 | return 713 | } 714 | 715 | onComplete?.(etag) 716 | resolve({ 717 | ETag: etag, 718 | ...(location ? { location } : undefined), 719 | }) 720 | }) 721 | 722 | xhr.addEventListener('error', (ev) => { 723 | cleanup() 724 | 725 | const error = new Error('Unknown error') 726 | error.source = ev.target 727 | reject(error) 728 | }) 729 | 730 | xhr.send(body) 731 | }) 732 | } 733 | 734 | #setS3MultipartState = (file, { key, uploadId }) => { 735 | const cFile = this.uppy.getFile(file.id) 736 | if (cFile == null) { 737 | // file was removed from store 738 | return 739 | } 740 | 741 | this.uppy.setFileState(file.id, { 742 | s3Multipart: { 743 | ...cFile.s3Multipart, 744 | key, 745 | uploadId, 746 | }, 747 | }) 748 | } 749 | 750 | #getFile = (file) => { 751 | return this.uppy.getFile(file.id) || file 752 | } 753 | 754 | #uploadLocalFile (file) { 755 | return new Promise((resolve, reject) => { 756 | const onProgress = (bytesUploaded, bytesTotal) => { 757 | this.uppy.emit('upload-progress', file, { 758 | uploader: this, 759 | bytesUploaded, 760 | bytesTotal, 761 | }) 762 | } 763 | 764 | const onError = (err) => { 765 | this.uppy.log(err) 766 | this.uppy.emit('upload-error', file, err) 767 | 768 | this.resetUploaderReferences(file.id) 769 | reject(err) 770 | } 771 | 772 | const onSuccess = (result) => { 773 | const uploadResp = { 774 | body: { 775 | ...result, 776 | }, 777 | uploadURL: result.location, 778 | } 779 | 780 | this.resetUploaderReferences(file.id) 781 | 782 | this.uppy.emit('upload-success', this.#getFile(file), uploadResp) 783 | 784 | if (result.location) { 785 | this.uppy.log(`Download ${file.name} from ${result.location}`) 786 | } 787 | 788 | resolve() 789 | } 790 | 791 | const onPartComplete = (part) => { 792 | this.uppy.emit('s3-multipart:part-uploaded', this.#getFile(file), part) 793 | } 794 | 795 | const upload = new MultipartUploader(file.data, { 796 | // .bind to pass the file object to each handler. 797 | companionComm: this.#companionCommunicationQueue, 798 | 799 | log: (...args) => this.uppy.log(...args), 800 | getChunkSize: this.opts.getChunkSize ? this.opts.getChunkSize.bind(this) : null, 801 | 802 | onProgress, 803 | onError, 804 | onSuccess, 805 | onPartComplete, 806 | 807 | file, 808 | shouldUseMultipart: this.opts.shouldUseMultipart, 809 | 810 | ...file.s3Multipart, 811 | }) 812 | 813 | this.uploaders[file.id] = upload 814 | const eventManager = new EventManager(this.uppy) 815 | this.uploaderEvents[file.id] = eventManager 816 | 817 | eventManager.onFileRemove(file.id, (removed) => { 818 | upload.abort() 819 | this.resetUploaderReferences(file.id, { abort: true }) 820 | resolve(`upload ${removed.id} was removed`) 821 | }) 822 | 823 | eventManager.onCancelAll(file.id, ({ reason } = {}) => { 824 | if (reason === 'user') { 825 | upload.abort() 826 | this.resetUploaderReferences(file.id, { abort: true }) 827 | } 828 | resolve(`upload ${file.id} was canceled`) 829 | }) 830 | 831 | eventManager.onFilePause(file.id, (isPaused) => { 832 | if (isPaused) { 833 | upload.pause() 834 | } else { 835 | upload.start() 836 | } 837 | }) 838 | 839 | eventManager.onPauseAll(file.id, () => { 840 | upload.pause() 841 | }) 842 | 843 | eventManager.onResumeAll(file.id, () => { 844 | upload.start() 845 | }) 846 | 847 | upload.start() 848 | }) 849 | } 850 | 851 | // eslint-disable-next-line class-methods-use-this 852 | #getCompanionClientArgs (file) { 853 | return { 854 | ...file.remote.body, 855 | protocol: 's3-multipart', 856 | size: file.data.size, 857 | metadata: file.meta, 858 | } 859 | } 860 | 861 | #upload = async (fileIDs) => { 862 | if (fileIDs.length === 0) return undefined 863 | 864 | const files = this.uppy.getFilesByIds(fileIDs) 865 | const filesFiltered = filterNonFailedFiles(files) 866 | const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered) 867 | 868 | this.uppy.emit('upload-start', filesToEmit) 869 | 870 | const promises = filesFiltered.map((file) => { 871 | if (file.isRemote) { 872 | const getQueue = () => this.requests 873 | this.#setResumableUploadsCapability(false) 874 | const controller = new AbortController() 875 | 876 | const removedHandler = (removedFile) => { 877 | if (removedFile.id === file.id) controller.abort() 878 | } 879 | this.uppy.on('file-removed', removedHandler) 880 | 881 | const uploadPromise = file.remote.requestClient.uploadRemoteFile( 882 | file, 883 | this.#getCompanionClientArgs(file), 884 | { signal: controller.signal, getQueue }, 885 | ) 886 | 887 | this.requests.wrapSyncFunction(() => { 888 | this.uppy.off('file-removed', removedHandler) 889 | }, { priority: -1 })() 890 | 891 | return uploadPromise 892 | } 893 | 894 | return this.#uploadLocalFile(file) 895 | }) 896 | 897 | const upload = await Promise.all(promises) 898 | // After the upload is done, another upload may happen with only local files. 899 | // We reset the capability so that the next upload can use resumable uploads. 900 | this.#setResumableUploadsCapability(true) 901 | return upload 902 | } 903 | 904 | #setCompanionHeaders = () => { 905 | this.#client.setCompanionHeaders(this.opts.companionHeaders) 906 | } 907 | 908 | #setResumableUploadsCapability = (boolean) => { 909 | const { capabilities } = this.uppy.getState() 910 | this.uppy.setState({ 911 | capabilities: { 912 | ...capabilities, 913 | resumableUploads: boolean, 914 | }, 915 | }) 916 | } 917 | 918 | #resetResumableCapability = () => { 919 | this.#setResumableUploadsCapability(true) 920 | } 921 | 922 | install () { 923 | this.#setResumableUploadsCapability(true) 924 | this.uppy.addPreProcessor(this.#setCompanionHeaders) 925 | this.uppy.addUploader(this.#upload) 926 | this.uppy.on('cancel-all', this.#resetResumableCapability) 927 | } 928 | 929 | uninstall () { 930 | this.uppy.removePreProcessor(this.#setCompanionHeaders) 931 | this.uppy.removeUploader(this.#upload) 932 | this.uppy.off('cancel-all', this.#resetResumableCapability) 933 | } 934 | } 935 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/src/index.test.js: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' 2 | 3 | import 'whatwg-fetch' 4 | import nock from 'nock' 5 | import Core from '@uppy/core' 6 | import AwsS3Multipart from './index.js' 7 | 8 | const KB = 1024 9 | const MB = KB * KB 10 | 11 | describe('AwsS3Multipart', () => { 12 | beforeEach(() => nock.disableNetConnect()) 13 | 14 | it('Registers AwsS3Multipart upload plugin', () => { 15 | const core = new Core() 16 | core.use(AwsS3Multipart) 17 | 18 | const pluginNames = core[Symbol.for('uppy test: getPlugins')]('uploader').map((plugin) => plugin.constructor.name) 19 | expect(pluginNames).toContain('AwsS3Multipart') 20 | }) 21 | 22 | describe('companionUrl assertion', () => { 23 | it('Throws an error for main functions if configured without companionUrl', () => { 24 | const core = new Core() 25 | core.use(AwsS3Multipart) 26 | const awsS3Multipart = core.getPlugin('AwsS3Multipart') 27 | 28 | const err = 'Expected a `companionUrl` option' 29 | const file = {} 30 | const opts = {} 31 | 32 | expect(() => awsS3Multipart.opts.createMultipartUpload(file)).toThrow( 33 | err, 34 | ) 35 | expect(() => awsS3Multipart.opts.listParts(file, opts)).toThrow(err) 36 | expect(() => awsS3Multipart.opts.completeMultipartUpload(file, opts)).toThrow(err) 37 | expect(() => awsS3Multipart.opts.abortMultipartUpload(file, opts)).toThrow(err) 38 | expect(() => awsS3Multipart.opts.signPart(file, opts)).toThrow(err) 39 | }) 40 | }) 41 | 42 | describe('non-multipart upload', () => { 43 | it('should handle POST uploads', async () => { 44 | const core = new Core() 45 | core.use(AwsS3Multipart, { 46 | shouldUseMultipart: false, 47 | limit: 0, 48 | getUploadParameters: () => ({ 49 | method: 'POST', 50 | url: 'https://bucket.s3.us-east-2.amazonaws.com/', 51 | fields: {}, 52 | }), 53 | }) 54 | const scope = nock( 55 | 'https://bucket.s3.us-east-2.amazonaws.com', 56 | ).defaultReplyHeaders({ 57 | 'access-control-allow-headers': '*', 58 | 'access-control-allow-method': 'POST', 59 | 'access-control-allow-origin': '*', 60 | 'access-control-expose-headers': 'ETag, Location', 61 | }) 62 | 63 | scope.options('/').reply(204, '') 64 | scope 65 | .post('/') 66 | .reply(201, '', { ETag: 'test', Location: 'http://example.com' }) 67 | 68 | const fileSize = 1 69 | 70 | core.addFile({ 71 | source: 'vi', 72 | name: 'multitest.dat', 73 | type: 'application/octet-stream', 74 | data: new File([new Uint8Array(fileSize)], { 75 | type: 'application/octet-stream', 76 | }), 77 | }) 78 | 79 | const uploadSuccessHandler = vi.fn() 80 | core.on('upload-success', uploadSuccessHandler) 81 | 82 | await core.upload() 83 | 84 | expect(uploadSuccessHandler.mock.calls).toHaveLength(1) 85 | expect(uploadSuccessHandler.mock.calls[0][1]).toStrictEqual({ 86 | body: { 87 | ETag: 'test', 88 | location: 'http://example.com', 89 | }, 90 | uploadURL: 'http://example.com', 91 | }) 92 | 93 | scope.done() 94 | }) 95 | }) 96 | 97 | describe('without companionUrl (custom main functions)', () => { 98 | let core 99 | let awsS3Multipart 100 | 101 | beforeEach(() => { 102 | core = new Core() 103 | core.use(AwsS3Multipart, { 104 | limit: 0, 105 | createMultipartUpload: vi.fn(() => { 106 | return { 107 | uploadId: '6aeb1980f3fc7ce0b5454d25b71992', 108 | key: 'test/upload/multitest.dat', 109 | } 110 | }), 111 | completeMultipartUpload: vi.fn(async () => ({ location: 'test' })), 112 | abortMultipartUpload: vi.fn(), 113 | prepareUploadParts: vi.fn(async (file, { parts }) => { 114 | const presignedUrls = {} 115 | parts.forEach(({ number }) => { 116 | presignedUrls[ 117 | number 118 | ] = `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${number}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test` 119 | }) 120 | return { presignedUrls, headers: { 1: { 'Content-MD5': 'foo' } } } 121 | }), 122 | }) 123 | awsS3Multipart = core.getPlugin('AwsS3Multipart') 124 | }) 125 | 126 | it('Calls the prepareUploadParts function totalChunks / limit times', async () => { 127 | const scope = nock( 128 | 'https://bucket.s3.us-east-2.amazonaws.com', 129 | ).defaultReplyHeaders({ 130 | 'access-control-allow-headers': '*', 131 | 'access-control-allow-method': 'PUT', 132 | 'access-control-allow-origin': '*', 133 | 'access-control-expose-headers': 'ETag, Content-MD5', 134 | }) 135 | // 6MB file will give us 2 chunks, so there will be 2 PUT and 2 OPTIONS 136 | // calls to the presigned URL from 1 prepareUploadParts calls 137 | const fileSize = 5 * MB + 1 * MB 138 | 139 | scope 140 | .options((uri) => uri.includes('test/upload/multitest.dat?partNumber=1')) 141 | .reply(function replyFn () { 142 | expect(this.req.headers['access-control-request-headers']).toEqual('Content-MD5') 143 | return [200, ''] 144 | }) 145 | scope 146 | .options((uri) => uri.includes('test/upload/multitest.dat?partNumber=2')) 147 | .reply(function replyFn () { 148 | expect(this.req.headers['access-control-request-headers']).toBeUndefined() 149 | return [200, ''] 150 | }) 151 | scope 152 | .put((uri) => uri.includes('test/upload/multitest.dat?partNumber=1')) 153 | .reply(200, '', { ETag: 'test1' }) 154 | scope 155 | .put((uri) => uri.includes('test/upload/multitest.dat?partNumber=2')) 156 | .reply(200, '', { ETag: 'test2' }) 157 | 158 | core.addFile({ 159 | source: 'vi', 160 | name: 'multitest.dat', 161 | type: 'application/octet-stream', 162 | data: new File([new Uint8Array(fileSize)], { 163 | type: 'application/octet-stream', 164 | }), 165 | }) 166 | 167 | await core.upload() 168 | 169 | expect( 170 | awsS3Multipart.opts.prepareUploadParts.mock.calls.length, 171 | ).toEqual(2) 172 | 173 | scope.done() 174 | }) 175 | 176 | it('Calls prepareUploadParts with a Math.ceil(limit / 2) minimum, instead of one at a time for the remaining chunks after the first limit batch', async () => { 177 | const scope = nock( 178 | 'https://bucket.s3.us-east-2.amazonaws.com', 179 | ).defaultReplyHeaders({ 180 | 'access-control-allow-headers': '*', 181 | 'access-control-allow-method': 'PUT', 182 | 'access-control-allow-origin': '*', 183 | 'access-control-expose-headers': 'ETag', 184 | }) 185 | // 50MB file will give us 10 chunks, so there will be 10 PUT and 10 OPTIONS 186 | // calls to the presigned URL from 3 prepareUploadParts calls 187 | // 188 | // The first prepareUploadParts call will be for 5 parts, the second 189 | // will be for 3 parts, the third will be for 2 parts. 190 | const fileSize = 50 * MB 191 | 192 | scope 193 | .options((uri) => uri.includes('test/upload/multitest.dat')) 194 | .reply(200, '') 195 | scope 196 | .put((uri) => uri.includes('test/upload/multitest.dat')) 197 | .reply(200, '', { ETag: 'test' }) 198 | scope.persist() 199 | 200 | core.addFile({ 201 | source: 'vi', 202 | name: 'multitest.dat', 203 | type: 'application/octet-stream', 204 | data: new File([new Uint8Array(fileSize)], { 205 | type: 'application/octet-stream', 206 | }), 207 | }) 208 | 209 | await core.upload() 210 | 211 | function validatePartData ({ parts }, expected) { 212 | expect(parts.map((part) => part.number)).toEqual(expected) 213 | 214 | for (const part of parts) { 215 | expect(part.chunk).toBeDefined() 216 | } 217 | } 218 | 219 | expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(10) 220 | 221 | validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[0][1], [1]) 222 | validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[1][1], [2]) 223 | validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[2][1], [3]) 224 | 225 | const completeCall = awsS3Multipart.opts.completeMultipartUpload.mock.calls[0][1] 226 | 227 | expect(completeCall.parts).toEqual([ 228 | { ETag: 'test', PartNumber: 1 }, 229 | { ETag: 'test', PartNumber: 2 }, 230 | { ETag: 'test', PartNumber: 3 }, 231 | { ETag: 'test', PartNumber: 4 }, 232 | { ETag: 'test', PartNumber: 5 }, 233 | { ETag: 'test', PartNumber: 6 }, 234 | { ETag: 'test', PartNumber: 7 }, 235 | { ETag: 'test', PartNumber: 8 }, 236 | { ETag: 'test', PartNumber: 9 }, 237 | { ETag: 'test', PartNumber: 10 }, 238 | ]) 239 | }) 240 | 241 | it('Keeps chunks marked as busy through retries until they complete', async () => { 242 | const scope = nock( 243 | 'https://bucket.s3.us-east-2.amazonaws.com', 244 | ).defaultReplyHeaders({ 245 | 'access-control-allow-headers': '*', 246 | 'access-control-allow-method': 'PUT', 247 | 'access-control-allow-origin': '*', 248 | 'access-control-expose-headers': 'ETag', 249 | }) 250 | 251 | const fileSize = 50 * MB 252 | 253 | scope 254 | .options((uri) => uri.includes('test/upload/multitest.dat')) 255 | .reply(200, '') 256 | scope 257 | .put((uri) => uri.includes('test/upload/multitest.dat') && !uri.includes('partNumber=7')) 258 | .reply(200, '', { ETag: 'test' }) 259 | 260 | // Fail the part 7 upload once, then let it succeed 261 | let calls = 0 262 | scope 263 | .put((uri) => uri.includes('test/upload/multitest.dat') && uri.includes('partNumber=7')) 264 | .reply(() => (calls++ === 0 ? [500] : [200, '', { ETag: 'test' }])) 265 | 266 | scope.persist() 267 | 268 | // Spy on the busy/done state of the test chunk (part 7, chunk index 6) 269 | let busySpy 270 | let doneSpy 271 | awsS3Multipart.setOptions({ 272 | retryDelays: [10], 273 | createMultipartUpload: vi.fn((file) => { 274 | const multipartUploader = awsS3Multipart.uploaders[file.id] 275 | const testChunkState = multipartUploader.chunkState[6] 276 | let busy = false 277 | let done = false 278 | busySpy = vi.fn((value) => { busy = value }) 279 | doneSpy = vi.fn((value) => { done = value }) 280 | Object.defineProperty(testChunkState, 'busy', { get: () => busy, set: busySpy }) 281 | Object.defineProperty(testChunkState, 'done', { get: () => done, set: doneSpy }) 282 | 283 | return { 284 | uploadId: '6aeb1980f3fc7ce0b5454d25b71992', 285 | key: 'test/upload/multitest.dat', 286 | } 287 | }), 288 | }) 289 | 290 | core.addFile({ 291 | source: 'vi', 292 | name: 'multitest.dat', 293 | type: 'application/octet-stream', 294 | data: new File([new Uint8Array(fileSize)], { 295 | type: 'application/octet-stream', 296 | }), 297 | }) 298 | 299 | await core.upload() 300 | 301 | // The chunk should be marked as done once 302 | expect(doneSpy.mock.calls.length).toEqual(1) 303 | expect(doneSpy.mock.calls[0][0]).toEqual(true) 304 | 305 | // Any changes that set busy to false should only happen after the chunk has been marked done, 306 | // otherwise a race condition occurs (see PR #3955) 307 | const doneCallOrderNumber = doneSpy.mock.invocationCallOrder[0] 308 | for (const [index, callArgs] of busySpy.mock.calls.entries()) { 309 | if (callArgs[0] === false) { 310 | expect(busySpy.mock.invocationCallOrder[index]).toBeGreaterThan(doneCallOrderNumber) 311 | } 312 | } 313 | 314 | expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(10) 315 | }) 316 | }) 317 | 318 | describe('MultipartUploader', () => { 319 | const createMultipartUpload = vi.fn(() => { 320 | return { 321 | uploadId: '6aeb1980f3fc7ce0b5454d25b71992', 322 | key: 'test/upload/multitest.dat', 323 | } 324 | }) 325 | 326 | const signPart = vi 327 | .fn(async (file, { partNumber }) => { 328 | return { url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test` } 329 | }) 330 | 331 | const uploadPartBytes = vi.fn() 332 | 333 | afterEach(() => vi.clearAllMocks()) 334 | 335 | it('retries uploadPartBytes when it fails once', async () => { 336 | const core = new Core() 337 | .use(AwsS3Multipart, { 338 | createMultipartUpload, 339 | completeMultipartUpload: vi.fn(async () => ({ location: 'test' })), 340 | // eslint-disable-next-line no-throw-literal 341 | abortMultipartUpload: vi.fn(() => { throw 'should ignore' }), 342 | signPart, 343 | uploadPartBytes: 344 | uploadPartBytes 345 | // eslint-disable-next-line prefer-promise-reject-errors 346 | .mockImplementationOnce(() => Promise.reject({ source: { status: 500 } })), 347 | }) 348 | const awsS3Multipart = core.getPlugin('AwsS3Multipart') 349 | const fileSize = 5 * MB + 1 * MB 350 | 351 | core.addFile({ 352 | source: 'vi', 353 | name: 'multitest.dat', 354 | type: 'application/octet-stream', 355 | data: new File([new Uint8Array(fileSize)], { 356 | type: 'application/octet-stream', 357 | }), 358 | }) 359 | 360 | await core.upload() 361 | 362 | expect(awsS3Multipart.opts.uploadPartBytes.mock.calls.length).toEqual(3) 363 | }) 364 | 365 | it('calls `upload-error` when uploadPartBytes fails after all retries', async () => { 366 | const core = new Core() 367 | .use(AwsS3Multipart, { 368 | retryDelays: [10], 369 | createMultipartUpload, 370 | completeMultipartUpload: vi.fn(async () => ({ location: 'test' })), 371 | abortMultipartUpload: vi.fn(), 372 | signPart, 373 | uploadPartBytes: uploadPartBytes 374 | // eslint-disable-next-line prefer-promise-reject-errors 375 | .mockImplementation(() => Promise.reject({ source: { status: 500 } })), 376 | }) 377 | const awsS3Multipart = core.getPlugin('AwsS3Multipart') 378 | const fileSize = 5 * MB + 1 * MB 379 | const mock = vi.fn() 380 | core.on('upload-error', mock) 381 | 382 | core.addFile({ 383 | source: 'vi', 384 | name: 'multitest.dat', 385 | type: 'application/octet-stream', 386 | data: new File([new Uint8Array(fileSize)], { 387 | type: 'application/octet-stream', 388 | }), 389 | }) 390 | 391 | await expect(core.upload()).rejects.toEqual({ source: { status: 500 } }) 392 | 393 | expect(awsS3Multipart.opts.uploadPartBytes.mock.calls.length).toEqual(3) 394 | expect(mock.mock.calls.length).toEqual(1) 395 | }) 396 | }) 397 | 398 | describe('dynamic companionHeader', () => { 399 | let core 400 | let awsS3Multipart 401 | const oldToken = 'old token' 402 | const newToken = 'new token' 403 | 404 | beforeEach(() => { 405 | core = new Core() 406 | core.use(AwsS3Multipart, { 407 | companionHeaders: { 408 | authorization: oldToken, 409 | }, 410 | }) 411 | awsS3Multipart = core.getPlugin('AwsS3Multipart') 412 | }) 413 | 414 | it('companionHeader is updated before uploading file', async () => { 415 | awsS3Multipart.setOptions({ 416 | companionHeaders: { 417 | authorization: newToken, 418 | }, 419 | }) 420 | 421 | await core.upload() 422 | 423 | const client = awsS3Multipart[Symbol.for('uppy test: getClient')]() 424 | 425 | expect(client[Symbol.for('uppy test: getCompanionHeaders')]().authorization).toEqual(newToken) 426 | }) 427 | }) 428 | 429 | describe('dynamic companionHeader using setOption', () => { 430 | let core 431 | let awsS3Multipart 432 | const newToken = 'new token' 433 | 434 | it('companionHeader is updated before uploading file', async () => { 435 | core = new Core() 436 | core.use(AwsS3Multipart) 437 | /* Set up preprocessor */ 438 | core.addPreProcessor(() => { 439 | awsS3Multipart = core.getPlugin('AwsS3Multipart') 440 | awsS3Multipart.setOptions({ 441 | companionHeaders: { 442 | authorization: newToken, 443 | }, 444 | }) 445 | }) 446 | 447 | await core.upload() 448 | 449 | const client = awsS3Multipart[Symbol.for('uppy test: getClient')]() 450 | 451 | expect(client[Symbol.for('uppy test: getCompanionHeaders')]().authorization).toEqual(newToken) 452 | }) 453 | }) 454 | 455 | describe('file metadata across custom main functions', () => { 456 | let core 457 | const createMultipartUpload = vi.fn(file => { 458 | core.setFileMeta(file.id, { 459 | ...file.meta, 460 | createMultipartUpload: true, 461 | }) 462 | 463 | return { 464 | uploadId: 'upload1234', 465 | key: file.name, 466 | } 467 | }) 468 | 469 | const signPart = vi.fn((file, partData) => { 470 | expect(file.meta.createMultipartUpload).toBe(true) 471 | 472 | core.setFileMeta(file.id, { 473 | ...file.meta, 474 | signPart: true, 475 | [`part${partData.partNumber}`]: partData.partNumber, 476 | }) 477 | 478 | return { 479 | url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partData.partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`, 480 | } 481 | }) 482 | 483 | const listParts = vi.fn((file) => { 484 | expect(file.meta.createMultipartUpload).toBe(true) 485 | core.setFileMeta(file.id, { 486 | ...file.meta, 487 | listParts: true, 488 | }) 489 | 490 | const partKeys = Object.keys(file.meta).filter(metaKey => metaKey.startsWith('part')) 491 | return partKeys.map(metaKey => ({ 492 | PartNumber: file.meta[metaKey], 493 | ETag: metaKey, 494 | Size: 5 * MB, 495 | })) 496 | }) 497 | 498 | const completeMultipartUpload = vi.fn((file) => { 499 | expect(file.meta.createMultipartUpload).toBe(true) 500 | expect(file.meta.signPart).toBe(true) 501 | for (let i = 1; i <= 10; i++) { 502 | expect(file.meta[`part${i}`]).toBe(i) 503 | } 504 | return {} 505 | }) 506 | 507 | const abortMultipartUpload = vi.fn((file) => { 508 | expect(file.meta.createMultipartUpload).toBe(true) 509 | expect(file.meta.signPart).toBe(true) 510 | expect(file.meta.abortingPart).toBe(5) 511 | return {} 512 | }) 513 | 514 | beforeEach(() => { 515 | createMultipartUpload.mockClear() 516 | signPart.mockClear() 517 | listParts.mockClear() 518 | abortMultipartUpload.mockClear() 519 | completeMultipartUpload.mockClear() 520 | }) 521 | 522 | it('preserves file metadata if upload is completed', async () => { 523 | core = new Core() 524 | .use(AwsS3Multipart, { 525 | createMultipartUpload, 526 | signPart, 527 | listParts, 528 | completeMultipartUpload, 529 | abortMultipartUpload, 530 | }) 531 | 532 | nock('https://bucket.s3.us-east-2.amazonaws.com') 533 | .defaultReplyHeaders({ 534 | 'access-control-allow-headers': '*', 535 | 'access-control-allow-method': 'PUT', 536 | 'access-control-allow-origin': '*', 537 | 'access-control-expose-headers': 'ETag', 538 | }) 539 | .put((uri) => uri.includes('test/upload/multitest.dat')) 540 | .reply(200, '', { ETag: 'test' }) 541 | .persist() 542 | 543 | const fileSize = 50 * MB 544 | core.addFile({ 545 | source: 'vi', 546 | name: 'multitest.dat', 547 | type: 'application/octet-stream', 548 | data: new File([new Uint8Array(fileSize)], { 549 | type: 'application/octet-stream', 550 | }), 551 | }) 552 | 553 | await core.upload() 554 | expect(createMultipartUpload).toHaveBeenCalled() 555 | expect(signPart).toHaveBeenCalledTimes(10) 556 | expect(completeMultipartUpload).toHaveBeenCalled() 557 | }) 558 | 559 | it('preserves file metadata if upload is aborted', async () => { 560 | const signPartWithAbort = vi.fn((file, partData) => { 561 | expect(file.meta.createMultipartUpload).toBe(true) 562 | if (partData.partNumber === 5) { 563 | core.setFileMeta(file.id, { 564 | ...file.meta, 565 | abortingPart: partData.partNumber, 566 | }) 567 | core.removeFile(file.id) 568 | return {} 569 | } 570 | 571 | core.setFileMeta(file.id, { 572 | ...file.meta, 573 | signPart: true, 574 | [`part${partData.partNumber}`]: partData.partNumber, 575 | }) 576 | 577 | return { 578 | url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partData.partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`, 579 | } 580 | }) 581 | 582 | core = new Core() 583 | .use(AwsS3Multipart, { 584 | createMultipartUpload, 585 | signPart: signPartWithAbort, 586 | listParts, 587 | completeMultipartUpload, 588 | abortMultipartUpload, 589 | }) 590 | 591 | nock('https://bucket.s3.us-east-2.amazonaws.com') 592 | .defaultReplyHeaders({ 593 | 'access-control-allow-headers': '*', 594 | 'access-control-allow-method': 'PUT', 595 | 'access-control-allow-origin': '*', 596 | 'access-control-expose-headers': 'ETag', 597 | }) 598 | .put((uri) => uri.includes('test/upload/multitest.dat')) 599 | .reply(200, '', { ETag: 'test' }) 600 | .persist() 601 | 602 | const fileSize = 50 * MB 603 | core.addFile({ 604 | source: 'vi', 605 | name: 'multitest.dat', 606 | type: 'application/octet-stream', 607 | data: new File([new Uint8Array(fileSize)], { 608 | type: 'application/octet-stream', 609 | }), 610 | }) 611 | 612 | await core.upload() 613 | expect(createMultipartUpload).toHaveBeenCalled() 614 | expect(signPartWithAbort).toHaveBeenCalled() 615 | expect(abortMultipartUpload).toHaveBeenCalled() 616 | }) 617 | 618 | it('preserves file metadata if upload is paused and resumed', async () => { 619 | const completeMultipartUploadAfterPause = vi.fn((file) => { 620 | expect(file.meta.createMultipartUpload).toBe(true) 621 | expect(file.meta.signPart).toBe(true) 622 | for (let i = 1; i <= 10; i++) { 623 | expect(file.meta[`part${i}`]).toBe(i) 624 | } 625 | 626 | expect(file.meta.listParts).toBe(true) 627 | return {} 628 | }) 629 | 630 | const signPartWithPause = vi.fn((file, partData) => { 631 | expect(file.meta.createMultipartUpload).toBe(true) 632 | if (partData.partNumber === 3) { 633 | core.setFileMeta(file.id, { 634 | ...file.meta, 635 | abortingPart: partData.partNumber, 636 | }) 637 | core.pauseResume(file.id) 638 | setTimeout(() => core.pauseResume(file.id), 500) 639 | } 640 | 641 | core.setFileMeta(file.id, { 642 | ...file.meta, 643 | signPart: true, 644 | [`part${partData.partNumber}`]: partData.partNumber, 645 | }) 646 | 647 | return { 648 | url: `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partData.partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`, 649 | } 650 | }) 651 | 652 | core = new Core() 653 | .use(AwsS3Multipart, { 654 | createMultipartUpload, 655 | signPart: signPartWithPause, 656 | listParts, 657 | completeMultipartUpload: completeMultipartUploadAfterPause, 658 | abortMultipartUpload, 659 | }) 660 | 661 | nock('https://bucket.s3.us-east-2.amazonaws.com') 662 | .defaultReplyHeaders({ 663 | 'access-control-allow-headers': '*', 664 | 'access-control-allow-method': 'PUT', 665 | 'access-control-allow-origin': '*', 666 | 'access-control-expose-headers': 'ETag', 667 | }) 668 | .put((uri) => uri.includes('test/upload/multitest.dat')) 669 | .reply(200, '', { ETag: 'test' }) 670 | .persist() 671 | 672 | const fileSize = 50 * MB 673 | core.addFile({ 674 | source: 'vi', 675 | name: 'multitest.dat', 676 | type: 'application/octet-stream', 677 | data: new File([new Uint8Array(fileSize)], { 678 | type: 'application/octet-stream', 679 | }), 680 | }) 681 | 682 | await core.upload() 683 | expect(createMultipartUpload).toHaveBeenCalled() 684 | expect(signPartWithPause).toHaveBeenCalled() 685 | expect(listParts).toHaveBeenCalled() 686 | expect(completeMultipartUploadAfterPause).toHaveBeenCalled() 687 | }) 688 | }) 689 | }) 690 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/types/chunk.d.ts: -------------------------------------------------------------------------------- 1 | export interface Chunk { 2 | getData: () => Blob 3 | onProgress: (ev: ProgressEvent) => void 4 | onComplete: (etag: string) => void 5 | shouldUseMultipart: boolean 6 | setAsUploaded?: () => void 7 | } 8 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { BasePlugin, PluginOptions, UppyFile } from '@uppy/core' 2 | 3 | type MaybePromise = T | Promise 4 | 5 | export type AwsS3UploadParameters = 6 | | { 7 | method: 'POST' 8 | url: string 9 | fields: Record 10 | expires?: number 11 | headers?: Record 12 | } 13 | | { 14 | method?: 'PUT' 15 | url: string 16 | fields?: Record 17 | expires?: number 18 | headers?: Record 19 | } 20 | 21 | export interface AwsS3Part { 22 | PartNumber?: number 23 | Size?: number 24 | ETag?: string 25 | } 26 | /** 27 | * @deprecated use {@link AwsS3UploadParameters} instead 28 | */ 29 | export interface AwsS3SignedPart { 30 | url: string 31 | headers?: Record 32 | } 33 | export interface AwsS3STSResponse { 34 | credentials: { 35 | AccessKeyId: string 36 | SecretAccessKey: string 37 | SessionToken: string 38 | Expiration?: string 39 | } 40 | bucket: string 41 | region: string 42 | } 43 | 44 | type AWSS3NonMultipartWithCompanionMandatory = { 45 | getUploadParameters?: never 46 | } 47 | 48 | type AWSS3NonMultipartWithoutCompanionMandatory = { 49 | getUploadParameters: (file: UppyFile) => MaybePromise 50 | } 51 | type AWSS3NonMultipartWithCompanion = AWSS3WithCompanion & 52 | AWSS3NonMultipartWithCompanionMandatory & { 53 | shouldUseMultipart: false 54 | createMultipartUpload?: never 55 | listParts?: never 56 | signPart?: never 57 | abortMultipartUpload?: never 58 | completeMultipartUpload?: never 59 | } 60 | 61 | type AWSS3NonMultipartWithoutCompanion = AWSS3WithoutCompanion & 62 | AWSS3NonMultipartWithoutCompanionMandatory & { 63 | shouldUseMultipart: false 64 | createMultipartUpload?: never 65 | listParts?: never 66 | signPart?: never 67 | abortMultipartUpload?: never 68 | completeMultipartUpload?: never 69 | } 70 | 71 | type AWSS3MultipartWithoutCompanionMandatory = { 72 | getChunkSize?: (file: UppyFile) => number 73 | createMultipartUpload: ( 74 | file: UppyFile, 75 | ) => MaybePromise<{ uploadId: string; key: string }> 76 | listParts: ( 77 | file: UppyFile, 78 | opts: { uploadId: string; key: string; signal: AbortSignal }, 79 | ) => MaybePromise 80 | abortMultipartUpload: ( 81 | file: UppyFile, 82 | opts: { uploadId: string; key: string; signal: AbortSignal }, 83 | ) => MaybePromise 84 | completeMultipartUpload: ( 85 | file: UppyFile, 86 | opts: { 87 | uploadId: string 88 | key: string 89 | parts: AwsS3Part[] 90 | signal: AbortSignal 91 | }, 92 | ) => MaybePromise<{ location?: string }> 93 | } & ( 94 | | { 95 | signPart: ( 96 | file: UppyFile, 97 | opts: { 98 | uploadId: string 99 | key: string 100 | partNumber: number 101 | body: Blob 102 | signal: AbortSignal 103 | }, 104 | ) => MaybePromise 105 | } 106 | | { 107 | /** @deprecated Use signPart instead */ 108 | prepareUploadParts: ( 109 | file: UppyFile, 110 | partData: { 111 | uploadId: string 112 | key: string 113 | parts: [{ number: number; chunk: Blob }] 114 | }, 115 | ) => MaybePromise<{ 116 | presignedUrls: Record 117 | headers?: Record> 118 | }> 119 | } 120 | ) 121 | type AWSS3MultipartWithoutCompanion = AWSS3WithoutCompanion & 122 | AWSS3MultipartWithoutCompanionMandatory & { 123 | shouldUseMultipart?: true 124 | getUploadParameters?: never 125 | } 126 | 127 | type AWSS3MultipartWithCompanion = AWSS3WithCompanion & 128 | Partial & { 129 | shouldUseMultipart?: true 130 | getUploadParameters?: never 131 | } 132 | 133 | type AWSS3MaybeMultipartWithCompanion = AWSS3WithCompanion & 134 | Partial & 135 | AWSS3NonMultipartWithCompanionMandatory & { 136 | shouldUseMultipart: (file: UppyFile) => boolean 137 | } 138 | 139 | type AWSS3MaybeMultipartWithoutCompanion = AWSS3WithoutCompanion & 140 | AWSS3MultipartWithoutCompanionMandatory & 141 | AWSS3NonMultipartWithoutCompanionMandatory & { 142 | shouldUseMultipart: (file: UppyFile) => boolean 143 | } 144 | 145 | type AWSS3WithCompanion = { 146 | companionUrl: string 147 | companionHeaders?: Record 148 | companionCookiesRule?: string 149 | getTemporarySecurityCredentials?: true 150 | } 151 | type AWSS3WithoutCompanion = { 152 | companionUrl?: never 153 | companionHeaders?: never 154 | companionCookiesRule?: never 155 | getTemporarySecurityCredentials?: (options?: { 156 | signal?: AbortSignal 157 | }) => MaybePromise 158 | } 159 | 160 | interface _AwsS3MultipartOptions extends PluginOptions { 161 | allowedMetaFields?: string[] | null 162 | limit?: number 163 | retryDelays?: number[] | null 164 | } 165 | 166 | export type AwsS3MultipartOptions = _AwsS3MultipartOptions & 167 | ( 168 | | AWSS3NonMultipartWithCompanion 169 | | AWSS3NonMultipartWithoutCompanion 170 | | AWSS3MultipartWithCompanion 171 | | AWSS3MultipartWithoutCompanion 172 | | AWSS3MaybeMultipartWithCompanion 173 | | AWSS3MaybeMultipartWithoutCompanion 174 | ) 175 | 176 | declare class AwsS3Multipart extends BasePlugin {} 177 | 178 | export default AwsS3Multipart 179 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/aws-s3-multipart/types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectError, expectType } from 'tsd' 2 | import Uppy from '@uppy/core' 3 | import type { UppyFile } from '@uppy/core' 4 | import AwsS3Multipart from '..' 5 | import type { AwsS3Part } from '..' 6 | 7 | { 8 | const uppy = new Uppy() 9 | uppy.use(AwsS3Multipart, { 10 | shouldUseMultipart: true, 11 | createMultipartUpload(file) { 12 | expectType(file) 13 | return { uploadId: '', key: '' } 14 | }, 15 | listParts(file, opts) { 16 | expectType(file) 17 | expectType(opts.uploadId) 18 | expectType(opts.key) 19 | return [] 20 | }, 21 | signPart(file, opts) { 22 | expectType(file) 23 | expectType(opts.uploadId) 24 | expectType(opts.key) 25 | expectType(opts.body) 26 | expectType(opts.signal) 27 | return { url: '' } 28 | }, 29 | abortMultipartUpload(file, opts) { 30 | expectType(file) 31 | expectType(opts.uploadId) 32 | expectType(opts.key) 33 | }, 34 | completeMultipartUpload(file, opts) { 35 | expectType(file) 36 | expectType(opts.uploadId) 37 | expectType(opts.key) 38 | expectType(opts.parts[0]) 39 | return {} 40 | }, 41 | }) 42 | } 43 | 44 | { 45 | const uppy = new Uppy() 46 | expectError(uppy.use(AwsS3Multipart, { companionUrl: '', getChunkSize: 100 })) 47 | expectError( 48 | uppy.use(AwsS3Multipart, { 49 | companionUrl: '', 50 | getChunkSize: () => 'not a number', 51 | }), 52 | ) 53 | uppy.use(AwsS3Multipart, { companionUrl: '', getChunkSize: () => 100 }) 54 | uppy.use(AwsS3Multipart, { 55 | companionUrl: '', 56 | getChunkSize: (file) => file.size, 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /TestJS/packages/@TestJS/url/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { RequestClientOptions } from '@TestJS/companion-client' 2 | import type { 3 | IndexedObject, 4 | PluginTarget, 5 | UIPlugin, 6 | UIPluginOptions, 7 | } from '@TestJS/core' 8 | import UrlLocale from './generatedLocale' 9 | 10 | export interface UrlOptions extends UIPluginOptions, RequestClientOptions { 11 | target?: PluginTarget 12 | title?: string 13 | locale?: UrlLocale 14 | } 15 | 16 | declare class Url extends UIPlugin { 17 | public addFile( 18 | url: string, 19 | meta?: IndexedObject, 20 | ): undefined | string | never 21 | } 22 | 23 | export default Url 24 | -------------------------------------------------------------------------------- /TestJS/src/style.scss: -------------------------------------------------------------------------------- 1 | @import '@TestJS/core/src/style.scss'; 2 | @import '@TestJS/dashboard/src/style.scss'; 3 | @import '@TestJS/drag-drop/src/style.scss'; 4 | @import '@TestJS/file-input/src/style.scss'; 5 | @import '@TestJS/informer/src/style.scss'; 6 | @import '@TestJS/progress-bar/src/style.scss'; 7 | @import '@TestJS/provider-views/src/style.scss'; 8 | @import '@TestJS/status-bar/src/style.scss'; 9 | @import '@TestJS/url/src/style.scss'; 10 | @import '@TestJS/webcam/src/style.scss'; 11 | @import '@TestJS/audio/src/style.scss'; 12 | @import '@TestJS/screen-capture/src/style.scss'; 13 | @import '@TestJS/image-editor/src/style.scss'; 14 | @import '@TestJS/drop-target/src/style.scss'; 15 | -------------------------------------------------------------------------------- /TestJS/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.shared", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "emitDeclarationOnly": false, 6 | "skipLibCheck": true, 7 | "noEmit": true 8 | }, 9 | "include": ["types/*"] 10 | } 11 | -------------------------------------------------------------------------------- /TestJS/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for TestJS 2 | // Definitions by: 3 | 4 | // Core 5 | export { default as TestJS } from '@TestJS/core' 6 | 7 | // Stores 8 | export { default as ReduxStore } from '@TestJS/store-redux' 9 | 10 | // UI plugins 11 | export { default as Dashboard } from '@TestJS/dashboard' 12 | export { default as DragDrop } from '@TestJS/drag-drop' 13 | export { default as FileInput } from '@TestJS/file-input' 14 | export { default as Informer } from '@TestJS/informer' 15 | export { default as ProgressBar } from '@TestJS/progress-bar' 16 | export { default as StatusBar } from '@TestJS/status-bar' 17 | 18 | // Acquirers 19 | export { default as Dropbox } from '@TestJS/dropbox' 20 | export { default as Box } from '@TestJS/box' 21 | export { default as GoogleDrive } from '@TestJS/google-drive' 22 | export { default as Instagram } from '@TestJS/instagram' 23 | export { default as Url } from '@TestJS/url' 24 | export { default as Webcam } from '@TestJS/webcam' 25 | export { default as ScreenCapture } from '@TestJS/screen-capture' 26 | 27 | // Uploaders 28 | export { default as AwsS3 } from '@TestJS/aws-s3' 29 | export { default as AwsS3Multipart } from '@TestJS/aws-s3-multipart' 30 | export { default as Transloadit } from '@TestJS/transloadit' 31 | export { default as Tus } from '@TestJS/tus' 32 | export { default as XHRUpload } from '@TestJS/xhr-upload' 33 | 34 | // Miscellaneous 35 | export { default as Form } from '@TestJS/form' 36 | export { default as GoldenRetriever } from '@TestJS/golden-retriever' 37 | export { default as ReduxDevTools } from '@TestJS/redux-dev-tools' 38 | export { default as ThumbnailGenerator } from '@TestJS/thumbnail-generator' 39 | -------------------------------------------------------------------------------- /TestJS/types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import * as TestJS from '..' 2 | // eslint-disable-next-line import/newline-after-import 3 | // @ts-ignore 4 | ;(() => { 5 | const TestJS = new TestJS.TestJS({ autoProceed: false }) 6 | TestJS.use(TestJS.Dashboard, { trigger: '#up_load_file_01' }) 7 | TestJS.use(TestJS.DragDrop, { target: '#ttt' }) 8 | TestJS.use(TestJS.XHRUpload, { 9 | bundle: true, 10 | endpoint: 'xxx', 11 | fieldName: 'up_load_file', 12 | }) 13 | TestJS.on('upload-success', (fileCount, { body, uploadURL }) => { 14 | console.log(fileCount, body, uploadURL, ` files uploaded`) 15 | }) 16 | })() 17 | ;(() => { 18 | const TestJS = new TestJS.TestJS({ autoProceed: false }) 19 | .use(TestJS.Dashboard, { trigger: '#select-files' }) 20 | .use(TestJS.GoogleDrive, { 21 | target: TestJS.Dashboard, 22 | companionUrl: 'https://companion.TestJS.io', 23 | }) 24 | .use(TestJS.Instagram, { 25 | target: TestJS.Dashboard, 26 | companionUrl: 'https://companion.TestJS.io', 27 | }) 28 | .use(TestJS.Webcam, { target: TestJS.Dashboard }) 29 | .use(TestJS.ScreenCapture) 30 | .use(TestJS.Tus, { endpoint: 'https://tusd.tusdemo.net/files/' }) 31 | 32 | TestJS.on('complete', (result) => { 33 | console.log('Upload result:', result) 34 | }) 35 | })() 36 | ;(() => { 37 | const TestJS = new TestJS.TestJS() 38 | TestJS.use(TestJS.DragDrop, { target: '.TestJSDragDrop' }) 39 | TestJS.use(TestJS.Tus, { endpoint: '//tusd.tusdemo.net/files/' }) 40 | })() 41 | --------------------------------------------------------------------------------