Browse Source

Merge branch `main`

Antoine du Hamel 1 year ago
parent
commit
35314b629a
100 changed files with 1996 additions and 1045 deletions
  1. 1 1
      .eslintrc.js
  2. 0 0
      .github/workflows/lockfile_check.yml
  3. 1 1
      BUNDLE-README.md
  4. 34 0
      CHANGELOG.md
  5. 53 53
      README.md
  6. 2 2
      examples/aws-nodejs/public/drag.html
  7. 2 2
      examples/aws-nodejs/public/index.html
  8. 3 3
      examples/cdn-example/index.html
  9. 2 0
      examples/custom-provider/client/MyCustomProvider.jsx
  10. 2 2
      examples/uppy-with-companion/client/index.html
  11. 2 2
      package.json
  12. 1 1
      packages/@uppy/aws-s3-multipart/package.json
  13. 1 1
      packages/@uppy/aws-s3-multipart/src/index.js
  14. 7 0
      packages/@uppy/aws-s3/CHANGELOG.md
  15. 1 1
      packages/@uppy/aws-s3/package.json
  16. 1 1
      packages/@uppy/aws-s3/src/index.js
  17. 1 1
      packages/@uppy/box/package.json
  18. 7 0
      packages/@uppy/companion-client/CHANGELOG.md
  19. 1 1
      packages/@uppy/companion-client/package.json
  20. 1 1
      packages/@uppy/companion-client/src/Provider.js
  21. 11 0
      packages/@uppy/companion/CHANGELOG.md
  22. 1 1
      packages/@uppy/companion/package.json
  23. 15 6
      packages/@uppy/companion/src/companion.js
  24. 34 23
      packages/@uppy/companion/src/server/Uploader.js
  25. 3 3
      packages/@uppy/companion/src/server/controllers/connect.js
  26. 1 1
      packages/@uppy/companion/src/server/controllers/logout.js
  27. 1 1
      packages/@uppy/companion/src/server/controllers/oauth-redirect.js
  28. 2 2
      packages/@uppy/companion/src/server/controllers/s3.js
  29. 1 1
      packages/@uppy/companion/src/server/helpers/jwt.js
  30. 3 2
      packages/@uppy/companion/src/server/middlewares.js
  31. 1 1
      packages/@uppy/companion/src/server/provider/Provider.js
  32. 2 2
      packages/@uppy/companion/src/server/provider/box/index.js
  33. 2 6
      packages/@uppy/companion/src/server/provider/drive/index.js
  34. 2 2
      packages/@uppy/companion/src/server/provider/dropbox/index.js
  35. 2 6
      packages/@uppy/companion/src/server/provider/facebook/index.js
  36. 7 25
      packages/@uppy/companion/src/server/provider/index.js
  37. 2 6
      packages/@uppy/companion/src/server/provider/instagram/graph/index.js
  38. 8 6
      packages/@uppy/companion/src/server/provider/onedrive/index.js
  39. 2 6
      packages/@uppy/companion/src/server/provider/zoom/index.js
  40. 29 12
      packages/@uppy/companion/src/server/s3-client.js
  41. 9 7
      packages/@uppy/companion/test/__tests__/provider-manager.js
  42. 7 0
      packages/@uppy/core/CHANGELOG.md
  43. 1 1
      packages/@uppy/core/package.json
  44. 0 85
      packages/@uppy/core/src/BasePlugin.js
  45. 106 0
      packages/@uppy/core/src/BasePlugin.ts
  46. 114 0
      packages/@uppy/core/src/EventManager.ts
  47. 0 137
      packages/@uppy/core/src/Restricter.js
  48. 204 0
      packages/@uppy/core/src/Restricter.ts
  49. 5 5
      packages/@uppy/core/src/UIPlugin.test.ts
  50. 61 29
      packages/@uppy/core/src/UIPlugin.ts
  51. 315 147
      packages/@uppy/core/src/Uppy.test.ts
  52. 530 151
      packages/@uppy/core/src/Uppy.ts
  53. 69 0
      packages/@uppy/core/src/__snapshots__/Uppy.test.ts.snap
  54. 4 1
      packages/@uppy/core/src/getFileName.ts
  55. 0 5
      packages/@uppy/core/src/index.js
  56. 5 0
      packages/@uppy/core/src/index.ts
  57. 2 1
      packages/@uppy/core/src/locale.ts
  58. 0 23
      packages/@uppy/core/src/loggers.js
  59. 25 0
      packages/@uppy/core/src/loggers.ts
  60. 13 6
      packages/@uppy/core/src/mocks/acquirerPlugin1.ts
  61. 13 6
      packages/@uppy/core/src/mocks/acquirerPlugin2.ts
  62. 0 0
      packages/@uppy/core/src/mocks/invalidPlugin.ts
  63. 0 19
      packages/@uppy/core/src/mocks/invalidPluginWithoutId.js
  64. 24 0
      packages/@uppy/core/src/mocks/invalidPluginWithoutId.ts
  65. 0 19
      packages/@uppy/core/src/mocks/invalidPluginWithoutType.js
  66. 24 0
      packages/@uppy/core/src/mocks/invalidPluginWithoutType.ts
  67. 0 29
      packages/@uppy/core/src/supportsUploadProgress.test.js
  68. 57 0
      packages/@uppy/core/src/supportsUploadProgress.test.ts
  69. 4 4
      packages/@uppy/core/src/supportsUploadProgress.ts
  70. 20 0
      packages/@uppy/core/tsconfig.build.json
  71. 16 0
      packages/@uppy/core/tsconfig.json
  72. 1 1
      packages/@uppy/dropbox/package.json
  73. 1 1
      packages/@uppy/facebook/package.json
  74. 1 1
      packages/@uppy/google-drive/package.json
  75. 7 0
      packages/@uppy/image-editor/CHANGELOG.md
  76. 1 1
      packages/@uppy/image-editor/package.json
  77. 1 1
      packages/@uppy/instagram/package.json
  78. 1 1
      packages/@uppy/onedrive/package.json
  79. 7 0
      packages/@uppy/provider-views/CHANGELOG.md
  80. 1 1
      packages/@uppy/provider-views/package.json
  81. 3 1
      packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx
  82. 2 0
      packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx
  83. 6 6
      packages/@uppy/provider-views/src/View.js
  84. 1 1
      packages/@uppy/store-default/package.json
  85. 1 1
      packages/@uppy/tus/package.json
  86. 1 1
      packages/@uppy/tus/src/index.js
  87. 1 1
      packages/@uppy/url/package.json
  88. 6 2
      packages/@uppy/url/src/Url.jsx
  89. 7 0
      packages/@uppy/utils/CHANGELOG.md
  90. 3 1
      packages/@uppy/utils/package.json
  91. 3 119
      packages/@uppy/utils/src/EventManager.ts
  92. 6 2
      packages/@uppy/utils/src/FileProgress.ts
  93. 13 7
      packages/@uppy/utils/src/Translator.ts
  94. 19 18
      packages/@uppy/utils/src/UppyFile.ts
  95. 1 1
      packages/@uppy/utils/src/emitSocketProgress.ts
  96. 8 3
      packages/@uppy/utils/src/fileFilters.ts
  97. 1 1
      packages/@uppy/utils/src/findDOMElement.ts
  98. 3 3
      packages/@uppy/utils/src/generateFileID.ts
  99. 9 9
      packages/@uppy/utils/src/getFileType.test.ts
  100. 1 1
      packages/@uppy/utils/src/getFileType.ts

+ 1 - 1
.eslintrc.js

