| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // If directory files changes too often, don't rescan directory more than once |
| // per specified interval |
| var SIMULTANEOUS_RESCAN_INTERVAL = 1000; |
| // Used for operations that require almost instant rescan. |
| var SHORT_RESCAN_INTERVAL = 100; |
| |
| /** |
| * Data model of the file manager. |
| * |
| * @constructor |
| * @param {DirectoryEntry} root File system root. |
| * @param {boolean} singleSelection True if only one file could be selected |
| * at the time. |
| * @param {MetadataCache} metadataCache The metadata cache service. |
| * @param {VolumeManager} volumeManager The volume manager. |
| * @param {boolean} isGDataEnabled True if GDATA enabled (initial value). |
| */ |
| function DirectoryModel(root, singleSelection, |
| metadataCache, volumeManager, isGDataEnabled) { |
| this.root_ = root; |
| var fileList = new cr.ui.ArrayDataModel([]); |
| this.fileListSelection_ = singleSelection ? |
| new cr.ui.ListSingleSelectionModel() : new cr.ui.ListSelectionModel(); |
| |
| this.runningScan_ = null; |
| this.pendingScan_ = null; |
| this.rescanTime_ = null; |
| this.scanFailures_ = 0; |
| this.gDataEnabled_ = isGDataEnabled; |
| |
| this.currentFileListContext_ = new FileListContext( |
| metadataCache, fileList, false); |
| this.currentDirContents_ = new DirectoryContentsBasic( |
| this.currentFileListContext_, root); |
| |
| this.rootsList_ = new cr.ui.ArrayDataModel([]); |
| this.rootsListSelection_ = new cr.ui.ListSingleSelectionModel(); |
| this.rootsListSelection_.addEventListener( |
| 'change', this.onRootChange_.bind(this)); |
| |
| /** |
| * A map root.fullPath -> currentDirectory.fullPath. |
| * @private |
| * @type {Object.<string, string>} |
| */ |
| this.currentDirByRoot_ = {}; |
| |
| this.volumeManager_ = volumeManager; |
| } |
| |
| /** |
| * Fake entry to be used in currentDirEntry_ when current directory is |
| * unmounted GDATA. |
| * @private |
| */ |
| DirectoryModel.fakeGDataEntry_ = { |
| fullPath: RootDirectory.GDATA, |
| isDirectory: true |
| }; |
| |
| /** |
| * DirectoryModel extends cr.EventTarget. |
| */ |
| DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype; |
| |
| /** |
| * Fills the root list and starts tracking changes. |
| */ |
| DirectoryModel.prototype.start = function() { |
| var volumesChangeHandler = this.onMountChanged_.bind(this); |
| this.volumeManager_.addEventListener('change', volumesChangeHandler); |
| this.updateRoots_(); |
| }; |
| |
| /** |
| * @return {cr.ui.ArrayDataModel} Files in the current directory. |
| */ |
| DirectoryModel.prototype.getFileList = function() { |
| return this.currentFileListContext_.fileList; |
| }; |
| |
| /** |
| * @return {MetadataCache} Metadata cache. |
| */ |
| DirectoryModel.prototype.getMetadataCache = function() { |
| return this.currentFileListContext_.metadataCache; |
| }; |
| |
| /** |
| * Sets whether GDATA appears in the roots list and |
| * if it could be used as current directory. |
| * @param {boolead} enabled True if GDATA enabled. |
| */ |
| DirectoryModel.prototype.setGDataEnabled = function(enabled) { |
| if (this.gDataEnabled_ == enabled) |
| return; |
| this.gDataEnabled_ = enabled; |
| this.updateRoots_(); |
| if (!enabled && this.getCurrentRootType() == RootType.GDATA) |
| this.changeDirectory(this.getDefaultDirectory()); |
| }; |
| |
| /** |
| * Sort the file list. |
| * @param {string} sortField Sort field. |
| * @param {string} sortDirection "asc" or "desc". |
| */ |
| DirectoryModel.prototype.sortFileList = function(sortField, sortDirection) { |
| this.getFileList().sort(sortField, sortDirection); |
| }; |
| |
| /** |
| * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection |
| * in the fileList. |
| */ |
| DirectoryModel.prototype.getFileListSelection = function() { |
| return this.fileListSelection_; |
| }; |
| |
| /** |
| * @return {RootType} Root type of current root. |
| */ |
| DirectoryModel.prototype.getCurrentRootType = function() { |
| return PathUtil.getRootType(this.currentDirContents_.getPath()); |
| }; |
| |
| /** |
| * @return {string} Root name. |
| */ |
| DirectoryModel.prototype.getCurrentRootName = function() { |
| var rootPath = PathUtil.split(this.getCurrentRootPath()); |
| return rootPath[rootPath.length - 1]; |
| }; |
| |
| /** |
| * @return {string} Root name. |
| */ |
| DirectoryModel.prototype.getCurrentRootPath = function() { |
| return PathUtil.getRootPath(this.currentDirContents_.getPath()); |
| }; |
| |
| /** |
| * @return {string} Root name. |
| */ |
| DirectoryModel.prototype.getCurrentRootUrl = function() { |
| return util.makeFilesystemUrl(this.getCurrentRootPath()); |
| }; |
| |
| /** |
| * @return {boolean} on True if offline. |
| */ |
| DirectoryModel.prototype.isOffline = function() { |
| return this.offline_; |
| }; |
| |
| /** |
| * @param {boolean} offline True if offline. |
| */ |
| DirectoryModel.prototype.setOffline = function(offline) { |
| this.offline_ = offline; |
| }; |
| |
| /** |
| * @return {boolean} True if current directory is read only. |
| */ |
| DirectoryModel.prototype.isReadOnly = function() { |
| return this.isPathReadOnly(this.getCurrentRootPath()); |
| }; |
| |
| /** |
| * @return {boolean} True if the a scan is active. |
| */ |
| DirectoryModel.prototype.isScanning = function() { |
| return this.currentDirContents_.isScanning(); |
| }; |
| |
| /** |
| * @return {boolean} True if search is in progress. |
| */ |
| DirectoryModel.prototype.isSearching = function() { |
| return this.currentDirContents_.isSearch(); |
| }; |
| |
| /** |
| * @param {string} path Path to check. |
| * @return {boolean} True if the |path| is read only. |
| */ |
| DirectoryModel.prototype.isPathReadOnly = function(path) { |
| switch (PathUtil.getRootType(path)) { |
| case RootType.REMOVABLE: |
| return !!this.volumeManager_.isReadOnly(PathUtil.getRootPath(path)); |
| case RootType.ARCHIVE: |
| return true; |
| case RootType.DOWNLOADS: |
| return false; |
| case RootType.GDATA: |
| return this.isOffline(); |
| default: |
| return true; |
| } |
| }; |
| |
| /** |
| * @return {boolean} If the files with names starting with "." are not shown. |
| */ |
| DirectoryModel.prototype.isFilterHiddenOn = function() { |
| return this.currentFileListContext_.isFilterHiddenOn(); |
| }; |
| |
| /** |
| * @param {boolean} value Whether files with leading "." are hidden. |
| */ |
| DirectoryModel.prototype.setFilterHidden = function(value) { |
| this.currentFileListContext_.setFilterHidden(value); |
| this.rescanSoon(); |
| }; |
| |
| /** |
| * @return {DirectoryEntry} Current directory. |
| */ |
| DirectoryModel.prototype.getCurrentDirEntry = function() { |
| return this.currentDirContents_.getDirectoryEntry(); |
| }; |
| |
| /** |
| * @return {string} Path for the current directory. |
| */ |
| DirectoryModel.prototype.getCurrentDirPath = function() { |
| return this.currentDirContents_.getPath(); |
| }; |
| |
| /** |
| * @private |
| * @return {Array.<string>} File paths of selected files. |
| */ |
| DirectoryModel.prototype.getSelectedPaths_ = function() { |
| var indexes = this.fileListSelection_.selectedIndexes; |
| var fileList = this.getFileList(); |
| if (fileList) { |
| return indexes.map(function(i) { |
| return fileList.item(i).fullPath; |
| }); |
| } |
| return []; |
| }; |
| |
| /** |
| * @private |
| * @param {Array.<string>} value List of file paths of selected files. |
| */ |
| DirectoryModel.prototype.setSelectedPaths_ = function(value) { |
| var indexes = []; |
| var fileList = this.getFileList(); |
| |
| function safeKey(key) { |
| // The transformation must: |
| // 1. Never generate a reserved name ('__proto__') |
| // 2. Keep different keys different. |
| return '#' + key; |
| } |
| |
| var hash = {}; |
| |
| for (var i = 0; i < value.length; i++) |
| hash[safeKey(value[i])] = 1; |
| |
| for (var i = 0; i < fileList.length; i++) { |
| if (hash.hasOwnProperty(safeKey(fileList.item(i).fullPath))) |
| indexes.push(i); |
| } |
| this.fileListSelection_.selectedIndexes = indexes; |
| }; |
| |
| /** |
| * @private |
| * @return {string} Lead item file path. |
| */ |
| DirectoryModel.prototype.getLeadPath_ = function() { |
| var index = this.fileListSelection_.leadIndex; |
| return index >= 0 && this.getFileList().item(index).fullPath; |
| }; |
| |
| /** |
| * @private |
| * @param {string} value The name of new lead index. |
| */ |
| DirectoryModel.prototype.setLeadPath_ = function(value) { |
| var fileList = this.getFileList(); |
| for (var i = 0; i < fileList.length; i++) { |
| if (fileList.item(i).fullPath === value) { |
| this.fileListSelection_.leadIndex = i; |
| return; |
| } |
| } |
| }; |
| |
| /** |
| * @return {cr.ui.ArrayDataModel} The list of roots. |
| */ |
| DirectoryModel.prototype.getRootsList = function() { |
| return this.rootsList_; |
| }; |
| |
| /** |
| * @return {cr.ui.ListSingleSelectionModel} Root list selection model. |
| */ |
| DirectoryModel.prototype.getRootsListSelectionModel = function() { |
| return this.rootsListSelection_; |
| }; |
| |
| /** |
| * Schedule rescan with short delay. |
| */ |
| DirectoryModel.prototype.rescanSoon = function() { |
| this.scheduleRescan(SHORT_RESCAN_INTERVAL); |
| }; |
| |
| /** |
| * Schedule rescan with delay. Designed to handle directory change |
| * notification. |
| */ |
| DirectoryModel.prototype.rescanLater = function() { |
| this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL); |
| }; |
| |
| /** |
| * Schedule rescan with delay. If another rescan has been scheduled does |
| * nothing. File operation may cause a few notifications what should cause |
| * a single refresh. |
| * @param {number} delay Delay in ms after which the rescan will be performed. |
| */ |
| DirectoryModel.prototype.scheduleRescan = function(delay) { |
| if (this.rescanTime_) { |
| if (this.rescanTime_ <= Date.now() + delay) |
| return; |
| clearTimeout(this.rescanTimeoutId_); |
| } |
| |
| this.rescanTime_ = Date.now() + delay; |
| this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay); |
| }; |
| |
| /** |
| * Cancel a rescan on timeout if it is scheduled. |
| * @private |
| */ |
| DirectoryModel.prototype.clearRescanTimeout_ = function() { |
| this.rescanTime_ = null; |
| if (this.rescanTimeoutId_) { |
| clearTimeout(this.rescanTimeoutId_); |
| this.rescanTimeoutId_ = null; |
| } |
| }; |
| |
| /** |
| * Rescan current directory. May be called indirectly through rescanLater or |
| * directly in order to reflect user action. Will first cache all the directory |
| * contents in an array, then seamlessly substitute the fileList contents, |
| * preserving the select element etc. |
| * |
| * This should be to scan the contents of current directory (or search). |
| */ |
| DirectoryModel.prototype.rescan = function() { |
| this.clearRescanTimeout_(); |
| if (this.runningScan_) { |
| this.pendingRescan_ = true; |
| return; |
| } |
| |
| var dirContents = this.currentDirContents_.clone(); |
| dirContents.setFileList([]); |
| |
| var successCallback = (function() { |
| this.replaceDirectoryContents_(dirContents); |
| cr.dispatchSimpleEvent(this, 'rescan-completed'); |
| }).bind(this); |
| |
| this.scan_(dirContents, successCallback); |
| }; |
| |
| /** |
| * Run scan on the current DirectoryContents. The active fileList is cleared and |
| * the entries are added directly. |
| * |
| * This should be used when changing directory or initiating a new search. |
| * |
| * @private |
| * @param {DirectoryContentes} newDirContents New DirectoryContents instance to |
| * replace currentDirContents_. |
| * @param {Function} opt_callback Called on success. |
| */ |
| DirectoryModel.prototype.clearAndScan_ = function(newDirContents, |
| opt_callback) { |
| this.currentDirContents_.cancelScan(); |
| this.currentDirContents_ = newDirContents; |
| this.clearRescanTimeout_(); |
| |
| if (this.pendingScan_) |
| this.pendingScan_ = false; |
| |
| if (this.runningScan_) { |
| this.runningScan_.cancelScan(); |
| this.runningScan_ = null; |
| } |
| |
| var onDone = function() { |
| cr.dispatchSimpleEvent(this, 'scan-completed'); |
| if (opt_callback) |
| opt_callback(); |
| }.bind(this); |
| |
| // Clear the table first. |
| var fileList = this.getFileList(); |
| fileList.splice(0, fileList.length); |
| cr.dispatchSimpleEvent(this, 'scan-started'); |
| this.scan_(this.currentDirContents_, onDone); |
| }; |
| |
| /** |
| * Perform a directory contents scan. Should be called only from rescan() and |
| * clearAndScan_(). |
| * |
| * @private |
| * @param {DirectoryContents} dirContents DirectoryContents instance on which |
| * the scan will be run. |
| * @param {function} successCallback Callback on success. |
| */ |
| DirectoryModel.prototype.scan_ = function(dirContents, successCallback) { |
| var self = this; |
| |
| /** |
| * Runs pending scan if there is one. |
| * |
| * @return {boolean} Did pending scan exist. |
| */ |
| function maybeRunPendingRescan() { |
| if (self.pendingRescan_) { |
| self.rescanSoon(); |
| self.pendingRescan_ = false; |
| return true; |
| } |
| return false; |
| } |
| |
| function onSuccess() { |
| self.runningScan_ = null; |
| successCallback(); |
| self.scanFailures_ = 0; |
| maybeRunPendingRescan(); |
| } |
| |
| function onFailure() { |
| self.runningScan_ = null; |
| self.scanFailures_++; |
| |
| if (maybeRunPendingRescan()) |
| return; |
| |
| if (self.scanFailures_ <= 1) |
| self.rescanLater(); |
| } |
| |
| this.runningScan_ = dirContents; |
| |
| dirContents.addEventListener('scan-completed', onSuccess); |
| dirContents.addEventListener('scan-failed', onFailure); |
| dirContents.addEventListener('scan-cancelled', this.dispatchEvent.bind(this)); |
| dirContents.scan(); |
| }; |
| |
| /** |
| * @private |
| * @param {DirectoryContents} dirContents DirectoryContents instance. |
| */ |
| DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) { |
| cr.dispatchSimpleEvent(this, 'begin-update-files'); |
| this.fileListSelection_.beginChange(); |
| |
| var selectedPaths = this.getSelectedPaths_(); |
| // Restore leadIndex in case leadName no longer exists. |
| var leadIndex = this.fileListSelection_.leadIndex; |
| var leadPath = this.getLeadPath_(); |
| |
| this.currentDirContents_ = dirContents; |
| dirContents.replaceContextFileList(); |
| |
| this.setSelectedPaths_(selectedPaths); |
| this.fileListSelection_.leadIndex = leadIndex; |
| this.setLeadPath_(leadPath); |
| this.fileListSelection_.endChange(); |
| cr.dispatchSimpleEvent(this, 'end-update-files'); |
| }; |
| |
| /** |
| * @param {string} name Filename. |
| */ |
| DirectoryModel.prototype.onEntryChanged = function(name) { |
| var currentEntry = this.getCurrentDirEntry(); |
| var fileList = this.getFileList(); |
| var self = this; |
| |
| function onEntryFound(entry) { |
| // Do nothing if current directory changed during async operations. |
| if (self.getCurrentDirEntry() != currentEntry) |
| return; |
| self.currentDirContents_.prefetchMetadata([entry], function() { |
| // Do nothing if current directory changed during async operations. |
| if (self.getCurrentDirEntry() != currentEntry) |
| return; |
| |
| var index = self.findIndexByName_(name); |
| if (index >= 0) |
| fileList.splice(index, 1, entry); |
| else |
| fileList.splice(fileList.length, 0, entry); |
| }); |
| }; |
| |
| function onError(err) { |
| if (err.code != FileError.NOT_FOUND_ERR) { |
| self.rescanLater(); |
| return; |
| } |
| |
| var index = self.findIndexByName_(name); |
| if (index >= 0) |
| fileList.splice(index, 1); |
| }; |
| |
| util.resolvePath(currentEntry, name, onEntryFound, onError); |
| }; |
| |
| /** |
| * @private |
| * @param {string} name Filename. |
| * @return {number} The index in the fileList. |
| */ |
| DirectoryModel.prototype.findIndexByName_ = function(name) { |
| var fileList = this.getFileList(); |
| for (var i = 0; i < fileList.length; i++) |
| if (fileList.item(i).name == name) |
| return i; |
| return -1; |
| }; |
| |
| /** |
| * Rename the entry in the filesystem and update the file list. |
| * @param {Entry} entry Entry to rename. |
| * @param {string} newName New name. |
| * @param {function} errorCallback Called on error. |
| * @param {function} opt_successCallback Called on success. |
| */ |
| DirectoryModel.prototype.renameEntry = function(entry, newName, |
| errorCallback, |
| opt_successCallback) { |
| var self = this; |
| var currentDirPath = this.getCurrentDirPath(); |
| function onSuccess(newEntry) { |
| self.currentDirContents_.prefetchMetadata([newEntry], function() { |
| // Do not change anything or call the callback if current |
| // directory changed. |
| if (currentDirPath != self.getCurrentDirPath()) |
| return; |
| |
| var index = self.findIndexByName_(entry.name); |
| if (index >= 0) |
| self.getFileList().splice(index, 1, newEntry); |
| self.selectEntry(newEntry.name); |
| // If the entry doesn't exist in the list it mean that it updated from |
| // outside (probably by directory rescan). |
| if (opt_successCallback) |
| opt_successCallback(); |
| }); |
| } |
| |
| function onParentFound(parentEntry) { |
| entry.moveTo(parentEntry, newName, onSuccess, errorCallback); |
| } |
| |
| entry.getParent(onParentFound, errorCallback); |
| }; |
| |
| /** |
| * Checks if current directory contains a file or directory with this name. |
| * @param {string} entry Entry to which newName will be given. |
| * @param {string} name Name to check. |
| * @param {function(boolean, boolean?)} callback Called when the result's |
| * available. First parameter is true if the entry exists and second |
| * is true if it's a file. |
| */ |
| DirectoryModel.prototype.doesExist = function(entry, name, callback) { |
| function onParentFound(parentEntry) { |
| util.resolvePath(parentEntry, name, |
| function(foundEntry) { |
| callback(true, foundEntry.isFile); |
| }, |
| callback.bind(window, false)); |
| } |
| |
| entry.getParent(onParentFound, callback.bind(window, false)); |
| }; |
| |
| /** |
| * Creates directory and updates the file list. |
| * |
| * @param {string} name Directory name. |
| * @param {function} successCallback Callback on success. |
| * @param {function} errorCallback Callback on failure. |
| */ |
| DirectoryModel.prototype.createDirectory = function(name, successCallback, |
| errorCallback) { |
| var currentDirPath = this.getCurrentDirPath(); |
| |
| var onSuccess = function(newEntry) { |
| // Do not change anything or call the callback if current |
| // directory changed. |
| if (currentDirPath != this.getCurrentDirPath()) |
| return; |
| |
| var existing = this.getFileList().slice().filter( |
| function(e) {return e.name == name;}); |
| |
| if (existing.length) { |
| this.selectEntry(name); |
| successCallback(existing[0]); |
| } else { |
| this.fileListSelection_.beginChange(); |
| this.getFileList().splice(0, 0, newEntry); |
| this.selectEntry(name); |
| this.fileListSelection_.endChange(); |
| successCallback(newEntry); |
| } |
| }; |
| |
| this.currentDirContents_.createDirectory(name, onSuccess.bind(this), |
| errorCallback); |
| }; |
| |
| /** |
| * Changes directory. Causes 'directory-change' event. |
| * |
| * @param {string} path New current directory path. |
| */ |
| DirectoryModel.prototype.changeDirectory = function(path) { |
| this.resolveDirectory(path, function(directoryEntry) { |
| this.changeDirectoryEntry_(false, directoryEntry); |
| }.bind(this), function(error) { |
| console.error('Error changing directory to ' + path + ': ', error); |
| }); |
| }; |
| |
| /** |
| * Resolves absolute directory path. Handles GData stub. |
| * @param {string} path Path to the directory. |
| * @param {function(DirectoryEntry} successCallback Success callback. |
| * @param {function(FileError} errorCallback Error callback. |
| */ |
| DirectoryModel.prototype.resolveDirectory = function(path, successCallback, |
| errorCallback) { |
| if (PathUtil.getRootType(path) == RootType.GDATA) { |
| if (!this.isGDataMounted_()) { |
| if (path == DirectoryModel.fakeGDataEntry_.fullPath) |
| successCallback(DirectoryModel.fakeGDataEntry_); |
| else // Subdirectory. |
| errorCallback({ code: FileError.NOT_FOUND_ERR }); |
| return; |
| } |
| } |
| |
| if (path == '/') { |
| successCallback(this.root_); |
| return; |
| } |
| |
| this.root_.getDirectory(path, {create: false}, |
| successCallback, errorCallback); |
| }; |
| |
| /** |
| * @private |
| * @return {Entry} Directory entry of the root selected in rootsList. |
| */ |
| DirectoryModel.prototype.getSelectedRootDirEntry_ = function() { |
| return this.rootsList_.item(this.rootsListSelection_.selectedIndex); |
| }; |
| |
| /** |
| * Handler for root item being clicked. |
| * @private |
| * @param {Entry} entry Entry to navigate to. |
| * @param {Event} event The event. |
| */ |
| DirectoryModel.prototype.onRootChange_ = function(entry, event) { |
| var newRootDir = this.getSelectedRootDirEntry_(); |
| if (newRootDir) |
| this.changeRoot(newRootDir.fullPath); |
| }; |
| |
| /** |
| * Changes directory. If path points to a root (except current one) |
| * then directory changed to the last used one for the root. |
| * |
| * @param {string} path New current directory path or new root. |
| */ |
| DirectoryModel.prototype.changeRoot = function(path) { |
| if (this.getCurrentRootPath() == path) |
| return; |
| if (this.currentDirByRoot_[path]) { |
| this.resolveDirectory( |
| this.currentDirByRoot_[path], |
| this.changeDirectoryEntry_.bind(this, false), |
| this.changeDirectory.bind(this, path)); |
| } else { |
| this.changeDirectory(path); |
| } |
| }; |
| |
| /** |
| * @private |
| * @param {DirectoryEntry} dirEntry The absolute path to the new directory. |
| * @param {function} opt_callback Executed if the directory loads successfully. |
| */ |
| DirectoryModel.prototype.changeDirectoryEntrySilent_ = function(dirEntry, |
| opt_callback) { |
| function onScanComplete() { |
| if (opt_callback) |
| opt_callback(); |
| // For tests that open the dialog to empty directories, everything |
| // is loaded at this point. |
| chrome.test.sendMessage('directory-change-complete'); |
| } |
| this.clearAndScan_(new DirectoryContentsBasic(this.currentFileListContext_, |
| dirEntry), |
| onScanComplete.bind(this)); |
| this.updateRootsListSelection_(); |
| this.currentDirByRoot_[this.getCurrentRootPath()] = dirEntry.fullPath; |
| }; |
| |
| /** |
| * Change the current directory to the directory represented by a |
| * DirectoryEntry. |
| * |
| * Dispatches the 'directory-changed' event when the directory is successfully |
| * changed. |
| * |
| * @private |
| * @param {boolean} initial True if it comes from setupPath and |
| * false if caused by an user action. |
| * @param {DirectoryEntry} dirEntry The absolute path to the new directory. |
| * @param {function} opt_callback Executed if the directory loads successfully. |
| */ |
| DirectoryModel.prototype.changeDirectoryEntry_ = function(initial, dirEntry, |
| opt_callback) { |
| if (dirEntry == DirectoryModel.fakeGDataEntry_) |
| this.volumeManager_.mountGData(function() {}, function() {}); |
| |
| this.clearSearch_(); |
| var previous = this.currentDirContents_.getDirectoryEntry(); |
| this.changeDirectoryEntrySilent_(dirEntry, opt_callback); |
| |
| var e = new cr.Event('directory-changed'); |
| e.previousDirEntry = previous; |
| e.newDirEntry = dirEntry; |
| e.initial = initial; |
| this.dispatchEvent(e); |
| }; |
| |
| /** |
| * Creates an object wich could say wether directory has changed while it has |
| * been active or not. Designed for long operations that should be canncelled |
| * if the used change current directory. |
| * @return {Object} Created object. |
| */ |
| DirectoryModel.prototype.createDirectoryChangeTracker = function() { |
| var tracker = { |
| dm_: this, |
| active_: false, |
| hasChanged: false, |
| exceptInitialChange: false, |
| |
| start: function() { |
| if (!this.active_) { |
| this.dm_.addEventListener('directory-changed', |
| this.onDirectoryChange_); |
| this.active_ = true; |
| this.hasChanged = false; |
| } |
| }, |
| |
| stop: function() { |
| if (this.active_) { |
| this.dm_.removeEventListener('directory-changed', |
| this.onDirectoryChange_); |
| active_ = false; |
| } |
| }, |
| |
| onDirectoryChange_: function(event) { |
| // this == tracker.dm_ here. |
| if (tracker.exceptInitialChange && event.initial) |
| return; |
| tracker.stop(); |
| tracker.hasChanged = true; |
| } |
| }; |
| return tracker; |
| }; |
| |
| /** |
| * Change the state of the model to reflect the specified path (either a |
| * file or directory). |
| * |
| * @param {string} path The root path to use |
| * @param {function=} opt_loadedCallback Invoked when the entire directory |
| * has been loaded and any default file selected. If there are any |
| * errors loading the directory this will not get called (even if the |
| * directory loads OK on retry later). Will NOT be called if another |
| * directory change happened while setupPath was in progress. |
| * @param {function=} opt_pathResolveCallback Invoked as soon as the path has |
| * been resolved, and called with the base and leaf portions of the path |
| * name, and a flag indicating if the entry exists. Will be called even |
| * if another directory change happened while setupPath was in progress, |
| * but will pass |false| as |exist| parameter. |
| */ |
| DirectoryModel.prototype.setupPath = function(path, opt_loadedCallback, |
| opt_pathResolveCallback) { |
| var tracker = this.createDirectoryChangeTracker(); |
| tracker.start(); |
| |
| var self = this; |
| function resolveCallback(directoryPath, fileName, exists) { |
| tracker.stop(); |
| if (!opt_pathResolveCallback) |
| return; |
| opt_pathResolveCallback(directoryPath, fileName, |
| exists && !tracker.hasChanged); |
| } |
| |
| function changeDirectoryEntry(directoryEntry, initial, opt_callback) { |
| tracker.stop(); |
| if (!tracker.hasChanged) |
| self.changeDirectoryEntry_(initial, directoryEntry, opt_callback); |
| } |
| |
| var INITIAL = true; |
| var EXISTS = true; |
| |
| function changeToDefault() { |
| var def = self.getDefaultDirectory(); |
| self.resolveDirectory(def, function(directoryEntry) { |
| resolveCallback(def, '', !EXISTS); |
| changeDirectoryEntry(directoryEntry, INITIAL); |
| }, function(error) { |
| console.error('Failed to resolve default directory: ' + def, error); |
| resolveCallback('/', '', !EXISTS); |
| }); |
| } |
| |
| function noParentDirectory(error) { |
| console.log('Can\'t resolve parent directory: ' + path, error); |
| changeToDefault(); |
| } |
| |
| if (DirectoryModel.isSystemDirectory(path)) { |
| changeToDefault(); |
| return; |
| } |
| |
| this.resolveDirectory(path, function(directoryEntry) { |
| resolveCallback(directoryEntry.fullPath, '', !EXISTS); |
| changeDirectoryEntry(directoryEntry, INITIAL); |
| }, function(error) { |
| // Usually, leaf does not exist, because it's just a suggested file name. |
| var fileExists = error.code == FileError.TYPE_MISMATCH_ERR; |
| if (fileExists || error.code == FileError.NOT_FOUND_ERR) { |
| var nameDelimiter = path.lastIndexOf('/'); |
| var parentDirectoryPath = path.substr(0, nameDelimiter); |
| if (DirectoryModel.isSystemDirectory(parentDirectoryPath)) { |
| changeToDefault(); |
| return; |
| } |
| self.resolveDirectory(parentDirectoryPath, |
| function(parentDirectoryEntry) { |
| var fileName = path.substr(nameDelimiter + 1); |
| resolveCallback(parentDirectoryEntry.fullPath, fileName, fileExists); |
| changeDirectoryEntry(parentDirectoryEntry, |
| !INITIAL /*HACK*/, |
| function() { |
| self.selectEntry(fileName); |
| if (opt_loadedCallback) |
| opt_loadedCallback(); |
| }); |
| // TODO(kaznacheev): Fix history.replaceState for the File Browser and |
| // change !INITIAL to INITIAL. Passing |false| makes things |
| // less ugly for now. |
| }, noParentDirectory); |
| } else { |
| // Unexpected errors. |
| console.error('Directory resolving error: ', error); |
| changeToDefault(); |
| } |
| }); |
| }; |
| |
| /** |
| * @param {function} opt_callback Callback on done. |
| */ |
| DirectoryModel.prototype.setupDefaultPath = function(opt_callback) { |
| this.setupPath(this.getDefaultDirectory(), opt_callback); |
| }; |
| |
| /** |
| * @return {string} The default directory. |
| */ |
| DirectoryModel.prototype.getDefaultDirectory = function() { |
| return RootDirectory.DOWNLOADS; |
| }; |
| |
| /** |
| * @param {string} name Filename. |
| */ |
| DirectoryModel.prototype.selectEntry = function(name) { |
| var fileList = this.getFileList(); |
| for (var i = 0; i < fileList.length; i++) { |
| if (fileList.item(i).name == name) { |
| this.selectIndex(i); |
| return; |
| } |
| } |
| }; |
| |
| /** |
| * @param {Array.<string>} urls Array of URLs. |
| */ |
| DirectoryModel.prototype.selectUrls = function(urls) { |
| var fileList = this.getFileList(); |
| this.fileListSelection_.beginChange(); |
| this.fileListSelection_.unselectAll(); |
| for (var i = 0; i < fileList.length; i++) { |
| if (urls.indexOf(fileList.item(i).toURL()) >= 0) |
| this.fileListSelection_.setIndexSelected(i, true); |
| } |
| this.fileListSelection_.endChange(); |
| }; |
| |
| /** |
| * @param {number} index Index of file. |
| */ |
| DirectoryModel.prototype.selectIndex = function(index) { |
| // this.focusCurrentList_(); |
| if (index >= this.getFileList().length) |
| return; |
| |
| // If a list bound with the model it will do scrollIndexIntoView(index). |
| this.fileListSelection_.selectedIndex = index; |
| }; |
| |
| /** |
| * Get root entries asynchronously. |
| * @private |
| * @param {function(Array.<Entry>)} callback Called when roots are resolved. |
| */ |
| DirectoryModel.prototype.resolveRoots_ = function(callback) { |
| var groups = { |
| downloads: null, |
| archives: null, |
| removables: null, |
| gdata: null |
| }; |
| var self = this; |
| |
| metrics.startInterval('Load.Roots'); |
| function done() { |
| for (var i in groups) |
| if (!groups[i]) |
| return; |
| |
| callback(groups.downloads. |
| concat(groups.gdata). |
| concat(groups.archives). |
| concat(groups.removables)); |
| metrics.recordInterval('Load.Roots'); |
| } |
| |
| function append(index, values, opt_error) { |
| groups[index] = values; |
| done(); |
| } |
| |
| function appendSingle(index, entry) { |
| groups[index] = [entry]; |
| done(); |
| } |
| |
| function onSingleError(index, defaultValue, error) { |
| groups[index] = defaultValue || []; |
| done(); |
| console.error('Error resolving root dir ', index, 'error: ', error); |
| } |
| |
| var root = this.root_; |
| function readSingle(dir, index, opt_defaultValue) { |
| root.getDirectory(dir, { create: false }, |
| appendSingle.bind(this, index), |
| onSingleError.bind(this, index, opt_defaultValue)); |
| } |
| |
| readSingle(RootDirectory.DOWNLOADS.substring(1), 'downloads'); |
| util.readDirectory(root, RootDirectory.ARCHIVE.substring(1), |
| append.bind(this, 'archives')); |
| util.readDirectory(root, RootDirectory.REMOVABLE.substring(1), |
| append.bind(this, 'removables')); |
| |
| if (this.gDataEnabled_) { |
| var fake = [DirectoryModel.fakeGDataEntry_]; |
| if (this.isGDataMounted_()) { |
| readSingle(RootDirectory.GDATA.substring(1), 'gdata', fake); |
| } else { |
| groups.gdata = fake; |
| } |
| } else { |
| groups.gdata = []; |
| } |
| }; |
| |
| /** |
| * Updates the roots list. |
| * @private |
| */ |
| DirectoryModel.prototype.updateRoots_ = function() { |
| var self = this; |
| this.resolveRoots_(function(rootEntries) { |
| var dm = self.rootsList_; |
| var args = [0, dm.length].concat(rootEntries); |
| dm.splice.apply(dm, args); |
| |
| self.updateRootsListSelection_(); |
| }); |
| }; |
| |
| /** |
| * Find roots list item by root path. |
| * |
| * @param {string} path Root path. |
| * @return {number} Index of the item. |
| */ |
| DirectoryModel.prototype.findRootsListIndex = function(path) { |
| var roots = this.rootsList_; |
| for (var index = 0; index < roots.length; index++) { |
| if (roots.item(index).fullPath == path) |
| return index; |
| } |
| return -1; |
| }; |
| |
| /** |
| * @private |
| */ |
| DirectoryModel.prototype.updateRootsListSelection_ = function() { |
| var rootPath = this.getCurrentRootPath(); |
| this.rootsListSelection_.selectedIndex = this.findRootsListIndex(rootPath); |
| }; |
| |
| /** |
| * @return {true} True if GDATA mounted. |
| * @private |
| */ |
| DirectoryModel.prototype.isGDataMounted_ = function() { |
| return this.volumeManager_.isMounted(RootDirectory.GDATA); |
| }; |
| |
| /** |
| * Handler for the VolumeManager's event. |
| * @private |
| */ |
| DirectoryModel.prototype.onMountChanged_ = function() { |
| this.updateRoots_(); |
| |
| var rootType = this.getCurrentRootType(); |
| |
| if ((rootType == RootType.ARCHIVE || rootType == RootType.REMOVABLE) && |
| !this.volumeManager_.isMounted(this.getCurrentRootPath())) { |
| this.changeDirectory(this.getDefaultDirectory()); |
| } |
| |
| if (rootType != RootType.GDATA) |
| return; |
| |
| var mounted = this.isGDataMounted_(); |
| if (this.getCurrentDirEntry() == DirectoryModel.fakeGDataEntry_) { |
| if (mounted) { |
| // Change fake entry to real one and rescan. |
| function onGotDirectory(entry) { |
| if (this.getCurrentDirEntry() == DirectoryModel.fakeGDataEntry_) { |
| this.changeDirectoryEntrySilent_(entry); |
| } |
| } |
| this.root_.getDirectory(RootDirectory.GDATA, {}, |
| onGotDirectory.bind(this)); |
| } |
| } else if (!mounted) { |
| // Current entry unmounted. Replace with fake one. |
| if (this.getCurrentDirPath() == DirectoryModel.fakeGDataEntry_.fullPath) { |
| // Replace silently and rescan. |
| this.changeDirectoryEntrySilent_(DirectoryModel.fakeGDataEntry_); |
| } else { |
| this.changeDirectoryEntry_(false, DirectoryModel.fakeGDataEntry_); |
| } |
| } |
| }; |
| |
| /** |
| * @param {string} path Path |
| * @return {boolean} If current directory is system. |
| */ |
| DirectoryModel.isSystemDirectory = function(path) { |
| path = path.replace(/\/+$/, ''); |
| return path === RootDirectory.REMOVABLE || path === RootDirectory.ARCHIVE; |
| }; |
| |
| /** |
| * Performs search and displays results. The search type is dependent on the |
| * current directory. If we are currently on gdata, server side content search |
| * over gdata mount point. If the current directory is not on the gdata, file |
| * name search over current directory wil be performed. |
| * |
| * @param {string} query Query that will be searched for. |
| * @param {function} onSearchRescan Function that will be called when the search |
| * directory is rescanned (i.e. search results are displayed) |
| * @param {function} onClearSearch Function to be called when search state gets |
| * cleared. |
| * TODO(olege): Change callbacks to events. |
| */ |
| DirectoryModel.prototype.search = function(query, |
| onSearchRescan, |
| onClearSearch) { |
| query = query.trimLeft(); |
| |
| var newDirContents; |
| if (!query) { |
| if (this.isSearching()) { |
| newDirContents = new DirectoryContentsBasic( |
| this.currentFileListContext_, |
| this.currentDirContents_.getDirectoryEntry()); |
| this.clearAndScan_(newDirContents); |
| this.clearSearch_(); |
| } |
| return; |
| } |
| |
| // If we already have event listener for an old search, we have to remove it. |
| if (this.onSearchCompleted_) |
| this.removeEventListener('scan-completed', this.onSearchCompleted_); |
| |
| // Current search will be cancelled. |
| if (this.onClearSearch_) |
| this.onClearSearch_(); |
| |
| this.onSearchCompleted_ = onSearchRescan; |
| this.onClearSearch_ = onClearSearch; |
| |
| this.addEventListener('scan-completed', this.onSearchCompleted_); |
| |
| // If we are offline, let's fallback to file name search inside dir. |
| if (this.getCurrentRootType() === RootType.GDATA && !this.isOffline()) { |
| newDirContents = new DirectoryContentsGDataSearch( |
| this.currentFileListContext_, this.getCurrentDirEntry(), query); |
| } else { |
| newDirContents = new DirectoryContentsLocalSearch( |
| this.currentFileListContext_, this.getCurrentDirEntry(), query); |
| } |
| this.clearAndScan_(newDirContents); |
| }; |
| |
| |
| /** |
| * In case the search was active, remove listeners and send notifications on |
| * its canceling. |
| * @private |
| */ |
| DirectoryModel.prototype.clearSearch_ = function() { |
| if (!this.isSearching()) |
| return; |
| |
| if (this.onSearchCompleted_) { |
| this.removeEventListener('scan-completed', this.onSearchCompleted_); |
| this.onSearchCompleted_ = null; |
| } |
| |
| if (this.onClearSearch_) { |
| this.onClearSearch_(); |
| this.onClearSearch_ = null; |
| } |
| }; |
| |
| /** |
| * @param {string} name Filter identifier. |
| * @param {Function(Entry)} callback A filter — a function receiving an Entry, |
| * and returning bool. |
| */ |
| DirectoryModel.prototype.addFilter = function(name, callback) { |
| this.currentFileListContext_.addFilter(name, callback); |
| }; |
| |
| /** |
| * @param {string} name Filter identifier. |
| */ |
| DirectoryModel.prototype.removeFilter = function(name) { |
| this.currentFileListContext_.removeFilter(name); |
| }; |
| |
| |
| /** |
| * @constructor |
| * @param {DirectoryEntry} root Root entry. |
| * @param {DirectoryModel} directoryModel Model to watch. |
| * @param {VolumeManager} volumeManager Manager to watch. |
| */ |
| function FileWatcher(root, directoryModel, volumeManager) { |
| this.root_ = root; |
| this.dm_ = directoryModel; |
| this.vm_ = volumeManager; |
| this.watchedDirectoryEntry_ = null; |
| this.updateWatchedDirectoryBound_ = |
| this.updateWatchedDirectory_.bind(this); |
| this.onFileChangedBound_ = |
| this.onFileChanged_.bind(this); |
| } |
| |
| /** |
| * Starts watching. |
| */ |
| FileWatcher.prototype.start = function() { |
| chrome.fileBrowserPrivate.onFileChanged.addListener( |
| this.onFileChangedBound_); |
| |
| this.dm_.addEventListener('directory-changed', |
| this.updateWatchedDirectoryBound_); |
| this.vm_.addEventListener('change', |
| this.updateWatchedDirectoryBound_); |
| |
| this.updateWatchedDirectory_(); |
| }; |
| |
| /** |
| * Stops watching (must be called before page unload). |
| */ |
| FileWatcher.prototype.stop = function() { |
| chrome.fileBrowserPrivate.onFileChanged.removeListener( |
| this.onFileChangedBound_); |
| |
| this.dm_.removeEventListener('directory-changed', |
| this.updateWatchedDirectoryBound_); |
| this.vm_.removeEventListener('change', |
| this.updateWatchedDirectoryBound_); |
| |
| if (this.watchedDirectoryEntry_) |
| this.changeWatchedEntry(null); |
| }; |
| |
| /** |
| * @param {Object} event chrome.fileBrowserPrivate.onFileChanged event. |
| * @private |
| */ |
| FileWatcher.prototype.onFileChanged_ = function(event) { |
| if (encodeURI(event.fileUrl) == this.watchedDirectoryEntry_.toURL()) |
| this.onFileInWatchedDirectoryChanged(); |
| }; |
| |
| /** |
| * Called when file in the watched directory changed. |
| */ |
| FileWatcher.prototype.onFileInWatchedDirectoryChanged = function() { |
| this.dm_.rescanLater(); |
| }; |
| |
| /** |
| * Called when directory changed or volumes mounted/unmounted. |
| * @private |
| */ |
| FileWatcher.prototype.updateWatchedDirectory_ = function() { |
| var current = this.watchedDirectoryEntry_; |
| switch (this.dm_.getCurrentRootType()) { |
| case RootType.GDATA: |
| if (!this.vm_.isMounted(RootDirectory.GDATA)) |
| break; |
| case RootType.DOWNLOADS: |
| case RootType.REMOVABLE: |
| if (!current || current.fullPath != this.dm_.getCurrentDirPath()) { |
| // TODO(serya): Changed in readonly removable directoried don't |
| // need to be tracked. |
| this.root_.getDirectory(this.dm_.getCurrentDirPath(), {}, |
| this.changeWatchedEntry.bind(this), |
| this.changeWatchedEntry.bind(this, null)); |
| } |
| return; |
| } |
| if (current) |
| this.changeWatchedEntry(null); |
| }; |
| |
| /** |
| * @param {Entry?} entry Null if no directory need to be watched or |
| * directory to watch. |
| */ |
| FileWatcher.prototype.changeWatchedEntry = function(entry) { |
| if (this.watchedDirectoryEntry_) { |
| chrome.fileBrowserPrivate.removeFileWatch( |
| this.watchedDirectoryEntry_.toURL(), |
| function(result) { |
| if (!result) { |
| console.log('Failed to remove file watch'); |
| } |
| }); |
| } |
| this.watchedDirectoryEntry_ = entry; |
| |
| if (this.watchedDirectoryEntry_) { |
| chrome.fileBrowserPrivate.addFileWatch( |
| this.watchedDirectoryEntry_.toURL(), |
| function(result) { |
| if (!result) { |
| console.log('Failed to add file watch'); |
| if (this.watchedDirectoryEntry_ == entry) |
| this.watchedDirectoryEntry_ = null; |
| } |
| }.bind(this)); |
| } |
| }; |
| |
| /** |
| * @return {DirectoryEntry} Current watched directory entry. |
| */ |
| FileWatcher.prototype.getWatchedDirectoryEntry = function() { |
| return this.watchedDirectoryEntry_; |
| }; |