Jelajahi Sumber

@uppy/companion: fix double tus uploads (#4816)

* fix double uploads

fixes #4815
also improve error logging when canceling

* don't close socket when pausing

because it prevented us from canceling an upload that is paused
note that we here sacrifice the ability to pause uploads and have other uploads take over the paused upload's rate limit queue spot.
this is sacrificed because we will soon remove the ability to pause/resume uploads anyways

* Update packages/@uppy/companion/src/server/Uploader.js

Co-authored-by: Merlijn Vos <merlijn@soverin.net>

* Revert "don't close socket when pausing"

This reverts commit e4dbe77d2fd8912beebd695d9d7cf80a3ca924d6.

---------

Co-authored-by: Merlijn Vos <merlijn@soverin.net>
Mikael Finstad 1 tahun lalu
induk
melakukan
df36e12fb7
1 mengubah file dengan 34 tambahan dan 23 penghapusan
  1. 34 23
      packages/@uppy/companion/src/server/Uploader.js

+ 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()
     })
     })
   }
   }