Browse Source

Merge pull request #366 from goto-bus-stop/feature/tl-restore

transloadit: implement assembly status restore
Renée Kooi 7 years ago
parent
commit
99099c8bbc

+ 88 - 99
package-lock.json

@@ -7327,7 +7327,7 @@
       "requires": {
         "attempt-x": "1.1.1",
         "has-to-string-tag-x": "1.4.1",
-        "is-object-like-x": "1.5.1",
+        "is-object-like-x": "1.6.0",
         "object-get-own-property-descriptor-x": "3.2.0",
         "to-string-tag-x": "1.4.2"
       }
@@ -7457,15 +7457,15 @@
       "dev": true
     },
     "is-function-x": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/is-function-x/-/is-function-x-3.2.0.tgz",
-      "integrity": "sha512-ANQAythCIUKu0UprLZubZsYwAhYcNoM/FlrQSyFIXDoBzeGcHo6SHNPHCAl/T7UQyNiGzBirfUq0znic8P/Bew==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/is-function-x/-/is-function-x-3.3.0.tgz",
+      "integrity": "sha512-SreSSU1dlgYaXR5c0mm4qJHKYHIiGiEY+7Cd8/aRLLoMP/VvofD2XcWgBnP833ajpU5XzXbUSpfysnfKZLJFlg==",
       "requires": {
         "attempt-x": "1.1.1",
         "has-to-string-tag-x": "1.4.1",
         "is-falsey-x": "1.0.1",
         "is-primitive": "2.0.0",
-        "normalize-space-x": "2.0.0",
+        "normalize-space-x": "3.0.0",
         "replace-comments-x": "2.0.0",
         "to-boolean-x": "1.0.1",
         "to-string-tag-x": "1.4.2"
@@ -7481,15 +7481,15 @@
       }
     },
     "is-index-x": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-index-x/-/is-index-x-1.0.0.tgz",
-      "integrity": "sha512-BJ7vtw0jvcjBX4UsT7KkpZUliAMX3vJugZimDKy4W6ilGDtvUZ8nYsYnROVrrsjNjg5LJ3MN9NvyRVALfDW/wQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-index-x/-/is-index-x-1.1.0.tgz",
+      "integrity": "sha512-qULKLMepQLGC8rSVdi8uF2vI4LiDrU9XSDg1D+Aa657GIB7GV1jHpga7uXgQvkt/cpQ5mVBHUFTpSehYSqT6+A==",
       "requires": {
-        "math-clamp-x": "1.1.0",
+        "math-clamp-x": "1.2.0",
         "max-safe-integer": "1.0.1",
-        "safe-to-string-x": "2.0.3",
-        "to-integer-x": "2.1.0",
-        "to-number-x": "1.1.0"
+        "to-integer-x": "3.0.0",
+        "to-number-x": "2.0.0",
+        "to-string-symbols-supported-x": "1.0.0"
       }
     },
     "is-my-json-valid": {
@@ -7544,11 +7544,11 @@
       }
     },
     "is-object-like-x": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/is-object-like-x/-/is-object-like-x-1.5.1.tgz",
-      "integrity": "sha512-AtUeYE4Xs8EbuHuG6yBHiLdIlWRPPFidcIs3JE6PJZ/mzUQFOK8X5J1OA+3cVi0rlrdUCjiX52obtCV2hxs+HA==",
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/is-object-like-x/-/is-object-like-x-1.6.0.tgz",
+      "integrity": "sha512-mc3dBMv1jEOdk0f1i2RkJFsZDux0MuHqGwHOoRo770ShUOf4VE6tWThAW8dAZARr9a5RN+iNX1yzMDA5ad1clQ==",
       "requires": {
-        "is-function-x": "3.2.0",
+        "is-function-x": "3.3.0",
         "is-primitive": "2.0.0"
       }
     },
@@ -9208,11 +9208,11 @@
       }
     },
     "math-clamp-x": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/math-clamp-x/-/math-clamp-x-1.1.0.tgz",
-      "integrity": "sha512-c7Hxz6Ji4HtwUSMI1HU3Y7pcWQyuINlnCeE1675ZfNbEELFHeqHnEQXrWB7kLiiNTMi6QM38txFAfKq2IYqZpQ==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/math-clamp-x/-/math-clamp-x-1.2.0.tgz",
+      "integrity": "sha512-tqpjpBcIf9UulApz3EjWXqTZpMlr2vLN9PryC9ghoyCuRmqZaf3JJhPddzgQpJnKLi2QhoFnvKBFtJekAIBSYg==",
       "requires": {
-        "to-number-x": "1.1.0"
+        "to-number-x": "2.0.0"
       }
     },
     "math-expression-evaluator": {
@@ -9222,12 +9222,12 @@
       "dev": true
     },
     "math-sign-x": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/math-sign-x/-/math-sign-x-2.1.0.tgz",