@@ -469,7 +469,7 @@ module.exports = {
     },
     },
     {
     {
       files: ['packages/@uppy/*/src/**/*.ts', 'packages/@uppy/*/src/**/*.tsx'],
       files: ['packages/@uppy/*/src/**/*.ts', 'packages/@uppy/*/src/**/*.tsx'],
-      excludedFiles: ['packages/@uppy/**/*.test.ts'],
+      excludedFiles: ['packages/@uppy/**/*.test.ts', 'packages/@uppy/core/src/mocks/*.ts'],
       rules: {
       rules: {
         '@typescript-eslint/explicit-function-return-type': 'error',
         '@typescript-eslint/explicit-function-return-type': 'error',
       },
       },

+ 0 - 0
.github/workflows/lockile_check.yml → .github/workflows/lockfile_check.yml


+ 1 - 1
BUNDLE-README.md

@@ -1,7 +1,7 @@
 # Uppy
 # Uppy
 
 
 Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can use
 Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can use
-this from a CDN (`<script src="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.js"></script>`) or bundle it with your webapp.
+this from a CDN (`<script src="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.js"></script>`) or bundle it with your webapp.
 
 
 Note that the recommended way to use Uppy is to install it with yarn/npm and use a
 Note that the recommended way to use Uppy is to install it with yarn/npm and use a
 bundler like Webpack so that you can create a smaller custom build with only the
 bundler like Webpack so that you can create a smaller custom build with only the

+ 34 - 0
CHANGELOG.md

@@ -12,6 +12,40 @@ Please add your entries in this format:
 
 
 In the current stage we aim to release a new version at least every month.
 In the current stage we aim to release a new version at least every month.
 
 
+## 3.21.0
+
+Released: 2023-12-12
+
+| Package                | Version | Package                | Version |
+| ---------------------- | ------- | ---------------------- | ------- |
+| @uppy/aws-s3           |   3.6.0 | @uppy/instagram        |   3.2.0 |
+| @uppy/aws-s3-multipart |  3.10.0 | @uppy/onedrive         |   3.2.0 |
+| @uppy/box              |   2.2.0 | @uppy/provider-views   |   3.8.0 |
+| @uppy/companion        |  4.12.0 | @uppy/store-default    |   3.2.0 |
+| @uppy/companion-client |   3.7.0 | @uppy/tus              |   3.5.0 |
+| @uppy/core             |   3.8.0 | @uppy/url              |   3.5.0 |
+| @uppy/dropbox          |   3.2.0 | @uppy/utils            |   5.7.0 |
+| @uppy/facebook         |   3.2.0 | @uppy/xhr-upload       |   3.6.0 |
+| @uppy/google-drive     |   3.4.0 | @uppy/zoom             |   2.2.0 |
+| @uppy/image-editor     |   2.4.0 | uppy                   |  3.21.0 |
+
+- @uppy/provider-views: fix uploadRemoteFile undefined (Mikael Finstad / #4814)
+- @uppy/companion: fix double tus uploads (Mikael Finstad / #4816)
+- @uppy/companion: fix accelerated endpoints for presigned POST  (Mikael Finstad / #4817)
+- @uppy/companion: fix `authProvider` property inconsistency (Mikael Finstad / #4672)
+- @uppy/companion:  send certain onedrive errors to the user (Mikael Finstad / #4671)
+- meta: fix typo in `lockfile_check.yml` name (Antoine du Hamel)
+- @uppy/aws-s3: change Companion URL in tests (Antoine du Hamel)
+- @uppy/set-state: fix types (Antoine du Hamel)
+- @uppy/companion: Provider user sessions (Mikael Finstad / #4619)
+- meta: fix `js2ts` script on Node.js 20+ (Merlijn Vos / #4802)
+- @uppy/companion-client: avoid unnecessary preflight requests (Antoine du Hamel / #4462)
+- meta: Migrate to AWS-SDK V3 syntax (Artur Paikin / #4810)
+- @uppy/utils: fix import in test files (Antoine du Hamel / #4806)
+- @uppy/core: Fix onBeforeFileAdded with Golden Retriever (Merlijn Vos / #4799)
+- @uppy/image-editor: respect `cropperOptions.initialAspectRatio` (Lucklj521 / #4805)
+
+
 ## 3.20.0
 ## 3.20.0
 
 
 Released: 2023-11-24
 Released: 2023-11-24

+ 53 - 53
README.md

@@ -65,7 +65,7 @@ const uppy = new Uppy()
 npm install @uppy/core @uppy/dashboard @uppy/tus
 npm install @uppy/core @uppy/dashboard @uppy/tus
 ```
 ```
 
 
-Add CSS [uppy.min.css](https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css), either to your HTML page’s `<head>` or include in JS, if your bundler of choice supports it.
+Add CSS [uppy.min.css](https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css), either to your HTML page’s `<head>` or include in JS, if your bundler of choice supports it.
 
 
 Alternatively, you can also use a pre-built bundle from Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object.
 Alternatively, you can also use a pre-built bundle from Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object.
 
 
@@ -73,12 +73,12 @@ Alternatively, you can also use a pre-built bundle from Transloadit’s CDN: Edg
 
 
 ```html
 ```html
 <!-- 1. Add CSS to `<head>` -->
 <!-- 1. Add CSS to `<head>` -->
-<link href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css" rel="stylesheet">
+<link href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css" rel="stylesheet">
 
 
 <!-- 2. Initialize -->
 <!-- 2. Initialize -->
 <div id="files-drag-drop"></div>
 <div id="files-drag-drop"></div>
 <script type="module">
 <script type="module">
-  import { Uppy, Dashboard, Tus } from "https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs"
+  import { Uppy, Dashboard, Tus } from "https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs"
 
 
   const uppy = new Uppy()
   const uppy = new Uppy()
   uppy.use(Dashboard, { target: '#files-drag-drop' })
   uppy.use(Dashboard, { target: '#files-drag-drop' })
@@ -180,7 +180,7 @@ If you’re using Uppy from CDN, those polyfills are already included in the leg
 bundle, so no need to include anything additionally:
 bundle, so no need to include anything additionally:
 
 
 ```html
 ```html
-<script src="https://releases.transloadit.com/uppy/v3.20.0/uppy.legacy.min.js"></script>
+<script src="https://releases.transloadit.com/uppy/v3.21.0/uppy.legacy.min.js"></script>
 ```
 ```
 
 
 ## FAQ
 ## FAQ
@@ -399,101 +399,101 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
 [dviry](https://github.com/dviry) |[galli-leo](https://github.com/galli-leo) |[leods92](https://github.com/leods92) |[leomelzer](https://github.com/leomelzer) |[dolphinigle](https://github.com/dolphinigle) |[louim](https://github.com/louim) |
 [dviry](https://github.com/dviry) |[galli-leo](https://github.com/galli-leo) |[leods92](https://github.com/leods92) |[leomelzer](https://github.com/leomelzer) |[dolphinigle](https://github.com/dolphinigle) |[louim](https://github.com/louim) |
 
 
-[<img alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117" width="117">](https://github.com/ombr) |[<img alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117" width="117">](https://github.com/lucaperret) |[<img alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117" width="117">](https://github.com/lucax88x) |[<img alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117" width="117">](https://github.com/marc-mabe) |[<img alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117" width="117">](https://github.com/onhate) |[<img alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117" width="117">](https://github.com/mperrando) |
+[<img alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117" width="117">](https://github.com/ombr) |[<img alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117" width="117">](https://github.com/lucaperret) |[<img alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117" width="117">](https://github.com/lucax88x) |[<img alt="Lucklj521" src="https://avatars.githubusercontent.com/u/93632042?v=4&s=117" width="117">](https://github.com/Lucklj521) |[<img alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117" width="117">](https://github.com/marc-mabe) |[<img alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117" width="117">](https://github.com/onhate) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[ombr](https://github.com/ombr) |[lucaperret](https://github.com/lucaperret) |[lucax88x](https://github.com/lucax88x) |[marc-mabe](https://github.com/marc-mabe) |[onhate](https://github.com/onhate) |[mperrando](https://github.com/mperrando) |
+[ombr](https://github.com/ombr) |[lucaperret](https://github.com/lucaperret) |[lucax88x](https://github.com/lucax88x) |[Lucklj521](https://github.com/Lucklj521) |[marc-mabe](https://github.com/marc-mabe) |[onhate](https://github.com/onhate) |
 
 
-[<img alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117" width="117">](https://github.com/marcosthejew) |[<img alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117" width="117">](https://github.com/marcusforsberg) |[<img alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117" width="117">](https://github.com/martin-brennan) |[<img alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117" width="117">](https://github.com/masaok) |[<img alt="masumulu28" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117" width="117">](https://github.com/masumulu28) |[<img alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117" width="117">](https://github.com/mateuscruz) |
+[<img alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117" width="117">](https://github.com/mperrando) |[<img alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117" width="117">](https://github.com/marcosthejew) |[<img alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117" width="117">](https://github.com/marcusforsberg) |[<img alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117" width="117">](https://github.com/martin-brennan) |[<img alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117" width="117">](https://github.com/masaok) |[<img alt="masumulu28" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117" width="117">](https://github.com/masumulu28) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[marcosthejew](https://github.com/marcosthejew) |[marcusforsberg](https://github.com/marcusforsberg) |[martin-brennan](https://github.com/martin-brennan) |[masaok](https://github.com/masaok) |[masumulu28](https://github.com/masumulu28) |[mateuscruz](https://github.com/mateuscruz) |
+[mperrando](https://github.com/mperrando) |[marcosthejew](https://github.com/marcosthejew) |[marcusforsberg](https://github.com/marcusforsberg) |[martin-brennan](https://github.com/martin-brennan) |[masaok](https://github.com/masaok) |[masumulu28](https://github.com/masumulu28) |
 
 
-[<img alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117" width="117">](https://github.com/mattfik) |[<img alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117" width="117">](https://github.com/mjesuele) |[<img alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117" width="117">](https://github.com/matthewhartstonge) |[<img alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117" width="117">](https://github.com/mauricioribeiro) |[<img alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117" width="117">](https://github.com/hrsh) |[<img alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117" width="117">](https://github.com/mhulet) |
+[<img alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117" width="117">](https://github.com/mateuscruz) |[<img alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117" width="117">](https://github.com/mattfik) |[<img alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117" width="117">](https://github.com/mjesuele) |[<img alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117" width="117">](https://github.com/matthewhartstonge) |[<img alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117" width="117">](https://github.com/mauricioribeiro) |[<img alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117" width="117">](https://github.com/hrsh) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[mattfik](https://github.com/mattfik) |[mjesuele](https://github.com/mjesuele) |[matthewhartstonge](https://github.com/matthewhartstonge) |[mauricioribeiro](https://github.com/mauricioribeiro) |[hrsh](https://github.com/hrsh) |[mhulet](https://github.com/mhulet) |
+[mateuscruz](https://github.com/mateuscruz) |[mattfik](https://github.com/mattfik) |[mjesuele](https://github.com/mjesuele) |[matthewhartstonge](https://github.com/matthewhartstonge) |[mauricioribeiro](https://github.com/mauricioribeiro) |[hrsh](https://github.com/hrsh) |
 
 
-[<img alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117" width="117">](https://github.com/mkopinsky) |[<img alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117" width="117">](https://github.com/ken-kuro) |[<img alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117" width="117">](https://github.com/achmiral) |[<img alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117" width="117">](https://github.com/boudra) |[<img alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117" width="117">](https://github.com/mnafees) |[<img alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117" width="117">](https://github.com/shahimclt) |
+[<img alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117" width="117">](https://github.com/mhulet) |[<img alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117" width="117">](https://github.com/mkopinsky) |[<img alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117" width="117">](https://github.com/ken-kuro) |[<img alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117" width="117">](https://github.com/achmiral) |[<img alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117" width="117">](https://github.com/boudra) |[<img alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117" width="117">](https://github.com/mnafees) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[mkopinsky](https://github.com/mkopinsky) |[ken-kuro](https://github.com/ken-kuro) |[achmiral](https://github.com/achmiral) |[boudra](https://github.com/boudra) |[mnafees](https://github.com/mnafees) |[shahimclt](https://github.com/shahimclt) |
+[mhulet](https://github.com/mhulet) |[mkopinsky](https://github.com/mkopinsky) |[ken-kuro](https://github.com/ken-kuro) |[achmiral](https://github.com/achmiral) |[boudra](https://github.com/boudra) |[mnafees](https://github.com/mnafees) |
 
 
-[<img alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117" width="117">](https://github.com/mogzol) |[<img alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117" width="117">](https://github.com/navruzm) |[<img alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117" width="117">](https://github.com/marton-laszlo-attila) |[<img alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117" width="117">](https://github.com/pleasespammelater) |[<img alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117" width="117">](https://github.com/naveed-ahmad) |[<img alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117" width="117">](https://github.com/trungcva10a6tn) |
+[<img alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117" width="117">](https://github.com/shahimclt) |[<img alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117" width="117">](https://github.com/mogzol) |[<img alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117" width="117">](https://github.com/navruzm) |[<img alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117" width="117">](https://github.com/marton-laszlo-attila) |[<img alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117" width="117">](https://github.com/pleasespammelater) |[<img alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117" width="117">](https://github.com/naveed-ahmad) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[mogzol](https://github.com/mogzol) |[navruzm](https://github.com/navruzm) |[marton-laszlo-attila](https://github.com/marton-laszlo-attila) |[pleasespammelater](https://github.com/pleasespammelater) |[naveed-ahmad](https://github.com/naveed-ahmad) |[trungcva10a6tn](https://github.com/trungcva10a6tn) |
+[shahimclt](https://github.com/shahimclt) |[mogzol](https://github.com/mogzol) |[navruzm](https://github.com/navruzm) |[marton-laszlo-attila](https://github.com/marton-laszlo-attila) |[pleasespammelater](https://github.com/pleasespammelater) |[naveed-ahmad](https://github.com/naveed-ahmad) |
 
 
-[<img alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117" width="117">](https://github.com/nicojones) |[<img alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117" width="117">](https://github.com/coreprocess) |[<img alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117" width="117">](https://github.com/nil1511) |[<img alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117" width="117">](https://github.com/leftdevel) |[<img alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117" width="117">](https://github.com/Ozodbek1405) |[<img alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117" width="117">](https://github.com/cryptic022) |
+[<img alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117" width="117">](https://github.com/trungcva10a6tn) |[<img alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117" width="117">](https://github.com/nicojones) |[<img alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117" width="117">](https://github.com/coreprocess) |[<img alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117" width="117">](https://github.com/nil1511) |[<img alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117" width="117">](https://github.com/leftdevel) |[<img alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117" width="117">](https://github.com/Ozodbek1405) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[nicojones](https://github.com/nicojones) |[coreprocess](https://github.com/coreprocess) |[nil1511](https://github.com/nil1511) |[leftdevel](https://github.com/leftdevel) |[Ozodbek1405](https://github.com/Ozodbek1405) |[cryptic022](https://github.com/cryptic022) |
+[trungcva10a6tn](https://github.com/trungcva10a6tn) |[nicojones](https://github.com/nicojones) |[coreprocess](https://github.com/coreprocess) |[nil1511](https://github.com/nil1511) |[leftdevel](https://github.com/leftdevel) |[Ozodbek1405](https://github.com/Ozodbek1405) |
 
 
-[<img alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117" width="117">](https://github.com/ParsaArvanehPA) |[<img alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117" width="117">](https://github.com/pascalwengerter) |[<img alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117" width="117">](https://github.com/patricklindsay) |[<img alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117" width="117">](https://github.com/plneto) |[<img alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117" width="117">](https://github.com/pedrofs) |[<img alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117" width="117">](https://github.com/pmusaraj) |
+[<img alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117" width="117">](https://github.com/cryptic022) |[<img alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117" width="117">](https://github.com/ParsaArvanehPA) |[<img alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117" width="117">](https://github.com/pascalwengerter) |[<img alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117" width="117">](https://github.com/patricklindsay) |[<img alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117" width="117">](https://github.com/plneto) |[<img alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117" width="117">](https://github.com/pedrofs) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[ParsaArvanehPA](https://github.com/ParsaArvanehPA) |[pascalwengerter](https://github.com/pascalwengerter) |[patricklindsay](https://github.com/patricklindsay) |[plneto](https://github.com/plneto) |[pedrofs](https://github.com/pedrofs) |[pmusaraj](https://github.com/pmusaraj) |
+[cryptic022](https://github.com/cryptic022) |[ParsaArvanehPA](https://github.com/ParsaArvanehPA) |[pascalwengerter](https://github.com/pascalwengerter) |[patricklindsay](https://github.com/patricklindsay) |[plneto](https://github.com/plneto) |[pedrofs](https://github.com/pedrofs) |
 
 
-[<img alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117" width="117">](https://github.com/phillipalexander) |[<img alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117" width="117">](https://github.com/ppadmavilasom) |[<img alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117" width="117">](https://github.com/Pzoco) |[<img alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117" width="117">](https://github.com/eman8519) |[<img alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117" width="117">](https://github.com/luarmr) |[<img alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117" width="117">](https://github.com/raulibanez) |
+[<img alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117" width="117">](https://github.com/pmusaraj) |[<img alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117" width="117">](https://github.com/phillipalexander) |[<img alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117" width="117">](https://github.com/ppadmavilasom) |[<img alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117" width="117">](https://github.com/Pzoco) |[<img alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117" width="117">](https://github.com/eman8519) |[<img alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117" width="117">](https://github.com/luarmr) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[phillipalexander](https://github.com/phillipalexander) |[ppadmavilasom](https://github.com/ppadmavilasom) |[Pzoco](https://github.com/Pzoco) |[eman8519](https://github.com/eman8519) |[luarmr](https://github.com/luarmr) |[raulibanez](https://github.com/raulibanez) |
+[pmusaraj](https://github.com/pmusaraj) |[phillipalexander](https://github.com/phillipalexander) |[ppadmavilasom](https://github.com/ppadmavilasom) |[Pzoco](https://github.com/Pzoco) |[eman8519](https://github.com/eman8519) |[luarmr](https://github.com/luarmr) |
 
 
-[<img alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117" width="117">](https://github.com/refo) |[<img alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117" width="117">](https://github.com/SxDx) |[<img alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117" width="117">](https://github.com/robwilson1) |[<img alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117" width="117">](https://github.com/scherroman) |[<img alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117" width="117">](https://github.com/rossng) |[<img alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117" width="117">](https://github.com/rart) |
+[<img alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117" width="117">](https://github.com/raulibanez) |[<img alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117" width="117">](https://github.com/refo) |[<img alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117" width="117">](https://github.com/SxDx) |[<img alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117" width="117">](https://github.com/robwilson1) |[<img alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117" width="117">](https://github.com/scherroman) |[<img alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117" width="117">](https://github.com/rossng) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[refo](https://github.com/refo) |[SxDx](https://github.com/SxDx) |[robwilson1](https://github.com/robwilson1) |[scherroman](https://github.com/scherroman) |[rossng](https://github.com/rossng) |[rart](https://github.com/rart) |
+[raulibanez](https://github.com/raulibanez) |[refo](https://github.com/refo) |[SxDx](https://github.com/SxDx) |[robwilson1](https://github.com/robwilson1) |[scherroman](https://github.com/scherroman) |[rossng](https://github.com/rossng) |
 
 
-[<img alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117" width="117">](https://github.com/GNURub) |[<img alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117" width="117">](https://github.com/fortunto2) |[<img alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117" width="117">](https://github.com/samuelcolburn) |[<img alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117" width="117">](https://github.com/sdebacker) |[<img alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117" width="117">](https://github.com/sebasegovia01) |[<img alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117" width="117">](https://github.com/sergei-zelinsky) |
+[<img alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117" width="117">](https://github.com/rart) |[<img alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117" width="117">](https://github.com/GNURub) |[<img alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117" width="117">](https://github.com/fortunto2) |[<img alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117" width="117">](https://github.com/samuelcolburn) |[<img alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117" width="117">](https://github.com/sdebacker) |[<img alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117" width="117">](https://github.com/sebasegovia01) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[GNURub](https://github.com/GNURub) |[fortunto2](https://github.com/fortunto2) |[samuelcolburn](https://github.com/samuelcolburn) |[sdebacker](https://github.com/sdebacker) |[sebasegovia01](https://github.com/sebasegovia01) |[sergei-zelinsky](https://github.com/sergei-zelinsky) |
+[rart](https://github.com/rart) |[GNURub](https://github.com/GNURub) |[fortunto2](https://github.com/fortunto2) |[samuelcolburn](https://github.com/samuelcolburn) |[sdebacker](https://github.com/sdebacker) |[sebasegovia01](https://github.com/sebasegovia01) |
 
 
-[<img alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117" width="117">](https://github.com/szh) |[<img alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117" width="117">](https://github.com/SpazzMarticus) |[<img alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117" width="117">](https://github.com/waptik) |[<img alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117" width="117">](https://github.com/quigebo) |[<img alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117" width="117">](https://github.com/amaitu) |[<img alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117" width="117">](https://github.com/steverob) |
+[<img alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117" width="117">](https://github.com/sergei-zelinsky) |[<img alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117" width="117">](https://github.com/szh) |[<img alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117" width="117">](https://github.com/SpazzMarticus) |[<img alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117" width="117">](https://github.com/waptik) |[<img alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117" width="117">](https://github.com/quigebo) |[<img alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117" width="117">](https://github.com/amaitu) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[szh](https://github.com/szh) |[SpazzMarticus](https://github.com/SpazzMarticus) |[waptik](https://github.com/waptik) |[quigebo](https://github.com/quigebo) |[amaitu](https://github.com/amaitu) |[steverob](https://github.com/steverob) |
+[sergei-zelinsky](https://github.com/sergei-zelinsky) |[szh](https://github.com/szh) |[SpazzMarticus](https://github.com/SpazzMarticus) |[waptik](https://github.com/waptik) |[quigebo](https://github.com/quigebo) |[amaitu](https://github.com/amaitu) |
 
 
-[<img alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117" width="117">](https://github.com/sjauld) |[<img alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117" width="117">](https://github.com/strayer) |[<img alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117" width="117">](https://github.com/taj) |[<img alt="Tashows" src="https://avatars.githubusercontent.com/u/16656928?v=4&s=117" width="117">](https://github.com/Tashows) |[<img alt="tcgj" src="https://avatars.githubusercontent.com/u/7994529?v=4&s=117" width="117">](https://github.com/tcgj) |[<img alt="twarlop" src="https://avatars.githubusercontent.com/u/2856082?v=4&s=117" width="117">](https://github.com/twarlop) |
+[<img alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117" width="117">](https://github.com/steverob) |[<img alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117" width="117">](https://github.com/sjauld) |[<img alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117" width="117">](https://github.com/strayer) |[<img alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117" width="117">](https://github.com/taj) |[<img alt="Tashows" src="https://avatars.githubusercontent.com/u/16656928?v=4&s=117" width="117">](https://github.com/Tashows) |[<img alt="tcgj" src="https://avatars.githubusercontent.com/u/7994529?v=4&s=117" width="117">](https://github.com/tcgj) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[sjauld](https://github.com/sjauld) |[strayer](https://github.com/strayer) |[taj](https://github.com/taj) |[Tashows](https://github.com/Tashows) |[tcgj](https://github.com/tcgj) |[twarlop](https://github.com/twarlop) |
+[steverob](https://github.com/steverob) |[sjauld](https://github.com/sjauld) |[strayer](https://github.com/strayer) |[taj](https://github.com/taj) |[Tashows](https://github.com/Tashows) |[tcgj](https://github.com/tcgj) |
 
 
-[<img alt="tmaier" src="https://avatars.githubusercontent.com/u/350038?v=4&s=117" width="117">](https://github.com/tmaier) |[<img alt="WIStudent" src="https://avatars.githubusercontent.com/u/2707930?v=4&s=117" width="117">](https://github.com/WIStudent) |[<img alt="tomsaleeba" src="https://avatars.githubusercontent.com/u/1773838?v=4&s=117" width="117">](https://github.com/tomsaleeba) |[<img alt="tomekp" src="https://avatars.githubusercontent.com/u/1856393?v=4&s=117" width="117">](https://github.com/tomekp) |[<img alt="tvaliasek" src="https://avatars.githubusercontent.com/u/8644946?v=4&s=117" width="117">](https://github.com/tvaliasek) |[<img alt="top-master" src="https://avatars.githubusercontent.com/u/31405473?v=4&s=117" width="117">](https://github.com/top-master) |
+[<img alt="twarlop" src="https://avatars.githubusercontent.com/u/2856082?v=4&s=117" width="117">](https://github.com/twarlop) |[<img alt="tmaier" src="https://avatars.githubusercontent.com/u/350038?v=4&s=117" width="117">](https://github.com/tmaier) |[<img alt="WIStudent" src="https://avatars.githubusercontent.com/u/2707930?v=4&s=117" width="117">](https://github.com/WIStudent) |[<img alt="tomsaleeba" src="https://avatars.githubusercontent.com/u/1773838?v=4&s=117" width="117">](https://github.com/tomsaleeba) |[<img alt="tomekp" src="https://avatars.githubusercontent.com/u/1856393?v=4&s=117" width="117">](https://github.com/tomekp) |[<img alt="tvaliasek" src="https://avatars.githubusercontent.com/u/8644946?v=4&s=117" width="117">](https://github.com/tvaliasek) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[tmaier](https://github.com/tmaier) |[WIStudent](https://github.com/WIStudent) |[tomsaleeba](https://github.com/tomsaleeba) |[tomekp](https://github.com/tomekp) |[tvaliasek](https://github.com/tvaliasek) |[top-master](https://github.com/top-master) |
+[twarlop](https://github.com/twarlop) |[tmaier](https://github.com/tmaier) |[WIStudent](https://github.com/WIStudent) |[tomsaleeba](https://github.com/tomsaleeba) |[tomekp](https://github.com/tomekp) |[tvaliasek](https://github.com/tvaliasek) |
 
 
-[<img alt="trivikr" src="https://avatars.githubusercontent.com/u/16024985?v=4&s=117" width="117">](https://github.com/trivikr) |[<img alt="vially" src="https://avatars.githubusercontent.com/u/433598?v=4&s=117" width="117">](https://github.com/vially) |[<img alt="valentinoli" src="https://avatars.githubusercontent.com/u/23453691?v=4&s=117" width="117">](https://github.com/valentinoli) |[<img alt="stiig" src="https://avatars.githubusercontent.com/u/8639922?v=4&s=117" width="117">](https://github.com/stiig) |[<img alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117" width="117">](https://github.com/nagyv) |[<img alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117" width="117">](https://github.com/dwnste) |
+[<img alt="top-master" src="https://avatars.githubusercontent.com/u/31405473?v=4&s=117" width="117">](https://github.com/top-master) |[<img alt="trivikr" src="https://avatars.githubusercontent.com/u/16024985?v=4&s=117" width="117">](https://github.com/trivikr) |[<img alt="vially" src="https://avatars.githubusercontent.com/u/433598?v=4&s=117" width="117">](https://github.com/vially) |[<img alt="valentinoli" src="https://avatars.githubusercontent.com/u/23453691?v=4&s=117" width="117">](https://github.com/valentinoli) |[<img alt="stiig" src="https://avatars.githubusercontent.com/u/8639922?v=4&s=117" width="117">](https://github.com/stiig) |[<img alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117" width="117">](https://github.com/nagyv) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[trivikr](https://github.com/trivikr) |[vially](https://github.com/vially) |[valentinoli](https://github.com/valentinoli) |[stiig](https://github.com/stiig) |[nagyv](https://github.com/nagyv) |[dwnste](https://github.com/dwnste) |
+[top-master](https://github.com/top-master) |[trivikr](https://github.com/trivikr) |[vially](https://github.com/vially) |[valentinoli](https://github.com/valentinoli) |[stiig](https://github.com/stiig) |[nagyv](https://github.com/nagyv) |
 
 
-[<img alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117" width="117">](https://github.com/weston-sankey-mark43) |[<img alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117" width="117">](https://github.com/willycamargo) |[<img alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117" width="117">](https://github.com/xhocquet) |[<img alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117" width="117">](https://github.com/YehudaKremer) |[<img alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117" width="117">](https://github.com/zachconner) |[<img alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117" width="117">](https://github.com/zlawson-ut) |
+[<img alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117" width="117">](https://github.com/dwnste) |[<img alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117" width="117">](https://github.com/weston-sankey-mark43) |[<img alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117" width="117">](https://github.com/willycamargo) |[<img alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117" width="117">](https://github.com/xhocquet) |[<img alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117" width="117">](https://github.com/YehudaKremer) |[<img alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117" width="117">](https://github.com/zachconner) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[weston-sankey-mark43](https://github.com/weston-sankey-mark43) |[willycamargo](https://github.com/willycamargo) |[xhocquet](https://github.com/xhocquet) |[YehudaKremer](https://github.com/YehudaKremer) |[zachconner](https://github.com/zachconner) |[zlawson-ut](https://github.com/zlawson-ut) |
+[dwnste](https://github.com/dwnste) |[weston-sankey-mark43](https://github.com/weston-sankey-mark43) |[willycamargo](https://github.com/willycamargo) |[xhocquet](https://github.com/xhocquet) |[YehudaKremer](https://github.com/YehudaKremer) |[zachconner](https://github.com/zachconner) |
 
 
-[<img alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117" width="117">](https://github.com/zackbloom) |[<img alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117" width="117">](https://github.com/sartoshi-foot-dao) |[<img alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117" width="117">](https://github.com/aduh95-test-account) |[<img alt="agreene-coursera" src="https://avatars.githubusercontent.com/u/30501355?v=4&s=117" width="117">](https://github.com/agreene-coursera) |[<img alt="alfatv" src="https://avatars.githubusercontent.com/u/62238673?v=4&s=117" width="117">](https://github.com/alfatv) |[<img alt="arggh" src="https://avatars.githubusercontent.com/u/17210302?v=4&s=117" width="117">](https://github.com/arggh) |
+[<img alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117" width="117">](https://github.com/zlawson-ut) |[<img alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117" width="117">](https://github.com/zackbloom) |[<img alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117" width="117">](https://github.com/sartoshi-foot-dao) |[<img alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117" width="117">](https://github.com/aduh95-test-account) |[<img alt="agreene-coursera" src="https://avatars.githubusercontent.com/u/30501355?v=4&s=117" width="117">](https://github.com/agreene-coursera) |[<img alt="alfatv" src="https://avatars.githubusercontent.com/u/62238673?v=4&s=117" width="117">](https://github.com/alfatv) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[zackbloom](https://github.com/zackbloom) |[sartoshi-foot-dao](https://github.com/sartoshi-foot-dao) |[aduh95-test-account](https://github.com/aduh95-test-account) |[agreene-coursera](https://github.com/agreene-coursera) |[alfatv](https://github.com/alfatv) |[arggh](https://github.com/arggh) |
+[zlawson-ut](https://github.com/zlawson-ut) |[zackbloom](https://github.com/zackbloom) |[sartoshi-foot-dao](https://github.com/sartoshi-foot-dao) |[aduh95-test-account](https://github.com/aduh95-test-account) |[agreene-coursera](https://github.com/agreene-coursera) |[alfatv](https://github.com/alfatv) |
 
 
-[<img alt="avalla" src="https://avatars.githubusercontent.com/u/986614?v=4&s=117" width="117">](https://github.com/avalla) |[<img alt="c0b41" src="https://avatars.githubusercontent.com/u/2834954?v=4&s=117" width="117">](https://github.com/c0b41) |[<img alt="canvasbh" src="https://avatars.githubusercontent.com/u/44477734?v=4&s=117" width="117">](https://github.com/canvasbh) |[<img alt="cgoinglove" src="https://avatars.githubusercontent.com/u/86150470?v=4&s=117" width="117">](https://github.com/cgoinglove) |[<img alt="christianwengert" src="https://avatars.githubusercontent.com/u/12936057?v=4&s=117" width="117">](https://github.com/christianwengert) |[<img alt="codehero7386" src="https://avatars.githubusercontent.com/u/56253286?v=4&s=117" width="117">](https://github.com/codehero7386) |
+[<img alt="arggh" src="https://avatars.githubusercontent.com/u/17210302?v=4&s=117" width="117">](https://github.com/arggh) |[<img alt="avalla" src="https://avatars.githubusercontent.com/u/986614?v=4&s=117" width="117">](https://github.com/avalla) |[<img alt="c0b41" src="https://avatars.githubusercontent.com/u/2834954?v=4&s=117" width="117">](https://github.com/c0b41) |[<img alt="canvasbh" src="https://avatars.githubusercontent.com/u/44477734?v=4&s=117" width="117">](https://github.com/canvasbh) |[<img alt="cgoinglove" src="https://avatars.githubusercontent.com/u/86150470?v=4&s=117" width="117">](https://github.com/cgoinglove) |[<img alt="christianwengert" src="https://avatars.githubusercontent.com/u/12936057?v=4&s=117" width="117">](https://github.com/christianwengert) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[avalla](https://github.com/avalla) |[c0b41](https://github.com/c0b41) |[canvasbh](https://github.com/canvasbh) |[cgoinglove](https://github.com/cgoinglove) |[christianwengert](https://github.com/christianwengert) |[codehero7386](https://github.com/codehero7386) |
+[arggh](https://github.com/arggh) |[avalla](https://github.com/avalla) |[c0b41](https://github.com/c0b41) |[canvasbh](https://github.com/canvasbh) |[cgoinglove](https://github.com/cgoinglove) |[christianwengert](https://github.com/christianwengert) |
 
 
-[<img alt="craigcbrunner" src="https://avatars.githubusercontent.com/u/2780521?v=4&s=117" width="117">](https://github.com/craigcbrunner) |[<img alt="darthf1" src="https://avatars.githubusercontent.com/u/17253332?v=4&s=117" width="117">](https://github.com/darthf1) |[<img alt="dkisic" src="https://avatars.githubusercontent.com/u/32257921?v=4&s=117" width="117">](https://github.com/dkisic) |[<img alt="dzcpy" src="https://avatars.githubusercontent.com/u/203980?v=4&s=117" width="117">](https://github.com/dzcpy) |[<img alt="elliotsayes" src="https://avatars.githubusercontent.com/u/7699058?v=4&s=117" width="117">](https://github.com/elliotsayes) |[<img alt="fingul" src="https://avatars.githubusercontent.com/u/894739?v=4&s=117" width="117">](https://github.com/fingul) |
+[<img alt="codehero7386" src="https://avatars.githubusercontent.com/u/56253286?v=4&s=117" width="117">](https://github.com/codehero7386) |[<img alt="craigcbrunner" src="https://avatars.githubusercontent.com/u/2780521?v=4&s=117" width="117">](https://github.com/craigcbrunner) |[<img alt="darthf1" src="https://avatars.githubusercontent.com/u/17253332?v=4&s=117" width="117">](https://github.com/darthf1) |[<img alt="dkisic" src="https://avatars.githubusercontent.com/u/32257921?v=4&s=117" width="117">](https://github.com/dkisic) |[<img alt="dzcpy" src="https://avatars.githubusercontent.com/u/203980?v=4&s=117" width="117">](https://github.com/dzcpy) |[<img alt="elliotsayes" src="https://avatars.githubusercontent.com/u/7699058?v=4&s=117" width="117">](https://github.com/elliotsayes) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[craigcbrunner](https://github.com/craigcbrunner) |[darthf1](https://github.com/darthf1) |[dkisic](https://github.com/dkisic) |[dzcpy](https://github.com/dzcpy) |[elliotsayes](https://github.com/elliotsayes) |[fingul](https://github.com/fingul) |
+[codehero7386](https://github.com/codehero7386) |[craigcbrunner](https://github.com/craigcbrunner) |[darthf1](https://github.com/darthf1) |[dkisic](https://github.com/dkisic) |[dzcpy](https://github.com/dzcpy) |[elliotsayes](https://github.com/elliotsayes) |
 
 
-[<img alt="franckl" src="https://avatars.githubusercontent.com/u/3875803?v=4&s=117" width="117">](https://github.com/franckl) |[<img alt="frederikhors" src="https://avatars.githubusercontent.com/u/41120635?v=4&s=117" width="117">](https://github.com/frederikhors) |[<img alt="gaelicwinter" src="https://avatars.githubusercontent.com/u/6510266?v=4&s=117" width="117">](https://github.com/gaelicwinter) |[<img alt="green-mike" src="https://avatars.githubusercontent.com/u/5584225?v=4&s=117" width="117">](https://github.com/green-mike) |[<img alt="hxgf" src="https://avatars.githubusercontent.com/u/56104?v=4&s=117" width="117">](https://github.com/hxgf) |[<img alt="johnmanjiro13" src="https://avatars.githubusercontent.com/u/28798279?v=4&s=117" width="117">](https://github.com/johnmanjiro13) |
+[<img alt="fingul" src="https://avatars.githubusercontent.com/u/894739?v=4&s=117" width="117">](https://github.com/fingul) |[<img alt="franckl" src="https://avatars.githubusercontent.com/u/3875803?v=4&s=117" width="117">](https://github.com/franckl) |[<img alt="frederikhors" src="https://avatars.githubusercontent.com/u/41120635?v=4&s=117" width="117">](https://github.com/frederikhors) |[<img alt="gaelicwinter" src="https://avatars.githubusercontent.com/u/6510266?v=4&s=117" width="117">](https://github.com/gaelicwinter) |[<img alt="green-mike" src="https://avatars.githubusercontent.com/u/5584225?v=4&s=117" width="117">](https://github.com/green-mike) |[<img alt="hxgf" src="https://avatars.githubusercontent.com/u/56104?v=4&s=117" width="117">](https://github.com/hxgf) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[franckl](https://github.com/franckl) |[frederikhors](https://github.com/frederikhors) |[gaelicwinter](https://github.com/gaelicwinter) |[green-mike](https://github.com/green-mike) |[hxgf](https://github.com/hxgf) |[johnmanjiro13](https://github.com/johnmanjiro13) |
+[fingul](https://github.com/fingul) |[franckl](https://github.com/franckl) |[frederikhors](https://github.com/frederikhors) |[gaelicwinter](https://github.com/gaelicwinter) |[green-mike](https://github.com/green-mike) |[hxgf](https://github.com/hxgf) |
 
 
-[<img alt="jur-ng" src="https://avatars.githubusercontent.com/u/111122756?v=4&s=117" width="117">](https://github.com/jur-ng) |[<img alt="sontixyou" src="https://avatars.githubusercontent.com/u/19817196?v=4&s=117" width="117">](https://github.com/sontixyou) |[<img alt="kode-ninja" src="https://avatars.githubusercontent.com/u/7857611?v=4&s=117" width="117">](https://github.com/kode-ninja) |[<img alt="jx-zyf" src="https://avatars.githubusercontent.com/u/26456842?v=4&s=117" width="117">](https://github.com/jx-zyf) |[<img alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117" width="117">](https://github.com/magumbo) |[<img alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117" width="117">](https://github.com/mdxiaohu) |
+[<img alt="johnmanjiro13" src="https://avatars.githubusercontent.com/u/28798279?v=4&s=117" width="117">](https://github.com/johnmanjiro13) |[<img alt="jur-ng" src="https://avatars.githubusercontent.com/u/111122756?v=4&s=117" width="117">](https://github.com/jur-ng) |[<img alt="sontixyou" src="https://avatars.githubusercontent.com/u/19817196?v=4&s=117" width="117">](https://github.com/sontixyou) |[<img alt="kode-ninja" src="https://avatars.githubusercontent.com/u/7857611?v=4&s=117" width="117">](https://github.com/kode-ninja) |[<img alt="jx-zyf" src="https://avatars.githubusercontent.com/u/26456842?v=4&s=117" width="117">](https://github.com/jx-zyf) |[<img alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117" width="117">](https://github.com/magumbo) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[jur-ng](https://github.com/jur-ng) |[sontixyou](https://github.com/sontixyou) |[kode-ninja](https://github.com/kode-ninja) |[jx-zyf](https://github.com/jx-zyf) |[magumbo](https://github.com/magumbo) |[mdxiaohu](https://github.com/mdxiaohu) |
+[johnmanjiro13](https://github.com/johnmanjiro13) |[jur-ng](https://github.com/jur-ng) |[sontixyou](https://github.com/sontixyou) |[kode-ninja](https://github.com/kode-ninja) |[jx-zyf](https://github.com/jx-zyf) |[magumbo](https://github.com/magumbo) |
 
 
-[<img alt="mjlumetta" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117" width="117">](https://github.com/mjlumetta) |[<img alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117" width="117">](https://github.com/mosi-kha) |[<img alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117" width="117">](https://github.com/neuronet77) |[<img alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117" width="117">](https://github.com/ninesalt) |[<img alt="odselsevier" src="https://avatars.githubusercontent.com/u/95745934?v=4&s=117" width="117">](https://github.com/odselsevier) |[<img alt="ordago" src="https://avatars.githubusercontent.com/u/6376814?v=4&s=117" width="117">](https://github.com/ordago) |
+[<img alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117" width="117">](https://github.com/mdxiaohu) |[<img alt="maddy-jo" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117" width="117">](https://github.com/maddy-jo) |[<img alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117" width="117">](https://github.com/mosi-kha) |[<img alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117" width="117">](https://github.com/neuronet77) |[<img alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117" width="117">](https://github.com/ninesalt) |[<img alt="odselsevier" src="https://avatars.githubusercontent.com/u/95745934?v=4&s=117" width="117">](https://github.com/odselsevier) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[mjlumetta](https://github.com/mjlumetta) |[mosi-kha](https://github.com/mosi-kha) |[neuronet77](https://github.com/neuronet77) |[ninesalt](https://github.com/ninesalt) |[odselsevier](https://github.com/odselsevier) |[ordago](https://github.com/ordago) |
+[mdxiaohu](https://github.com/mdxiaohu) |[maddy-jo](https://github.com/maddy-jo) |[mosi-kha](https://github.com/mosi-kha) |[neuronet77](https://github.com/neuronet77) |[ninesalt](https://github.com/ninesalt) |[odselsevier](https://github.com/odselsevier) |
 
 
-[<img alt="phil714" src="https://avatars.githubusercontent.com/u/7584581?v=4&s=117" width="117">](https://github.com/phil714) |[<img alt="luntta" src="https://avatars.githubusercontent.com/u/14221637?v=4&s=117" width="117">](https://github.com/luntta) |[<img alt="rhymes" src="https://avatars.githubusercontent.com/u/146201?v=4&s=117" width="117">](https://github.com/rhymes) |[<img alt="rlebosse" src="https://avatars.githubusercontent.com/u/2794137?v=4&s=117" width="117">](https://github.com/rlebosse) |[<img alt="rmoura-92" src="https://avatars.githubusercontent.com/u/419044?v=4&s=117" width="117">](https://github.com/rmoura-92) |[<img alt="rtaieb" src="https://avatars.githubusercontent.com/u/35224301?v=4&s=117" width="117">](https://github.com/rtaieb) |
+[<img alt="ordago" src="https://avatars.githubusercontent.com/u/6376814?v=4&s=117" width="117">](https://github.com/ordago) |[<img alt="phil714" src="https://avatars.githubusercontent.com/u/7584581?v=4&s=117" width="117">](https://github.com/phil714) |[<img alt="luntta" src="https://avatars.githubusercontent.com/u/14221637?v=4&s=117" width="117">](https://github.com/luntta) |[<img alt="rhymes" src="https://avatars.githubusercontent.com/u/146201?v=4&s=117" width="117">](https://github.com/rhymes) |[<img alt="rlebosse" src="https://avatars.githubusercontent.com/u/2794137?v=4&s=117" width="117">](https://github.com/rlebosse) |[<img alt="rmoura-92" src="https://avatars.githubusercontent.com/u/419044?v=4&s=117" width="117">](https://github.com/rmoura-92) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[phil714](https://github.com/phil714) |[luntta](https://github.com/luntta) |[rhymes](https://github.com/rhymes) |[rlebosse](https://github.com/rlebosse) |[rmoura-92](https://github.com/rmoura-92) |[rtaieb](https://github.com/rtaieb) |
+[ordago](https://github.com/ordago) |[phil714](https://github.com/phil714) |[luntta](https://github.com/luntta) |[rhymes](https://github.com/rhymes) |[rlebosse](https://github.com/rlebosse) |[rmoura-92](https://github.com/rmoura-92) |
 
 
-[<img alt="slawexxx44" src="https://avatars.githubusercontent.com/u/11180644?v=4&s=117" width="117">](https://github.com/slawexxx44) |[<img alt="stduhpf" src="https://avatars.githubusercontent.com/u/28208228?v=4&s=117" width="117">](https://github.com/stduhpf) |[<img alt="thanhthot" src="https://avatars.githubusercontent.com/u/50633205?v=4&s=117" width="117">](https://github.com/thanhthot) |[<img alt="tusharjkhunt" src="https://avatars.githubusercontent.com/u/31904234?v=4&s=117" width="117">](https://github.com/tusharjkhunt) |[<img alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117" width="117">](https://github.com/vedran555) |[<img alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117" width="117">](https://github.com/yoann-hellopret) |
+[<img alt="rtaieb" src="https://avatars.githubusercontent.com/u/35224301?v=4&s=117" width="117">](https://github.com/rtaieb) |[<img alt="slawexxx44" src="https://avatars.githubusercontent.com/u/11180644?v=4&s=117" width="117">](https://github.com/slawexxx44) |[<img alt="stduhpf" src="https://avatars.githubusercontent.com/u/28208228?v=4&s=117" width="117">](https://github.com/stduhpf) |[<img alt="thanhthot" src="https://avatars.githubusercontent.com/u/50633205?v=4&s=117" width="117">](https://github.com/thanhthot) |[<img alt="tusharjkhunt" src="https://avatars.githubusercontent.com/u/31904234?v=4&s=117" width="117">](https://github.com/tusharjkhunt) |[<img alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117" width="117">](https://github.com/vedran555) |
 :---: |:---: |:---: |:---: |:---: |:---: |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[slawexxx44](https://github.com/slawexxx44) |[stduhpf](https://github.com/stduhpf) |[thanhthot](https://github.com/thanhthot) |[tusharjkhunt](https://github.com/tusharjkhunt) |[vedran555](https://github.com/vedran555) |[yoann-hellopret](https://github.com/yoann-hellopret) |
+[rtaieb](https://github.com/rtaieb) |[slawexxx44](https://github.com/slawexxx44) |[stduhpf](https://github.com/stduhpf) |[thanhthot](https://github.com/thanhthot) |[tusharjkhunt](https://github.com/tusharjkhunt) |[vedran555](https://github.com/vedran555) |
 
 
-[<img alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117" width="117">](https://github.com/olitomas) |[<img alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117" width="117">](https://github.com/JimmyLv) |
-:---: |:---: |
-[olitomas](https://github.com/olitomas) |[JimmyLv](https://github.com/JimmyLv) |
+[<img alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117" width="117">](https://github.com/yoann-hellopret) |[<img alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117" width="117">](https://github.com/olitomas) |[<img alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117" width="117">](https://github.com/JimmyLv) |
+:---: |:---: |:---: |
+[yoann-hellopret](https://github.com/yoann-hellopret) |[olitomas](https://github.com/olitomas) |[JimmyLv](https://github.com/JimmyLv) |
 
 
 <!--/contributors-->
 <!--/contributors-->
 
 

+ 2 - 2
examples/aws-nodejs/public/drag.html

@@ -4,7 +4,7 @@
     <meta charset="utf-8" />
     <meta charset="utf-8" />
     <title>Uppy</title>
     <title>Uppy</title>
     <link
     <link
-      href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css"
+      href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
       rel="stylesheet"
       rel="stylesheet"
     />
     />
   </head>
   </head>
@@ -22,7 +22,7 @@
           DragDrop,
           DragDrop,
           ProgressBar,
           ProgressBar,
           AwsS3,
           AwsS3,
-        } from 'https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs'
+        } from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
 
 
         // Function for displaying uploaded files
         // Function for displaying uploaded files
         const onUploadSuccess = (elForUploadedFiles) => (file, response) => {
         const onUploadSuccess = (elForUploadedFiles) => (file, response) => {

+ 2 - 2
examples/aws-nodejs/public/index.html

@@ -4,7 +4,7 @@
     <meta charset="utf-8" />
     <meta charset="utf-8" />
     <title>Uppy – AWS upload example</title>
     <title>Uppy – AWS upload example</title>
     <link
     <link
-      href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css"
+      href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
       rel="stylesheet"
       rel="stylesheet"
     />
     />
   </head>
   </head>
@@ -16,7 +16,7 @@
         Uppy,
         Uppy,
         Dashboard,
         Dashboard,
         AwsS3,
         AwsS3,
-      } from 'https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs'
+      } from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
       /**
       /**
        * This generator transforms a deep object into URL-encodable pairs
        * This generator transforms a deep object into URL-encodable pairs
        * to work with `URLSearchParams` on the client and `body-parser` on the server.
        * to work with `URLSearchParams` on the client and `body-parser` on the server.

+ 3 - 3
examples/cdn-example/index.html

@@ -5,7 +5,7 @@
     <meta charset="UTF-8" />
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link
     <link
-      href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css"
+      href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
       rel="stylesheet"
       rel="stylesheet"
     />
     />
   </head>
   </head>
@@ -19,7 +19,7 @@
         Dashboard,
         Dashboard,
         Webcam,
         Webcam,
         Tus,
         Tus,
-      } from 'https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs'
+      } from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
 
 
       const uppy = new Uppy({ debug: true, autoProceed: false })
       const uppy = new Uppy({ debug: true, autoProceed: false })
         .use(Dashboard, { trigger: '#uppyModalOpener' })
         .use(Dashboard, { trigger: '#uppyModalOpener' })
@@ -34,7 +34,7 @@
     <!-- To support older browsers, you can use the legacy bundle which adds a global `Uppy` object.  -->
     <!-- To support older browsers, you can use the legacy bundle which adds a global `Uppy` object.  -->
     <script
     <script
       nomodule
       nomodule
-      src="https://releases.transloadit.com/uppy/v3.20.0/uppy.legacy.min.js"
+      src="https://releases.transloadit.com/uppy/v3.21.0/uppy.legacy.min.js"
     ></script>
     ></script>
     <script nomodule>
     <script nomodule>
       {
       {

+ 2 - 0
examples/custom-provider/client/MyCustomProvider.jsx

@@ -27,6 +27,8 @@ export default class MyCustomProvider extends UIPlugin {
       pluginId: this.id,
       pluginId: this.id,
     })
     })
 
 
+    uppy.registerRequestClient(MyCustomProvider.name, this.provider)
+
     this.defaultLocale = {
     this.defaultLocale = {
       strings: {
       strings: {
         pluginNameMyUnsplash: 'MyUnsplash',
         pluginNameMyUnsplash: 'MyUnsplash',

+ 2 - 2
examples/uppy-with-companion/client/index.html

@@ -5,7 +5,7 @@
     <meta charset="UTF-8" />
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link
     <link
-      href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css"
+      href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
       rel="stylesheet"
       rel="stylesheet"
     />
     />
   </head>
   </head>
@@ -19,7 +19,7 @@
         Instagram,
         Instagram,
         GoogleDrive,
         GoogleDrive,
         Tus,
         Tus,
-      } from 'https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs'
+      } from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
 
 
       const uppy = new Uppy({ debug: true, autoProceed: false })
       const uppy = new Uppy({ debug: true, autoProceed: false })
         .use(Dashboard, { trigger: '#uppyModalOpener' })
         .use(Dashboard, { trigger: '#uppyModalOpener' })

+ 2 - 2
package.json

@@ -115,7 +115,7 @@
     "start:companion": "bash bin/companion.sh",
     "start:companion": "bash bin/companion.sh",
     "start:companion:with-loadbalancer": "e2e/start-companion-with-load-balancer.mjs",
     "start:companion:with-loadbalancer": "e2e/start-companion-with-load-balancer.mjs",
     "build:bundle": "yarn node ./bin/build-bundle.mjs",
     "build:bundle": "yarn node ./bin/build-bundle.mjs",
-    "build:clean": "rm -rf packages/*/lib packages/@uppy/*/lib packages/*/dist packages/@uppy/*/dist",
+    "build:clean": "git clean -e node_modules -xfd packages e2e .parcel-cache coverage",
     "build:companion": "yarn workspace @uppy/companion build",
     "build:companion": "yarn workspace @uppy/companion build",
     "build:css": "yarn node ./bin/build-css.js",
     "build:css": "yarn node ./bin/build-css.js",
     "build:svelte": "yarn workspace @uppy/svelte build",
     "build:svelte": "yarn workspace @uppy/svelte build",
@@ -140,7 +140,7 @@
     "format": "prettier -w .",
     "format": "prettier -w .",
     "release": "PACKAGES=$(yarn workspaces list --json) yarn workspace @uppy-dev/release interactive",
     "release": "PACKAGES=$(yarn workspaces list --json) yarn workspace @uppy-dev/release interactive",
     "size": "echo 'JS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.css | gzip | wc -c",
     "size": "echo 'JS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.css | gzip | wc -c",
-    "e2e": "yarn build && yarn e2e:skip-build",
+    "e2e": "build:clean && yarn build && yarn e2e:skip-build",
     "e2e:skip-build": "npm-run-all --parallel watch:js:lib e2e:client start:companion:with-loadbalancer e2e:cypress",
     "e2e:skip-build": "npm-run-all --parallel watch:js:lib e2e:client start:companion:with-loadbalancer e2e:cypress",
     "e2e:ci": "start-server-and-test 'npm-run-all --parallel e2e:client start:companion:with-loadbalancer' '1234|3020' e2e:headless",
     "e2e:ci": "start-server-and-test 'npm-run-all --parallel e2e:client start:companion:with-loadbalancer' '1234|3020' e2e:headless",
     "e2e:client": "yarn workspace e2e client:start",
     "e2e:client": "yarn workspace e2e client:start",

+ 1 - 1
packages/@uppy/aws-s3-multipart/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/aws-s3-multipart",
   "name": "@uppy/aws-s3-multipart",
   "description": "Upload to Amazon S3 with Uppy and S3's Multipart upload strategy",
   "description": "Upload to Amazon S3 with Uppy and S3's Multipart upload strategy",
-  "version": "3.9.0",
+  "version": "3.10.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/@uppy/aws-s3-multipart/src/index.js

@@ -878,7 +878,7 @@ export default class AwsS3Multipart extends BasePlugin {
         }
         }
         this.uppy.on('file-removed', removedHandler)
         this.uppy.on('file-removed', removedHandler)
 
 
-        const uploadPromise = file.remote.requestClient.uploadRemoteFile(
+        const uploadPromise = this.uppy.getRequestClientForFile(file).uploadRemoteFile(
           file,
           file,
           this.#getCompanionClientArgs(file),
           this.#getCompanionClientArgs(file),
           { signal: controller.signal, getQueue },
           { signal: controller.signal, getQueue },

+ 7 - 0
packages/@uppy/aws-s3/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/aws-s3
 # @uppy/aws-s3
 
 
+## 3.6.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/aws-s3: change Companion URL in tests (Antoine du Hamel)
+
 ## 3.3.0
 ## 3.3.0
 
 
 Released: 2023-09-05
 Released: 2023-09-05

+ 1 - 1
packages/@uppy/aws-s3/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/aws-s3",
   "name": "@uppy/aws-s3",
   "description": "Upload to Amazon S3 with Uppy",
   "description": "Upload to Amazon S3 with Uppy",
-  "version": "3.5.0",
+  "version": "3.6.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/@uppy/aws-s3/src/index.js

@@ -281,7 +281,7 @@ export default class AwsS3 extends BasePlugin {
       }
       }
       this.uppy.on('file-removed', removedHandler)
       this.uppy.on('file-removed', removedHandler)
 
 
-      const uploadPromise = file.remote.requestClient.uploadRemoteFile(
+      const uploadPromise = this.uppy.getRequestClientForFile(file).uploadRemoteFile(
         file,
         file,
         this.#getCompanionClientArgs(file),
         this.#getCompanionClientArgs(file),
         { signal: controller.signal, getQueue },
         { signal: controller.signal, getQueue },

+ 1 - 1
packages/@uppy/box/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/box",
   "name": "@uppy/box",
   "description": "Import files from Box, into Uppy.",
   "description": "Import files from Box, into Uppy.",
-  "version": "2.1.4",
+  "version": "2.2.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 7 - 0
packages/@uppy/companion-client/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/companion-client
 # @uppy/companion-client
 
 
+## 3.7.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/companion-client: avoid unnecessary preflight requests (Antoine du Hamel / #4462)
+
 ## 3.6.1
 ## 3.6.1
 
 
 Released: 2023-11-24
 Released: 2023-11-24

+ 1 - 1
packages/@uppy/companion-client/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/companion-client",
   "name": "@uppy/companion-client",
   "description": "Client library for communication with Companion. Intended for use in Uppy plugins.",
   "description": "Client library for communication with Companion. Intended for use in Uppy plugins.",
-  "version": "3.6.1",
+  "version": "3.7.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/@uppy/companion-client/src/Provider.js

@@ -286,7 +286,7 @@ export default class Provider extends RequestClient {
       plugin.opts.companionAllowedHosts = pattern
       plugin.opts.companionAllowedHosts = pattern
     } else if (/^(?!https?:\/\/).*$/i.test(opts.companionUrl)) {
     } else if (/^(?!https?:\/\/).*$/i.test(opts.companionUrl)) {
       // does not start with https://
       // does not start with https://
-      plugin.opts.companionAllowedHosts = `https://${opts.companionUrl.replace(/^\/\//, '')}`
+      plugin.opts.companionAllowedHosts = `https://${opts.companionUrl?.replace(/^\/\//, '')}`
     } else {
     } else {
       plugin.opts.companionAllowedHosts = new URL(opts.companionUrl).origin
       plugin.opts.companionAllowedHosts = new URL(opts.companionUrl).origin
     }
     }

+ 11 - 0
packages/@uppy/companion/CHANGELOG.md

@@ -1,5 +1,16 @@
 # @uppy/companion
 # @uppy/companion
 
 
+## 4.12.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/companion: fix double tus uploads (Mikael Finstad / #4816)
+- @uppy/companion: fix accelerated endpoints for presigned POST  (Mikael Finstad / #4817)
+- @uppy/companion: fix `authProvider` property inconsistency (Mikael Finstad / #4672)
+- @uppy/companion:  send certain onedrive errors to the user (Mikael Finstad / #4671)
+- @uppy/companion: Provider user sessions (Mikael Finstad / #4619)
+
 ## 4.11.0
 ## 4.11.0
 
 
 Released: 2023-11-08
 Released: 2023-11-08

+ 1 - 1
packages/@uppy/companion/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@uppy/companion",
   "name": "@uppy/companion",
-  "version": "4.11.0",
+  "version": "4.12.0",
   "description": "OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:",
   "description": "OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:",
   "main": "lib/companion.js",
   "main": "lib/companion.js",
   "types": "lib/companion.d.ts",
   "types": "lib/companion.d.ts",

+ 15 - 6
packages/@uppy/companion/src/companion.js

@@ -20,8 +20,10 @@ const { ProviderApiError, ProviderUserError, ProviderAuthError } = require('./se
 const { getCredentialsOverrideMiddleware } = require('./server/provider/credentials')
 const { getCredentialsOverrideMiddleware } = require('./server/provider/credentials')
 // @ts-ignore
 // @ts-ignore
 const { version } = require('../package.json')
 const { version } = require('../package.json')
+const { isOAuthProvider } = require('./server/provider/Provider')
 
 
-function setLoggerProcessName ({ loggerProcessName }) {
+
+function setLoggerProcessName({ loggerProcessName }) {
   if (loggerProcessName != null) logger.setProcessName(loggerProcessName)
   if (loggerProcessName != null) logger.setProcessName(loggerProcessName)
 }
 }
 
 
@@ -72,13 +74,15 @@ module.exports.app = (optionsArg = {}) => {
 
 
   const providers = providerManager.getDefaultProviders()
   const providers = providerManager.getDefaultProviders()
 
 
-  providerManager.addProviderOptions(options, grantConfig)
-
   const { customProviders } = options
   const { customProviders } = options
   if (customProviders) {
   if (customProviders) {
     providerManager.addCustomProviders(customProviders, providers, grantConfig)
     providerManager.addCustomProviders(customProviders, providers, grantConfig)
   }
   }
 
 
+  const getAuthProvider = (providerName) => providers[providerName]?.authProvider
+
+  providerManager.addProviderOptions(options, grantConfig, getAuthProvider)
+
   // mask provider secrets from log messages
   // mask provider secrets from log messages
   logger.setMaskables(getMaskableSecrets(options))
   logger.setMaskables(getMaskableSecrets(options))
 
 
@@ -147,13 +151,18 @@ module.exports.app = (optionsArg = {}) => {
       // for simplicity, we just return the normal credentials for the provider, but in a real-world scenario,
       // for simplicity, we just return the normal credentials for the provider, but in a real-world scenario,
       // we would query based on parameters
       // we would query based on parameters
       const { key, secret } = options.providerOptions[providerName]
       const { key, secret } = options.providerOptions[providerName]
+
+      function getRedirectUri() {
+        const authProvider = getAuthProvider(providerName)
+        if (!isOAuthProvider(authProvider)) return undefined
+        return grantConfig[authProvider]?.redirect_uri
+      }
+
       res.send({
       res.send({
         credentials: {
         credentials: {
           key,
           key,
           secret,
           secret,
-          redirect_uri: providerManager.getGrantConfigForProvider({
-            providerName, companionOptions: options, grantConfig,
-          })?.redirect_uri,
+          redirect_uri: getRedirectUri(),
         },
         },
       })
       })
     })
     })

+ 34 - 23
packages/@uppy/companion/src/server/Uploader.js

@@ -56,10 +56,6 @@ function sanitizeMetadata(inputMetadata) {
   return outputMetadata
   return outputMetadata
 }
 }
 
 
-class AbortError extends Error {
-  isAbortError = true
-}
-
 class ValidationError extends Error {
 class ValidationError extends Error {
   constructor(message) {
   constructor(message) {
     super(message)
     super(message)
@@ -139,6 +135,13 @@ function validateOptions(options) {
   }
   }
 }
 }
 
 
+const states = {
+  idle: 'idle',
+  uploading: 'uploading',
+  paused: 'paused',
+  done: 'done',
+}
+
 class Uploader {
 class Uploader {
   /**
   /**
    * Uploads file to destination based on the supplied protocol (tus, s3-multipart, multipart)
    * Uploads file to destination based on the supplied protocol (tus, s3-multipart, multipart)
@@ -176,10 +179,7 @@ class Uploader {
       ? this.options.metadata.name.substring(0, MAX_FILENAME_LENGTH)
       ? this.options.metadata.name.substring(0, MAX_FILENAME_LENGTH)
       : this.fileName
       : this.fileName
 
 
-    this.uploadStopped = false
-
     this.storage = options.storage
     this.storage = options.storage
-    this._paused = false
 
 
     this.downloadedBytes = 0
     this.downloadedBytes = 0
 
 
@@ -188,7 +188,8 @@ class Uploader {
     if (this.options.protocol === PROTOCOLS.tus) {
     if (this.options.protocol === PROTOCOLS.tus) {
       emitter().on(`pause:${this.token}`, () => {
       emitter().on(`pause:${this.token}`, () => {
         logger.debug('Received from client: pause', 'uploader', this.shortToken)
         logger.debug('Received from client: pause', 'uploader', this.shortToken)
-        this._paused = true
+        if (this.#uploadState !== states.uploading) return
+        this.#uploadState = states.paused
         if (this.tus) {
         if (this.tus) {
           this.tus.abort()
           this.tus.abort()
         }
         }
@@ -196,7 +197,8 @@ class Uploader {
 
 
       emitter().on(`resume:${this.token}`, () => {
       emitter().on(`resume:${this.token}`, () => {
         logger.debug('Received from client: resume', 'uploader', this.shortToken)
         logger.debug('Received from client: resume', 'uploader', this.shortToken)
-        this._paused = false
+        if (this.#uploadState !== states.paused) return
+        this.#uploadState = states.uploading
         if (this.tus) {
         if (this.tus) {
           this.tus.start()
           this.tus.start()
         }
         }
@@ -205,17 +207,21 @@ class Uploader {
 
 
     emitter().on(`cancel:${this.token}`, () => {
     emitter().on(`cancel:${this.token}`, () => {
       logger.debug('Received from client: cancel', 'uploader', this.shortToken)
       logger.debug('Received from client: cancel', 'uploader', this.shortToken)
-      this._paused = true
       if (this.tus) {
       if (this.tus) {
         const shouldTerminate = !!this.tus.url
         const shouldTerminate = !!this.tus.url
         this.tus.abort(shouldTerminate).catch(() => { })
         this.tus.abort(shouldTerminate).catch(() => { })
       }
       }
-      this.abortReadStream(new AbortError())
+      this.#canceled = true
+      this.abortReadStream(new Error('Canceled'))
     })
     })
   }
   }
 
 
+  #uploadState = states.idle
+
+  #canceled = false
+
   abortReadStream(err) {
   abortReadStream(err) {
-    this.uploadStopped = true
+    this.#uploadState = states.done
     if (this.readStream) this.readStream.destroy(err)
     if (this.readStream) this.readStream.destroy(err)
   }
   }
 
 
@@ -244,7 +250,9 @@ class Uploader {
 
 
     const onData = (chunk) => {
     const onData = (chunk) => {
       this.downloadedBytes += chunk.length
       this.downloadedBytes += chunk.length
-      if (exceedsMaxFileSize(this.options.companionOptions.maxFileSize, this.downloadedBytes)) this.abortReadStream(new Error('maxFileSize exceeded'))
+      if (exceedsMaxFileSize(this.options.companionOptions.maxFileSize, this.downloadedBytes)) {
+        this.abortReadStream(new Error('maxFileSize exceeded'))
+      }
       this.onProgress(0, undefined)
       this.onProgress(0, undefined)
     }
     }
 
 
@@ -271,9 +279,11 @@ class Uploader {
    */
    */
   async uploadStream(stream) {
   async uploadStream(stream) {
     try {
     try {
-      if (this.uploadStopped) throw new Error('Cannot upload stream after upload stopped')
+      if (this.#uploadState !== states.idle) throw new Error('Can only start an upload in the idle state')
       if (this.readStream) throw new Error('Already uploading')
       if (this.readStream) throw new Error('Already uploading')
 
 
+      this.#uploadState = states.uploading
+
       this.readStream = stream
       this.readStream = stream
       if (this._needDownloadFirst()) {
       if (this._needDownloadFirst()) {
         logger.debug('need to download the whole file first', 'controller.get.provider.size', this.shortToken)
         logger.debug('need to download the whole file first', 'controller.get.provider.size', this.shortToken)
@@ -282,7 +292,7 @@ class Uploader {
         // The stream will then typically come from a "Transfer-Encoding: chunked" response
         // The stream will then typically come from a "Transfer-Encoding: chunked" response
         await this._downloadStreamAsFile(this.readStream)
         await this._downloadStreamAsFile(this.readStream)
       }
       }
-      if (this.uploadStopped) return undefined
+      if (this.#uploadState !== states.uploading) return undefined
 
 
       const { url, extraData } = await Promise.race([
       const { url, extraData } = await Promise.race([
         this._uploadByProtocol(),
         this._uploadByProtocol(),
@@ -291,6 +301,7 @@ class Uploader {
       ])
       ])
       return { url, extraData }
       return { url, extraData }
     } finally {
     } finally {
+      this.#uploadState = states.done
       logger.debug('cleanup', this.shortToken)
       logger.debug('cleanup', this.shortToken)
       if (this.readStream && !this.readStream.destroyed) this.readStream.destroy()
       if (this.readStream && !this.readStream.destroyed) this.readStream.destroy()
       await this.tryDeleteTmpPath()
       await this.tryDeleteTmpPath()
@@ -314,11 +325,10 @@ class Uploader {
       const { url, extraData } = ret
       const { url, extraData } = ret
       this.#emitSuccess(url, extraData)
       this.#emitSuccess(url, extraData)
     } catch (err) {
     } catch (err) {
-      if (err?.isAbortError) {
+      if (this.#canceled) {
         logger.error('Aborted upload', 'uploader.aborted', this.shortToken)
         logger.error('Aborted upload', 'uploader.aborted', this.shortToken)
         return
         return
       }
       }
-      // console.log(err)
       logger.error(err, 'uploader.error', this.shortToken)
       logger.error(err, 'uploader.error', this.shortToken)
       this.#emitError(err)
       this.#emitError(err)
     } finally {
     } finally {
@@ -458,7 +468,7 @@ class Uploader {
 
 
     const formattedPercentage = percentage.toFixed(2)
     const formattedPercentage = percentage.toFixed(2)
 
 
-    if (this._paused || this.uploadStopped) {
+    if (this.#uploadState !== states.uploading) {
       return
       return
     }
     }
 
 
@@ -519,7 +529,8 @@ class Uploader {
     const chunkSize = this.options.chunkSize || (isFileStream ? Infinity : 50e6)
     const chunkSize = this.options.chunkSize || (isFileStream ? Infinity : 50e6)
 
 
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
-      this.tus = new tus.Upload(stream, {
+
+      const tusOptions = {
         endpoint: this.options.endpoint,
         endpoint: this.options.endpoint,
         uploadUrl: this.options.uploadUrl,
         uploadUrl: this.options.uploadUrl,
         uploadLengthDeferred: !isFileStream,
         uploadLengthDeferred: !isFileStream,
@@ -564,11 +575,11 @@ class Uploader {
         onSuccess() {
         onSuccess() {
           resolve({ url: uploader.tus.url })
           resolve({ url: uploader.tus.url })
         },
         },
-      })
-
-      if (!this._paused) {
-        this.tus.start()
       }
       }
+
+      this.tus = new tus.Upload(stream, tusOptions)
+
+      this.tus.start()
     })
     })
   }
   }
 
 

+ 3 - 3
packages/@uppy/companion/src/server/controllers/connect.js

@@ -30,7 +30,7 @@ module.exports = function connect(req, res) {
   }
   }
 
 
   const state = oAuthState.encodeState(stateObj, secret)
   const state = oAuthState.encodeState(stateObj, secret)
-  const { provider, providerGrantConfig } = req.companion
+  const { providerClass, providerGrantConfig } = req.companion
 
 
   // pass along grant's dynamic config (if specified for the provider in its grant config `dynamic` section)
   // pass along grant's dynamic config (if specified for the provider in its grant config `dynamic` section)
   // this is needed for things like custom oauth domain (e.g. webdav)
   // this is needed for things like custom oauth domain (e.g. webdav)
@@ -45,12 +45,12 @@ module.exports = function connect(req, res) {
     ]]
     ]]
   }) || [])
   }) || [])
 
 
-  const providerName = provider.authProvider
+  const { authProvider } = providerClass
   const qs = queryString({
   const qs = queryString({
     ...grantDynamicConfig,
     ...grantDynamicConfig,
     state,
     state,
   })
   })
 
 
   // Now we redirect to grant's /connect endpoint, see `app.use(Grant(grantConfig))`
   // Now we redirect to grant's /connect endpoint, see `app.use(Grant(grantConfig))`
-  res.redirect(req.companion.buildURL(`/connect/${providerName}${qs}`, true))
+  res.redirect(req.companion.buildURL(`/connect/${authProvider}${qs}`, true))
 }
 }

+ 1 - 1
packages/@uppy/companion/src/server/controllers/logout.js

@@ -26,7 +26,7 @@ async function logout (req, res, next) {
     const { accessToken } = providerUserSession
     const { accessToken } = providerUserSession
     const data = await companion.provider.logout({ token: accessToken, providerUserSession, companion })
     const data = await companion.provider.logout({ token: accessToken, providerUserSession, companion })
     delete companion.providerUserSession
     delete companion.providerUserSession
-    tokenService.removeFromCookies(res, companion.options, companion.provider.authProvider)
+    tokenService.removeFromCookies(res, companion.options, companion.providerClass.authProvider)
     cleanSession()
     cleanSession()
     res.json({ ok: true, ...data })
     res.json({ ok: true, ...data })
   } catch (err) {
   } catch (err) {

+ 1 - 1
packages/@uppy/companion/src/server/controllers/oauth-redirect.js

@@ -10,7 +10,7 @@ const oAuthState = require('../helpers/oauth-state')
  */
  */
 module.exports = function oauthRedirect (req, res) {
 module.exports = function oauthRedirect (req, res) {
   const params = qs.stringify(req.query)
   const params = qs.stringify(req.query)
-  const { authProvider } = req.companion.provider
+  const { authProvider } = req.companion.providerClass
   if (!req.companion.options.server.oauthDomain) {
   if (!req.companion.options.server.oauthDomain) {
     res.redirect(req.companion.buildURL(`/connect/${authProvider}/callback?${params}`, true))
     res.redirect(req.companion.buildURL(`/connect/${authProvider}/callback?${params}`, true))
     return
     return

+ 2 - 2
packages/@uppy/companion/src/server/controllers/s3.js

@@ -24,11 +24,11 @@ module.exports = function s3 (config) {
     throw new TypeError('s3: The `getKey` option must be a function')
     throw new TypeError('s3: The `getKey` option must be a function')
   }
   }
 
 
-  function getS3Client (req, res) {
+  function getS3Client (req, res, createPresignedPostMode = false) {
     /**
     /**
      * @type {import('@aws-sdk/client-s3').S3Client}
      * @type {import('@aws-sdk/client-s3').S3Client}
      */
      */
-    const client = req.companion.s3Client
+    const client = createPresignedPostMode ? req.companion.s3ClientCreatePresignedPost : req.companion.s3Client
     if (!client) res.status(400).json({ error: 'This Companion server does not support uploading to S3' })
     if (!client) res.status(400).json({ error: 'This Companion server does not support uploading to S3' })
     return client
     return client
   }
   }

+ 1 - 1
packages/@uppy/companion/src/server/helpers/jwt.js

@@ -125,7 +125,7 @@ module.exports.addToCookiesIfNeeded = (req, res, uppyAuthToken, maxAge) => {
       res,
       res,
       token: uppyAuthToken,
       token: uppyAuthToken,
       companionOptions: req.companion.options,
       companionOptions: req.companion.options,
-      authProvider: req.companion.provider.authProvider,
+      authProvider: req.companion.providerClass.authProvider,
       maxAge,
       maxAge,
     })
     })
   }
   }

+ 3 - 2
packages/@uppy/companion/src/server/middlewares.js

@@ -122,7 +122,7 @@ exports.gentleVerifyToken = (req, res, next) => {
 }
 }
 
 
 exports.cookieAuthToken = (req, res, next) => {
 exports.cookieAuthToken = (req, res, next) => {
-  req.companion.authToken = req.cookies[`uppyAuthToken--${req.companion.provider.authProvider}`]
+  req.companion.authToken = req.cookies[`uppyAuthToken--${req.companion.providerClass.authProvider}`]
   return next()
   return next()
 }
 }
 
 
@@ -204,7 +204,8 @@ exports.getCompanionMiddleware = (options) => {
   const middleware = (req, res, next) => {
   const middleware = (req, res, next) => {
     req.companion = {
     req.companion = {
       options,
       options,
-      s3Client: getS3Client(options),
+      s3Client: getS3Client(options, false),
+      s3ClientCreatePresignedPost: getS3Client(options, true),
       authToken: req.header('uppy-auth-token') || req.query.uppyAuthToken,
       authToken: req.header('uppy-auth-token') || req.query.uppyAuthToken,
       buildURL: getURLBuilder(options),
       buildURL: getURLBuilder(options),
     }
     }

+ 1 - 1
packages/@uppy/companion/src/server/provider/Provider.js

@@ -122,5 +122,5 @@ class Provider {
 }
 }
 
 
 module.exports = Provider
 module.exports = Provider
-// OAuth providers are those that have a `static authProvider` set. It means they require OAuth authentication to work
+// OAuth providers are those that have an `authProvider` set. It means they require OAuth authentication to work
 module.exports.isOAuthProvider = (authProvider) => typeof authProvider === 'string' && authProvider.length > 0
 module.exports.isOAuthProvider = (authProvider) => typeof authProvider === 'string' && authProvider.length > 0

+ 2 - 2
packages/@uppy/companion/src/server/provider/box/index.js

@@ -31,7 +31,6 @@ async function list ({ directory, query, token }) {
 class Box extends Provider {
 class Box extends Provider {
   constructor (options) {
   constructor (options) {
     super(options)
     super(options)
-    this.authProvider = Box.authProvider
     // needed for the thumbnails fetched via companion
     // needed for the thumbnails fetched via companion
     this.needsCookieAuth = true
     this.needsCookieAuth = true
   }
   }
@@ -116,11 +115,12 @@ class Box extends Provider {
     })
     })
   }
   }
 
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
     return withProviderErrorHandling({
       fn,
       fn,
       tag,
       tag,
-      providerName: this.authProvider,
+      providerName: Box.authProvider,
       isAuthError: (response) => response.statusCode === 401,
       isAuthError: (response) => response.statusCode === 401,
       getJsonErrorMessage: (body) => body?.message,
       getJsonErrorMessage: (body) => body?.message,
     })
     })

+ 2 - 6
packages/@uppy/companion/src/server/provider/drive/index.js

@@ -51,11 +51,6 @@ async function getStats ({ id, token }) {
  * Adapter for API https://developers.google.com/drive/api/v3/
  * Adapter for API https://developers.google.com/drive/api/v3/
  */
  */
 class Drive extends Provider {
 class Drive extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = Drive.authProvider
-  }
-
   static get authProvider () {
   static get authProvider () {
     return 'google'
     return 'google'
   }
   }
@@ -200,11 +195,12 @@ class Drive extends Provider {
     })
     })
   }
   }
 
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
     return withProviderErrorHandling({
       fn,
       fn,
       tag,
       tag,
-      providerName: this.authProvider,
+      providerName: Drive.authProvider,
       isAuthError: (response) => (
       isAuthError: (response) => (
         response.statusCode === 401
         response.statusCode === 401
         || (response.statusCode === 400 && response.body?.error === 'invalid_grant') // Refresh token has expired or been revoked
         || (response.statusCode === 400 && response.body?.error === 'invalid_grant') // Refresh token has expired or been revoked

+ 2 - 2
packages/@uppy/companion/src/server/provider/dropbox/index.js

@@ -56,7 +56,6 @@ async function userInfo ({ token }) {
 class DropBox extends Provider {
 class DropBox extends Provider {
   constructor (options) {
   constructor (options) {
     super(options)
     super(options)
-    this.authProvider = DropBox.authProvider
     this.needsCookieAuth = true
     this.needsCookieAuth = true
   }
   }
 
 
@@ -136,11 +135,12 @@ class DropBox extends Provider {
     })
     })
   }
   }
 
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
     return withProviderErrorHandling({
       fn,
       fn,
       tag,
       tag,
-      providerName: this.authProvider,
+      providerName: DropBox.authProvider,
       isAuthError: (response) => response.statusCode === 401,
       isAuthError: (response) => response.statusCode === 401,
       getJsonErrorMessage: (body) => body?.error_summary,
       getJsonErrorMessage: (body) => body?.error_summary,
     })
     })

+ 2 - 6
packages/@uppy/companion/src/server/provider/facebook/index.js

@@ -24,11 +24,6 @@ async function getMediaUrl ({ token, id }) {
  * Adapter for API https://developers.facebook.com/docs/graph-api/using-graph-api/
  * Adapter for API https://developers.facebook.com/docs/graph-api/using-graph-api/
  */
  */
 class Facebook extends Provider {
 class Facebook extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = Facebook.authProvider
-  }
-
   static get authProvider () {
   static get authProvider () {
     return 'facebook'
     return 'facebook'
   }
   }
@@ -86,11 +81,12 @@ class Facebook extends Provider {
     })
     })
   }
   }
 
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
     return withProviderErrorHandling({
       fn,
       fn,
       tag,
       tag,
-      providerName: this.authProvider,
+      providerName: Facebook.authProvider,
       isAuthError: (response) => typeof response.body === 'object' && response.body?.error?.code === 190, // Invalid OAuth 2.0 Access Token
       isAuthError: (response) => typeof response.body === 'object' && response.body?.error?.code === 190, // Invalid OAuth 2.0 Access Token
       getJsonErrorMessage: (body) => body?.error?.message,
       getJsonErrorMessage: (body) => body?.error?.message,
     })
     })

+ 7 - 25
packages/@uppy/companion/src/server/provider/index.js

@@ -12,7 +12,6 @@ const zoom = require('./zoom')
 const { getURLBuilder } = require('../helpers/utils')
 const { getURLBuilder } = require('../helpers/utils')
 const logger = require('../logger')
 const logger = require('../logger')
 const { getCredentialsResolver } = require('./credentials')
 const { getCredentialsResolver } = require('./credentials')
-// eslint-disable-next-line
 const Provider = require('./Provider')
 const Provider = require('./Provider')
 
 
 const { isOAuthProvider } = Provider
 const { isOAuthProvider } = Provider
@@ -25,26 +24,6 @@ const validOptions = (options) => {
   return options.server.host && options.server.protocol
   return options.server.host && options.server.protocol
 }
 }
 
 
-/**
- *
- * @param {string} name of the provider
- * @param {{server: object, providerOptions: object}} options
- * @returns {string} the authProvider for this provider
- */
-const providerNameToAuthName = (name, options) => { // eslint-disable-line no-unused-vars
-  const providers = exports.getDefaultProviders()
-  return (providers[name] || {}).authProvider
-}
-
-function getGrantConfigForProvider({ providerName, companionOptions, grantConfig }) {
-  const authProvider = providerNameToAuthName(providerName, companionOptions)
-
-  if (!isOAuthProvider(authProvider)) return undefined
-  return grantConfig[authProvider]
-}
-
-module.exports.getGrantConfigForProvider = getGrantConfigForProvider
-
 /**
 /**
  * adds the desired provider module to the request object,
  * adds the desired provider module to the request object,
  * based on the providerName parameter specified
  * based on the providerName parameter specified
@@ -106,10 +85,11 @@ module.exports.addCustomProviders = (customProviders, providers, grantConfig) =>
 
 
     // eslint-disable-next-line no-param-reassign
     // eslint-disable-next-line no-param-reassign
     providers[providerName] = customProvider.module
     providers[providerName] = customProvider.module
+    const { authProvider } = customProvider.module
 
 
-    if (isOAuthProvider(customProvider.module.authProvider)) {
+    if (isOAuthProvider(authProvider)) {
       // eslint-disable-next-line no-param-reassign
       // eslint-disable-next-line no-param-reassign
-      grantConfig[providerName] = {
+      grantConfig[authProvider] = {
         ...customProvider.config,
         ...customProvider.config,
         // todo: consider setting these options from a universal point also used
         // todo: consider setting these options from a universal point also used
         // by official providers. It'll prevent these from getting left out if the
         // by official providers. It'll prevent these from getting left out if the
@@ -125,8 +105,9 @@ module.exports.addCustomProviders = (customProviders, providers, grantConfig) =>
  *
  *
  * @param {{server: object, providerOptions: object}} companionOptions
  * @param {{server: object, providerOptions: object}} companionOptions
  * @param {object} grantConfig
  * @param {object} grantConfig
+ * @param {(a: string) => string} getAuthProvider
  */
  */
-module.exports.addProviderOptions = (companionOptions, grantConfig) => {
+module.exports.addProviderOptions = (companionOptions, grantConfig, getAuthProvider) => {
   const { server, providerOptions } = companionOptions
   const { server, providerOptions } = companionOptions
   if (!validOptions({ server })) {
   if (!validOptions({ server })) {
     logger.warn('invalid provider options detected. Providers will not be loaded', 'provider.options.invalid')
     logger.warn('invalid provider options detected. Providers will not be loaded', 'provider.options.invalid')
@@ -143,7 +124,8 @@ module.exports.addProviderOptions = (companionOptions, grantConfig) => {
   const { oauthDomain } = server
   const { oauthDomain } = server
   const keys = Object.keys(providerOptions).filter((key) => key !== 'server')
   const keys = Object.keys(providerOptions).filter((key) => key !== 'server')
   keys.forEach((providerName) => {
   keys.forEach((providerName) => {
-    const authProvider = providerNameToAuthName(providerName, companionOptions)
+    const authProvider = getAuthProvider?.(providerName)
+
     if (isOAuthProvider(authProvider) && grantConfig[authProvider]) {
     if (isOAuthProvider(authProvider) && grantConfig[authProvider]) {
       // explicitly add providerOptions so users don't override other providerOptions.
       // explicitly add providerOptions so users don't override other providerOptions.
       // eslint-disable-next-line no-param-reassign
       // eslint-disable-next-line no-param-reassign

+ 2 - 6
packages/@uppy/companion/src/server/provider/instagram/graph/index.js

@@ -23,11 +23,6 @@ async function getMediaUrl ({ token, id }) {
  * Adapter for API https://developers.facebook.com/docs/instagram-api/overview
  * Adapter for API https://developers.facebook.com/docs/instagram-api/overview
  */
  */
 class Instagram extends Provider {
 class Instagram extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = Instagram.authProvider
-  }
-
   // for "grant"
   // for "grant"
   static getExtraConfig () {
   static getExtraConfig () {
     return {
     return {
@@ -86,11 +81,12 @@ class Instagram extends Provider {
     return { revoked: false, manual_revoke_url: 'https://www.instagram.com/accounts/manage_access/' }
     return { revoked: false, manual_revoke_url: 'https://www.instagram.com/accounts/manage_access/' }
   }
   }
 
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
     return withProviderErrorHandling({
       fn,
       fn,
       tag,
       tag,
-      providerName: this.authProvider,
+      providerName: Instagram.authProvider,
       isAuthError: (response) => typeof response.body === 'object' && response.body?.error?.code === 190, // Invalid OAuth 2.0 Access Token
       isAuthError: (response) => typeof response.body === 'object' && response.body?.error?.code === 190, // Invalid OAuth 2.0 Access Token
       getJsonErrorMessage: (body) => body?.error?.message,
       getJsonErrorMessage: (body) => body?.error?.message,
     })
     })

+ 8 - 6
packages/@uppy/companion/src/server/provider/onedrive/index.js

@@ -23,11 +23,6 @@ const getRootPath = (query) => (query.driveId ? `drives/${query.driveId}` : 'me/
  * Adapter for API https://docs.microsoft.com/en-us/onedrive/developer/rest-api/
  * Adapter for API https://docs.microsoft.com/en-us/onedrive/developer/rest-api/
  */
  */
 class OneDrive extends Provider {
 class OneDrive extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = OneDrive.authProvider
-  }
-
   static get authProvider () {
   static get authProvider () {
     return 'microsoft'
     return 'microsoft'
   }
   }
@@ -98,12 +93,19 @@ class OneDrive extends Provider {
     })
     })
   }
   }
 
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
     return withProviderErrorHandling({
       fn,
       fn,
       tag,
       tag,
-      providerName: this.authProvider,
+      providerName: OneDrive.authProvider,
       isAuthError: (response) => response.statusCode === 401,
       isAuthError: (response) => response.statusCode === 401,
+      isUserFacingError: (response) => [400, 403].includes(response.statusCode),
+      // onedrive gives some errors here that the user might want to know about
+      // e.g. these happen if you try to login to a users in an organization,
+      // without an Office365 licence or OneDrive account setup completed
+      // 400: Tenant does not have a SPO license
+      // 403: You do not have access to create this personal site or you do not have a valid license
       getJsonErrorMessage: (body) => body?.error?.message,
       getJsonErrorMessage: (body) => body?.error?.message,
     })
     })
   }
   }

+ 2 - 6
packages/@uppy/companion/src/server/provider/zoom/index.js

@@ -29,11 +29,6 @@ async function findFile ({ client, meetingId, fileId, recordingStart }) {
  * Adapter for API https://marketplace.zoom.us/docs/api-reference/zoom-api
  * Adapter for API https://marketplace.zoom.us/docs/api-reference/zoom-api
  */
  */
 class Zoom extends Provider {
 class Zoom extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = Zoom.authProvider
-  }
-
   static get authProvider () {
   static get authProvider () {
     return 'zoom'
     return 'zoom'
   }
   }
@@ -157,6 +152,7 @@ class Zoom extends Provider {
     })
     })
   }
   }
 
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
   async #withErrorHandling (tag, fn) {
     const authErrorCodes = [
     const authErrorCodes = [
       124, // expired token
       124, // expired token
@@ -166,7 +162,7 @@ class Zoom extends Provider {
     return withProviderErrorHandling({
     return withProviderErrorHandling({
       fn,
       fn,
       tag,
       tag,
-      providerName: this.authProvider,
+      providerName: Zoom.authProvider,
       isAuthError: (response) => authErrorCodes.includes(response.statusCode),
       isAuthError: (response) => authErrorCodes.includes(response.statusCode),
       getJsonErrorMessage: (body) => body?.message,
       getJsonErrorMessage: (body) => body?.message,
     })
     })

+ 29 - 12
packages/@uppy/companion/src/server/s3-client.js

@@ -4,8 +4,9 @@ const { S3Client } = require('@aws-sdk/client-s3')
  * instantiates the aws-sdk s3 client that will be used for s3 uploads.
  * instantiates the aws-sdk s3 client that will be used for s3 uploads.
  *
  *
  * @param {object} companionOptions the companion options object
  * @param {object} companionOptions the companion options object
+ * @param {boolean} createPresignedPostMode whether this s3 client is for createPresignedPost
  */
  */
-module.exports = (companionOptions) => {
+module.exports = (companionOptions, createPresignedPostMode = false) => {
   let s3Client = null
   let s3Client = null
   if (companionOptions.s3) {
   if (companionOptions.s3) {
     const { s3 } = companionOptions
     const { s3 } = companionOptions
@@ -19,23 +20,39 @@ module.exports = (companionOptions) => {
       throw new Error('Found unsupported `providerOptions.s3.awsClientOptions.accessKeyId` or `providerOptions.s3.awsClientOptions.secretAccessKey` configuration. Please use the `providerOptions.s3.key` and `providerOptions.s3.secret` options instead.')
       throw new Error('Found unsupported `providerOptions.s3.awsClientOptions.accessKeyId` or `providerOptions.s3.awsClientOptions.secretAccessKey` configuration. Please use the `providerOptions.s3.key` and `providerOptions.s3.secret` options instead.')
     }
     }
 
 
+    let { endpoint } = s3
+    if (typeof endpoint === 'string') {
+      // TODO: deprecate those replacements in favor of what AWS SDK supports out of the box.
+      endpoint = endpoint.replace(/{service}/, 's3').replace(/{region}/, s3.region)
+    }
+
+    /** @type {import('@aws-sdk/client-s3').S3ClientConfig} */
     let s3ClientOptions = {
     let s3ClientOptions = {
-      endpoint: s3.endpoint,
       region: s3.region,
       region: s3.region,
     }
     }
-    if (typeof s3.endpoint === 'string') {
-      // TODO: deprecate those replacements in favor of what AWS SDK supports out of the box.
-      s3ClientOptions.endpoint = s3.endpoint.replace(/{service}/, 's3').replace(/{region}/, s3.region)
-    }
 
 
-    if (s3.useAccelerateEndpoint && s3.bucket != null) {
+    if (s3.useAccelerateEndpoint) {
+      // https://github.com/transloadit/uppy/issues/4809#issuecomment-1847320742
+      if (createPresignedPostMode) {
+        if (s3.bucket != null) {
+          s3ClientOptions = {
+            ...s3ClientOptions,
+            useAccelerateEndpoint: true,
+            // This is a workaround for lacking support for useAccelerateEndpoint in createPresignedPost
+            // See https://github.com/transloadit/uppy/issues/4135#issuecomment-1276450023
+            endpoint: `https://${s3.bucket}.s3-accelerate.amazonaws.com/`,
+          }
+        }
+      } else { // normal useAccelerateEndpoint mode
+        s3ClientOptions = {
+          ...s3ClientOptions,
+          useAccelerateEndpoint: true,
+        }
+      }
+    } else { // no accelearate, we allow custom s3 endpoint
       s3ClientOptions = {
       s3ClientOptions = {
         ...s3ClientOptions,
         ...s3ClientOptions,
-        useAccelerateEndpoint: true,
-        bucketEndpoint: true,
-        // This is a workaround for lacking support for useAccelerateEndpoint in createPresignedPost
-        // See https://github.com/transloadit/uppy/issues/4135#issuecomment-1276450023
-        endpoint: `https://${s3.bucket}.s3-accelerate.amazonaws.com/`,
+        endpoint,
       }
       }
     }
     }
 
 

+ 9 - 7
packages/@uppy/companion/test/__tests__/provider-manager.js

@@ -5,6 +5,8 @@ const { setDefaultEnv } = require('../mockserver')
 let grantConfig
 let grantConfig
 let companionOptions
 let companionOptions
 
 
+const getAuthProvider = (providerName) => providerManager.getDefaultProviders()[providerName]?.authProvider
+
 describe('Test Provider options', () => {
 describe('Test Provider options', () => {
   beforeEach(() => {
   beforeEach(() => {
     setDefaultEnv()
     setDefaultEnv()
@@ -14,7 +16,7 @@ describe('Test Provider options', () => {
   })
   })
 
 
   test('adds provider options', () => {
   test('adds provider options', () => {
-    providerManager.addProviderOptions(companionOptions, grantConfig)
+    providerManager.addProviderOptions(companionOptions, grantConfig, getAuthProvider)
     expect(grantConfig.dropbox.key).toBe('dropbox_key')
     expect(grantConfig.dropbox.key).toBe('dropbox_key')
     expect(grantConfig.dropbox.secret).toBe('dropbox_secret')
     expect(grantConfig.dropbox.secret).toBe('dropbox_secret')
 
 
@@ -33,7 +35,7 @@ describe('Test Provider options', () => {
 
 
   test('adds extra provider config', () => {
   test('adds extra provider config', () => {
     process.env.COMPANION_INSTAGRAM_KEY = '123456'
     process.env.COMPANION_INSTAGRAM_KEY = '123456'
-    providerManager.addProviderOptions(getCompanionOptions(), grantConfig)
+    providerManager.addProviderOptions(getCompanionOptions(), grantConfig, getAuthProvider)
     expect(grantConfig.instagram).toEqual({
     expect(grantConfig.instagram).toEqual({
       transport: 'session',
       transport: 'session',
       callback: '/instagram/callback',
       callback: '/instagram/callback',
@@ -102,7 +104,7 @@ describe('Test Provider options', () => {
 
 
     companionOptions = getCompanionOptions()
     companionOptions = getCompanionOptions()
 
 
-    providerManager.addProviderOptions(companionOptions, grantConfig)
+    providerManager.addProviderOptions(companionOptions, grantConfig, getAuthProvider)
 
 
     expect(grantConfig.dropbox.secret).toBe('xobpord')
     expect(grantConfig.dropbox.secret).toBe('xobpord')
     expect(grantConfig.box.secret).toBe('xwbepqd')
     expect(grantConfig.box.secret).toBe('xwbepqd')
@@ -116,7 +118,7 @@ describe('Test Provider options', () => {
     delete companionOptions.server.host
     delete companionOptions.server.host
     delete companionOptions.server.protocol
     delete companionOptions.server.protocol
 
 
-    providerManager.addProviderOptions(companionOptions, grantConfig)
+    providerManager.addProviderOptions(companionOptions, grantConfig, getAuthProvider)
     expect(grantConfig.dropbox.key).toBeUndefined()
     expect(grantConfig.dropbox.key).toBeUndefined()
     expect(grantConfig.dropbox.secret).toBeUndefined()
     expect(grantConfig.dropbox.secret).toBeUndefined()
 
 
@@ -135,7 +137,7 @@ describe('Test Provider options', () => {
 
 
   test('sets a main redirect uri, if oauthDomain is set', () => {
   test('sets a main redirect uri, if oauthDomain is set', () => {
     companionOptions.server.oauthDomain = 'domain.com'
     companionOptions.server.oauthDomain = 'domain.com'
-    providerManager.addProviderOptions(companionOptions, grantConfig)
+    providerManager.addProviderOptions(companionOptions, grantConfig, getAuthProvider)
 
 
     expect(grantConfig.dropbox.redirect_uri).toBe('http://domain.com/dropbox/redirect')
     expect(grantConfig.dropbox.redirect_uri).toBe('http://domain.com/dropbox/redirect')
     expect(grantConfig.box.redirect_uri).toBe('http://domain.com/box/redirect')
     expect(grantConfig.box.redirect_uri).toBe('http://domain.com/box/redirect')
@@ -158,8 +160,8 @@ describe('Test Custom Provider options', () => {
       },
       },
     }, providers, grantConfig)
     }, providers, grantConfig)
 
 
-    expect(grantConfig.foo.key).toBe('foo_key')
-    expect(grantConfig.foo.secret).toBe('foo_secret')
+    expect(grantConfig.some_provider.key).toBe('foo_key')
+    expect(grantConfig.some_provider.secret).toBe('foo_secret')
     expect(providers.foo).toBeTruthy()
     expect(providers.foo).toBeTruthy()
   })
   })
 })
 })

+ 7 - 0
packages/@uppy/core/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/core
 # @uppy/core
 
 
+## 3.8.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/core: Fix onBeforeFileAdded with Golden Retriever (Merlijn Vos / #4799)
+
 ## 3.7.1
 ## 3.7.1
 
 
 Released: 2023-11-12
 Released: 2023-11-12

+ 1 - 1
packages/@uppy/core/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/core",
   "name": "@uppy/core",
   "description": "Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
   "description": "Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
-  "version": "3.7.1",
+  "version": "3.8.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "style": "dist/style.min.css",
   "style": "dist/style.min.css",

+ 0 - 85
packages/@uppy/core/src/BasePlugin.js

@@ -1,85 +0,0 @@
-/**
- * Core plugin logic that all plugins share.
- *
- * BasePlugin does not contain DOM rendering so it can be used for plugins
- * without a user interface.
- *
- * See `Plugin` for the extended version with Preact rendering for interfaces.
- */
-
-import Translator from '@uppy/utils/lib/Translator'
-
-export default class BasePlugin {
-  constructor (uppy, opts = {}) {
-    this.uppy = uppy
-    this.opts = opts
-  }
-
-  getPluginState () {
-    const { plugins } = this.uppy.getState()
-    return plugins[this.id] || {}
-  }
-
-  setPluginState (update) {
-    const { plugins } = this.uppy.getState()
-
-    this.uppy.setState({
-      plugins: {
-        ...plugins,
-        [this.id]: {
-          ...plugins[this.id],
-          ...update,
-        },
-      },
-    })
-  }
-
-  setOptions (newOpts) {
-    this.opts = { ...this.opts, ...newOpts }
-    this.setPluginState() // so that UI re-renders with new options
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    const onMissingKey = (key) => this.uppy.log(`Missing i18n string: ${key}`, 'error')
-    const translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale], { onMissingKey })
-    this.i18n = translator.translate.bind(translator)
-    this.i18nArray = translator.translateArray.bind(translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
-  /**
-   * Extendable methods
-   * ==================
-   * These methods are here to serve as an overview of the extendable methods as well as
-   * making them not conditional in use, such as `if (this.afterUpdate)`.
-   */
-
-  // eslint-disable-next-line class-methods-use-this
-  addTarget () {
-    throw new Error('Extend the addTarget method to add your plugin to another plugin\'s target')
-  }
-
-  // eslint-disable-next-line class-methods-use-this
-  install () {}
-
-  // eslint-disable-next-line class-methods-use-this
-  uninstall () {}
-
-  /**
-   * Called when plugin is mounted, whether in DOM or into another plugin.
-   * Needed because sometimes plugins are mounted separately/after `install`,
-   * so this.el and this.parent might not be available in `install`.
-   * This is the case with @uppy/react plugins, for example.
-   */
-  render () {
-    throw new Error('Extend the render method to add your plugin to a DOM element')
-  }
-
-  // eslint-disable-next-line class-methods-use-this
-  update () {}
-
-  // Called after every state update, after everything's mounted. Debounced.
-  // eslint-disable-next-line class-methods-use-this
-  afterUpdate () {}
-}

+ 106 - 0
packages/@uppy/core/src/BasePlugin.ts

@@ -0,0 +1,106 @@
+/* eslint-disable class-methods-use-this */
+/* eslint-disable @typescript-eslint/no-empty-function */
+
+/**
+ * Core plugin logic that all plugins share.
+ *
+ * BasePlugin does not contain DOM rendering so it can be used for plugins
+ * without a user interface.
+ *
+ * See `Plugin` for the extended version with Preact rendering for interfaces.
+ */
+
+import Translator from '@uppy/utils/lib/Translator'
+import type { I18n, Locale } from '@uppy/utils/lib/Translator'
+import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
+import type { Uppy } from '.'
+
+export type PluginOpts = { locale?: Locale; [key: string]: unknown }
+
+export default class BasePlugin<
+  Opts extends PluginOpts,
+  M extends Meta,
+  B extends Body,
+> {
+  uppy: Uppy<M, B>
+
+  opts: Opts
+
+  id: string
+
+  defaultLocale: Locale
+
+  i18n: I18n
+
+  i18nArray: Translator['translateArray']
+
+  type: string
+
+  VERSION: string
+
+  constructor(uppy: Uppy<M, B>, opts: Opts) {
+    this.uppy = uppy
+    this.opts = opts ?? {}
+  }
+
+  getPluginState(): Record<string, unknown> {
+    const { plugins } = this.uppy.getState()
+    return plugins?.[this.id] || {}
+  }
+
+  setPluginState(update: unknown): void {
+    if (!update) return
+    const { plugins } = this.uppy.getState()
+
+    this.uppy.setState({
+      plugins: {
+        ...plugins,
+        [this.id]: {
+          ...plugins[this.id],
+          ...update,
+        },
+      },
+    })
+  }
+
+  setOptions(newOpts: Partial<Opts>): void {
+    this.opts = { ...this.opts, ...newOpts }
+    this.setPluginState(undefined) // so that UI re-renders with new options
+    this.i18nInit()
+  }
+
+  i18nInit(): void {
+    const translator = new Translator([
+      this.defaultLocale,
+      this.uppy.locale,
+      this.opts.locale,
+    ])
+    this.i18n = translator.translate.bind(translator)
+    this.i18nArray = translator.translateArray.bind(translator)
+    this.setPluginState(undefined) // so that UI re-renders and we see the updated locale
+  }
+
+  /**
+   * Extendable methods
+   * ==================
+   * These methods are here to serve as an overview of the extendable methods as well as
+   * making them not conditional in use, such as `if (this.afterUpdate)`.
+   */
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  addTarget(plugin: unknown): HTMLElement {
+    throw new Error(
+      "Extend the addTarget method to add your plugin to another plugin's target",
+    )
+  }
+
+  install(): void {}
+
+  uninstall(): void {}
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  update(state: any): void {}
+
+  // Called after every state update, after everything's mounted. Debounced.
+  afterUpdate(): void {}
+}

+ 114 - 0
packages/@uppy/core/src/EventManager.ts

@@ -0,0 +1,114 @@
+import type { Meta, Body, UppyFile } from '@uppy/utils/lib/UppyFile'
+import type {
+  DeprecatedUppyEventMap,
+  Uppy,
+  UppyEventMap,
+  _UppyEventMap,
+} from './Uppy'
+
+/**
+ * Create a wrapper around an event emitter with a `remove` method to remove
+ * all events that were added using the wrapped emitter.
+ */
+export default class EventManager<M extends Meta, B extends Body> {
+  #uppy: Uppy<M, B>
+
+  #events: Array<[keyof UppyEventMap<M, B>, (...args: any[]) => void]> = []
+
+  constructor(uppy: Uppy<M, B>) {
+    this.#uppy = uppy
+  }
+
+  on<K extends keyof _UppyEventMap<M, B>>(
+    event: K,
+    fn: _UppyEventMap<M, B>[K],
+  ): Uppy<M, B>
+
+  /** @deprecated */
+  on<K extends keyof DeprecatedUppyEventMap<M, B>>(
+    event: K,
+    fn: DeprecatedUppyEventMap<M, B>[K],
+  ): Uppy<M, B>
+
+  on<K extends keyof UppyEventMap<M, B>>(
+    event: K,
+    fn: UppyEventMap<M, B>[K],
+  ): Uppy<M, B> {
+    this.#events.push([event, fn])
+    return this.#uppy.on(event as keyof _UppyEventMap<M, B>, fn)
+  }
+
+  remove(): void {
+    for (const [event, fn] of this.#events.splice(0)) {
+      this.#uppy.off(event, fn)
+    }
+  }
+
+  onFilePause(
+    fileID: UppyFile<M, B>['id'],
+    cb: (isPaused: boolean) => void,
+  ): void {
+    this.on('upload-pause', (targetFileID, isPaused) => {
+      if (fileID === targetFileID) {
+        cb(isPaused)
+      }
+    })
+  }
+
+  onFileRemove(
+    fileID: UppyFile<M, B>['id'],
+    cb: (isPaused: UppyFile<M, B>['id']) => void,
+  ): void {
+    this.on('file-removed', (file) => {
+      if (fileID === file.id) cb(file.id)
+    })
+  }
+
+  onPause(fileID: UppyFile<M, B>['id'], cb: (isPaused: boolean) => void): void {
+    this.on('upload-pause', (targetFileID, isPaused) => {
+      if (fileID === targetFileID) {
+        // const isPaused = this.#uppy.pauseResume(fileID)
+        cb(isPaused)
+      }
+    })
+  }
+
+  onRetry(fileID: UppyFile<M, B>['id'], cb: () => void): void {
+    this.on('upload-retry', (targetFileID) => {
+      if (fileID === targetFileID) {
+        cb()
+      }
+    })
+  }
+
+  onRetryAll(fileID: UppyFile<M, B>['id'], cb: () => void): void {
+    this.on('retry-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onPauseAll(fileID: UppyFile<M, B>['id'], cb: () => void): void {
+    this.on('pause-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onCancelAll(
+    fileID: UppyFile<M, B>['id'],
+    eventHandler: UppyEventMap<M, B>['cancel-all'],
+  ): void {
+    this.on('cancel-all', (...args) => {
+      if (!this.#uppy.getFile(fileID)) return
+      eventHandler(...args)
+    })
+  }
+
+  onResumeAll(fileID: UppyFile<M, B>['id'], cb: () => void): void {
+    this.on('resume-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+}

+ 0 - 137
packages/@uppy/core/src/Restricter.js

@@ -1,137 +0,0 @@
-/* eslint-disable max-classes-per-file, class-methods-use-this */
-import prettierBytes from '@transloadit/prettier-bytes'
-import match from 'mime-match'
-
-const defaultOptions = {
-  maxFileSize: null,
-  minFileSize: null,
-  maxTotalFileSize: null,
-  maxNumberOfFiles: null,
-  minNumberOfFiles: null,
-  allowedFileTypes: null,
-  requiredMetaFields: [],
-}
-
-class RestrictionError extends Error {
-  constructor (message, { isUserFacing = true, file } = {}) {
-    super(message)
-    this.isUserFacing = isUserFacing
-    if (file != null) this.file = file // only some restriction errors are related to a particular file
-  }
-
-  isRestriction = true
-}
-
-class Restricter {
-  constructor (getOpts, i18n) {
-    this.i18n = i18n
-    this.getOpts = () => {
-      const opts = getOpts()
-
-      if (opts.restrictions.allowedFileTypes != null
-          && !Array.isArray(opts.restrictions.allowedFileTypes)) {
-        throw new TypeError('`restrictions.allowedFileTypes` must be an array')
-      }
-      return opts
-    }
-  }
-
-  // Because these operations are slow, we cannot run them for every file (if we are adding multiple files)
-  validateAggregateRestrictions (existingFiles, addingFiles) {
-    const { maxTotalFileSize, maxNumberOfFiles } = this.getOpts().restrictions
-
-    if (maxNumberOfFiles) {
-      const nonGhostFiles = existingFiles.filter(f => !f.isGhost)
-      if (nonGhostFiles.length + addingFiles.length > maxNumberOfFiles) {
-        throw new RestrictionError(`${this.i18n('youCanOnlyUploadX', { smart_count: maxNumberOfFiles })}`)
-      }
-    }
-
-    if (maxTotalFileSize) {
-      let totalFilesSize = existingFiles.reduce((total, f) => (total + f.size), 0)
-
-      for (const addingFile of addingFiles) {
-        if (addingFile.size != null) { // We can't check maxTotalFileSize if the size is unknown.
-          totalFilesSize += addingFile.size
-
-          if (totalFilesSize > maxTotalFileSize) {
-            throw new RestrictionError(this.i18n('exceedsSize', {
-              size: prettierBytes(maxTotalFileSize),
-              file: addingFile.name,
-            }))
-          }
-        }
-      }
-    }
-  }
-
-  validateSingleFile (file) {
-    const { maxFileSize, minFileSize, allowedFileTypes } = this.getOpts().restrictions
-
-    if (allowedFileTypes) {
-      const isCorrectFileType = allowedFileTypes.some((type) => {
-        // check if this is a mime-type
-        if (type.includes('/')) {
-          if (!file.type) return false
-          return match(file.type.replace(/;.*?$/, ''), type)
-        }
-
-        // otherwise this is likely an extension
-        if (type[0] === '.' && file.extension) {
-          return file.extension.toLowerCase() === type.slice(1).toLowerCase()
-        }
-        return false
-      })
-
-      if (!isCorrectFileType) {
-        const allowedFileTypesString = allowedFileTypes.join(', ')
-        throw new RestrictionError(this.i18n('youCanOnlyUploadFileTypes', { types: allowedFileTypesString }), { file })
-      }
-    }
-
-    // We can't check maxFileSize if the size is unknown.
-    if (maxFileSize && file.size != null && file.size > maxFileSize) {
-      throw new RestrictionError(this.i18n('exceedsSize', {
-        size: prettierBytes(maxFileSize),
-        file: file.name,
-      }), { file })
-    }
-
-    // We can't check minFileSize if the size is unknown.
-    if (minFileSize && file.size != null && file.size < minFileSize) {
-      throw new RestrictionError(this.i18n('inferiorSize', {
-        size: prettierBytes(minFileSize),
-      }), { file })
-    }
-  }
-
-  validate (existingFiles, addingFiles) {
-    addingFiles.forEach((addingFile) => {
-      this.validateSingleFile(addingFile)
-    })
-    this.validateAggregateRestrictions(existingFiles, addingFiles)
-  }
-
-  validateMinNumberOfFiles (files) {
-    const { minNumberOfFiles } = this.getOpts().restrictions
-    if (Object.keys(files).length < minNumberOfFiles) {
-      throw new RestrictionError(this.i18n('youHaveToAtLeastSelectX', { smart_count: minNumberOfFiles }))
-    }
-  }
-
-  getMissingRequiredMetaFields (file) {
-    const error = new RestrictionError(this.i18n('missingRequiredMetaFieldOnFile', { fileName: file.name }))
-    const { requiredMetaFields } = this.getOpts().restrictions
-    const missingFields = []
-
-    for (const field of requiredMetaFields) {
-      if (!Object.hasOwn(file.meta, field) || file.meta[field] === '') {
-        missingFields.push(field)
-      }
-    }
-
-    return { missingFields, error }
-  }
-}
-
-export { Restricter, defaultOptions, RestrictionError }

+ 204 - 0
packages/@uppy/core/src/Restricter.ts

@@ -0,0 +1,204 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+/* eslint-disable max-classes-per-file, class-methods-use-this */
+// @ts-ignore untyped
+import prettierBytes from '@transloadit/prettier-bytes'
+// @ts-ignore untyped
+import match from 'mime-match'
+import Translator from '@uppy/utils/lib/Translator'
+import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
+import type { I18n } from '@uppy/utils/lib/Translator'
+import type { State, NonNullableUppyOptions } from './Uppy'
+
+export type Restrictions = {
+  maxFileSize: number | null
+  minFileSize: number | null
+  maxTotalFileSize: number | null
+  maxNumberOfFiles: number | null
+  minNumberOfFiles: number | null
+  allowedFileTypes: string[] | null
+  requiredMetaFields: string[]
+}
+
+const defaultOptions = {
+  maxFileSize: null,
+  minFileSize: null,
+  maxTotalFileSize: null,
+  maxNumberOfFiles: null,
+  minNumberOfFiles: null,
+  allowedFileTypes: null,
+  requiredMetaFields: [],
+}
+
+class RestrictionError<M extends Meta, B extends Body> extends Error {
+  isUserFacing: boolean
+
+  file: UppyFile<M, B>
+
+  constructor(
+    message: string,
+    opts?: { isUserFacing?: boolean; file?: UppyFile<M, B> },
+  ) {
+    super(message)
+    this.isUserFacing = opts?.isUserFacing ?? true
+    if (opts?.file) {
+      this.file = opts.file // only some restriction errors are related to a particular file
+    }
+  }
+
+  isRestriction = true
+}
+
+class Restricter<M extends Meta, B extends Body> {
+  i18n: Translator['translate']
+
+  getOpts: () => NonNullableUppyOptions<M, B>
+
+  constructor(getOpts: () => NonNullableUppyOptions<M, B>, i18n: I18n) {
+    this.i18n = i18n
+    this.getOpts = (): NonNullableUppyOptions<M, B> => {
+      const opts = getOpts()
+
+      if (
+        opts.restrictions?.allowedFileTypes != null &&
+        !Array.isArray(opts.restrictions.allowedFileTypes)
+      ) {
+        throw new TypeError('`restrictions.allowedFileTypes` must be an array')
+      }
+      return opts
+    }
+  }
+
+  // Because these operations are slow, we cannot run them for every file (if we are adding multiple files)
+  validateAggregateRestrictions(
+    existingFiles: UppyFile<M, B>[],
+    addingFiles: UppyFile<M, B>[],
+  ): void {
+    const { maxTotalFileSize, maxNumberOfFiles } = this.getOpts().restrictions
+
+    if (maxNumberOfFiles) {
+      const nonGhostFiles = existingFiles.filter((f) => !f.isGhost)
+      if (nonGhostFiles.length + addingFiles.length > maxNumberOfFiles) {
+        throw new RestrictionError(
+          `${this.i18n('youCanOnlyUploadX', {
+            smart_count: maxNumberOfFiles,
+          })}`,
+        )
+      }
+    }
+
+    if (maxTotalFileSize) {
+      let totalFilesSize = existingFiles.reduce(
+        (total, f) => (total + (f.size ?? 0)) as number,
+        0,
+      )
+
+      for (const addingFile of addingFiles) {
+        if (addingFile.size != null) {
+          // We can't check maxTotalFileSize if the size is unknown.
+          totalFilesSize += addingFile.size
+
+          if (totalFilesSize > maxTotalFileSize) {
+            throw new RestrictionError(
+              this.i18n('exceedsSize', {
+                size: prettierBytes(maxTotalFileSize),
+                file: addingFile.name,
+              }),
+            )
+          }
+        }
+      }
+    }
+  }
+
+  validateSingleFile(file: UppyFile<M, B>): void {
+    const { maxFileSize, minFileSize, allowedFileTypes } =
+      this.getOpts().restrictions
+
+    if (allowedFileTypes) {
+      const isCorrectFileType = allowedFileTypes.some((type) => {
+        // check if this is a mime-type
+        if (type.includes('/')) {
+          if (!file.type) return false
+          return match(file.type.replace(/;.*?$/, ''), type)
+        }
+
+        // otherwise this is likely an extension
+        if (type[0] === '.' && file.extension) {
+          return file.extension.toLowerCase() === type.slice(1).toLowerCase()
+        }
+        return false
+      })
+
+      if (!isCorrectFileType) {
+        const allowedFileTypesString = allowedFileTypes.join(', ')
+        throw new RestrictionError(
+          this.i18n('youCanOnlyUploadFileTypes', {
+            types: allowedFileTypesString,
+          }),
+          { file },
+        )
+      }
+    }
+
+    // We can't check maxFileSize if the size is unknown.
+    if (maxFileSize && file.size != null && file.size > maxFileSize) {
+      throw new RestrictionError(
+        this.i18n('exceedsSize', {
+          size: prettierBytes(maxFileSize),
+          file: file.name,
+        }),
+        { file },
+      )
+    }
+
+    // We can't check minFileSize if the size is unknown.
+    if (minFileSize && file.size != null && file.size < minFileSize) {
+      throw new RestrictionError(
+        this.i18n('inferiorSize', {
+          size: prettierBytes(minFileSize),
+        }),
+        { file },
+      )
+    }
+  }
+
+  validate(
+    existingFiles: UppyFile<M, B>[],
+    addingFiles: UppyFile<M, B>[],
+  ): void {
+    addingFiles.forEach((addingFile) => {
+      this.validateSingleFile(addingFile)
+    })
+    this.validateAggregateRestrictions(existingFiles, addingFiles)
+  }
+
+  validateMinNumberOfFiles(files: State<M, B>['files']): void {
+    const { minNumberOfFiles } = this.getOpts().restrictions
+    if (minNumberOfFiles && Object.keys(files).length < minNumberOfFiles) {
+      throw new RestrictionError(
+        this.i18n('youHaveToAtLeastSelectX', { smart_count: minNumberOfFiles }),
+      )
+    }
+  }
+
+  getMissingRequiredMetaFields(file: UppyFile<M, B>): {
+    missingFields: string[]
+    error: RestrictionError<M, B>
+  } {
+    const error = new RestrictionError<M, B>(
+      this.i18n('missingRequiredMetaFieldOnFile', { fileName: file.name }),
+    )
+    const { requiredMetaFields } = this.getOpts().restrictions
+    const missingFields: string[] = []
+
+    for (const field of requiredMetaFields) {
+      if (!Object.hasOwn(file.meta, field) || file.meta[field] === '') {
+        missingFields.push(field)
+      }
+    }
+
+    return { missingFields, error }
+  }
+}
+
+export { Restricter, defaultOptions, RestrictionError }

+ 5 - 5
packages/@uppy/core/src/UIPlugin.test.js → packages/@uppy/core/src/UIPlugin.test.ts

@@ -1,12 +1,12 @@
 import { describe, expect, it } from 'vitest'
 import { describe, expect, it } from 'vitest'
-import UIPlugin from './UIPlugin.js'
-import Core from './index.js'
+import UIPlugin from './UIPlugin.ts'
+import Core from './index.ts'
 
 
 describe('UIPlugin', () => {
 describe('UIPlugin', () => {
   describe('getPluginState', () => {
   describe('getPluginState', () => {
     it('returns an empty object if no state is available', () => {
     it('returns an empty object if no state is available', () => {
-      class Example extends UIPlugin {}
-      const inst = new Example(new Core(), {})
+      class Example extends UIPlugin<any, any, any> {}
+      const inst = new Example(new Core<any, any>(), {})
 
 
       expect(inst.getPluginState()).toEqual({})
       expect(inst.getPluginState()).toEqual({})
     })
     })
@@ -14,7 +14,7 @@ describe('UIPlugin', () => {
 
 
   describe('setPluginState', () => {
   describe('setPluginState', () => {
     it('applies patches', () => {
     it('applies patches', () => {
-      class Example extends UIPlugin {}
+      class Example extends UIPlugin<any, any, any> {}
       const inst = new Example(new Core(), {})
       const inst = new Example(new Core(), {})
 
 
       inst.setPluginState({ a: 1 })
       inst.setPluginState({ a: 1 })

+ 61 - 29
packages/@uppy/core/src/UIPlugin.js → packages/@uppy/core/src/UIPlugin.ts

@@ -1,18 +1,21 @@
-import { render } from 'preact'
+/* eslint-disable class-methods-use-this */
+/* eslint-disable @typescript-eslint/no-empty-function */
+import { render, type ComponentChild } from 'preact'
 import findDOMElement from '@uppy/utils/lib/findDOMElement'
 import findDOMElement from '@uppy/utils/lib/findDOMElement'
 import getTextDirection from '@uppy/utils/lib/getTextDirection'
 import getTextDirection from '@uppy/utils/lib/getTextDirection'
 
 
-import BasePlugin from './BasePlugin.js'
+import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
+import BasePlugin from './BasePlugin.ts'
+import type { PluginOpts } from './BasePlugin.ts'
 
 
 /**
 /**
  * Defer a frequent call to the microtask queue.
  * Defer a frequent call to the microtask queue.
- *
- * @param {() => T} fn
- * @returns {Promise<T>}
  */
  */
-function debounce (fn) {
-  let calling = null
-  let latestArgs = null
+function debounce<T extends (...args: any[]) => any>(
+  fn: T,
+): (...args: Parameters<T>) => Promise<ReturnType<T>> {
+  let calling: Promise<ReturnType<T>> | null = null
+  let latestArgs: Parameters<T>
   return (...args) => {
   return (...args) => {
     latestArgs = args
     latestArgs = args
     if (!calling) {
     if (!calling) {
@@ -35,10 +38,20 @@ function debounce (fn) {
  *
  *
  * For plugins without an user interface, see BasePlugin.
  * For plugins without an user interface, see BasePlugin.
  */
  */
-class UIPlugin extends BasePlugin {
-  #updateUI
+class UIPlugin<
+  Opts extends PluginOpts & { direction?: 'ltr' | 'rtl' },
+  M extends Meta,
+  B extends Body,
+> extends BasePlugin<Opts, M, B> {
+  #updateUI: (state: any) => void
+
+  isTargetDOMEl: boolean
+
+  el: HTMLElement | null
+
+  parent: unknown
 
 
-  getTargetPlugin (target) {
+  getTargetPlugin(target: unknown): UIPlugin<any, any, any> | undefined {
     let targetPlugin
     let targetPlugin
     if (typeof target === 'object' && target instanceof UIPlugin) {
     if (typeof target === 'object' && target instanceof UIPlugin) {
       // Targeting a plugin *instance*
       // Targeting a plugin *instance*
@@ -47,7 +60,7 @@ class UIPlugin extends BasePlugin {
       // Targeting a plugin type
       // Targeting a plugin type
       const Target = target
       const Target = target
       // Find the target plugin instance.
       // Find the target plugin instance.
-      this.uppy.iteratePlugins(p => {
+      this.uppy.iteratePlugins((p) => {
         if (p instanceof Target) {
         if (p instanceof Target) {
           targetPlugin = p
           targetPlugin = p
         }
         }
@@ -62,7 +75,10 @@ class UIPlugin extends BasePlugin {
    * If it’s an object — target is a plugin, and we search `plugins`
    * If it’s an object — target is a plugin, and we search `plugins`
    * for a plugin with same name and return its target.
    * for a plugin with same name and return its target.
    */
    */
-  mount (target, plugin) {
+  mount(
+    target: HTMLElement | string,
+    plugin: UIPlugin<any, any, any>,
+  ): HTMLElement {
     const callerPluginName = plugin.id
     const callerPluginName = plugin.id
 
 
     const targetElement = findDOMElement(target)
     const targetElement = findDOMElement(target)
@@ -85,7 +101,9 @@ class UIPlugin extends BasePlugin {
         this.afterUpdate()
         this.afterUpdate()
       })
       })
 
 
-      this.uppy.log(`Installing ${callerPluginName} to a DOM element '${target}'`)
+      this.uppy.log(
+        `Installing ${callerPluginName} to a DOM element '${target}'`,
+      )
 
 
       if (this.opts.replaceTargetContent) {
       if (this.opts.replaceTargetContent) {
         // Doing render(h(null), targetElement), which should have been
         // Doing render(h(null), targetElement), which should have been
@@ -99,7 +117,8 @@ class UIPlugin extends BasePlugin {
       targetElement.appendChild(uppyRootElement)
       targetElement.appendChild(uppyRootElement)
 
 
       // Set the text direction if the page has not defined one.
       // Set the text direction if the page has not defined one.
-      uppyRootElement.dir = this.opts.direction || getTextDirection(uppyRootElement) || 'ltr'
+      uppyRootElement.dir =
+        this.opts.direction || getTextDirection(uppyRootElement) || 'ltr'
 
 
       this.onMount()
       this.onMount()
 
 
@@ -121,37 +140,50 @@ class UIPlugin extends BasePlugin {
 
 
     let message = `Invalid target option given to ${callerPluginName}.`
     let message = `Invalid target option given to ${callerPluginName}.`
     if (typeof target === 'function') {
     if (typeof target === 'function') {
-      message += ' The given target is not a Plugin class. '
-        + 'Please check that you\'re not specifying a React Component instead of a plugin. '
-        + 'If you are using @uppy/* packages directly, make sure you have only 1 version of @uppy/core installed: '
-        + 'run `npm ls @uppy/core` on the command line and verify that all the versions match and are deduped correctly.'
+      message +=
+        ' The given target is not a Plugin class. ' +
+        "Please check that you're not specifying a React Component instead of a plugin. " +
+        'If you are using @uppy/* packages directly, make sure you have only 1 version of @uppy/core installed: ' +
+        'run `npm ls @uppy/core` on the command line and verify that all the versions match and are deduped correctly.'
     } else {
     } else {
-      message += 'If you meant to target an HTML element, please make sure that the element exists. '
-        + 'Check that the <script> tag initializing Uppy is right before the closing </body> tag at the end of the page. '
-        + '(see https://github.com/transloadit/uppy/issues/1042)\n\n'
-        + 'If you meant to target a plugin, please confirm that your `import` statements or `require` calls are correct.'
+      message +=
+        'If you meant to target an HTML element, please make sure that the element exists. ' +
+        'Check that the <script> tag initializing Uppy is right before the closing </body> tag at the end of the page. ' +
+        '(see https://github.com/transloadit/uppy/issues/1042)\n\n' +
+        'If you meant to target a plugin, please confirm that your `import` statements or `require` calls are correct.'
     }
     }
     throw new Error(message)
     throw new Error(message)
   }
   }
 
 
-  update (state) {
+  /**
+   * Called when plugin is mounted, whether in DOM or into another plugin.
+   * Needed because sometimes plugins are mounted separately/after `install`,
+   * so this.el and this.parent might not be available in `install`.
+   * This is the case with @uppy/react plugins, for example.
+   */
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  render(state: Record<string, unknown>): ComponentChild {
+    throw new Error(
+      'Extend the render method to add your plugin to a DOM element',
+    )
+  }
+
+  update(state: any): void {
     if (this.el != null) {
     if (this.el != null) {
       this.#updateUI?.(state)
       this.#updateUI?.(state)
     }
     }
   }
   }
 
 
-  unmount () {
+  unmount(): void {
     if (this.isTargetDOMEl) {
     if (this.isTargetDOMEl) {
       this.el?.remove()
       this.el?.remove()
     }
     }
     this.onUnmount()
     this.onUnmount()
   }
   }
 
 
-  // eslint-disable-next-line class-methods-use-this
-  onMount () {}
+  onMount(): void {}
 
 
-  // eslint-disable-next-line class-methods-use-this
-  onUnmount () {}
+  onUnmount(): void {}
 }
 }
 
 
 export default UIPlugin
 export default UIPlugin

File diff suppressed because it is too large
+ 315 - 147
packages/@uppy/core/src/Uppy.test.ts


File diff suppressed because it is too large
+ 530 - 151
packages/@uppy/core/src/Uppy.ts


+ 69 - 0
packages/@uppy/core/src/__snapshots__/Uppy.test.ts.snap

@@ -0,0 +1,69 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`src/Core > plugins > should not be able to add a plugin that has no id 1`] = `"Your plugin must have an id"`;
+
+exports[`src/Core > plugins > should not be able to add a plugin that has no type 1`] = `"Your plugin must have a type"`;
+
+exports[`src/Core > plugins > should not be able to add an invalid plugin 1`] = `"Expected a plugin class, but got object. Please verify that the plugin was imported and spelled correctly."`;
+
+exports[`src/Core > plugins > should prevent the same plugin from being added more than once 1`] = `
+"Already found a plugin named 'TestSelector1'. Tried to use: 'TestSelector1'.
+Uppy plugins must have unique \`id\` options. See https://uppy.io/docs/plugins/#id."
+`;
+
+exports[`src/Core > uploading a file > should only upload files that are not already assigned to another upload id 1`] = `
+{
+  "failed": [],
+  "successful": [
+    {
+      "data": Uint8Array [],
+      "extension": "jpg",
+      "id": "uppy-foo/jpg-1e-image/jpeg",
+      "isGhost": false,
+      "isRemote": false,
+      "meta": {
+        "name": "foo.jpg",
+        "type": "image/jpeg",
+      },
+      "name": "foo.jpg",
+      "preview": undefined,
+      "progress": {
+        "bytesTotal": null,
+        "bytesUploaded": 0,
+        "percentage": 0,
+        "uploadComplete": false,
+        "uploadStarted": null,
+      },
+      "remote": "",
+      "size": null,
+      "source": "vi",
+      "type": "image/jpeg",
+    },
+    {
+      "data": Uint8Array [],
+      "extension": "jpg",
+      "id": "uppy-bar/jpg-1e-image/jpeg",
+      "isGhost": false,
+      "isRemote": false,
+      "meta": {
+        "name": "bar.jpg",
+        "type": "image/jpeg",
+      },
+      "name": "bar.jpg",
+      "preview": undefined,
+      "progress": {
+        "bytesTotal": null,
+        "bytesUploaded": 0,
+        "percentage": 0,
+        "uploadComplete": false,
+        "uploadStarted": null,
+      },
+      "remote": "",
+      "size": null,
+      "source": "vi",
+      "type": "image/jpeg",
+    },
+  ],
+  "uploadID": "cjd09qwxb000dlql4tp4doz8h",
+}
+`;

+ 4 - 1
packages/@uppy/core/src/getFileName.js → packages/@uppy/core/src/getFileName.ts

@@ -1,4 +1,7 @@
-export default function getFileName (fileType, fileDescriptor) {
+export default function getFileName(
+  fileType: string,
+  fileDescriptor: { name?: string },
+): string {
   if (fileDescriptor.name) {
   if (fileDescriptor.name) {
     return fileDescriptor.name
     return fileDescriptor.name
   }
   }

+ 0 - 5
packages/@uppy/core/src/index.js

@@ -1,5 +0,0 @@
-export { default } from './Uppy.js'
-export { default as Uppy } from './Uppy.js'
-export { default as UIPlugin } from './UIPlugin.js'
-export { default as BasePlugin } from './BasePlugin.js'
-export { debugLogger } from './loggers.js'

+ 5 - 0
packages/@uppy/core/src/index.ts

@@ -0,0 +1,5 @@
+export { default } from './Uppy.ts'
+export { default as Uppy } from './Uppy.ts'
+export { default as UIPlugin } from './UIPlugin.ts'
+export { default as BasePlugin } from './BasePlugin.ts'
+export { debugLogger } from './loggers.ts'

+ 2 - 1
packages/@uppy/core/src/locale.js → packages/@uppy/core/src/locale.ts

@@ -58,6 +58,7 @@ export default {
       0: 'Added %{smart_count} file from %{folder}',
       0: 'Added %{smart_count} file from %{folder}',
       1: 'Added %{smart_count} files from %{folder}',
       1: 'Added %{smart_count} files from %{folder}',
     },
     },
-    additionalRestrictionsFailed: '%{count} additional restrictions were not fulfilled',
+    additionalRestrictionsFailed:
+      '%{count} additional restrictions were not fulfilled',
   },
   },
 }
 }

+ 0 - 23
packages/@uppy/core/src/loggers.js

@@ -1,23 +0,0 @@
-/* eslint-disable no-console */
-import getTimeStamp from '@uppy/utils/lib/getTimeStamp'
-
-// Swallow all logs, except errors.
-// default if logger is not set or debug: false
-const justErrorsLogger = {
-  debug: () => {},
-  warn: () => {},
-  error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
-}
-
-// Print logs to console with namespace + timestamp,
-// set by logger: Uppy.debugLogger or debug: true
-const debugLogger = {
-  debug: (...args) => console.debug(`[Uppy] [${getTimeStamp()}]`, ...args),
-  warn: (...args) => console.warn(`[Uppy] [${getTimeStamp()}]`, ...args),
-  error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
-}
-
-export {
-  justErrorsLogger,
-  debugLogger,
-}

+ 25 - 0
packages/@uppy/core/src/loggers.ts

@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+/* eslint-disable no-console */
+import getTimeStamp from '@uppy/utils/lib/getTimeStamp'
+
+// Swallow all logs, except errors.
+// default if logger is not set or debug: false
+const justErrorsLogger = {
+  debug: (): void => {},
+  warn: (): void => {},
+  error: (...args: any[]): void =>
+    console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
+}
+
+// Print logs to console with namespace + timestamp,
+// set by logger: Uppy.debugLogger or debug: true
+const debugLogger = {
+  debug: (...args: any[]): void =>
+    console.debug(`[Uppy] [${getTimeStamp()}]`, ...args),
+  warn: (...args: any[]): void =>
+    console.warn(`[Uppy] [${getTimeStamp()}]`, ...args),
+  error: (...args: any[]): void =>
+    console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
+}
+
+export { justErrorsLogger, debugLogger }

+ 13 - 6
packages/@uppy/core/src/mocks/acquirerPlugin1.js → packages/@uppy/core/src/mocks/acquirerPlugin1.ts

@@ -1,8 +1,15 @@
 import { vi } from 'vitest' // eslint-disable-line import/no-extraneous-dependencies
 import { vi } from 'vitest' // eslint-disable-line import/no-extraneous-dependencies
-import UIPlugin from '../UIPlugin.js'
+import UIPlugin from '../UIPlugin.ts'
+import type Uppy from '../Uppy.ts'
 
 
-export default class TestSelector1 extends UIPlugin {
-  constructor (uppy, opts) {
+type mock = ReturnType<typeof vi.fn>
+
+export default class TestSelector1 extends UIPlugin<any, any, any> {
+  name: string
+
+  mocks: { run: mock; update: mock; uninstall: mock }
+
+  constructor(uppy: Uppy<any, any>, opts: any) {
     super(uppy, opts)
     super(uppy, opts)
     this.type = 'acquirer'
     this.type = 'acquirer'
     this.id = 'TestSelector1'
     this.id = 'TestSelector1'
@@ -15,7 +22,7 @@ export default class TestSelector1 extends UIPlugin {
     }
     }
   }
   }
 
 
-  run (results) {
+  run(results: any) {
     this.uppy.log({
     this.uppy.log({
       class: this.constructor.name,
       class: this.constructor.name,
       method: 'run',
       method: 'run',
@@ -25,11 +32,11 @@ export default class TestSelector1 extends UIPlugin {
     return Promise.resolve('success')
     return Promise.resolve('success')
   }
   }
 
 
-  update (state) {
+  update(state: any) {
     this.mocks.update(state)
     this.mocks.update(state)
   }
   }
 
 
-  uninstall () {
+  uninstall() {
     this.mocks.uninstall()
     this.mocks.uninstall()
   }
   }
 }
 }

+ 13 - 6
packages/@uppy/core/src/mocks/acquirerPlugin2.js → packages/@uppy/core/src/mocks/acquirerPlugin2.ts

@@ -1,8 +1,15 @@
 import { vi } from 'vitest' // eslint-disable-line import/no-extraneous-dependencies
 import { vi } from 'vitest' // eslint-disable-line import/no-extraneous-dependencies
-import UIPlugin from '../UIPlugin.js'
+import UIPlugin from '../UIPlugin.ts'
+import type Uppy from '../Uppy.ts'
 
 
-export default class TestSelector2 extends UIPlugin {
-  constructor (uppy, opts) {
+type mock = ReturnType<typeof vi.fn>
+
+export default class TestSelector2 extends UIPlugin<any, any, any> {
+  name: string
+
+  mocks: { run: mock; update: mock; uninstall: mock }
+
+  constructor(uppy: Uppy<any, any>, opts: any) {
     super(uppy, opts)
     super(uppy, opts)
     this.type = 'acquirer'
     this.type = 'acquirer'
     this.id = 'TestSelector2'
     this.id = 'TestSelector2'
@@ -15,7 +22,7 @@ export default class TestSelector2 extends UIPlugin {
     }
     }
   }
   }
 
 
-  run (results) {
+  run(results: any) {
     this.uppy.log({
     this.uppy.log({
       class: this.constructor.name,
       class: this.constructor.name,
       method: 'run',
       method: 'run',
@@ -25,11 +32,11 @@ export default class TestSelector2 extends UIPlugin {
     return Promise.resolve('success')
     return Promise.resolve('success')
   }
   }
 
 
-  update (state) {
+  update(state: any) {
     this.mocks.update(state)
     this.mocks.update(state)
   }
   }
 
 
-  uninstall () {
+  uninstall() {
     this.mocks.uninstall()
     this.mocks.uninstall()
   }
   }
 }
 }

+ 0 - 0
packages/@uppy/core/src/mocks/invalidPlugin.js → packages/@uppy/core/src/mocks/invalidPlugin.ts


+ 0 - 19
packages/@uppy/core/src/mocks/invalidPluginWithoutId.js

@@ -1,19 +0,0 @@
-import UIPlugin from '../UIPlugin.js'
-
-export default class InvalidPluginWithoutName extends UIPlugin {
-  constructor (uppy, opts) {
-    super(uppy, opts)
-    this.type = 'acquirer'
-    this.name = this.constructor.name
-  }
-
-  run (results) {
-    this.uppy.log({
-      class: this.constructor.name,
-      method: 'run',
-      results,
-    })
-
-    return Promise.resolve('success')
-  }
-}

+ 24 - 0
packages/@uppy/core/src/mocks/invalidPluginWithoutId.ts

@@ -0,0 +1,24 @@
+import UIPlugin from '../UIPlugin.ts'
+import type Uppy from '../Uppy.ts'
+
+export default class InvalidPluginWithoutName extends UIPlugin<any, any, any> {
+  public type: string
+
+  public name: string
+
+  constructor(uppy: Uppy<any, any>, opts: any) {
+    super(uppy, opts)
+    this.type = 'acquirer'
+    this.name = this.constructor.name
+  }
+
+  run(results: any) {
+    this.uppy.log({
+      class: this.constructor.name,
+      method: 'run',
+      results,
+    })
+
+    return Promise.resolve('success')
+  }
+}

+ 0 - 19
packages/@uppy/core/src/mocks/invalidPluginWithoutType.js

@@ -1,19 +0,0 @@
-import UIPlugin from '../UIPlugin.js'
-
-export default class InvalidPluginWithoutType extends UIPlugin {
-  constructor (uppy, opts) {
-    super(uppy, opts)
-    this.id = 'InvalidPluginWithoutType'
-    this.name = this.constructor.name
-  }
-
-  run (results) {
-    this.uppy.log({
-      class: this.constructor.name,
-      method: 'run',
-      results,
-    })
-
-    return Promise.resolve('success')
-  }
-}

+ 24 - 0
packages/@uppy/core/src/mocks/invalidPluginWithoutType.ts

@@ -0,0 +1,24 @@
+import UIPlugin from '../UIPlugin.ts'
+import type Uppy from '../Uppy.ts'
+
+export default class InvalidPluginWithoutType extends UIPlugin<any, any, any> {
+  public id: string
+
+  public name: string
+
+  constructor(uppy: Uppy<any, any>, opts: any) {
+    super(uppy, opts)
+    this.id = 'InvalidPluginWithoutType'
+    this.name = this.constructor.name
+  }
+
+  run(results: any) {
+    this.uppy.log({
+      class: this.constructor.name,
+      method: 'run',
+      results,
+    })
+
+    return Promise.resolve('success')
+  }
+}

+ 0 - 29
packages/@uppy/core/src/supportsUploadProgress.test.js

@@ -1,29 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import supportsUploadProgress from './supportsUploadProgress.js'
-
-describe('supportsUploadProgress', () => {
-  it('returns true in working browsers', () => {
-    // Firefox 64 (dev edition)
-    expect(supportsUploadProgress('Mozilla/5.0 (X11; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0')).toBe(true)
-
-    // Chromium 70
-    expect(supportsUploadProgress('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36')).toBe(true)
-
-    // IE 11
-    expect(supportsUploadProgress('Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; rv:11.0) like Gecko')).toBe(true)
-
-    // MS Edge 14
-    expect(supportsUploadProgress('Chrome (AppleWebKit/537.1; Chrome50.0; Windows NT 6.3) AppleWebKit/537.36 (KHTML like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393')).toBe(true)
-
-    // MS Edge 18, supposedly fixed
-    expect(supportsUploadProgress('Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/18.18218')).toBe(true)
-  })
-
-  it('returns false in broken browsers', () => {
-    // MS Edge 15, first broken version
-    expect(supportsUploadProgress('Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063')).toBe(false)
-
-    // MS Edge 17
-    expect(supportsUploadProgress('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134')).toBe(false)
-  })
-})

+ 57 - 0
packages/@uppy/core/src/supportsUploadProgress.test.ts

@@ -0,0 +1,57 @@
+import { describe, expect, it } from 'vitest'
+import supportsUploadProgress from './supportsUploadProgress.ts'
+
+describe('supportsUploadProgress', () => {
+  it('returns true in working browsers', () => {
+    // Firefox 64 (dev edition)
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (X11; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0',
+      ),
+    ).toBe(true)
+
+    // Chromium 70
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
+      ),
+    ).toBe(true)
+
+    // IE 11
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; rv:11.0) like Gecko',
+      ),
+    ).toBe(true)
+
+    // MS Edge 14
+    expect(
+      supportsUploadProgress(
+        'Chrome (AppleWebKit/537.1; Chrome50.0; Windows NT 6.3) AppleWebKit/537.36 (KHTML like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393',
+      ),
+    ).toBe(true)
+
+    // MS Edge 18, supposedly fixed
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/18.18218',
+      ),
+    ).toBe(true)
+  })
+
+  it('returns false in broken browsers', () => {
+    // MS Edge 15, first broken version
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063',
+      ),
+    ).toBe(false)
+
+    // MS Edge 17
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134',
+      ),
+    ).toBe(false)
+  })
+})

+ 4 - 4
packages/@uppy/core/src/supportsUploadProgress.js → packages/@uppy/core/src/supportsUploadProgress.ts

@@ -1,7 +1,7 @@
 // Edge 15.x does not fire 'progress' events on uploads.
 // Edge 15.x does not fire 'progress' events on uploads.
 // See https://github.com/transloadit/uppy/issues/945
 // See https://github.com/transloadit/uppy/issues/945
 // And https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12224510/
 // And https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12224510/
-export default function supportsUploadProgress (userAgent) {
+export default function supportsUploadProgress(userAgent?: string): boolean {
   // Allow passing in userAgent for tests
   // Allow passing in userAgent for tests
   if (userAgent == null && typeof navigator !== 'undefined') {
   if (userAgent == null && typeof navigator !== 'undefined') {
     // eslint-disable-next-line no-param-reassign
     // eslint-disable-next-line no-param-reassign
@@ -14,9 +14,9 @@ export default function supportsUploadProgress (userAgent) {
   if (!m) return true
   if (!m) return true
 
 
   const edgeVersion = m[1]
   const edgeVersion = m[1]
-  let [major, minor] = edgeVersion.split('.')
-  major = parseInt(major, 10)
-  minor = parseInt(minor, 10)
+  const version = edgeVersion.split('.', 2)
+  const major = parseInt(version[0], 10)
+  const minor = parseInt(version[1], 10)
 
 
   // Worked before:
   // Worked before:
   // Edge 40.15063.0.0
   // Edge 40.15063.0.0

+ 20 - 0
packages/@uppy/core/tsconfig.build.json

@@ -0,0 +1,20 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "outDir": "./lib",
+    "rootDir": "./src",
+    "resolveJsonModule": false,
+    "noImplicitAny": false,
+    "skipLibCheck": true
+  },
+  "include": ["./src/**/*.*"],
+  "exclude": ["./src/**/*.test.ts"],
+  "references": [
+    {
+      "path": "../store-default/tsconfig.build.json"
+    },
+    {
+      "path": "../utils/tsconfig.build.json"
+    }
+  ]
+}

+ 16 - 0
packages/@uppy/core/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "emitDeclarationOnly": false,
+    "noEmit": true
+  },
+  "include": ["./package.json", "./src/**/*.*"],
+  "references": [
+    {
+      "path": "../store-default/tsconfig.build.json"
+    },
+    {
+      "path": "../utils/tsconfig.build.json"
+    }
+  ]
+}

+ 1 - 1
packages/@uppy/dropbox/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/dropbox",
   "name": "@uppy/dropbox",
   "description": "Import files from Dropbox, into Uppy.",
   "description": "Import files from Dropbox, into Uppy.",
-  "version": "3.1.4",
+  "version": "3.2.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/@uppy/facebook/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/facebook",
   "name": "@uppy/facebook",
   "description": "Import files from Facebook, into Uppy.",
   "description": "Import files from Facebook, into Uppy.",
-  "version": "3.1.3",
+  "version": "3.2.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/@uppy/google-drive/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/google-drive",
   "name": "@uppy/google-drive",
   "description": "The Google Drive plugin for Uppy lets users import files from their Google Drive account",
   "description": "The Google Drive plugin for Uppy lets users import files from their Google Drive account",
-  "version": "3.3.0",
+  "version": "3.4.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 7 - 0
packages/@uppy/image-editor/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/image-editor
 # @uppy/image-editor
 
 
+## 2.4.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/image-editor: respect `cropperOptions.initialAspectRatio` (Lucklj521 / #4805)
+
 ## 2.3.0
 ## 2.3.0
 
 
 Released: 2023-11-08
 Released: 2023-11-08

+ 1 - 1
packages/@uppy/image-editor/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/image-editor",
   "name": "@uppy/image-editor",
   "description": "Image editor and cropping UI",
   "description": "Image editor and cropping UI",
-  "version": "2.3.0",
+  "version": "2.4.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "style": "dist/style.min.css",
   "style": "dist/style.min.css",

+ 1 - 1
packages/@uppy/instagram/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/instagram",
   "name": "@uppy/instagram",
   "description": "Import photos and videos from Instagram, into Uppy.",
   "description": "Import photos and videos from Instagram, into Uppy.",
-  "version": "3.1.3",
+  "version": "3.2.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/@uppy/onedrive/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/onedrive",
   "name": "@uppy/onedrive",
   "description": "Import files from OneDrive, into Uppy.",
   "description": "Import files from OneDrive, into Uppy.",
-  "version": "3.1.4",
+  "version": "3.2.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 7 - 0
packages/@uppy/provider-views/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/provider-views
 # @uppy/provider-views
 
 
+## 3.8.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/provider-views: fix uploadRemoteFile undefined (Mikael Finstad / #4814)
+
 ## 3.5.0
 ## 3.5.0
 
 
 Released: 2023-08-15
 Released: 2023-08-15

+ 1 - 1
packages/@uppy/provider-views/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/provider-views",
   "name": "@uppy/provider-views",
   "description": "View library for Uppy remote provider plugins.",
   "description": "View library for Uppy remote provider plugins.",
-  "version": "3.7.0",
+  "version": "3.8.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "style": "dist/style.min.css",
   "style": "dist/style.min.css",

+ 3 - 1
packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx

@@ -75,6 +75,8 @@ export default class ProviderView extends View {
       isSearchVisible: false,
       isSearchVisible: false,
       currentSelection: [],
       currentSelection: [],
     })
     })
+
+    this.registerRequestClient()
   }
   }
 
 
   // eslint-disable-next-line class-methods-use-this
   // eslint-disable-next-line class-methods-use-this
@@ -399,7 +401,7 @@ export default class ProviderView extends View {
         // finished all async operations before we add any file
         // finished all async operations before we add any file
         // see https://github.com/transloadit/uppy/pull/4384
         // see https://github.com/transloadit/uppy/pull/4384
         this.plugin.uppy.log('Adding files from a remote provider')
         this.plugin.uppy.log('Adding files from a remote provider')
-        this.plugin.uppy.addFiles(newFiles.map((file) => this.getTagFile(file)))
+        this.plugin.uppy.addFiles(newFiles.map((file) => this.getTagFile(file, this.requestClientId)))
 
 
         this.plugin.setPluginState({ filterInput: '' })
         this.plugin.setPluginState({ filterInput: '' })
         messages.forEach(message => this.plugin.uppy.info(message))
         messages.forEach(message => this.plugin.uppy.info(message))

+ 2 - 0
packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx

@@ -54,6 +54,8 @@ export default class SearchProviderView extends View {
 
 
     // Set default state for the plugin
     // Set default state for the plugin
     this.plugin.setPluginState(this.defaultState)
     this.plugin.setPluginState(this.defaultState)
+
+    this.registerRequestClient()
   }
   }
 
 
   // eslint-disable-next-line class-methods-use-this
   // eslint-disable-next-line class-methods-use-this

+ 6 - 6
packages/@uppy/provider-views/src/View.js

@@ -57,6 +57,11 @@ export default class View {
     uppy.info({ message, details: error.toString() }, 'error', 5000)
     uppy.info({ message, details: error.toString() }, 'error', 5000)
   }
   }
 
 
+  registerRequestClient() {
+    this.requestClientId = this.provider.provider;
+    this.plugin.uppy.registerRequestClient(this.requestClientId, this.provider)
+  }
+
   // todo document what is a "tagFile" or get rid of this concept
   // todo document what is a "tagFile" or get rid of this concept
   getTagFile (file) {
   getTagFile (file) {
     const tagFile = {
     const tagFile = {
@@ -78,15 +83,10 @@ export default class View {
         },
         },
         providerName: this.provider.name,
         providerName: this.provider.name,
         provider: this.provider.provider,
         provider: this.provider.provider,
+        requestClientId: this.requestClientId,
       },
       },
     }
     }
 
 
-    // all properties on this object get saved into the Uppy store.
-    // Some users might serialize their store (for example using JSON.stringify),
-    // or when using Golden Retriever it will serialize state into e.g. localStorage.
-    // However RequestClient is not serializable so we need to prevent it from being serialized.
-    Object.defineProperty(tagFile.remote, 'requestClient', { value: this.provider, enumerable: false })
-
     const fileType = getFileType(tagFile)
     const fileType = getFileType(tagFile)
 
 
     // TODO Should we just always use the thumbnail URL if it exists?
     // TODO Should we just always use the thumbnail URL if it exists?

+ 1 - 1
packages/@uppy/store-default/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/store-default",
   "name": "@uppy/store-default",
   "description": "The default simple object-based store for Uppy.",
   "description": "The default simple object-based store for Uppy.",
-  "version": "3.1.0",
+  "version": "3.2.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/@uppy/tus/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/tus",
   "name": "@uppy/tus",
   "description": "Resumable uploads for Uppy using Tus.io",
   "description": "Resumable uploads for Uppy using Tus.io",
-  "version": "3.4.0",
+  "version": "3.5.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/@uppy/tus/src/index.js

@@ -489,7 +489,7 @@ export default class Tus extends BasePlugin {
         }
         }
         this.uppy.on('file-removed', removedHandler)
         this.uppy.on('file-removed', removedHandler)
 
 
-        const uploadPromise = file.remote.requestClient.uploadRemoteFile(
+        const uploadPromise = this.uppy.getRequestClientForFile(file).uploadRemoteFile(
           file,
           file,
           this.#getCompanionClientArgs(file),
           this.#getCompanionClientArgs(file),
           { signal: controller.signal, getQueue },
           { signal: controller.signal, getQueue },

+ 1 - 1
packages/@uppy/url/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/url",
   "name": "@uppy/url",
   "description": "The Url plugin lets users import files from the Internet. Paste any URL and it’ll be added!",
   "description": "The Url plugin lets users import files from the Internet. Paste any URL and it’ll be added!",
-  "version": "3.4.0",
+  "version": "3.5.0",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "style": "dist/style.min.css",
   "style": "dist/style.min.css",

+ 6 - 2
packages/@uppy/url/src/Url.jsx

@@ -48,6 +48,7 @@ function getFileNameFromUrl (url) {
   const { pathname } = new URL(url)
   const { pathname } = new URL(url)
   return pathname.substring(pathname.lastIndexOf('/') + 1)
   return pathname.substring(pathname.lastIndexOf('/') + 1)
 }
 }
+
 /**
 /**
  * Url
  * Url
  *
  *
@@ -55,6 +56,8 @@ function getFileNameFromUrl (url) {
 export default class Url extends UIPlugin {
 export default class Url extends UIPlugin {
   static VERSION = packageJson.version
   static VERSION = packageJson.version
 
 
+  static requestClientId = Url.name
+
   constructor (uppy, opts) {
   constructor (uppy, opts) {
     super(uppy, opts)
     super(uppy, opts)
     this.id = this.opts.id || 'Url'
     this.id = this.opts.id || 'Url'
@@ -88,6 +91,8 @@ export default class Url extends UIPlugin {
       companionHeaders: this.opts.companionHeaders,
       companionHeaders: this.opts.companionHeaders,
       companionCookiesRule: this.opts.companionCookiesRule,
       companionCookiesRule: this.opts.companionCookiesRule,
     })
     })
+
+    this.uppy.registerRequestClient(Url.requestClientId, this.client)
   }
   }
 
 
   getMeta (url) {
   getMeta (url) {
@@ -132,11 +137,10 @@ export default class Url extends UIPlugin {
             fileId: url,
             fileId: url,
             url,
             url,
           },
           },
+          requestClientId: Url.requestClientId,
         },
         },
       }
       }
 
 
-      Object.defineProperty(tagFile.remote, 'requestClient', { value: this.client, enumerable: false })
-
       this.uppy.log('[Url] Adding remote file')
       this.uppy.log('[Url] Adding remote file')
       try {
       try {
         return this.uppy.addFile(tagFile)
         return this.uppy.addFile(tagFile)

+ 7 - 0
packages/@uppy/utils/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/utils
 # @uppy/utils
 
 
+## 5.7.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/utils: fix import in test files (Antoine du Hamel / #4806)
+
 ## 5.6.0
 ## 5.6.0
 
 
 Released: 2023-11-08
 Released: 2023-11-08

+ 3 - 1
packages/@uppy/utils/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@uppy/utils",
   "name": "@uppy/utils",
   "description": "Shared utility functions for Uppy Core and plugins maintained by the Uppy team.",
   "description": "Shared utility functions for Uppy Core and plugins maintained by the Uppy team.",
-  "version": "5.6.0",
+  "version": "5.7.0",
   "license": "MIT",
   "license": "MIT",
   "type": "module",
   "type": "module",
   "keywords": [
   "keywords": [
@@ -64,6 +64,8 @@
     "./lib/FOCUSABLE_ELEMENTS.js": "./lib/FOCUSABLE_ELEMENTS.js",
     "./lib/FOCUSABLE_ELEMENTS.js": "./lib/FOCUSABLE_ELEMENTS.js",
     "./lib/fileFilters": "./lib/fileFilters.js",
     "./lib/fileFilters": "./lib/fileFilters.js",
     "./lib/VirtualList": "./lib/VirtualList.js",
     "./lib/VirtualList": "./lib/VirtualList.js",
+    "./lib/UppyFile": "./lib/UppyFile.js",
+    "./lib/FileProgress": "./lib/FileProgress.js",
     "./src/microtip.scss": "./src/microtip.scss",
     "./src/microtip.scss": "./src/microtip.scss",
     "./lib/UserFacingApiError": "./lib/UserFacingApiError.js"
     "./lib/UserFacingApiError": "./lib/UserFacingApiError.js"
   },
   },

+ 3 - 119
packages/@uppy/utils/src/EventManager.ts

@@ -1,119 +1,3 @@
-import type {
-  Uppy,
-  UploadPauseCallback,
-  FileRemovedCallback,
-  UploadRetryCallback,
-  GenericEventCallback,
-  // @ts-expect-error @uppy/core has not been typed yet
-} from '@uppy/core'
-import type { UppyFile } from './UppyFile'
-/**
- * Create a wrapper around an event emitter with a `remove` method to remove
- * all events that were added using the wrapped emitter.
- */
-export default class EventManager {
-  #uppy: Uppy
-
-  #events: Array<Parameters<typeof Uppy.prototype.on>> = []
-
-  constructor(uppy: Uppy) {
-    this.#uppy = uppy
-  }
-
-  on(
-    event: Parameters<typeof Uppy.prototype.on>[0],
-    fn: Parameters<typeof Uppy.prototype.on>[1],
-  ): Uppy {
-    this.#events.push([event, fn])
-    return this.#uppy.on(event, fn)
-  }
-
-  remove(): void {
-    for (const [event, fn] of this.#events.splice(0)) {
-      this.#uppy.off(event, fn)
-    }
-  }
-
-  onFilePause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void {
-    this.on(
-      'upload-pause',
-      (
-        targetFileID: Parameters<UploadPauseCallback>[0],
-        isPaused: Parameters<UploadPauseCallback>[1],
-      ) => {
-        if (fileID === targetFileID) {
-          // @ts-expect-error @uppy/core has not been typed yet
-          cb(isPaused)
-        }
-      },
-    )
-  }
-
-  onFileRemove(
-    fileID: UppyFile['id'],
-    cb: (isPaused: UppyFile['id']) => void,
-  ): void {
-    this.on('file-removed', (file: Parameters<FileRemovedCallback<any>>[0]) => {
-      // @ts-expect-error @uppy/core has not been typed yet
-      if (fileID === file.id) cb(file.id)
-    })
-  }
-
-  onPause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void {
-    this.on(
-      'upload-pause',
-      (
-        targetFileID: Parameters<UploadPauseCallback>[0],
-        isPaused: Parameters<UploadPauseCallback>[1],
-      ) => {
-        if (fileID === targetFileID) {
-          // const isPaused = this.#uppy.pauseResume(fileID)
-          // @ts-expect-error @uppy/core has not been typed yet
-          cb(isPaused)
-        }
-      },
-    )
-  }
-
-  onRetry(fileID: UppyFile['id'], cb: () => void): void {
-    this.on(
-      'upload-retry',
-      (targetFileID: Parameters<UploadRetryCallback>[0]) => {
-        if (fileID === targetFileID) {
-          cb()
-        }
-      },
-    )
-  }
-
-  onRetryAll(fileID: UppyFile['id'], cb: () => void): void {
-    this.on('retry-all', () => {
-      if (!this.#uppy.getFile(fileID)) return
-      cb()
-    })
-  }
-
-  onPauseAll(fileID: UppyFile['id'], cb: () => void): void {
-    this.on('pause-all', () => {
-      if (!this.#uppy.getFile(fileID)) return
-      cb()
-    })
-  }
-
-  onCancelAll(
-    fileID: UppyFile['id'],
-    eventHandler: GenericEventCallback,
-  ): void {
-    this.on('cancel-all', (...args: Parameters<GenericEventCallback>) => {
-      if (!this.#uppy.getFile(fileID)) return
-      eventHandler(...args)
-    })
-  }
-
-  onResumeAll(fileID: UppyFile['id'], cb: () => void): void {
-    this.on('resume-all', () => {
-      if (!this.#uppy.getFile(fileID)) return
-      cb()
-    })
-  }
-}
+// eslint-disable-next-line
+// @ts-ignore Circular project reference
+export { default } from '@uppy/core/lib/EventManager.js'

+ 6 - 2
packages/@uppy/utils/src/FileProgress.ts

@@ -1,8 +1,10 @@
 interface FileProgressBase {
 interface FileProgressBase {
-  progress: number
+  progress?: number
   uploadComplete: boolean
   uploadComplete: boolean
   percentage: number
   percentage: number
   bytesTotal: number
   bytesTotal: number
+  preprocess?: { mode: string; message?: string; value?: number }
+  postprocess?: { mode: string; message?: string; value?: number }
 }
 }
 
 
 // FileProgress is either started or not started. We want to make sure TS doesn't
 // FileProgress is either started or not started. We want to make sure TS doesn't
@@ -10,9 +12,11 @@ interface FileProgressBase {
 export type FileProgressStarted = FileProgressBase & {
 export type FileProgressStarted = FileProgressBase & {
   uploadStarted: number
   uploadStarted: number
   bytesUploaded: number
   bytesUploaded: number
+  progress: number
 }
 }
 export type FileProgressNotStarted = FileProgressBase & {
 export type FileProgressNotStarted = FileProgressBase & {
   uploadStarted: null
   uploadStarted: null
-  bytesUploaded: false
+  // TODO: remove `|0` (or maybe `false|`?)
+  bytesUploaded: false | 0
 }
 }
 export type FileProgress = FileProgressStarted | FileProgressNotStarted
 export type FileProgress = FileProgressStarted | FileProgressNotStarted

+ 13 - 7
packages/@uppy/utils/src/Translator.ts

@@ -4,6 +4,13 @@ export interface Locale<T extends number = number> {
   pluralize: (n: number) => T
   pluralize: (n: number) => T
 }
 }
 
 
+export type OptionalPluralizeLocale<T extends number = number> =
+  | (Omit<Locale<T>, 'pluralize'> & Partial<Pick<Locale<T>, 'pluralize'>>)
+  | undefined
+
+// eslint-disable-next-line no-use-before-define
+export type I18n = Translator['translate']
+
 type Options = {
 type Options = {
   smart_count?: number
   smart_count?: number
 } & {
 } & {
@@ -98,10 +105,10 @@ const defaultOnMissingKey = (key: string): void => {
  * Usage example: `translator.translate('files_chosen', {smart_count: 3})`
  * Usage example: `translator.translate('files_chosen', {smart_count: 3})`
  */
  */
 export default class Translator {
 export default class Translator {
-  protected locale: Locale
+  readonly locale: Locale
 
 
   constructor(
   constructor(
-    locales: Locale | Locale[],
+    locales: Locale | Array<OptionalPluralizeLocale | undefined>,
     { onMissingKey = defaultOnMissingKey } = {},
     { onMissingKey = defaultOnMissingKey } = {},
   ) {
   ) {
     this.locale = {
     this.locale = {
@@ -125,17 +132,16 @@ export default class Translator {
 
 
   #onMissingKey
   #onMissingKey
 
 
-  #apply(locale?: Locale): void {
+  #apply(locale?: OptionalPluralizeLocale): void {
     if (!locale?.strings) {
     if (!locale?.strings) {
       return
       return
     }
     }
 
 
     const prevLocale = this.locale
     const prevLocale = this.locale
-    this.locale = {
-      ...prevLocale,
+    Object.assign(this.locale, {
       strings: { ...prevLocale.strings, ...locale.strings },
       strings: { ...prevLocale.strings, ...locale.strings },
-    } as any
-    this.locale.pluralize = locale.pluralize || prevLocale.pluralize
+      pluralize: locale.pluralize || prevLocale.pluralize,
+    })
   }
   }
 
 
   /**
   /**

+ 19 - 18
packages/@uppy/utils/src/UppyFile.ts

@@ -1,41 +1,42 @@
 import type { FileProgress } from './FileProgress'
 import type { FileProgress } from './FileProgress'
 
 
-interface IndexedObject<T> {
-  [key: string]: T
-  [key: number]: T
-}
+export type Meta = Record<string, unknown>
+
+export type Body = Record<string, unknown>
 
 
 export type InternalMetadata = { name: string; type?: string }
 export type InternalMetadata = { name: string; type?: string }
 
 
-export interface UppyFile<
-  TMeta = IndexedObject<any>,
-  TBody = IndexedObject<any>,
-> {
+export interface UppyFile<M extends Meta, B extends Body> {
   data: Blob | File
   data: Blob | File
-  error?: Error
+  error?: string | null
   extension: string
   extension: string
   id: string
   id: string
   isPaused?: boolean
   isPaused?: boolean
   isRestored?: boolean
   isRestored?: boolean
   isRemote: boolean
   isRemote: boolean
-  meta: InternalMetadata & TMeta
+  isGhost: boolean
+  meta: InternalMetadata & M
   name: string
   name: string
   preview?: string
   preview?: string
-  progress?: FileProgress
+  progress: FileProgress
+  missingRequiredMetaFields?: string[]
   remote?: {
   remote?: {
-    host: string
-    url: string
     body?: Record<string, unknown>
     body?: Record<string, unknown>
-    provider?: string
     companionUrl: string
     companionUrl: string
+    host: string
+    provider?: string
+    requestClientId: string
+    url: string
   }
   }
-  serverToken: string
-  size: number
+  serverToken?: string
+  size: number | null
   source?: string
   source?: string
   type?: string
   type?: string
+  uploadURL?: string
   response?: {
   response?: {
-    body: TBody
+    body: B
     status: number
     status: number
-    uploadURL: string | undefined
+    bytesUploaded?: number
+    uploadURL: string
   }
   }
 }
 }

+ 1 - 1
packages/@uppy/utils/src/emitSocketProgress.ts

@@ -5,7 +5,7 @@ import type { FileProgress } from './FileProgress'
 function emitSocketProgress(
 function emitSocketProgress(
   uploader: any,
   uploader: any,
   progressData: FileProgress,
   progressData: FileProgress,
-  file: UppyFile,
+  file: UppyFile<any, any>,
 ): void {
 ): void {
   const { progress, bytesUploaded, bytesTotal } = progressData
   const { progress, bytesUploaded, bytesTotal } = progressData
   if (progress) {
   if (progress) {

+ 8 - 3
packages/@uppy/utils/src/fileFilters.ts

@@ -1,13 +1,18 @@
 import type { UppyFile } from './UppyFile'
 import type { UppyFile } from './UppyFile'
 
 
-export function filterNonFailedFiles(files: UppyFile[]): UppyFile[] {
-  const hasError = (file: UppyFile): boolean => 'error' in file && !!file.error
+export function filterNonFailedFiles(
+  files: UppyFile<any, any>[],
+): UppyFile<any, any>[] {
+  const hasError = (file: UppyFile<any, any>): boolean =>
+    'error' in file && !!file.error
 
 
   return files.filter((file) => !hasError(file))
   return files.filter((file) => !hasError(file))
 }
 }
 
 
 // Don't double-emit upload-started for Golden Retriever-restored files that were already started
 // Don't double-emit upload-started for Golden Retriever-restored files that were already started
-export function filterFilesToEmitUploadStarted(files: UppyFile[]): UppyFile[] {
+export function filterFilesToEmitUploadStarted(
+  files: UppyFile<any, any>[],
+): UppyFile<any, any>[] {
   return files.filter(
   return files.filter(
     (file) => !file.progress?.uploadStarted || !file.isRestored,
     (file) => !file.progress?.uploadStarted || !file.isRestored,
   )
   )

+ 1 - 1
packages/@uppy/utils/src/findDOMElement.ts

@@ -6,7 +6,7 @@ import isDOMElement from './isDOMElement.ts'
 export default function findDOMElement(
 export default function findDOMElement(
   element: Node | string,
   element: Node | string,
   context = document,
   context = document,
-): Node | null {
+): Element | null {
   if (typeof element === 'string') {
   if (typeof element === 'string') {
     return context.querySelector(element)
     return context.querySelector(element)
   }
   }

+ 3 - 3
packages/@uppy/utils/src/generateFileID.ts

@@ -19,7 +19,7 @@ function encodeFilename(name: string): string {
  * Takes a file object and turns it into fileID, by converting file.name to lowercase,
  * Takes a file object and turns it into fileID, by converting file.name to lowercase,
  * removing extra characters and adding type, size and lastModified
  * removing extra characters and adding type, size and lastModified
  */
  */
-export default function generateFileID(file: UppyFile): string {
+export default function generateFileID(file: UppyFile<any, any>): string {
   // It's tempting to do `[items].filter(Boolean).join('-')` here, but that
   // It's tempting to do `[items].filter(Boolean).join('-')` here, but that
   // is slower! simple string concatenation is fast
   // is slower! simple string concatenation is fast
 
 
@@ -48,7 +48,7 @@ export default function generateFileID(file: UppyFile): string {
 
 
 // If the provider has a stable, unique ID, then we can use that to identify the file.
 // If the provider has a stable, unique ID, then we can use that to identify the file.
 // Then we don't have to generate our own ID, and we can add the same file many times if needed (different path)
 // Then we don't have to generate our own ID, and we can add the same file many times if needed (different path)
-function hasFileStableId(file: UppyFile): boolean {
+function hasFileStableId(file: UppyFile<any, any>): boolean {
   if (!file.isRemote || !file.remote) return false
   if (!file.isRemote || !file.remote) return false
   // These are the providers that it seems like have stable IDs for their files. The other's I haven't checked yet.
   // These are the providers that it seems like have stable IDs for their files. The other's I haven't checked yet.
   const stableIdProviders = new Set([
   const stableIdProviders = new Set([
@@ -61,7 +61,7 @@ function hasFileStableId(file: UppyFile): boolean {
   return stableIdProviders.has(file.remote.provider as any)
   return stableIdProviders.has(file.remote.provider as any)
 }
 }
 
 
-export function getSafeFileId(file: UppyFile): string {
+export function getSafeFileId(file: UppyFile<any, any>): string {
   if (hasFileStableId(file)) return file.id
   if (hasFileStableId(file)) return file.id
 
 
   const fileType = getFileType(file)
   const fileType = getFileType(file)

+ 9 - 9
packages/@uppy/utils/src/getFileType.test.ts

@@ -8,7 +8,7 @@ describe('getFileType', () => {
       isRemote: true,
       isRemote: true,
       type: 'audio/webm',
       type: 'audio/webm',
       name: 'foo.webm',
       name: 'foo.webm',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     expect(getFileType(file)).toEqual('audio/webm')
     expect(getFileType(file)).toEqual('audio/webm')
   })
   })
 
 
@@ -17,7 +17,7 @@ describe('getFileType', () => {
       type: 'audio/webm',
       type: 'audio/webm',
       name: 'foo.webm',
       name: 'foo.webm',
       data: 'sdfsdfhq9efbicw',
       data: 'sdfsdfhq9efbicw',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     expect(getFileType(file)).toEqual('audio/webm')
     expect(getFileType(file)).toEqual('audio/webm')
   })
   })
 
 
@@ -25,24 +25,24 @@ describe('getFileType', () => {
     const fileMP3 = {
     const fileMP3 = {
       name: 'foo.mp3',
       name: 'foo.mp3',
       data: 'sdfsfhfh329fhwihs',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     const fileYAML = {
     const fileYAML = {
       name: 'bar.yaml',
       name: 'bar.yaml',
       data: 'sdfsfhfh329fhwihs',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     const fileMKV = {
     const fileMKV = {
       name: 'bar.mkv',
       name: 'bar.mkv',
       data: 'sdfsfhfh329fhwihs',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     const fileDicom = {
     const fileDicom = {
       name: 'bar.dicom',
       name: 'bar.dicom',
       data: 'sdfsfhfh329fhwihs',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     const fileWebp = {
     const fileWebp = {
       name: 'bar.webp',
       name: 'bar.webp',
       data: 'sdfsfhfh329fhwihs',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
-    const toUpper = (file: UppyFile) => ({
+    } as any as UppyFile<any, any>
+    const toUpper = (file: UppyFile<any, any>) => ({
       ...file,
       ...file,
       name: file.name.toUpperCase(),
       name: file.name.toUpperCase(),
     })
     })
@@ -62,7 +62,7 @@ describe('getFileType', () => {
     const file = {
     const file = {
       name: 'foobar',
       name: 'foobar',
       data: 'sdfsfhfh329fhwihs',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     expect(getFileType(file)).toEqual('application/octet-stream')
     expect(getFileType(file)).toEqual('application/octet-stream')
   })
   })
 })
 })

+ 1 - 1
packages/@uppy/utils/src/getFileType.ts

@@ -2,7 +2,7 @@ import type { UppyFile } from './UppyFile'
 import getFileNameAndExtension from './getFileNameAndExtension.ts'
 import getFileNameAndExtension from './getFileNameAndExtension.ts'
 import mimeTypes from './mimeTypes.ts'
 import mimeTypes from './mimeTypes.ts'
 
 
-export default function getFileType(file: UppyFile): string {
+export default function getFileType(file: Partial<UppyFile<any, any>>): string {
   if (file.type) return file.type
   if (file.type) return file.type
 
 
   const fileExtension = file.name
   const fileExtension = file.name

Some files were not shown because too many files changed in this diff