Upstream version 11.40.271.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / background / js / import_history.js
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 /**
6  * @constructor
7  * @struct
8  *
9  * @param {!RecordStorage} storage
10  */
11 function ImportHistory(storage) {
12
13   /** @private {!RecordStorage} */
14   this.storage_ = storage;
15
16   /** @private {!Object.<string, !Array.<string>>} */
17   this.entries_ = {};
18
19   /** @private {!Promise.<!ImportHistory>} */
20   this.whenReady_ = this.refresh_();
21 }
22
23 /**
24  * Loads history from disk and merges in any previously existing entries
25  * that are not present in the newly loaded data. Should be called
26  * when the file is changed by an external source.
27  *
28  * @return {!Promise.<!ImportHistory>} Resolves when history has been refreshed.
29  * @private
30  */
31 ImportHistory.prototype.refresh_ = function() {
32   var oldEntries = this.entries_;
33   this.entries_ = {};
34   return this.storage_.readAll()
35       .then(this.updateHistoryRecords_.bind(this))
36       .then(this.mergeEntries_.bind(this, oldEntries))
37       .then(
38           /**
39            * @return {!ImportHistory}
40            * @this {ImportHistory}
41            */
42           function() {
43             return this;
44           }.bind(this));
45 };
46
47 /**
48  * Adds all entries not already present in history.
49  *
50  * @param {!Object.<string, !Array.<string>>} entries
51  * @return {!Promise.<?>} Resolves once all updates are completed.
52  * @private
53  */
54 ImportHistory.prototype.mergeEntries_ = function(entries) {
55   var promises = [];
56   Object.keys(entries).forEach(
57       /**
58        * @param {string} key
59        * @this {ImportHistory}
60        */
61       function(key) {
62         entries[key].forEach(
63             /**
64              * @param {string} key
65              * @this {ImportHistory}
66              */
67             function(destination) {
68               if (this.getDestinations_(key).indexOf(destination) >= 0) {
69                 this.updateHistoryRecord_(key, destination);
70                 promises.push(this.storage_.write([key, destination]));
71               }
72         }.bind(this));
73       }.bind(this));
74   return Promise.all(promises);
75 };
76
77 /**
78  * Reloads history from disk. Should be called when the file
79  * is changed by an external source.
80  *
81  * @return {!Promise.<!ImportHistory>} Resolves when history has been refreshed.
82  */
83 ImportHistory.prototype.refresh = function() {
84   this.whenReady_ = this.refresh_();
85   return this.whenReady_;
86 };
87
88 /**
89  * @return {!Promise.<!ImportHistory>}
90  */
91 ImportHistory.prototype.whenReady = function() {
92   return this.whenReady_;
93 };
94
95 /**
96  * Adds a history entry to the in-memory history model.
97  * @param {!Array.<!Array.<*>>} records
98  * @private
99  */
100 ImportHistory.prototype.updateHistoryRecords_ = function(records) {
101   records.forEach(
102       /**
103        * @param {!Array.<*>} entry
104        * @this {ImportHistory}
105        */
106       function(record) {
107         this.updateHistoryRecord_(record[0], record[1]);
108       }.bind(this));
109 };
110
111 /**
112  * Adds a history entry to the in-memory history model.
113  * @param {string} key
114  * @param {string} destination
115  * @private
116  */
117 ImportHistory.prototype.updateHistoryRecord_ = function(key, destination) {
118   if (key in this.entries_) {
119     this.entries_[key].push(destination);
120   } else {
121     this.entries_[key] = [destination];
122   }
123 };
124
125 /**
126  * @param {!FileEntry} entry
127  * @param {string} destination
128  * @return {!Promise.<boolean>} Resolves with true if the FileEntry
129  *     was previously imported to the specified destination.
130  */
131 ImportHistory.prototype.wasImported = function(entry, destination) {
132   return this.whenReady_
133       .then(this.createKey_.bind(this, entry))
134       .then(
135           /**
136            * @param {string} key
137            * @return {!Promise.<boolean>}
138            * @this {ImportHistory}
139            */
140           function(key) {
141             return this.getDestinations_(key).indexOf(destination) >= 0;
142           }.bind(this));
143 };
144
145 /**
146  * @param {!FileEntry} entry
147  * @param {string} destination
148  * @return {!Promise.<?>} Resolves when the operation is completed.
149  */
150 ImportHistory.prototype.markImported = function(entry, destination) {
151   return this.whenReady_
152       .then(this.createKey_.bind(this, entry))
153       .then(
154           /**
155            * @param {string} key
156            * @return {!Promise.<?>}
157            * @this {ImportHistory}
158            */
159           function(key) {
160             return this.addDestination_(destination, key);
161           }.bind(this));
162 };
163
164 /**
165  * @param {string} destination
166  * @param {string} key
167  * @return {!Promise.<?>} Resolves once the write has been completed.
168  * @private
169  */
170 ImportHistory.prototype.addDestination_ = function(destination, key) {
171   this.updateHistoryRecord_(key, destination);
172   return this.storage_.write([key, destination]);
173 };
174
175 /**
176  * @param {string} key
177  * @return {!Array.<string>} The list of previously noted
178  *     destinations, or an empty array, if none.
179  * @private
180  */
181 ImportHistory.prototype.getDestinations_ = function(key) {
182   return key in this.entries_ ? this.entries_[key] : [];
183 };
184
185 /**
186  * @param {!FileEntry} fileEntry
187  * @return {!Promise.<string>} Resolves with a the key is available.
188  * @private
189  */
190 ImportHistory.prototype.createKey_ = function(fileEntry) {
191   var entry = new PromisaryFileEntry(fileEntry);
192   return new Promise(
193       /**
194        * @param {function()} resolve
195        * @param {function()} reject
196        * @this {ImportHistory}
197        */
198       function(resolve, reject) {
199         entry.getMetadata()
200             .then(
201                 /**
202                  * @param {!Object} metadata
203                  * @return {!Promise.<string>}
204                  * @this {ImportHistory}
205                  */
206                 function(metadata) {
207                   if (!('modificationTime' in metadata)) {
208                     reject('File entry missing "modificationTime" field.');
209                   } else if (!('size' in metadata)) {
210                     reject('File entry missing "size" field.');
211                   } else {
212                     resolve(
213                       metadata['modificationTime'] + '_' + metadata['size']);
214                   }
215                 }.bind(this));
216       }.bind(this));
217 };
218
219 /**
220  * Provider of lazy loaded ImportHistory. This is the main
221  * access point for a fully prepared {@code ImportHistory} object.
222  *
223  * @interface
224  */
225 function HistoryLoader() {}
226
227 /**
228  * Instantiates an {@code ImportHistory} object and manages any
229  * necessary ongoing maintenance of the object with respect to
230  * its external dependencies.
231  *
232  * @see SynchronizedHistoryLoader for an example.
233  *
234  * @return {!Promise.<!ImportHistory>} Resolves when history instance is ready.
235  */
236 HistoryLoader.prototype.loadHistory;
237
238 /**
239  * Class responsible for lazy loading of {@code ImportHistory},
240  * and reloading when the underlying data is updated (via sync).
241  *
242  * @constructor
243  * @implements {HistoryLoader}
244  * @struct
245  *
246  * @param {!SyncFileEntryProvider} fileProvider
247  */
248 function SynchronizedHistoryLoader(fileProvider) {
249
250   /** @private {!SyncFileEntryProvider} */
251   this.fileProvider_ = fileProvider;
252
253   /** @private {!ImportHistory|undefined} */
254   this.history_;
255 }
256
257 /** @override */
258 SynchronizedHistoryLoader.prototype.loadHistory = function() {
259   if (this.history_) {
260     return this.history_.whenReady();
261   }
262
263   this.fileProvider_.addSyncListener(
264       this.onSyncedDataChanged_.bind(this));
265
266   return this.fileProvider_.getSyncFileEntry()
267       .then(
268           /**
269            * @param {!FileEntry} fileEntry
270            * @return {!Promise.<!ImportHistory>}
271            * @this {SynchronizedHistoryLoader}
272            */
273           function(fileEntry) {
274             var storage = new FileEntryRecordStorage(fileEntry);
275             var history = new ImportHistory(storage);
276             return history.refresh().then(
277                 /**
278                  * @return {!ImportHistory}
279                  * @this {SynchronizedHistoryLoader}
280                  */
281                 function() {
282                   this.history_ = history;
283                   return history;
284                 }.bind(this));
285           }.bind(this));
286 };
287
288 /**
289  * Handles file sync events, by simply reloading history. The presumption
290  * is that 99% of the time these events will basically be happening when
291  * there is no active import process.
292  *
293  * @private
294  */
295 SynchronizedHistoryLoader.prototype.onSyncedDataChanged_ = function() {
296   if (this.history_) {
297     this.history_.refresh();  // Reload history entries.
298   }
299 };
300
301 /**
302  * Factory interface for creating/accessing synced {@code FileEntry}
303  * instances and listening to sync events on those files.
304  *
305  * @interface
306  */
307 function SyncFileEntryProvider() {}
308
309 /**
310  * Adds a listener to be notified when the the FileEntry owned/managed
311  * by this class is updated via sync.
312  *
313  * @param {function()} syncListener
314  */
315 SyncFileEntryProvider.prototype.addSyncListener;
316
317 /**
318  * Provides accsess to the sync FileEntry owned/managed by this class.
319  *
320  * @return {!Promise.<!FileEntry>}
321  */
322 SyncFileEntryProvider.prototype.getSyncFileEntry;
323
324 /**
325  * Factory for synchronized files based on chrome.syncFileSystem.
326  *
327  * @constructor
328  * @implements {SyncFileEntryProvider}
329  * @struct
330  */
331 function ChromeSyncFileEntryProvider() {
332
333   /** @private {!Array.<function()>} */
334   this.syncListeners_ = [];
335
336   /** @private {!Promise.<!FileEntry>|undefined} */
337   this.fileEntryPromise_;
338 }
339
340 /** @private @const {string} */
341 ChromeSyncFileEntryProvider.FILE_NAME_ = 'import-history.data';
342
343 /**
344  * Wraps chrome.syncFileSystem.onFileStatusChanged
345  * so that we can report to our listeners when our file has changed.
346  * @private
347  */
348 ChromeSyncFileEntryProvider.prototype.monitorSyncEvents_ = function() {
349   chrome.syncFileSystem.onFileStatusChanged.addListener(
350       this.handleSyncEvent_.bind(this));
351 };
352
353 /** @override */
354 ChromeSyncFileEntryProvider.prototype.addSyncListener = function(listener) {
355   if (this.syncListeners_.indexOf(listener) === -1) {
356     this.syncListeners_.push(listener);
357   }
358 };
359
360 /** @override */
361 ChromeSyncFileEntryProvider.prototype.getSyncFileEntry = function() {
362   if (this.fileEntryPromise_) {
363     return this.fileEntryPromise_;
364   };
365
366   this.fileEntryPromise_ = this.getFileSystem_()
367       .then(
368           /**
369            * @param {!FileSystem} fileSystem
370            * @return {!Promise.<!FileEntry>}
371            * @this {ChromeSyncFileEntryProvider}
372            */
373           function(fileSystem) {
374             return this.getFileEntry_(fileSystem);
375           }.bind(this));
376
377   return this.fileEntryPromise_;
378 };
379
380 /**
381  * Wraps chrome.syncFileSystem in a Promise.
382  *
383  * @return {!Promise.<!FileSystem>}
384  * @private
385  */
386 ChromeSyncFileEntryProvider.prototype.getFileSystem_ = function() {
387   return new Promise(
388       /**
389        * @param {function()} resolve
390        * @param {function()} reject
391        * @this {ChromeSyncFileEntryProvider}
392        */
393       function(resolve, reject) {
394         chrome.syncFileSystem.requestFileSystem(
395             /**
396               * @param {FileSystem} fileSystem
397               * @this {ChromeSyncFileEntryProvider}
398               */
399             function(fileSystem) {
400               if (chrome.runtime.lastError) {
401                 reject(chrome.runtime.lastError.message);
402               } else {
403                 resolve(/** @type {!FileSystem} */ (fileSystem));
404               }
405             });
406       }.bind(this));
407 };
408
409 /**
410  * @param {!FileSystem} fileSystem
411  * @return {!Promise.<!FileEntry>}
412  * @private
413  */
414 ChromeSyncFileEntryProvider.prototype.getFileEntry_ = function(fileSystem) {
415   return new Promise(
416       /**
417        * @param {function()} resolve
418        * @param {function()} reject
419        * @this {ChromeSyncFileEntryProvider}
420        */
421       function(resolve, reject) {
422         fileSystem.root.getFile(
423             ChromeSyncFileEntryProvider.FILE_NAME_,
424             {
425               create: true,
426               exclusive: false
427             },
428             resolve,
429             reject);
430       }.bind(this));
431 };
432
433 /**
434  * Handles sync events. Checks to see if the event is for the file
435  * we track, and sync-direction, and if so, notifies syncListeners.
436  *
437  * @see https://developer.chrome.com/apps/syncFileSystem
438  *     #event-onFileStatusChanged
439  *
440  * @param {!Object} event Having a structure not unlike: {
441  *     fileEntry: Entry,
442  *     status: string,
443  *     action: (string|undefined),
444  *     direction: (string|undefined)}
445  *
446  * @private
447  */
448 ChromeSyncFileEntryProvider.prototype.handleSyncEvent_ = function(event) {
449   if (!this.fileEntryPromise_) {
450     return;
451   }
452
453   this.fileEntryPromise_.then(
454       /**
455        * @param {!FileEntry} fileEntry
456        * @this {ChromeSyncFileEntryProvider}
457        */
458       function(fileEntry) {
459         if (event['fileEntry'].fullPath !== fileEntry.fullPath) {
460           return;
461         }
462
463         if (event.direction && event.direction !== 'remote_to_local') {
464           return;
465         }
466
467         if (event.action && event.action !== 'updated') {
468           console.error(
469             'Unexpected sync event action for history file: ' + event.action);
470           return;
471         }
472
473         this.syncListeners_.forEach(
474             /**
475              * @param {function()} listener
476              * @this {ChromeSyncFileEntryProvider}
477              */
478             function(listener) {
479               // Notify by way of a promise so that it is fully asynchronous
480               // (which can rationalize testing).
481               Promise.resolve().then(listener);
482             }.bind(this));
483       }.bind(this));
484 };
485
486 /**
487  * An simple record storage mechanism.
488  *
489  * @interface
490  */
491 function RecordStorage() {}
492
493 /**
494  * Adds a new record.
495  *
496  * @param {!Array.<*>} record
497  * @return {!Promise.<?>} Resolves when record is added.
498  */
499 RecordStorage.prototype.write;
500
501 /**
502  * Reads all records.
503  *
504  * @return {!Promise.<!Array.<!Array.<*>>>}
505  */
506 RecordStorage.prototype.readAll;
507
508 /**
509  * A {@code RecordStore} that persists data in a {@code FileEntry}.
510  *
511  * @param {!FileEntry} fileEntry
512  *
513  * @constructor
514  * @implements {RecordStorage}
515  * @struct
516  */
517 function FileEntryRecordStorage(fileEntry) {
518   /** @private {!PromisaryFileEntry} */
519   this.fileEntry_ = new PromisaryFileEntry(fileEntry);
520 }
521
522 /** @override */
523 FileEntryRecordStorage.prototype.write = function(record) {
524   // TODO(smckay): should we make an effort to reuse a file writer?
525   return this.fileEntry_.createWriter()
526       .then(this.writeRecord_.bind(this, record));
527 };
528
529 /**
530  * Appends a new record to the end of the file.
531  *
532  * @param {!Object} record
533  * @param {!FileWriter} writer
534  * @return {!Promise.<?>} Resolves when write is complete.
535  * @private
536  */
537 FileEntryRecordStorage.prototype.writeRecord_ = function(record, writer) {
538   return new Promise(
539       /**
540        * @param {function()} resolve
541        * @param {function()} reject
542        * @this {FileEntryRecordStorage}
543        */
544       function(resolve, reject) {
545         var blob = new Blob(
546             [JSON.stringify(record) + ',\n'],
547             {type: 'text/plain; charset=UTF-8'});
548
549         writer.onwriteend = resolve;
550         writer.onerror = reject;
551
552         writer.seek(writer.length);
553         writer.write(blob);
554       }.bind(this));
555 };
556
557 /** @override */
558 FileEntryRecordStorage.prototype.readAll = function() {
559   return this.fileEntry_.file()
560       .then(
561           this.readFileAsText_.bind(this),
562           /**
563            * @return {string}
564            * @this {FileEntryRecordStorage}
565            */
566           function() {
567             console.error('Unable to read from history file.');
568             return '';
569           }.bind(this))
570       .then(
571           /**
572            * @param {string} fileContents
573            * @this {FileEntryRecordStorage}
574            */
575           function(fileContents) {
576             return this.parse_(fileContents);
577           }.bind(this));
578 };
579
580 /**
581  * Reads the entire entry as a single string value.
582  *
583  * @param {!File} file
584  * @return {!Promise.<string>}
585  * @private
586  */
587 FileEntryRecordStorage.prototype.readFileAsText_ = function(file) {
588   return new Promise(
589       /**
590        * @param {function()} resolve
591        * @param {function()} reject
592        * @this {FileEntryRecordStorage}
593        */
594       function(resolve, reject) {
595         var reader = new FileReader();
596
597         reader.onloadend = function() {
598           if (reader.error) {
599             console.error(reader.error);
600             reject();
601           } else {
602             resolve(reader.result);
603           }
604         }.bind(this);
605
606         reader.onerror = function(error) {
607             console.error(error);
608           reject(error);
609         }.bind(this);
610
611         reader.readAsText(file);
612       }.bind(this));
613 };
614
615 /**
616  * Parses the text.
617  *
618  * @param {string} text
619  * @return {!Promise.<!Array.<!Array.<*>>>}
620  * @private
621  */
622 FileEntryRecordStorage.prototype.parse_ = function(text) {
623   return new Promise(
624       /**
625        * @param {function()} resolve
626        * @param {function()} reject
627        * @this {FileEntryRecordStorage}
628        */
629       function(resolve, reject) {
630         if (text.length === 0) {
631           resolve([]);
632         } else {
633           // Dress up the contents of the file like an array,
634           // so the JSON object can parse it using JSON.parse.
635           // That means we need to both:
636           //   1) Strip the trailing ',\n' from the last record
637           //   2) Surround the whole string in brackets.
638           // NOTE: JSON.parse is WAY faster than parsing this
639           // ourselves in javascript.
640           var json = '[' + text.substring(0, text.length - 2) + ']';
641           resolve(JSON.parse(json));
642         }
643       }.bind(this));
644 };
645
646 /**
647  * A wrapper for FileEntry that provides Promises.
648  *
649  * @param {!FileEntry} fileEntry
650  *
651  * @constructor
652  * @struct
653  */
654 function PromisaryFileEntry(fileEntry) {
655   /** @private {!FileEntry} */
656   this.fileEntry_ = fileEntry;
657 }
658
659 /**
660  * A "Promisary" wrapper around entry.getWriter.
661  * @return {!Promise.<!FileWriter>}
662  */
663 PromisaryFileEntry.prototype.createWriter = function() {
664   return new Promise(this.fileEntry_.createWriter.bind(this.fileEntry_));
665 };
666
667 /**
668  * A "Promisary" wrapper around entry.file.
669  * @return {!Promise.<!File>}
670  */
671 PromisaryFileEntry.prototype.file = function() {
672   return new Promise(this.fileEntry_.file.bind(this.fileEntry_));
673 };
674
675 /**
676  * @return {!Promise.<!Object>}
677  */
678 PromisaryFileEntry.prototype.getMetadata = function() {
679   return new Promise(this.fileEntry_.getMetadata.bind(this.fileEntry_));
680 };