-      "integrity": "sha512-3shFG0Ea5vOMCgQCrylyzu3POQRTvvaclb4VArnICToTgshMfA4Dlb9q9lZO1SD/rUD9mOTJZ7dTtlfCq7I91A==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/math-sign-x/-/math-sign-x-3.0.0.tgz",
+      "integrity": "sha512-OzPas41Pn4d16KHnaXmGxxY3/l3zK4OIXtmIwdhgZsxz4FDDcNnbrABYPg2vGfxIkaT9ezGnzDviRH7RfF44jQ==",
       "requires": {
         "is-nan-x": "1.0.1",
-        "to-number-x": "1.1.0"
+        "to-number-x": "2.0.0"
       }
     },
     "max-safe-integer": {
@@ -9710,6 +9710,11 @@
       "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=",
       "dev": true
     },
+    "nan-x": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/nan-x/-/nan-x-1.0.0.tgz",
+      "integrity": "sha512-yw4Fhe2/UTzanQ4f0yHWkRnfTuHZFAi4GZDjXS4G+qv5BqXTqPJBbSxpa7MyyW9v4Y4ZySZQik1vcbNkhdnIOg=="
+    },
     "nanoraf": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/nanoraf/-/nanoraf-3.0.1.tgz",
@@ -10210,13 +10215,13 @@
       "dev": true
     },
     "normalize-space-x": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/normalize-space-x/-/normalize-space-x-2.0.0.tgz",
-      "integrity": "sha512-R3nAbBlbEtn649TVgKzhgALTjilK5bgsbIsbk7+dtiDcEpuVVr7cUezwO7Tnm5e3IgaULi2s+FfVGj9WzVq1WA==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-space-x/-/normalize-space-x-3.0.0.tgz",
+      "integrity": "sha512-tbCJerqZCCHPst4rRKgsTanLf45fjOyeAU5zE3mhDxJtFJKt66q39g2XArWhXelgTFVib8mNBUm6Wrd0LxYcfQ==",
       "requires": {
         "cached-constructors-x": "1.0.0",
-        "trim-x": "2.0.2",
-        "white-space-x": "2.0.3"
+        "trim-x": "3.0.0",
+        "white-space-x": "3.0.0"
       }
     },
     "normalize-url": {
@@ -10377,10 +10382,10 @@
       "integrity": "sha512-Z/0fIrptD9YuzN+SNK/1kxAEaBcPQM4gSrtOSMSi9eplnL/AbyQcAyAlreAoAzmBon+DQ1Z+AdhxyQSvav5Fyg==",
       "requires": {
         "attempt-x": "1.1.1",
-        "has-own-property-x": "3.1.1",
+        "has-own-property-x": "3.2.0",
         "has-symbol-support-x": "1.4.1",
         "is-falsey-x": "1.0.1",
-        "is-index-x": "1.0.0",
+        "is-index-x": "1.1.0",
         "is-primitive": "2.0.0",
         "is-string": "1.0.4",
         "property-is-enumerable-x": "1.1.0",
@@ -10731,6 +10736,17 @@
         "is-glob": "2.0.1"
       }
     },
+    "parse-int-x": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/parse-int-x/-/parse-int-x-2.0.0.tgz",
+      "integrity": "sha512-NIMm52gmd1+0qxJK8lV3OZ4zzWpRH1xcz9xCHXl+DNzddwUdS4NEtd7BmTeK7iCIXoaK5e6BoDMHgieH2eNIhg==",
+      "requires": {
+        "cached-constructors-x": "1.0.0",
+        "nan-x": "1.0.0",
+        "to-string-x": "1.4.2",
+        "trim-left-x": "3.0.0"
+      }
+    },
     "parse-json": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
@@ -12087,6 +12103,15 @@
         "to-string-x": "1.4.2"
       }
     },
+    "require-coercible-to-string-x": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/require-coercible-to-string-x/-/require-coercible-to-string-x-1.0.0.tgz",
+      "integrity": "sha512-Rpfd4sMdflPAKecdKhfAtQHlZzzle4UMUgxJ01hXtTcNWMV8w9GeZnKhEyrT73kgrflBOP1zg41amUPZGcNspA==",
+      "requires": {
+        "require-object-coercible-x": "1.4.1",
+        "to-string-x": "1.4.2"
+      }
+    },
     "require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -12258,14 +12283,6 @@
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz",
       "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c="
     },
-    "safe-to-string-x": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/safe-to-string-x/-/safe-to-string-x-2.0.3.tgz",
-      "integrity": "sha512-hbxWZc0a+3VG7SpSKpZbBiXwQOV/bW/hwNvMFRPCL/60Ze/6Y8atHqv/0dWiWc9sHmkFqVCMR5gjmukMzKnA6A==",
-      "requires": {
-        "to-string-symbols-supported-x": "1.0.0"
-      }
-    },
     "sane": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/sane/-/sane-1.6.0.tgz",
@@ -13621,54 +13638,26 @@
       "dev": true
     },
     "to-integer-x": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/to-integer-x/-/to-integer-x-2.1.0.tgz",
-      "integrity": "sha512-M9iETTi+xMMZtUC70q4VE63XL2mXmNABxwxsIebOfd8K4ZHKCJLD9GyE6RlEPnbOPZj21QinuoVkqWsBsBknRA==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/to-integer-x/-/to-integer-x-3.0.0.tgz",
+      "integrity": "sha512-794L2Lpwjtynm7RxahJi2YdbRY75gTxUW27TMuN26UgwPkmJb/+HPhkFEFbz+E4vNoiP0dxq5tq5fkXoXLaK/w==",
       "requires": {
         "is-finite-x": "3.0.2",
         "is-nan-x": "1.0.1",
-        "math-sign-x": "2.1.0",
-        "to-number-x": "1.1.0"
+        "math-sign-x": "3.0.0",
+        "to-number-x": "2.0.0"
       }
     },
     "to-number-x": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/to-number-x/-/to-number-x-1.1.0.tgz",
