diff --git a/apps/files/src/mixins.js b/apps/files/src/mixins.js index 2ee3fcaf96d..605d2276fd0 100644 --- a/apps/files/src/mixins.js +++ b/apps/files/src/mixins.js @@ -40,9 +40,10 @@ export default { 'highlightedFile', 'publicLinkPassword', 'fileSortField', + 'currentFolder', 'fileSortDirectionDesc' ]), - ...mapGetters(['getToken', 'capabilities']), + ...mapGetters(['getToken', 'capabilities', 'configuration']), _sidebarOpen() { return this.highlightedFile !== null @@ -408,14 +409,27 @@ export default { } else { basePath = this.path || '' relativePath = pathUtil.join(basePath, relativePath) - promise = this.uploadQueue.add(() => - this.$client.files.putFileContents(relativePath, file, { - onProgress: progress => { - this.$_ocUpload_onProgress(progress, file) - }, - overwrite: overwrite + // FIXME: this might break if relativePath is not the currentFolder + // and is a mount point that has no chunk support + if (this.browserSupportsChunked && this.currentFolder.isChunkedUploadSupported) { + promise = this.uploadQueue.add(() => { + return this.uploadChunkedFile(file, pathUtil.dirname(relativePath), { + chunkSize: this.configuration.uploadChunkSize, + emitProgress: progress => { + this.$_ocUpload_onProgress(progress, file) + } + }) }) - ) + } else { + promise = this.uploadQueue.add(() => + this.$client.files.putFileContents(relativePath, file, { + onProgress: progress => { + this.$_ocUpload_onProgress(progress, file) + }, + overwrite: overwrite + }) + ) + } } promise diff --git a/apps/files/src/store/actions.js b/apps/files/src/store/actions.js index c0421629715..e57f4e431ea 100644 --- a/apps/files/src/store/actions.js +++ b/apps/files/src/store/actions.js @@ -93,7 +93,8 @@ function _buildFile(file) { }, isReceivedShare: function() { return this.permissions.indexOf('S') >= 0 - } + }, + isChunkedUploadSupported: !!(file.getTusSupport && file.getTusSupport()) } } diff --git a/changelog/unreleased/3345 b/changelog/unreleased/3345 new file mode 100644 index 00000000000..29fad36b633 --- /dev/null +++ b/changelog/unreleased/3345 @@ -0,0 +1,9 @@ +Enhancement: Add chunked upload with tus-js-client + +Whenever the backend server advertises TUS support, uploading files will +use TUS as well for uploading, which makes it possible to resume failed uploads. +It is also possible to optionally set a chunk size by setting a numeric value +for "uploadChunkSize" in bytes in config.json. + +https://github.com/owncloud/phoenix/issues/67 +https://github.com/owncloud/phoenix/pull/3345 diff --git a/package.json b/package.json index 58ba3457cbd..014deb556ad 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "rimraf": "^3.0.0", "start-server-and-test": "^1.9.1", "style-loader": "^1.0.0", + "tus-js-client": "^1.8.0", "url-search-params-polyfill": "^8.0.0", "vue": "^2.6.10", "vue-clipboard2": "^0.3.1", diff --git a/src/phoenix.js b/src/phoenix.js index c351613f518..dc988a867ec 100644 --- a/src/phoenix.js +++ b/src/phoenix.js @@ -36,6 +36,7 @@ import coreTranslations from '../l10n/translations.json' import MediaSource from './plugins/mediaSource.js' import PhoenixPlugin from './plugins/phoenix' +import ChunkedUpload from './plugins/upload' // --- Drag Drop ---- @@ -64,6 +65,7 @@ Vue.use(VueMeta, { // optional pluginOptions refreshOnceOnNavigation: true }) +Vue.use(ChunkedUpload) Vue.component('drag', Drag) Vue.component('drop', Drop) diff --git a/src/plugins/upload.js b/src/plugins/upload.js new file mode 100644 index 00000000000..c1b4a5052a4 --- /dev/null +++ b/src/plugins/upload.js @@ -0,0 +1,57 @@ +import tus from 'tus-js-client' + +export default { + install(Vue) { + Vue.mixin({ + computed: { + browserSupportsChunked() { + return tus.isSupported + } + }, + methods: { + uploadChunkedFile(file, path, options) { + return new Promise((resolve, reject) => { + const headers = this.$client.helpers.buildHeaders() + delete headers['OCS-APIREQUEST'] + var mtime = null + if (file.lastModifiedDate) { + mtime = file.lastModifiedDate.getTime() / 1000 + } + if (file.lastModified) { + mtime = file.lastModified / 1000 + } + const upload = new tus.Upload(file, { + endpoint: this.$client.files.getFileUrlV2(path), + headers: headers, + chunkSize: options.chunkSize || Infinity, + removeFingerprintOnSuccess: true, + retryDelays: [0, 3000, 5000, 10000, 20000], + metadata: { + filename: file.name, + filetype: file.type, + size: file.size, + mtime: mtime + }, + + onError: error => { + console.error(`Error uploading file "${file}" to "${path}"`, error) + reject(error) + }, + + onProgress: (bytesUploaded, bytesTotal) => { + options.emitProgress({ loaded: bytesUploaded, total: bytesTotal }) + }, + + onSuccess: () => { + resolve(`File ${upload.file.name} was successfully uploaded`) + } + }) + + upload.start() + return upload + }) + } + } + }) + } +} diff --git a/src/store/config.js b/src/store/config.js index 788039baac4..81e41d5974c 100644 --- a/src/store/config.js +++ b/src/store/config.js @@ -45,6 +45,7 @@ const mutations = { state.auth = config.auth state.openIdConnect = config.openIdConnect state.rootFolder = config.rootFolder === undefined ? '/' : config.rootFolder + state.uploadChunkSize = config.uploadChunkSize === undefined ? Infinity : config.uploadChunkSize state.state = config.state === undefined ? 'working' : config.state state.applications = config.applications === undefined ? [] : config.applications if (config.corrupted) state.corrupted = config.corrupted diff --git a/yarn.lock b/yarn.lock index 18a1d9ffde1..f078f90c609 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1956,6 +1956,11 @@ buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-from@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0" + integrity sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg== + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -2386,6 +2391,14 @@ colors@^1.1.2, colors@^1.3.3, colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== +combine-errors@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/combine-errors/-/combine-errors-3.0.3.tgz#f4df6740083e5703a3181110c2b10551f003da86" + integrity sha1-9N9nQAg+VwOjGBEQwrEFUfAD2oY= + dependencies: + custom-error-instance "2.1.1" + lodash.uniqby "4.5.0" + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2824,6 +2837,11 @@ cuint@^0.2.2: resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= +custom-error-instance@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/custom-error-instance/-/custom-error-instance-2.1.1.tgz#3cf6391487a6629a6247eb0ca0ce00081b7e361a" + integrity sha1-PPY5FIemYppiR+sMoM4ACBt+Nho= + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -3887,7 +3905,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@3.0.2, extend@~3.0.2: +extend@3.0.2, extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -5400,6 +5418,11 @@ join-path@^1.1.1: url-join "0.0.1" valid-url "^1" +js-base64@^2.4.9: + version "2.5.2" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209" + integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ== + js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" @@ -5839,11 +5862,36 @@ lodash._basefor@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" integrity sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI= +lodash._baseiteratee@~4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz#34a9b5543572727c3db2e78edae3c0e9e66bd102" + integrity sha1-NKm1VDVycnw9sueO2uPA6eZr0QI= + dependencies: + lodash._stringtopath "~4.8.0" + +lodash._basetostring@~4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz#9327c9dc5158866b7fa4b9d42f4638e5766dd9df" + integrity sha1-kyfJ3FFYhmt/pLnUL0Y45XZt2d8= + +lodash._baseuniq@~4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" + integrity sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg= + dependencies: + lodash._createset "~4.0.0" + lodash._root "~3.0.0" + lodash._bindcallback@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= +lodash._createset@~4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" + integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= + lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -5854,6 +5902,18 @@ lodash._isiterateecall@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw= +lodash._root@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI= + +lodash._stringtopath@~4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz#941bcf0e64266e5fc1d66fed0a6959544c576824" + integrity sha1-lBvPDmQmbl/B1m/tCmlZVExXaCQ= + dependencies: + lodash._basetostring "~4.12.0" + lodash.clone@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-3.0.3.tgz#84688c73d32b5a90ca25616963f189252a997043" @@ -5917,11 +5977,24 @@ lodash.merge@^4.6.1, lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + lodash.union@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= +lodash.uniqby@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz#a3a17bbf62eeb6240f491846e97c1c4e2a5e1e21" + integrity sha1-o6F7v2LutiQPSRhG6XwcTipeHiE= + dependencies: + lodash._baseiteratee "~4.7.0" + lodash._baseuniq "~4.6.0" + lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -7526,6 +7599,14 @@ promise@^8.0.3: dependencies: asap "~2.0.6" +proper-lockfile@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-2.0.1.tgz#159fb06193d32003f4b3691dd2ec1a634aa80d1d" + integrity sha1-FZ+wYZPTIAP0s2kd0uwaY0qoDR0= + dependencies: + graceful-fs "^4.1.2" + retry "^0.10.0" + proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -8125,6 +8206,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" + integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= + retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -9220,6 +9306,19 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tus-js-client@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/tus-js-client/-/tus-js-client-1.8.0.tgz#0402357bdaa90e9dee6f6734c24473808bff272b" + integrity sha512-qPX3TywqzxocTxUZtcS8X7Aik72SVMa0jKi4hWyfvRV+s9raVzzYGaP4MoJGaF0yOgm2+b6jXaVEHogxcJ8LGw== + dependencies: + buffer-from "^0.1.1" + combine-errors "^3.0.3" + extend "^3.0.2" + js-base64 "^2.4.9" + lodash.throttle "^4.1.1" + proper-lockfile "^2.0.1" + url-parse "^1.4.3" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"