storageRequest.js (8140B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2009 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 27 /** 28 * Transfer request for storage sync 29 * 30 * @param {Object} options 31 * @param {String} options.type 32 * @param {Integer} options.libraryID 33 * @param {String} options.name - Identifier for request (e.g., "[libraryID]/[key]") 34 * @param {Function|Function[]} [options.onStart] 35 * @param {Function|Function[]} [options.onProgress] 36 * @param {Function|Function[]} [options.onStop] 37 */ 38 Zotero.Sync.Storage.Request = function (options) { 39 if (!options.type) throw new Error("type must be provided"); 40 if (!options.libraryID) throw new Error("libraryID must be provided"); 41 if (!options.name) throw new Error("name must be provided"); 42 ['type', 'libraryID', 'name'].forEach(x => this[x] = options[x]); 43 44 Zotero.debug(`Initializing ${this.type} request ${this.name}`); 45 46 this.callbacks = ['onStart', 'onProgress', 'onStop']; 47 48 this.Type = Zotero.Utilities.capitalize(this.type); 49 this.channel = null; 50 this.queue = null; 51 this.progress = 0; 52 this.progressMax = 0; 53 54 this._deferred = Zotero.Promise.defer(); 55 this._running = false; 56 this._stopping = false; 57 this._percentage = 0; 58 this._remaining = null; 59 this._maxSize = null; 60 this._finished = false; 61 62 for (let name of this.callbacks) { 63 if (!options[name]) continue; 64 this['_' + name] = Array.isArray(options[name]) ? options[name] : [options[name]]; 65 } 66 } 67 68 69 Zotero.Sync.Storage.Request.prototype.setMaxSize = function (size) { 70 this._maxSize = size; 71 }; 72 73 74 /** 75 * Add callbacks from another request to this request 76 */ 77 Zotero.Sync.Storage.Request.prototype.importCallbacks = function (request) { 78 for (let name of this.callbacks) { 79 name = '_' + name; 80 if (request[name]) { 81 // If no handlers for this event, add them all 82 if (!this[name]) { 83 this[name] = request[name]; 84 continue; 85 } 86 // Otherwise add functions that don't already exist 87 var add = true; 88 for (let newFunc of request[name]) { 89 for (let currentFunc of this[name]) { 90 if (newFunc.toString() === currentFunc.toString()) { 91 Zotero.debug("Callback already exists in request -- not importing"); 92 add = false; 93 break; 94 } 95 } 96 if (add) { 97 this[name].push(newFunc); 98 } 99 } 100 } 101 } 102 } 103 104 105 Zotero.Sync.Storage.Request.prototype.__defineGetter__('percentage', function () { 106 if (this._finished) { 107 return 100; 108 } 109 110 if (this.progressMax == 0) { 111 return 0; 112 } 113 114 var percentage = Math.round((this.progress / this.progressMax) * 100); 115 if (percentage < this._percentage) { 116 Zotero.debug(percentage + " is less than last percentage of " 117 + this._percentage + " for request " + this.name, 2); 118 Zotero.debug(this.progress); 119 Zotero.debug(this.progressMax); 120 percentage = this._percentage; 121 } 122 else if (percentage > 100) { 123 Zotero.debug(percentage + " is greater than 100 for " 124 + "request " + this.name, 2); 125 Zotero.debug(this.progress); 126 Zotero.debug(this.progressMax); 127 percentage = 100; 128 } 129 else { 130 this._percentage = percentage; 131 } 132 //Zotero.debug("Request '" + this.name + "' percentage is " + percentage); 133 return percentage; 134 }); 135 136 137 Zotero.Sync.Storage.Request.prototype.__defineGetter__('remaining', function () { 138 if (this._finished) { 139 return 0; 140 } 141 142 if (!this.progressMax) { 143 if (this.type == 'upload' && this._maxSize) { 144 return Math.round(Zotero.Sync.Storage.compressionTracker.ratio * this._maxSize); 145 } 146 147 //Zotero.debug("Remaining not yet available for request '" + this.name + "'"); 148 return 0; 149 } 150 151 var remaining = this.progressMax - this.progress; 152 if (this._remaining === null) { 153 this._remaining = remaining; 154 } 155 else if (remaining > this._remaining) { 156 Zotero.debug(remaining + " is greater than the last remaining amount of " 157 + this._remaining + " for request " + this.name); 158 remaining = this._remaining; 159 } 160 else if (remaining < 0) { 161 Zotero.debug(remaining + " is less than 0 for request " + this.name); 162 } 163 else { 164 this._remaining = remaining; 165 } 166 //Zotero.debug("Request '" + this.name + "' remaining is " + remaining); 167 return remaining; 168 }); 169 170 171 Zotero.Sync.Storage.Request.prototype.setChannel = function (channel) { 172 this.channel = channel; 173 } 174 175 176 Zotero.Sync.Storage.Request.prototype.start = Zotero.Promise.coroutine(function* () { 177 Zotero.debug("Starting " + this.type + " request " + this.name); 178 179 if (this._running) { 180 throw new Error(this.type + " request " + this.name + " already running"); 181 } 182 183 if (!this._onStart) { 184 throw new Error("onStart not provided -- nothing to do!"); 185 } 186 187 this._running = true; 188 189 // this._onStart is an array of promises for objects of result flags, which are combined 190 // into a single object here 191 // 192 // The main sync logic is triggered here. 193 try { 194 var results = yield Zotero.Promise.all(this._onStart.map(f => f(this))); 195 196 var result = new Zotero.Sync.Storage.Result; 197 result.updateFromResults(results); 198 199 Zotero.debug(this.Type + " request " + this.name + " finished"); 200 Zotero.debug(result + ""); 201 202 return result; 203 } 204 catch (e) { 205 Zotero.logError(this.Type + " request " + this.name + " failed"); 206 throw e; 207 } 208 finally { 209 this._finished = true; 210 this._running = false; 211 212 Zotero.Sync.Storage.setItemDownloadPercentage(this.name, false); 213 214 if (this._onStop) { 215 this._onStop.forEach(x => x()); 216 } 217 } 218 }); 219 220 221 Zotero.Sync.Storage.Request.prototype.isRunning = function () { 222 return this._running; 223 } 224 225 226 Zotero.Sync.Storage.Request.prototype.isFinished = function () { 227 return this._finished; 228 } 229 230 231 /** 232 * Update counters for given request 233 * 234 * Also updates progress meter 235 * 236 * @param {Integer} progress Progress so far 237 * (usually bytes transferred) 238 * @param {Integer} progressMax Max progress value for this request 239 * (usually total bytes) 240 */ 241 Zotero.Sync.Storage.Request.prototype.onProgress = function (progress, progressMax) { 242 //Zotero.debug(progress + "/" + progressMax + " for request " + this.name); 243 244 if (!this._running) { 245 Zotero.debug("Trying to update finished request " + this.name + " in " 246 + "Zotero.Sync.Storage.Request.onProgress() " 247 + "(" + progress + "/" + progressMax + ")", 2); 248 return; 249 } 250 251 // Workaround for invalid progress values (possibly related to 252 // https://bugzilla.mozilla.org/show_bug.cgi?id=451991 and fixed in 3.1) 253 if (progress < this.progress) { 254 Zotero.debug("Invalid progress for request '" 255 + this.name + "' (" + progress + " < " + this.progress + ")"); 256 return; 257 } 258 259 if (progressMax != this.progressMax) { 260 Zotero.debug("progressMax has changed from " + this.progressMax 261 + " to " + progressMax + " for request '" + this.name + "'", 2); 262 } 263 264 this.progress = progress; 265 this.progressMax = progressMax; 266 267 if (this.type == 'download') { 268 Zotero.Sync.Storage.setItemDownloadPercentage(this.name, this.percentage); 269 } 270 271 if (this._onProgress) { 272 for (let f of this._onProgress) { 273 f(progress, progressMax); 274 } 275 } 276 } 277 278 279 /** 280 * Stop the request's underlying network request, if there is one 281 */ 282 Zotero.Sync.Storage.Request.prototype.stop = function (force) { 283 if (this.channel && this.channel.isPending()) { 284 this._stopping = true; 285 286 try { 287 Zotero.debug(`Stopping ${this.type} request '${this.name} '`); 288 this.channel.cancel(0x804b0002); // NS_BINDING_ABORTED 289 } 290 catch (e) { 291 Zotero.debug(e, 1); 292 } 293 } 294 }