-      "integrity": "sha512-m0v+VykgXsJ8JUSDKcvaASl977pSx5U1D6kyApFxP/KarJr7ZzVWrlHYTOxGDDodytsyh+iwZXpxBZpaWOvv4g==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-number-x/-/to-number-x-2.0.0.tgz",
+      "integrity": "sha512-lGOnCoccUoSzjZ/9Uen8TC4+VFaQcFGhTroWTv2tYWxXgyJV1zqAZ8hEIMkez/Eo790fBMOjidTnQ/OJSCvAoQ==",
       "requires": {
+        "cached-constructors-x": "1.0.0",
+        "nan-x": "1.0.0",
+        "parse-int-x": "2.0.0",
         "to-primitive-x": "1.1.0",
-        "trim-x": "1.0.4"
-      },
-      "dependencies": {
-        "trim-left-x": {
-          "version": "1.3.7",
-          "resolved": "https://registry.npmjs.org/trim-left-x/-/trim-left-x-1.3.7.tgz",
-          "integrity": "sha512-0UMUaK+dyb1UVs/slcjkikjQ5O1VOHLUB5VPTcIDJ9IRJCau5ENeH71hu+B4yuPpudXnbwQGyq5AS49kbEIpMw==",
-          "requires": {
-            "cached-constructors-x": "1.0.0",
-            "to-string-x": "1.4.2",
-            "white-space-x": "2.0.3"
-          }
-        },
-        "trim-right-x": {
-          "version": "1.3.4",
-          "resolved": "https://registry.npmjs.org/trim-right-x/-/trim-right-x-1.3.4.tgz",
-          "integrity": "sha512-W/NQvE5MS+rZhTMnsscwnQpto+VszsUWPWGaxcHrUut/QhkRvabz2HOZIAg+u5jUt9rIZwfEpMB3UUIJLtmtxA==",
-          "requires": {
-            "cached-constructors-x": "1.0.0",
-            "to-string-x": "1.4.2",
-            "white-space-x": "2.0.3"
-          }
-        },
-        "trim-x": {
-          "version": "1.0.4",
-          "resolved": "https://registry.npmjs.org/trim-x/-/trim-x-1.0.4.tgz",
-          "integrity": "sha512-+9P1RH7/k6S6OYQjk2pnvYsYx/CUiD6bVKmJwkPmSYhQkOYeXFahwDWxMMxEWw+5Sxj1rMkohZh93x0OoE6KZA==",
-          "requires": {
-            "trim-left-x": "1.3.7",
-            "trim-right-x": "1.3.4"
-          }
-        }
+        "trim-x": "3.0.0"
       }
     },
     "to-object-x": {
@@ -13687,7 +13676,7 @@
       "requires": {
         "has-symbol-support-x": "1.4.1",
         "is-date-object": "1.0.1",
-        "is-function-x": "3.2.0",
+        "is-function-x": "3.3.0",
         "is-nil-x": "1.4.1",
         "is-primitive": "2.0.0",
         "is-symbol": "1.0.1",
@@ -13804,13 +13793,13 @@
       "dev": true
     },
     "trim-left-x": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/trim-left-x/-/trim-left-x-2.0.1.tgz",
-      "integrity": "sha512-7JTQAjTmsUB07eDuVoBxAtRbvrC141gYhEnKoP5FHZGc7phaqjbqII7+nFT15gc73F0D7qPb7W+Ny8Im0Kip/Q==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/trim-left-x/-/trim-left-x-3.0.0.tgz",
+      "integrity": "sha512-+m6cqkppI+CxQBTwWEZliOHpOBnCArGyMnS1WCLb6IRgukhTkiQu/TNEN5Lj2eM9jk8ewJsc7WxFZfmwNpRXWQ==",
       "requires": {
         "cached-constructors-x": "1.0.0",
         "require-coercible-to-string-x": "1.0.0",
-        "white-space-x": "2.0.3"
+        "white-space-x": "3.0.0"
       }
     },
     "trim-newlines": {
@@ -13826,22 +13815,22 @@
       "dev": true
     },
     "trim-right-x": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/trim-right-x/-/trim-right-x-2.0.1.tgz",
-      "integrity": "sha512-hdz1fDE/roIkWWNtA43matOTi3dHgLhDkKTo+hFgLwlYSqjNt7Qr0QKZyik8ZDTpjUmrgHtU5/lb+gL/pngWvQ==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/trim-right-x/-/trim-right-x-3.0.0.tgz",
+      "integrity": "sha512-iIqEsWEbWVodqdixJHi4FoayJkUxhoL4AvSNGp4FF4FfQKRPGizt8++/RnyC9od75y7P/S6EfONoVqP+NddiKA==",
       "requires": {
         "cached-constructors-x": "1.0.0",
         "require-coercible-to-string-x": "1.0.0",
-        "white-space-x": "2.0.3"
+        "white-space-x": "3.0.0"
       }
     },
     "trim-x": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/trim-x/-/trim-x-2.0.2.tgz",
