diff --git a/ReactNativeClient/lib/WebDavApi.js b/ReactNativeClient/lib/WebDavApi.js index 84f13c180..5752aef75 100644 --- a/ReactNativeClient/lib/WebDavApi.js +++ b/ReactNativeClient/lib/WebDavApi.js @@ -161,6 +161,8 @@ class WebDavApi { let response = null; + // console.info('WebDAV', method + ' ' + path, headers, options); + if (options.source == 'file' && (method == 'POST' || method == 'PUT')) { response = await shim.uploadBlob(url, fetchOptions); } else if (options.target == 'string') { diff --git a/ReactNativeClient/lib/file-api-driver-webdav.js b/ReactNativeClient/lib/file-api-driver-webdav.js index 460d70ae1..a42a7f555 100644 --- a/ReactNativeClient/lib/file-api-driver-webdav.js +++ b/ReactNativeClient/lib/file-api-driver-webdav.js @@ -4,6 +4,8 @@ const { basicDelta } = require('lib/file-api'); const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js'); const Entities = require('html-entities').AllHtmlEntities; const html_entity_decode = (new Entities()).decode; +const { shim } = require('lib/shim'); +const { basename } = require('lib/path-utils'); class FileApiDriverWebDav { @@ -20,6 +22,7 @@ class FileApiDriverWebDav { const result = await this.api().execPropFind(path, 0, [ 'd:getlastmodified', 'd:resourcetype', + 'd:getcontentlength', // Remove this once PUT call issue is sorted out ]); const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]); @@ -34,6 +37,9 @@ class FileApiDriverWebDav { const isCollection = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:resourcetype', 0, 'd:collection', 0]); const lastModifiedString = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]); + const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0])); + if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource)); + if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(resource)); const lastModifiedDate = new Date(lastModifiedString); @@ -44,6 +50,7 @@ class FileApiDriverWebDav { created_time: lastModifiedDate.getTime(), updated_time: lastModifiedDate.getTime(), isDir: isCollection === '', + sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on. }; } @@ -229,7 +236,32 @@ class FileApiDriverWebDav { } async put(path, content, options = null) { - await this.api().exec('PUT', path, content, null, options); + // In theory, if a client doesn't complete an upload, the file will not appear in the Nextcloud app. Likewise if + // the server interrupts the upload midway, the client should receive some kind of error and try uploading the + // file again next time. At the very least the file should not appear half-uploaded on the server. In practice + // however it seems some files might end up half uploaded on the server (at least on ocloud.de) so, for now, + // instead of doing a simple PUT, we do it to a temp file on Nextcloud, then check the file size and, if it + // matches, move it its actual place (hoping the server won't mess up and only copy half of the file). + // This is innefficient so once the bug is better understood it should hopefully be possible to go back to + // using a single PUT call. + + let contentSize = 0; + if (content) contentSize = content.length; + if (options && options.path) { + const stat = await shim.fsDriver().stat(options.path); + contentSize = stat.size; + } + + const tempPath = this.fileApi_.tempDirName() + '/' + basename(path) + '_' + Date.now(); + await this.api().exec('PUT', tempPath, content, null, options); + + const stat = await this.stat(tempPath); + if (stat.sizeDONOTUSE != contentSize) { + // await this.delete(tempPath); + throw new Error('WebDAV PUT - Size check failed for ' + tempPath + ' Expected: ' + contentSize + '. Found: ' + stat.sizeDONOTUSE); + } + + await this.move(tempPath, path); } async delete(path) { @@ -241,7 +273,9 @@ class FileApiDriverWebDav { } async move(oldPath, newPath) { - throw new Error('Not implemented'); + await this.api().exec('MOVE', oldPath, null, { + 'Destination': this.api().baseUrl() + '/' + newPath, + }); } format() { diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index 5ec29fe7b..0ed87dcbe 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -12,6 +12,17 @@ class FileApi { this.driver_ = driver; this.logger_ = new Logger(); this.syncTargetId_ = null; + this.tempDirName_ = null; + this.driver_.fileApi_ = this; + } + + tempDirName() { + if (this.tempDirName_ === null) throw Error('Temp dir not set!'); + return this.tempDirName_; + } + + setTempDirName(v) { + this.tempDirName_ = v; } fsDriver() { @@ -40,9 +51,10 @@ class FileApi { } fullPath_(path) { - let output = this.baseDir_; - if (path != '') output += '/' + path; - return output; + let output = []; + if (this.baseDir_) output.push(this.baseDir_); + if (path) output.push(path); + return output.join('/'); } // DRIVER MUST RETURN PATHS RELATIVE TO `path` diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index 743f889b5..f2d9d0359 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -18,7 +18,7 @@ class Synchronizer { this.state_ = 'idle'; this.db_ = db; this.api_ = api; - //this.syncDirName_ = '.sync'; + this.syncDirName_ = '.sync'; this.resourceDirName_ = '.resource'; this.logger_ = new Logger(); this.appType_ = appType; @@ -195,7 +195,8 @@ class Synchronizer { this.logSyncOperation('starting', null, null, 'Starting synchronisation to target ' + syncTargetId + '... [' + synchronizationId + ']'); try { - //await this.api().mkdir(this.syncDirName_); + await this.api().mkdir(this.syncDirName_); + this.api().setTempDirName(this.syncDirName_); await this.api().mkdir(this.resourceDirName_); let donePaths = [];