Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 'use strict'; | |
| 6 | |
| 7 /** | |
| 8 * @constructor | |
| 9 * | |
| 10 * @param {!RecordStorage} storage | |
| 11 */ | |
| 12 function ImportHistory(storage) { | |
| 13 | |
| 14 /** @private {!RecordStorage} */ | |
| 15 this.storage_ = storage; | |
| 16 | |
| 17 /** @private {!Object.<string, !Array.<string>>} */ | |
| 18 this.history_ = {}; | |
| 19 }; | |
|
mtomasz
2014/10/29 01:10:41
nit: ; seems redundant here.
Steve McKay
2014/10/29 16:57:21
Done.
| |
| 20 | |
| 21 /** | |
| 22 * Prints an error to the console. | |
| 23 * | |
| 24 * @param {!Error} error | |
| 25 * @private | |
| 26 */ | |
| 27 ImportHistory.handleError_ = function(error) { | |
| 28 console.error(error.stack || error); | |
| 29 }; | |
| 30 | |
| 31 /** | |
| 32 * Use this factory method to get a fully ready instance of ImportHistory. | |
| 33 * | |
| 34 * @param {!RecordStorage} storage | |
| 35 * | |
| 36 * @return {!Promise.<!ImportHistory>} Resolves when history instance is ready. | |
| 37 */ | |
| 38 ImportHistory.load = function(storage) { | |
| 39 var history = new ImportHistory(storage); | |
| 40 return history.reload().then( | |
| 41 function() { | |
| 42 return history; | |
| 43 }); | |
| 44 }; | |
| 45 | |
| 46 /** | |
| 47 * Reloads history from disk. Should be called when the file | |
| 48 * is synced. | |
| 49 * | |
| 50 * @return {!Promise} Resolves when history has been loaded. | |
| 51 */ | |
| 52 ImportHistory.prototype.reload = function() { | |
| 53 this.history_ = {}; | |
| 54 return this.storage_.readAll() | |
| 55 .then( | |
| 56 function(entries) { | |
| 57 entries.forEach( | |
| 58 function(entry) { | |
| 59 this.updateHistoryRecord_(entry[0], entry[1]); | |
| 60 }.bind(this)); | |
| 61 }.bind(this)) | |
| 62 .catch(ImportHistory.handleError_); | |
| 63 }; | |
| 64 | |
| 65 /** | |
| 66 * Adds a history entry to the in-memory history model. | |
| 67 * @param {string} key | |
| 68 * @param {string} destination | |
| 69 */ | |
| 70 ImportHistory.prototype.updateHistoryRecord_ = function(key, destination) { | |
| 71 if (key in this.history_) { | |
| 72 this.history_[key].push(destination); | |
| 73 } else { | |
| 74 this.history_[key] = [destination]; | |
| 75 } | |
| 76 }; | |
| 77 | |
| 78 /** | |
| 79 * @param {!FileEntry} entry | |
| 80 * @param {string} destination | |
| 81 * @return {!Promise.<boolean>} Settles with true if the FileEntry | |
| 82 * was previously imported to the specified destination. | |
| 83 */ | |
| 84 ImportHistory.prototype.wasImported = function(entry, destination) { | |
| 85 return this.createKey_(entry) | |
| 86 .then( | |
| 87 function(key) { | |
| 88 return this.getDestinations_(key).indexOf(destination) >= 0; | |
| 89 }.bind(this)) | |
| 90 .catch(ImportHistory.handleError_); | |
| 91 }; | |
| 92 | |
| 93 /** | |
| 94 * @param {!FileEntry} entry | |
| 95 * @param {string} destination | |
| 96 * @return {!Promise.<boolean>} Settles with true if the FileEntry | |
| 97 * was previously imported to the specified destination. | |
| 98 */ | |
| 99 ImportHistory.prototype.markImported = function(entry, destination) { | |
| 100 return this.createKey_(entry) | |
| 101 .then(this.addDestination_.bind(this, destination)) | |
| 102 .catch(ImportHistory.handleError_); | |
| 103 }; | |
| 104 | |
| 105 /** | |
| 106 * @param {string} key | |
| 107 * @return {!Array.<string>} The list of previously noted | |
| 108 * destinations, or an empty array, if none. | |
| 109 */ | |
| 110 ImportHistory.prototype.getDestinations_ = function(key) { | |
| 111 return key in this.history_ ? this.history_[key] : []; | |
| 112 }; | |
| 113 | |
| 114 /** | |
| 115 * @param {string} destination | |
| 116 * @param {string} key | |
| 117 * @return {!Promise} | |
| 118 */ | |
| 119 ImportHistory.prototype.addDestination_ = function(destination, key) { | |
| 120 this.updateHistoryRecord_(key, destination); | |
| 121 return this.storage_.write([key, destination]); | |
| 122 }; | |
| 123 | |
| 124 /** | |
| 125 * @param {!FileEntry} entry | |
| 126 * @return {!Promise.<string>} Settles with a the key is available. | |
| 127 */ | |
| 128 ImportHistory.prototype.createKey_ = function(fileEntry) { | |
| 129 var entry = new FileEntryWrapper(fileEntry); | |
| 130 return new Promise( | |
| 131 function(resolve, reject) { | |
| 132 entry.getMetadata() | |
| 133 .then( | |
| 134 function(metadata) { | |
| 135 if (!('modificationTime' in metadata)) { | |
| 136 reject('File entry missing "modificationTime" field.'); | |
| 137 } else if (!('size' in metadata)) { | |
| 138 reject('File entry missing "size" field.'); | |
| 139 } else { | |
| 140 resolve( | |
| 141 metadata['modificationTime'] + '_' + metadata['size']); | |
| 142 } | |
| 143 }.bind(this)); | |
| 144 }.bind(this)); | |
| 145 }; | |
| 146 | |
| 147 /** | |
| 148 * An simple record storage mechanism. | |
| 149 * | |
| 150 * @interface | |
| 151 */ | |
| 152 function RecordStorage(entry) {}; | |
| 153 | |
| 154 /** | |
| 155 * Adds a new record. | |
| 156 * | |
| 157 * @param {!Array.<*>} record | |
| 158 * @return {!Promise} Resolves when record is added. | |
| 159 */ | |
| 160 RecordStorage.prototype.write; | |
|
mtomasz
2014/10/29 01:10:41
Will this compile? We used to write an empty metho
Steve McKay
2014/10/29 16:57:21
This is purely in anticipation of Closure compilat
| |
| 161 | |
| 162 /** | |
| 163 * Reads all records. | |
| 164 * | |
| 165 * @return {!Promise.<!Array.<!Array.<*>>>} | |
| 166 */ | |
| 167 RecordStorage.prototype.readAll; | |
| 168 | |
| 169 /** | |
| 170 * A {@code RecordStore} that persists data in a {@code FileEntry}. | |
| 171 * | |
| 172 * @param {!FileEntry} entry | |
| 173 * | |
| 174 * @constructor | |
| 175 * @implements {RecordStorage} | |
| 176 */ | |
| 177 function FileEntryRecordStorage(entry) { | |
| 178 /** @private {!FileEntryWrapper} */ | |
| 179 this.entry_ = new FileEntryWrapper(entry); | |
| 180 }; | |
| 181 | |
| 182 /** | |
| 183 * Prints an error to the console. | |
| 184 * | |
| 185 * @param {!Error} error | |
| 186 * @private | |
| 187 */ | |
| 188 FileEntryRecordStorage.handleError_ = function(error) { | |
| 189 console.error(error.stack || error); | |
| 190 }; | |
| 191 | |
| 192 /** @override */ | |
| 193 FileEntryRecordStorage.prototype.write = function(record) { | |
| 194 // TODO(smckay): should we make an effort to reuse a file writer? | |
| 195 return this.entry_.createWriter() | |
| 196 .then(this.writeRecord_.bind(this, record)) | |
| 197 .catch(FileEntryRecordStorage.handleError_); | |
| 198 }; | |
| 199 | |
| 200 /** | |
| 201 * Appends a new record to the end of the file. | |
| 202 * | |
| 203 * @param {!Object} record | |
| 204 * @param {!FileWriter} writer | |
| 205 * @return {!Promise} Resolves when write is complete. | |
| 206 * @private | |
| 207 */ | |
| 208 FileEntryRecordStorage.prototype.writeRecord_ = function(record, writer) { | |
| 209 return new Promise( | |
| 210 function(resolve, reject) { | |
| 211 var blob = new Blob( | |
| 212 [JSON.stringify(record) + ',\n'], | |
| 213 {type: 'application/json; charset=utf-8'}); | |
|
mtomasz
2014/10/29 01:10:41
It's not really a valid json. How about simply pla
Steve McKay
2014/10/29 16:57:21
Done. text/plain; charset=UTF-8
| |
| 214 | |
| 215 writer.onwriteend = function() { | |
| 216 resolve(); | |
| 217 }; | |
| 218 writer.onerror = function() { | |
| 219 reject(); | |
| 220 }; | |
| 221 | |
| 222 writer.seek(writer.length); | |
| 223 writer.write(blob); | |
| 224 }.bind(this)); | |
| 225 }; | |
| 226 | |
| 227 /** @override */ | |
| 228 FileEntryRecordStorage.prototype.readAll = function() { | |
| 229 return this.entry_.file() | |
| 230 .then(this.readFileAsText_.bind(this), function() { return '';}) | |
|
mtomasz
2014/10/29 01:10:41
nit: space after ; missing.
Steve McKay
2014/10/29 16:57:21
Done.
| |
| 231 .then(this.parse_.bind(this)) | |
| 232 .then( | |
| 233 function(entries) { | |
| 234 return entries; | |
| 235 }) | |
| 236 .catch(FileEntryRecordStorage.handleError_); | |
| 237 }; | |
| 238 | |
| 239 /** | |
| 240 * Reads all lines from the entry. | |
| 241 * | |
| 242 * @param {!File} file | |
| 243 * @return {!Promise.<!Array.<string>>} | |
| 244 * @private | |
| 245 */ | |
| 246 FileEntryRecordStorage.prototype.readFileAsText_ = function(file) { | |
| 247 return new Promise( | |
| 248 function(resolve, reject) { | |
| 249 | |
| 250 var reader = new FileReader(); | |
| 251 | |
| 252 reader.onloadend = function() { | |
| 253 if (!!reader.error) { | |
| 254 FileEntryRecordStorage.handleError_(reader.error); | |
| 255 resolve(''); | |
| 256 } else { | |
| 257 resolve(reader.result); | |
| 258 } | |
| 259 }; | |
| 260 | |
| 261 reader.onerror = function(error) { | |
| 262 FileEntryRecordStorage.handleError_(error); | |
| 263 reject(e); | |
| 264 }; | |
| 265 | |
| 266 reader.readAsText(file); | |
| 267 }.bind(this)); | |
| 268 }; | |
| 269 | |
| 270 /** | |
| 271 * Parses the text. | |
| 272 * | |
| 273 * @param {string} text | |
| 274 * @return {!Promise.<!Array.<!Object>>} | |
| 275 * @private | |
| 276 */ | |
| 277 FileEntryRecordStorage.prototype.parse_ = function(text) { | |
| 278 return new Promise( | |
| 279 function(resolve, reject) { | |
| 280 if (text.length == 0) { | |
| 281 resolve([]); | |
| 282 } else { | |
| 283 // Dress up the contents of the file like an array, | |
| 284 // so the JSON object can parse it using JSON.parse. | |
| 285 // That means we need to both: | |
| 286 // 1) Strip the trailing ',\n' from the last record | |
| 287 // 2) Surround the whole string in brackets. | |
| 288 // NOTE: JSON.parse is WAY faster than parsing this | |
| 289 // ourselves in javascript. | |
| 290 var json = '[' + text.substring(0, text.length - 2) + ']'; | |
| 291 resolve(JSON.parse(json)); | |
|
mtomasz
2014/10/29 01:10:41
Date objects will not deserialize properly from js
Steve McKay
2014/10/29 16:57:22
The value is just a string. There's no need to par
| |
| 292 } | |
| 293 }.bind(this)); | |
| 294 }; | |
| 295 | |
| 296 /** | |
| 297 * An Object storage mechanism built on top of a FileEntry. | |
| 298 * | |
| 299 * @param {!FileEntry} entry | |
| 300 * | |
| 301 * @constructor | |
| 302 * @private | |
| 303 */ | |
| 304 function FileEntryWrapper(entry) { | |
| 305 /** @private {!FileEntry} */ | |
| 306 this.entry_ = entry; | |
| 307 }; | |
| 308 | |
| 309 /** | |
| 310 * A "Promisary" wrapper around entry.getWriter. | |
| 311 * @return {!Promise.<!FileWriter>} | |
| 312 */ | |
| 313 FileEntryWrapper.prototype.createWriter = function() { | |
| 314 return new Promise(this.entry_.createWriter.bind(this.entry_)); | |
| 315 }; | |
| 316 | |
| 317 /** | |
| 318 * A "Promisary" wrapper around entry.file. | |
| 319 * @return {!Promise.<!File>} | |
| 320 */ | |
| 321 FileEntryWrapper.prototype.file = function() { | |
| 322 return new Promise(this.entry_.file.bind(this.entry_)); | |
| 323 }; | |
| 324 | |
| 325 /** | |
| 326 * @return {!Promise.<!Object>} Data format not yet verified. | |
| 327 * Either a string, or an object. | |
| 328 */ | |
| 329 FileEntryWrapper.prototype.getMetadata = function() { | |
| 330 return new Promise(this.entry_.getMetadata.bind(this.entry_)); | |
| 331 }; | |
| OLD | NEW |