-      "integrity": "sha512-FnvMjV360hsj/OQpAaXqAKspNqyawkVe5zkWH/aOVOGcSnbeJYpeOYiaKIZYpu0ZQes3pq7IRm4whHJqAoev7w==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/trim-x/-/trim-x-3.0.0.tgz",
+      "integrity": "sha512-w8s38RAUScQ6t3XqMkS75iz5ZkIYLQpVnv2lp3IuTS36JdlVzC54oe6okOf4Wz3UH4rr3XAb2xR3kR5Xei82fw==",
       "requires": {
-        "trim-left-x": "2.0.1",
-        "trim-right-x": "2.0.1"
+        "trim-left-x": "3.0.0",
+        "trim-right-x": "3.0.0"
       }
     },
     "tryit": {
@@ -13866,9 +13855,9 @@
       }
     },
     "tus-js-client": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-1.4.4.tgz",
-      "integrity": "sha1-l5Ry9K4oq81o790iRfzuuMW2Hmc=",
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-1.4.5.tgz",
+      "integrity": "sha1-7lJd+KLE3EETvPkz3dfUCj20O5U=",
       "requires": {
         "buffer-from": "0.1.1",
         "extend": "3.0.1",
@@ -15391,9 +15380,9 @@
       "dev": true
     },
     "white-space-x": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/white-space-x/-/white-space-x-2.0.3.tgz",
-      "integrity": "sha512-An6uHDfZizY0t7x8iyY8nLej1lnqyaFSyTKjwwqS0VIhvV4tof6a+Et4uJVFlZh7HUAOgKoZfm5hFzl/D4xDgw=="
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/white-space-x/-/white-space-x-3.0.0.tgz",
+      "integrity": "sha512-nMPVXGMdi/jQepXKryxqzEh/vCwdOYY/u6NZy40glMHvZfEr7/+vQKnDhEq4rZ1nniOFq9GWohQYB30uW/5Olg=="
     },
     "wide-align": {
       "version": "1.1.2",

+ 1 - 1
package.json

@@ -113,7 +113,7 @@
     "prettier-bytes": "1.0.4",
     "prop-types": "^15.5.10",
     "socket.io-client": "2.0.2",
-    "tus-js-client": "^1.4.4",
+    "tus-js-client": "^1.4.5",
     "url-parse": "1.1.9",
     "whatwg-fetch": "2.0.3",
     "yo-yo": "1.4.0",

+ 33 - 2
src/core/Core.js

@@ -101,6 +101,7 @@ class Uppy {
     this.setState({
       plugins: {},
       files: {},
+      currentUploads: {},
       capabilities: {
         resumableUploads: false
       },
@@ -385,11 +386,36 @@ class Uppy {
   }
 
   removeFile (fileID) {
-    const updatedFiles = Object.assign({}, this.getState().files)
+    const { files, currentUploads } = this.state
+    const updatedFiles = Object.assign({}, files)
     const removedFile = updatedFiles[fileID]
     delete updatedFiles[fileID]
 
-    this.setState({files: updatedFiles})
+    // Remove this file from its `currentUpload`.
+    const updatedUploads = Object.assign({}, currentUploads)
+    const removeUploads = []
+    Object.keys(updatedUploads).forEach((uploadID) => {
+      const newFileIDs = currentUploads[uploadID].fileIDs.filter((uploadFileID) => uploadFileID !== fileID)
+      // Remove the upload if no files are associated with it anymore.
+      if (newFileIDs.length === 0) {
+        removeUploads.push(uploadID)
+        return
+      }
+
+      updatedUploads[uploadID] = Object.assign({}, currentUploads[uploadID], {
+        fileIDs: newFileIDs
+      })
+    })
+
+    this.setState({
+      currentUploads: updatedUploads,
+      files: updatedFiles
+    })
+
+    removeUploads.forEach((uploadID) => {
+      this.removeUpload(uploadID)
+    })
+
     this._calculateTotalProgress()
     this.emit('file-removed', fileID)
 
@@ -693,6 +719,11 @@ class Uppy {
       this.setState({ files: files })
     })
 
+    this.on('restored', () => {
+      // Files may have changed--ensure progress is still accurate.
+      this._calculateTotalProgress()
+    })
+
     // show informer if offline
     if (typeof window !== 'undefined') {
       window.addEventListener('online', () => this.updateOnlineStatus())

+ 6 - 0
src/core/Core.test.js

@@ -152,6 +152,7 @@ describe('src/Core', () => {
         bee: 'boo',
         capabilities: { resumableUploads: false },
         files: {},
+        currentUploads: {},
         foo: 'baar',
         info: { isHidden: true, message: '', type: 'info' },
         meta: {},
@@ -174,6 +175,7 @@ describe('src/Core', () => {
         bee: 'boo',
         capabilities: { resumableUploads: false },
         files: {},
+        currentUploads: {},
         foo: 'bar',
         info: { isHidden: true, message: '', type: 'info' },
         meta: {},
@@ -185,6 +187,7 @@ describe('src/Core', () => {
         bee: 'boo',
         capabilities: { resumableUploads: false },
         files: {},
+        currentUploads: {},
         foo: 'baar',
         info: { isHidden: true, message: '', type: 'info' },
         meta: {},
@@ -201,6 +204,7 @@ describe('src/Core', () => {
       expect(core.getState()).toEqual({
         capabilities: { resumableUploads: false },
         files: {},
+        currentUploads: {},
         foo: 'bar',
         info: { isHidden: true, message: '', type: 'info' },
         meta: {},
@@ -227,6 +231,7 @@ describe('src/Core', () => {
     expect(coreStateUpdateEventMock.mock.calls[1][1]).toEqual({
       capabilities: { resumableUploads: false },
       files: {},
+      currentUploads: {},
       foo: 'bar',
       info: { isHidden: true, message: '', type: 'info' },
       meta: {},
@@ -254,6 +259,7 @@ describe('src/Core', () => {
     expect(coreStateUpdateEventMock.mock.calls[0][1]).toEqual({
       capabilities: { resumableUploads: false },
       files: {},
+      currentUploads: {},
       info: { isHidden: true, message: '', type: 'info' },
       meta: {},
       plugins: {},

+ 33 - 14
src/plugins/GoldenRetriever/index.js

@@ -48,8 +48,13 @@ module.exports = class GoldenRetriever extends Plugin {
     const savedState = this.MetaDataStore.load()
 
     if (savedState) {
-      this.uppy.log('Recovered some state from Local Storage')
-      this.uppy.setState(savedState)
+      this.uppy.log('[GoldenRetriever] Recovered some state from Local Storage')
+      this.uppy.setState({
+        currentUploads: savedState.currentUploads || {},
+        files: savedState.files || {}
+      })
+
+      this.savedPluginData = savedState.pluginData
     }
   }
 
@@ -99,9 +104,18 @@ module.exports = class GoldenRetriever extends Plugin {
       this.getUploadingFiles()
     )
 
+    const pluginData = {}
+    // TODO Find a better way to do this?
+    // Other plugins can attach a restore:get-data listener that receives this callback.
+    // Plugins can then use this callback (sync) to provide data to be stored.
+    this.uppy.emit('restore:get-data', (data) => {
+      Object.assign(pluginData, data)
+    })
+
     this.MetaDataStore.save({
       currentUploads: this.uppy.state.currentUploads,
-      files: filesToSave
+      files: filesToSave,
+      pluginData: pluginData
     })
   }
 
@@ -110,11 +124,11 @@ module.exports = class GoldenRetriever extends Plugin {
       const numberOfFilesRecovered = Object.keys(blobs).length
       const numberOfFilesTryingToRecover = Object.keys(this.uppy.state.files).length
       if (numberOfFilesRecovered === numberOfFilesTryingToRecover) {
-        this.uppy.log(`Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`)
+        this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`)
         this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
         this.onBlobsLoaded(blobs)
       } else {
-        this.uppy.log('Failed to recover blobs from Service Worker, trying IndexedDB now...')
+        this.uppy.log('[GoldenRetriever] Failed to recover blobs from Service Worker, trying IndexedDB now...')
         this.loadFileBlobsFromIndexedDB()
       }
     })
@@ -125,11 +139,11 @@ module.exports = class GoldenRetriever extends Plugin {
       const numberOfFilesRecovered = Object.keys(blobs).length
 
       if (numberOfFilesRecovered > 0) {
-        this.uppy.log(`Successfully recovered ${numberOfFilesRecovered} blobs from Indexed DB!`)
+        this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Indexed DB!`)
         this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
         return this.onBlobsLoaded(blobs)
       }
-      this.uppy.log('Couldn’t recover anything from IndexedDB :(')
+      this.uppy.log('[GoldenRetriever] Couldn’t recover anything from IndexedDB :(')
     })
   }
 
@@ -154,14 +168,16 @@ module.exports = class GoldenRetriever extends Plugin {
 
       this.uppy.generatePreview(updatedFile)
     })
+
     this.uppy.setState({
       files: updatedFiles
     })
-    this.uppy.emit('restored')
+
+    this.uppy.emit('restored', this.savedPluginData)
 
     if (obsoleteBlobs.length) {
       this.deleteBlobs(obsoleteBlobs).then(() => {
-        this.uppy.log(`[GoldenRetriever] cleaned up ${obsoleteBlobs.length} old files`)
+        this.uppy.log(`[GoldenRetriever] Cleaned up ${obsoleteBlobs.length} old files`)
       })
     }
   }
@@ -184,12 +200,15 @@ module.exports = class GoldenRetriever extends Plugin {
 
     if (Object.keys(this.uppy.state.files).length > 0) {
       if (this.ServiceWorkerStore) {
-        this.uppy.log('Attempting to load files from Service Worker...')
+        this.uppy.log('[GoldenRetriever] Attempting to load files from Service Worker...')
         this.loadFileBlobsFromServiceWorker()
       } else {
-        this.uppy.log('Attempting to load files from Indexed DB...')
+        this.uppy.log('[GoldenRetriever] Attempting to load files from Indexed DB...')
         this.loadFileBlobsFromIndexedDB()
       }
+    } else {
+      this.uppy.log('[GoldenRetriever] No files need to be loaded, only restoring processing state...')
+      this.onBlobsLoaded([])
     }
 
     this.uppy.on('file-added', (file) => {
@@ -197,13 +216,13 @@ module.exports = class GoldenRetriever extends Plugin {
 
       if (this.ServiceWorkerStore) {
         this.ServiceWorkerStore.put(file).catch((err) => {
-          this.uppy.log('Could not store file', 'error')
+          this.uppy.log('[GoldenRetriever] Could not store file', 'error')
           this.uppy.log(err)
         })
       }
 
       this.IndexedDBStore.put(file).catch((err) => {
-        this.uppy.log('Could not store file', 'error')
+        this.uppy.log('[GoldenRetriever] Could not store file', 'error')
         this.uppy.log(err)
       })
     })
@@ -216,7 +235,7 @@ module.exports = class GoldenRetriever extends Plugin {
     this.uppy.on('complete', ({ successful }) => {
       const fileIDs = successful.map((file) => file.id)
       this.deleteBlobs(fileIDs).then(() => {
-        this.uppy.log(`[GoldenRetriever] removed ${successful.length} files that finished uploading`)
+        this.uppy.log(`[GoldenRetriever] Removed ${successful.length} files that finished uploading`)
       })
     })
 

+ 3 - 7
src/plugins/StatusBar/StatusBar.js

@@ -15,10 +15,6 @@ const STATE_POSTPROCESSING = 'postprocessing'
 const STATE_COMPLETE = 'complete'
 
 function getUploadingState (props, files) {
-  // if (props.error) {
-  //   return STATE_ERROR
-  // }
-
   if (props.isAllErrored) {
     return STATE_ERROR
   }
@@ -198,9 +194,9 @@ const ProgressBarError = ({ error, retryAll, i18n }) => {
     <div class="UppyStatusBar-content" role="alert">
         <strong>${i18n('uploadFailed')}.</strong>
         <span>${i18n('pleasePressRetry')}</span>
-        <span class="UppyStatusBar-details" 
-              data-balloon="${error}" 
-              data-balloon-pos="up" 
+        <span class="UppyStatusBar-details"
+              data-balloon="${error}"
+              data-balloon-pos="up"
               data-balloon-length="large">?</span>
       </div>
   `

+ 247 - 13
src/plugins/Transloadit/index.js

@@ -3,6 +3,14 @@ const Plugin = require('../../core/Plugin')
 const Client = require('./Client')
 const StatusSocket = require('./Socket')
 
+function defaultGetAssemblyOptions (file, options) {
+  return {
+    params: options.params,
+    signature: options.signature,
+    fields: options.fields
+  }
+}
+
 /**
  * Upload files to Transloadit using Tus.
  */
@@ -29,13 +37,7 @@ module.exports = class Transloadit extends Plugin {
       signature: null,
       params: null,
       fields: {},
-      getAssemblyOptions (file, options) {
-        return {
-          params: options.params,
-          signature: options.signature,
-          fields: options.fields
-        }
-      },
+      getAssemblyOptions: defaultGetAssemblyOptions,
       locale: defaultLocale
     }
 
@@ -50,6 +52,8 @@ module.exports = class Transloadit extends Plugin {
     this.prepareUpload = this.prepareUpload.bind(this)
     this.afterUpload = this.afterUpload.bind(this)
     this.onFileUploadURLAvailable = this.onFileUploadURLAvailable.bind(this)
+    this.onRestored = this.onRestored.bind(this)
+    this.getPersistentData = this.getPersistentData.bind(this)
 
     if (this.opts.params) {
       this.validateParams(this.opts.params)
@@ -120,7 +124,7 @@ module.exports = class Transloadit extends Plugin {
   createAssembly (fileIDs, uploadID, options) {
     const pluginOptions = this.opts
 
-    this.uppy.log('Transloadit: create assembly')
+    this.uppy.log('[Transloadit] create assembly')
 
     return this.client.createAssembly({
       params: options.params,
@@ -187,7 +191,7 @@ module.exports = class Transloadit extends Plugin {
       return this.connectSocket(assembly)
         .then(() => assembly)
     }).then((assembly) => {
-      this.uppy.log('Transloadit: Created assembly')
+      this.uppy.log('[Transloadit] Created assembly')
       return assembly
     }).catch((err) => {
       this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
@@ -237,9 +241,20 @@ module.exports = class Transloadit extends Plugin {
       if (!files.hasOwnProperty(id)) {
         continue
       }
+      // Completed file upload.
       if (files[id].uploadURL === uploadedFile.tus_upload_url) {
         return files[id]
       }
+      // In-progress file upload.
+      if (files[id].tus && files[id].tus.uploadUrl === uploadedFile.tus_upload_url) {
+        return files[id]
+      }
+      if (!uploadedFile.is_tus_file) {
+        // Fingers-crossed check for non-tus uploads, eg imported from S3.
+        if (files[id].name === uploadedFile.name && files[id].size === uploadedFile.size) {
+          return files[id]
+        }
+      }
     }
   }
 
@@ -249,6 +264,7 @@ module.exports = class Transloadit extends Plugin {
     this.setPluginState({
       files: Object.assign({}, state.files, {
         [uploadedFile.id]: {
+          assembly: assemblyId,
           id: file.id,
           uploadedFile
         }
@@ -263,8 +279,15 @@ module.exports = class Transloadit extends Plugin {
     // The `file` may not exist if an import robot was used instead of a file upload.
     result.localId = file ? file.id : null
 
+    const entry = {
+      result,
+      stepName,
+      id: result.id,
+      assembly: assemblyId
+    }
+
     this.setPluginState({
-      results: state.results.concat(result)
+      results: [...state.results, entry]
     })
     this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assemblyId))
   }
@@ -281,6 +304,200 @@ module.exports = class Transloadit extends Plugin {
     })
   }
 
+  getPersistentData (setData) {
+    const state = this.getPluginState()
+    const assemblies = state.assemblies
+    const uploadsAssemblies = state.uploadsAssemblies
+    const uploads = Object.keys(state.files)
+    const results = state.results.map((result) => result.id)
+
+    setData({
+      [this.id]: {
+        assemblies,
+        uploadsAssemblies,
+        uploads,
+        results
+      }
+    })
+  }
+
+  /**
+   * Emit the necessary events that must have occured to get from the `prevState`,
+   * to the current state.
+   * For completed uploads, `transloadit:upload` is emitted.
+   * For new results, `transloadit:result` is emitted.
+   * For completed or errored assemblies, `transloadit:complete` or `transloadit:assembly-error` is emitted.
+   */
+  emitEventsDiff (prevState) {
+    const opts = this.opts
+    const state = this.getPluginState()
+
+    const emitMissedEvents = () => {
+      // Emit events for completed uploads and completed results
+      // that we've missed while we were away.
+      const newUploads = Object.keys(state.files).filter((fileID) => {
+        return !prevState.files.hasOwnProperty(fileID)
+      }).map((fileID) => state.files[fileID])
+      const newResults = state.results.filter((result) => {
+        return !prevState.results.some((prev) => prev.id === result.id)
+      })
+
+      this.uppy.log('[Transloadit] New fully uploaded files since restore:')
+      this.uppy.log(newUploads)
+      newUploads.forEach(({ assembly, uploadedFile }) => {
+        this.uppy.log(`[Transloadit]  emitting transloadit:upload ${uploadedFile.id}`)
+        this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assembly))
+      })
+      this.uppy.log('[Transloadit] New results since restore:')
+      this.uppy.log(newResults)
+      newResults.forEach(({ assembly, stepName, result, id }) => {
+        this.uppy.log(`[Transloadit]  emitting transloadit:result ${stepName}, ${id}`)
+        this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assembly))
+      })
+
+      const newAssemblies = state.assemblies
+      const previousAssemblies = prevState.assemblies
+      this.uppy.log('[Transloadit] Current assembly status after restore')
+      this.uppy.log(newAssemblies)
+      this.uppy.log('[Transloadit] Assembly status before restore')
+      this.uppy.log(previousAssemblies)
+      Object.keys(newAssemblies).forEach((assemblyId) => {
+        const oldAssembly = previousAssemblies[assemblyId]
+        diffAssemblyStatus(oldAssembly, newAssemblies[assemblyId])
+      })
+    }
+
+    // Emit events for assemblies that have completed or errored while we were away.
+    const diffAssemblyStatus = (prev, next) => {
+      this.uppy.log('[Transloadit] Diff assemblies')
+      this.uppy.log(prev)
+      this.uppy.log(next)
+
+      if (opts.waitForEncoding && next.ok === 'ASSEMBLY_COMPLETED' && prev.ok !== 'ASSEMBLY_COMPLETED') {
+        this.uppy.log(`[Transloadit]  Emitting transloadit:complete for ${next.assembly_id}`)
+        this.uppy.log(next)
+        this.uppy.emit('transloadit:complete', next)
+      } else if (opts.waitForMetadata && next.upload_meta_data_extracted && !prev.upload_meta_data_extracted) {
+        this.uppy.log(`[Transloadit]  Emitting transloadit:complete after metadata extraction for ${next.assembly_id}`)
+        this.uppy.log(next)
+        this.uppy.emit('transloadit:complete', next)
+      }
+
+      if (next.error && !prev.error) {
+        this.uppy.log(`[Transloadit]  !!! Emitting transloadit:assembly-error for ${next.assembly_id}`)
+        this.uppy.log(next)
+        this.uppy.emit('transloadit:assembly-error', next, new Error(next.message))
+      }
+    }
+
+    emitMissedEvents()
+  }
+
+  onRestored (pluginData) {
+    const savedState = pluginData && pluginData[this.id] ? pluginData[this.id] : {}
+    const knownUploads = savedState.files || []
+    const knownResults = savedState.results || []
+    const previousAssemblies = savedState.assemblies || {}
+    const uploadsAssemblies = savedState.uploadsAssemblies || {}
+
+    if (Object.keys(uploadsAssemblies).length === 0) {
+      // Nothing to restore.
+      return
+    }
+
+    // Fetch up-to-date assembly statuses.
+    const loadAssemblies = () => {
+      const assemblyIDs = []
+      Object.keys(uploadsAssemblies).forEach((uploadID) => {
+        assemblyIDs.push(...uploadsAssemblies[uploadID])
+      })
+
+      return Promise.all(
+        assemblyIDs.map((assemblyID) => {
+          const url = `https://api2.transloadit.com/assemblies/${assemblyID}`
+          return this.client.getAssemblyStatus(url)
+        })
+      )
+    }
+
+    const reconnectSockets = (assemblies) => {
+      return Promise.all(assemblies.map((assembly) => {
+        // No need to connect to the socket if the assembly has completed by now.
+        if (assembly.ok === 'ASSEMBLY_COMPLETE') {
+          return null
+        }
+        return this.connectSocket(assembly)
+      }))
+    }
+
+    // Convert loaded assembly statuses to a Transloadit plugin state object.
+    const restoreState = (assemblies) => {
+      const assembliesById = {}
+      const files = {}
+      const results = []
+      assemblies.forEach((assembly) => {
+        assembliesById[assembly.assembly_id] = assembly
+
+        assembly.uploads.forEach((uploadedFile) => {
+          const file = this.findFile(uploadedFile)
+          files[uploadedFile.id] = {
+            id: file.id,
+            assembly: assembly.assembly_id,
+            uploadedFile
+          }
+        })
+
+        const state = this.getPluginState()
+        Object.keys(assembly.results).forEach((stepName) => {
+          assembly.results[stepName].forEach((result) => {
+            const file = state.files[result.original_id]
+            result.localId = file ? file.id : null
+            results.push({
+              id: result.id,
+              result,
+              stepName,
+              assembly: assembly.assembly_id
+            })
+          })
+        })
+      })
+
+      this.setPluginState({
+        assemblies: assembliesById,
+        files: files,
+        results: results,
+        uploadsAssemblies: uploadsAssemblies
+      })
+    }
+
+    // Restore all assembly state.
+    this.restored = Promise.resolve()
+      .then(loadAssemblies)
+      .then((assemblies) => {
+        restoreState(assemblies)
+        return reconnectSockets(assemblies)
+      })
+      .then(() => {
+        // Return a callback that will be called by `afterUpload`
+        // once it has attached event listeners etc.
+        const newState = this.getPluginState()
+        const previousFiles = {}
+        knownUploads.forEach((id) => {
+          previousFiles[id] = newState.files[id]
+        })
+        return () => this.emitEventsDiff({
+          assemblies: previousAssemblies,
+          files: previousFiles,
+          results: newState.results.filter(({ id }) => knownResults.indexOf(id) !== -1),
+          uploadsAssemblies
+        })
+      })
+
+    this.restored.then(() => {
+      this.restored = null
+    })
+  }
+
   connectSocket (assembly) {
     const socket = new StatusSocket(
       assembly.websocket_url,
@@ -304,7 +521,6 @@ module.exports = class Transloadit extends Plugin {
     } else if (this.opts.waitForMetadata) {
       socket.on('metadata', () => {
         this.onAssemblyFinished(assembly.assembly_ssl_url)
-        this.uppy.emit('transloadit:complete', assembly)
       })
     }
 
@@ -312,7 +528,7 @@ module.exports = class Transloadit extends Plugin {
       socket.on('connect', resolve)
       socket.on('error', reject)
     }).then(() => {
-      this.uppy.log('Transloadit: Socket is ready')
+      this.uppy.log('[Transloadit] Socket is ready')
     })
   }
 
@@ -374,6 +590,16 @@ module.exports = class Transloadit extends Plugin {
     fileIDs = fileIDs.filter((file) => !file.error)
 
     const state = this.getPluginState()
+
+    // If we're still restoring state, wait for that to be done.
+    if (this.restored) {
+      return this.restored.then((emitMissedEvents) => {
+        const promise = this.afterUpload(fileIDs, uploadID)
+        emitMissedEvents()
+        return promise
+      })
+    }
+
     const assemblyIDs = state.uploadsAssemblies[uploadID]
 
     // If we don't have to wait for encoding metadata or results, we can close
@@ -405,8 +631,10 @@ module.exports = class Transloadit extends Plugin {
       const onAssemblyFinished = (assembly) => {
         // An assembly for a different upload just finished. We can ignore it.
         if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
+          this.uppy.log(`[Transloadit] afterUpload(): Ignoring finished assembly ${assembly.assembly_id}`)
           return
         }
+        this.uppy.log(`[Transloadit] afterUpload(): Got assembly finish ${assembly.assembly_id}`)
 
         // TODO set the `file.uploadURL` to a result?
         // We will probably need an option here so the plugin user can tell us
@@ -421,10 +649,13 @@ module.exports = class Transloadit extends Plugin {
       }
 
       const onAssemblyError = (assembly, error) => {
-        // An assembly for a different upload just finished. We can ignore it.
+        // An assembly for a different upload just errored. We can ignore it.
         if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
+          this.uppy.log(`[Transloadit] afterUpload(): Ignoring errored assembly ${assembly.assembly_id}`)
           return
         }
+        this.uppy.log(`[Transloadit] afterUpload(): Got assembly error ${assembly.assembly_id}`)
+        this.uppy.log(error)
 
         // Clear postprocessing state for all our files.
         const files = this.getAssemblyFiles(assembly.assembly_id)
@@ -486,6 +717,9 @@ module.exports = class Transloadit extends Plugin {
       this.uppy.on('upload-success', this.onFileUploadURLAvailable)
     }
 
+    this.uppy.on('restore:get-data', this.getPersistentData)
+    this.uppy.on('restored', this.onRestored)
+
     this.setPluginState({
       // Contains assembly status objects, indexed by their ID.
       assemblies: {},