| // 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. |
| |
| /** |
| * @fileoverview This file should contain utility functions used only by the |
| * files app. Other shared utility functions can be found in base/*_util.js, |
| * which allows finer-grained control over introducing dependencies. |
| */ |
| |
| /** |
| * Namespace for utility functions. |
| */ |
| const util = {}; |
| |
| /** |
| * @param {!chrome.fileManagerPrivate.IconSet} iconSet Set of icons. |
| * @return {string} CSS value. |
| */ |
| util.iconSetToCSSBackgroundImageValue = iconSet => { |
| let lowDpiPart = null; |
| let highDpiPart = null; |
| if (iconSet.icon16x16Url) { |
| lowDpiPart = 'url(' + iconSet.icon16x16Url + ') 1x'; |
| } |
| if (iconSet.icon32x32Url) { |
| highDpiPart = 'url(' + iconSet.icon32x32Url + ') 2x'; |
| } |
| |
| if (lowDpiPart && highDpiPart) { |
| return '-webkit-image-set(' + lowDpiPart + ', ' + highDpiPart + ')'; |
| } else if (lowDpiPart) { |
| return '-webkit-image-set(' + lowDpiPart + ')'; |
| } else if (highDpiPart) { |
| return '-webkit-image-set(' + highDpiPart + ')'; |
| } |
| |
| return 'none'; |
| }; |
| |
| /** |
| * @param {string} name File error name. |
| * @return {string} Translated file error string. |
| */ |
| util.getFileErrorString = name => { |
| let candidateMessageFragment; |
| switch (name) { |
| case 'NotFoundError': |
| candidateMessageFragment = 'NOT_FOUND'; |
| break; |
| case 'SecurityError': |
| candidateMessageFragment = 'SECURITY'; |
| break; |
| case 'NotReadableError': |
| candidateMessageFragment = 'NOT_READABLE'; |
| break; |
| case 'NoModificationAllowedError': |
| candidateMessageFragment = 'NO_MODIFICATION_ALLOWED'; |
| break; |
| case 'InvalidStateError': |
| candidateMessageFragment = 'INVALID_STATE'; |
| break; |
| case 'InvalidModificationError': |
| candidateMessageFragment = 'INVALID_MODIFICATION'; |
| break; |
| case 'PathExistsError': |
| candidateMessageFragment = 'PATH_EXISTS'; |
| break; |
| case 'QuotaExceededError': |
| candidateMessageFragment = 'QUOTA_EXCEEDED'; |
| break; |
| } |
| |
| return loadTimeData.getString('FILE_ERROR_' + candidateMessageFragment) || |
| loadTimeData.getString('FILE_ERROR_GENERIC'); |
| }; |
| |
| /** |
| * Mapping table for FileError.code style enum to DOMError.name string. |
| * |
| * @enum {string} |
| * @const |
| */ |
| util.FileError = { |
| ABORT_ERR: 'AbortError', |
| INVALID_MODIFICATION_ERR: 'InvalidModificationError', |
| INVALID_STATE_ERR: 'InvalidStateError', |
| NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', |
| NOT_FOUND_ERR: 'NotFoundError', |
| NOT_READABLE_ERR: 'NotReadable', |
| PATH_EXISTS_ERR: 'PathExistsError', |
| QUOTA_EXCEEDED_ERR: 'QuotaExceededError', |
| TYPE_MISMATCH_ERR: 'TypeMismatchError', |
| ENCODING_ERR: 'EncodingError', |
| }; |
| Object.freeze(util.FileError); |
| |
| /** |
| * @param {string} str String to escape. |
| * @return {string} Escaped string. |
| */ |
| util.htmlEscape = str => { |
| return str.replace(/[<>&]/g, entity => { |
| switch (entity) { |
| case '<': |
| return '<'; |
| case '>': |
| return '>'; |
| case '&': |
| return '&'; |
| } |
| }); |
| }; |
| |
| /** |
| * @param {string} str String to unescape. |
| * @return {string} Unescaped string. |
| */ |
| util.htmlUnescape = str => { |
| return str.replace(/&(lt|gt|amp);/g, entity => { |
| switch (entity) { |
| case '<': |
| return '<'; |
| case '>': |
| return '>'; |
| case '&': |
| return '&'; |
| } |
| }); |
| }; |
| |
| /** |
| * Renames the entry to newName. |
| * @param {Entry} entry The entry to be renamed. |
| * @param {string} newName The new name. |
| * @param {function(Entry)} successCallback Callback invoked when the rename |
| * is successfully done. |
| * @param {function(DOMError)} errorCallback Callback invoked when an error |
| * is found. |
| */ |
| util.rename = (entry, newName, successCallback, errorCallback) => { |
| entry.getParent(parentEntry => { |
| const parent = /** @type {!DirectoryEntry} */ (parentEntry); |
| |
| // Before moving, we need to check if there is an existing entry at |
| // parent/newName, since moveTo will overwrite it. |
| // Note that this way has some timing issue. After existing check, |
| // a new entry may be create on background. However, there is no way not to |
| // overwrite the existing file, unfortunately. The risk should be low, |
| // assuming the unsafe period is very short. |
| (entry.isFile ? parent.getFile : parent.getDirectory) |
| .call( |
| parent, newName, {create: false}, |
| entry => { |
| // The entry with the name already exists. |
| errorCallback( |
| util.createDOMError(util.FileError.PATH_EXISTS_ERR)); |
| }, |
| error => { |
| if (error.name != util.FileError.NOT_FOUND_ERR) { |
| // Unexpected error is found. |
| errorCallback(error); |
| return; |
| } |
| |
| // No existing entry is found. |
| entry.moveTo(parent, newName, successCallback, errorCallback); |
| }); |
| }, errorCallback); |
| }; |
| |
| /** |
| * Converts DOMError of util.rename to error message. |
| * @param {DOMError} error |
| * @param {!Entry} entry |
| * @param {string} newName |
| * @return {string} |
| */ |
| util.getRenameErrorMessage = (error, entry, newName) => { |
| if (error && |
| (error.name == util.FileError.PATH_EXISTS_ERR || |
| error.name == util.FileError.TYPE_MISMATCH_ERR)) { |
| // Check the existing entry is file or not. |
| // 1) If the entry is a file: |
| // a) If we get PATH_EXISTS_ERR, a file exists. |
| // b) If we get TYPE_MISMATCH_ERR, a directory exists. |
| // 2) If the entry is a directory: |
| // a) If we get PATH_EXISTS_ERR, a directory exists. |
| // b) If we get TYPE_MISMATCH_ERR, a file exists. |
| return strf( |
| (entry.isFile && error.name == util.FileError.PATH_EXISTS_ERR) || |
| (!entry.isFile && |
| error.name == util.FileError.TYPE_MISMATCH_ERR) ? |
| 'FILE_ALREADY_EXISTS' : |
| 'DIRECTORY_ALREADY_EXISTS', |
| newName); |
| } |
| |
| return strf( |
| 'ERROR_RENAMING', entry.name, util.getFileErrorString(error.name)); |
| }; |
| |
| /** |
| * Remove a file or a directory. |
| * @param {Entry} entry The entry to remove. |
| * @param {function()} onSuccess The success callback. |
| * @param {function(DOMError)} onError The error callback. |
| */ |
| util.removeFileOrDirectory = (entry, onSuccess, onError) => { |
| if (entry.isDirectory) { |
| entry.removeRecursively(onSuccess, onError); |
| } else { |
| entry.remove(onSuccess, onError); |
| } |
| }; |
| |
| /** |
| * Convert a number of bytes into a human friendly format, using the correct |
| * number separators. |
| * |
| * @param {number} bytes The number of bytes. |
| * @return {string} Localized string. |
| */ |
| util.bytesToString = bytes => { |
| // Translation identifiers for size units. |
| const UNITS = [ |
| 'SIZE_BYTES', |
| 'SIZE_KB', |
| 'SIZE_MB', |
| 'SIZE_GB', |
| 'SIZE_TB', |
| 'SIZE_PB', |
| ]; |
| |
| // Minimum values for the units above. |
| const STEPS = [ |
| 0, |
| Math.pow(2, 10), |
| Math.pow(2, 20), |
| Math.pow(2, 30), |
| Math.pow(2, 40), |
| Math.pow(2, 50), |
| ]; |
| |
| const str = (n, u) => { |
| return strf(u, n.toLocaleString()); |
| }; |
| |
| const fmt = (s, u) => { |
| const rounded = Math.round(bytes / s * 10) / 10; |
| return str(rounded, u); |
| }; |
| |
| // Less than 1KB is displayed like '80 bytes'. |
| if (bytes < STEPS[1]) { |
| return str(bytes, UNITS[0]); |
| } |
| |
| // Up to 1MB is displayed as rounded up number of KBs. |
| if (bytes < STEPS[2]) { |
| const rounded = Math.ceil(bytes / STEPS[1]); |
| return str(rounded, UNITS[1]); |
| } |
| |
| // This loop index is used outside the loop if it turns out |bytes| |
| // requires the largest unit. |
| let i; |
| |
| for (i = 2 /* MB */; i < UNITS.length - 1; i++) { |
| if (bytes < STEPS[i + 1]) { |
| return fmt(STEPS[i], UNITS[i]); |
| } |
| } |
| |
| return fmt(STEPS[i], UNITS[i]); |
| }; |
| |
| /** |
| * Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event |
| * modifiers. Convenient for writing out conditions in keyboard handlers. |
| * |
| * @param {Event} event The keyboard event. |
| * @return {string} Modifiers. |
| */ |
| util.getKeyModifiers = event => { |
| return (event.ctrlKey ? 'Ctrl-' : '') + (event.altKey ? 'Alt-' : '') + |
| (event.shiftKey ? 'Shift-' : '') + (event.metaKey ? 'Meta-' : ''); |
| }; |
| |
| /** |
| * @typedef {?{ |
| * scaleX: number, |
| * scaleY: number, |
| * rotate90: number |
| * }} |
| */ |
| util.Transform; |
| |
| /** |
| * @param {Element} element Element to transform. |
| * @param {util.Transform} transform Transform object, |
| * contains scaleX, scaleY and rotate90 properties. |
| */ |
| util.applyTransform = (element, transform) => { |
| // The order of rotate and scale matters. |
| element.style.transform = transform ? |
| 'rotate(' + transform.rotate90 * 90 + 'deg)' + |
| 'scaleX(' + transform.scaleX + ') ' + |
| 'scaleY(' + transform.scaleY + ') ' : |
| ''; |
| }; |
| |
| /** |
| * Extracts path from filesystem: URL. |
| * @param {string} url Filesystem URL. |
| * @return {?string} The path. |
| */ |
| util.extractFilePath = url => { |
| const match = |
| /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent|temporary)(\/.*)$/ |
| .exec(url); |
| const path = match && match[2]; |
| if (!path) { |
| return null; |
| } |
| return decodeURIComponent(path); |
| }; |
| |
| /** |
| * A shortcut function to create a child element with given tag and class. |
| * |
| * @param {!HTMLElement} parent Parent element. |
| * @param {string=} opt_className Class name. |
| * @param {string=} opt_tag Element tag, DIV is omitted. |
| * @return {!HTMLElement} Newly created element. |
| */ |
| util.createChild = (parent, opt_className, opt_tag) => { |
| const child = parent.ownerDocument.createElement(opt_tag || 'div'); |
| if (opt_className) { |
| child.className = opt_className; |
| } |
| parent.appendChild(child); |
| return /** @type {!HTMLElement} */ (child); |
| }; |
| |
| /** |
| * Obtains the element that should exist, decorates it with given type, and |
| * returns it. |
| * @param {string} query Query for the element. |
| * @param {function(new: T, ...)} type Type used to decorate. |
| * @template T |
| * @return {!T} Decorated element. |
| */ |
| util.queryDecoratedElement = (query, type) => { |
| const element = queryRequiredElement(query); |
| cr.ui.decorate(element, type); |
| return element; |
| }; |
| |
| /** |
| * Returns a translated string. |
| * |
| * Wrapper function to make dealing with translated strings more concise. |
| * Equivalent to loadTimeData.getString(id). |
| * |
| * @param {string} id The id of the string to return. |
| * @return {string} The translated string. |
| */ |
| function str(id) { |
| return loadTimeData.getString(id); |
| } |
| |
| /** |
| * Returns a translated string with arguments replaced. |
| * |
| * Wrapper function to make dealing with translated strings more concise. |
| * Equivalent to loadTimeData.getStringF(id, ...). |
| * |
| * @param {string} id The id of the string to return. |
| * @param {...*} var_args The values to replace into the string. |
| * @return {string} The translated string with replaced values. |
| */ |
| function strf(id, var_args) { |
| return loadTimeData.getStringF.apply(loadTimeData, arguments); |
| } |
| |
| /** |
| * @return {boolean} True if the Files app is running as an open files or a |
| * select folder dialog. False otherwise. |
| */ |
| util.runningInBrowser = () => { |
| return !window.appID; |
| }; |
| |
| /** |
| * Adds an isFocused method to the current window object. |
| */ |
| util.addIsFocusedMethod = () => { |
| let focused = true; |
| |
| window.addEventListener('focus', () => { |
| focused = true; |
| }); |
| |
| window.addEventListener('blur', () => { |
| focused = false; |
| }); |
| |
| /** |
| * @return {boolean} True if focused. |
| */ |
| window.isFocused = () => { |
| return focused; |
| }; |
| }; |
| |
| /** |
| * Checks, if the Files app's window is in a full screen mode. |
| * |
| * @param {chrome.app.window.AppWindow} appWindow App window to be maximized. |
| * @return {boolean} True if the full screen mode is enabled. |
| */ |
| util.isFullScreen = appWindow => { |
| if (appWindow) { |
| return appWindow.isFullscreen(); |
| } else { |
| console.error( |
| 'App window not passed. Unable to check status of the full screen ' + |
| 'mode.'); |
| return false; |
| } |
| }; |
| |
| /** |
| * Toggles the full screen mode. |
| * |
| * @param {chrome.app.window.AppWindow} appWindow App window to be maximized. |
| * @param {boolean} enabled True for enabling, false for disabling. |
| */ |
| util.toggleFullScreen = (appWindow, enabled) => { |
| if (appWindow) { |
| if (enabled) { |
| appWindow.fullscreen(); |
| } else { |
| appWindow.restore(); |
| } |
| return; |
| } |
| |
| console.error( |
| 'App window not passed. Unable to toggle the full screen mode.'); |
| }; |
| |
| /** |
| * The type of a file operation. |
| * @enum {string} |
| * @const |
| */ |
| util.FileOperationType = { |
| COPY: 'COPY', |
| MOVE: 'MOVE', |
| ZIP: 'ZIP', |
| }; |
| Object.freeze(util.FileOperationType); |
| |
| /** |
| * The type of a file operation error. |
| * @enum {number} |
| * @const |
| */ |
| util.FileOperationErrorType = { |
| UNEXPECTED_SOURCE_FILE: 0, |
| TARGET_EXISTS: 1, |
| FILESYSTEM_ERROR: 2, |
| }; |
| Object.freeze(util.FileOperationErrorType); |
| |
| /** |
| * The kind of an entry changed event. |
| * @enum {number} |
| * @const |
| */ |
| util.EntryChangedKind = { |
| CREATED: 0, |
| DELETED: 1, |
| }; |
| Object.freeze(util.EntryChangedKind); |
| |
| /** |
| * Obtains whether an entry is fake or not. |
| * @param {(!Entry|!FilesAppEntry)} entry Entry or a fake entry. |
| * @return {boolean} True if the given entry is fake. |
| * @suppress {missingProperties} Closure compiler doesn't allow to call isNative |
| * on Entry which is native and thus doesn't define this property, however we |
| * handle undefined accordingly. |
| * TODO(lucmult): Remove @suppress once all entries are sub-type of |
| * FilesAppEntry. |
| */ |
| util.isFakeEntry = entry => { |
| return ( |
| entry.getParent === undefined || |
| (entry.isNativeType !== undefined && !entry.isNativeType)); |
| }; |
| |
| /** |
| * Obtains whether an entry is the root directory of a Shared Drive. |
| * @param {Entry|FilesAppEntry} entry Entry or a fake entry. |
| * @return {boolean} True if the given entry is root of a Shared Drive. |
| */ |
| util.isTeamDriveRoot = entry => { |
| if (entry === null) { |
| return false; |
| } |
| if (!entry.fullPath) { |
| return false; |
| } |
| const tree = entry.fullPath.split('/'); |
| return tree.length == 3 && util.isSharedDriveEntry(entry); |
| }; |
| |
| /** |
| * Obtains whether an entry is the grand root directory of Shared Drives. |
| * @param {(!Entry|!FakeEntry)} entry Entry or a fake entry. |
| * @return {boolean} True if the given entry is the grand root of Shared Drives. |
| */ |
| util.isTeamDrivesGrandRoot = entry => { |
| if (!entry.fullPath) { |
| return false; |
| } |
| const tree = entry.fullPath.split('/'); |
| return tree.length == 2 && util.isSharedDriveEntry(entry); |
| }; |
| |
| /** |
| * Obtains whether an entry is descendant of the Shared Drives directory. |
| * @param {!Entry|!FilesAppEntry} entry Entry or a fake entry. |
| * @return {boolean} True if the given entry is under Shared Drives. |
| */ |
| util.isSharedDriveEntry = entry => { |
| if (!entry.fullPath) { |
| return false; |
| } |
| const tree = entry.fullPath.split('/'); |
| return tree[0] == '' && |
| tree[1] == VolumeManagerCommon.SHARED_DRIVES_DIRECTORY_NAME; |
| }; |
| |
| /** |
| * Extracts Shared Drive name from entry path. |
| * @param {(!Entry|!FakeEntry)} entry Entry or a fake entry. |
| * @return {string} The name of Shared Drive. Empty string if |entry| is not |
| * under Shared Drives. |
| */ |
| util.getTeamDriveName = entry => { |
| if (!entry.fullPath || !util.isSharedDriveEntry(entry)) { |
| return ''; |
| } |
| const tree = entry.fullPath.split('/'); |
| if (tree.length < 3) { |
| return ''; |
| } |
| return tree[2]; |
| }; |
| |
| /** |
| * Returns true if the given entry is the root folder of recent files. |
| * @param {!Entry|!FilesAppEntry} entry Entry or a fake entry. |
| * @returns {boolean} |
| */ |
| util.isRecentRoot = entry => { |
| return util.isFakeEntry(entry) && |
| entry.rootType == VolumeManagerCommon.RootType.RECENT; |
| }; |
| |
| /** |
| * Obtains whether an entry is the root directory of a Computer. |
| * @param {Entry|FilesAppEntry} entry Entry or a fake entry. |
| * @return {boolean} True if the given entry is root of a Computer. |
| */ |
| util.isComputersRoot = entry => { |
| if (entry === null) { |
| return false; |
| } |
| if (!entry.fullPath) { |
| return false; |
| } |
| const tree = entry.fullPath.split('/'); |
| return tree.length == 3 && util.isComputersEntry(entry); |
| }; |
| |
| /** |
| * Obtains whether an entry is descendant of the My Computers directory. |
| * @param {!Entry|!FilesAppEntry} entry Entry or a fake entry. |
| * @return {boolean} True if the given entry is under My Computers. |
| */ |
| util.isComputersEntry = entry => { |
| if (!entry.fullPath) { |
| return false; |
| } |
| const tree = entry.fullPath.split('/'); |
| return tree[0] == '' && |
| tree[1] == VolumeManagerCommon.COMPUTERS_DIRECTORY_NAME; |
| }; |
| |
| /** |
| * Creates an instance of UserDOMError subtype of DOMError because DOMError is |
| * deprecated and its Closure extern is wrong, doesn't have the constructor |
| * with 2 arguments. This DOMError looks like a FileError except that it does |
| * not have the deprecated FileError.code member. |
| * |
| * @param {string} name Error name for the file error. |
| * @param {string=} opt_message optional message. |
| * @return {DOMError} DOMError instance |
| */ |
| util.createDOMError = (name, opt_message) => { |
| return new util.UserDOMError(name, opt_message); |
| }; |
| |
| /** |
| * Creates a DOMError-like object to be used in place of returning file errors. |
| */ |
| util.UserDOMError = class UserDOMError extends DOMError { |
| /** |
| * @param {string} name Error name for the file error. |
| * @param {string=} opt_message Optional message for this error. |
| * @suppress {checkTypes} Closure externs for DOMError doesn't have |
| * constructor with 2 args. |
| */ |
| constructor(name, opt_message) { |
| super(name, opt_message); |
| |
| /** |
| * @type {string} |
| * @private |
| */ |
| this.name_ = name; |
| |
| /** |
| * @type {string} |
| * @private |
| */ |
| this.message_ = opt_message || ''; |
| Object.freeze(this); |
| } |
| |
| /** |
| * @return {string} File error name. |
| */ |
| get name() { |
| return this.name_; |
| } |
| |
| /** |
| * @return {string} Error message. |
| */ |
| get message() { |
| return this.message_; |
| } |
| }; |
| |
| /** |
| * Compares two entries. |
| * @param {Entry|FilesAppEntry} entry1 The entry to be compared. Can |
| * be a fake. |
| * @param {Entry|FilesAppEntry} entry2 The entry to be compared. Can |
| * be a fake. |
| * @return {boolean} True if the both entry represents a same file or |
| * directory. Returns true if both entries are null. |
| */ |
| util.isSameEntry = (entry1, entry2) => { |
| if (!entry1 && !entry2) { |
| return true; |
| } |
| if (!entry1 || !entry2) { |
| return false; |
| } |
| return entry1.toURL() === entry2.toURL(); |
| }; |
| |
| /** |
| * Compares two entry arrays. |
| * @param {Array<!Entry>} entries1 The entry array to be compared. |
| * @param {Array<!Entry>} entries2 The entry array to be compared. |
| * @return {boolean} True if the both arrays contain same files or directories |
| * in the same order. Returns true if both arrays are null. |
| */ |
| util.isSameEntries = (entries1, entries2) => { |
| if (!entries1 && !entries2) { |
| return true; |
| } |
| if (!entries1 || !entries2) { |
| return false; |
| } |
| if (entries1.length !== entries2.length) { |
| return false; |
| } |
| for (let i = 0; i < entries1.length; i++) { |
| if (!util.isSameEntry(entries1[i], entries2[i])) { |
| return false; |
| } |
| } |
| return true; |
| }; |
| |
| /** |
| * Compares two file systems. |
| * @param {FileSystem} fileSystem1 The file system to be compared. |
| * @param {FileSystem} fileSystem2 The file system to be compared. |
| * @return {boolean} True if the both file systems are equal. Also, returns true |
| * if both file systems are null. |
| */ |
| util.isSameFileSystem = (fileSystem1, fileSystem2) => { |
| if (!fileSystem1 && !fileSystem2) { |
| return true; |
| } |
| if (!fileSystem1 || !fileSystem2) { |
| return false; |
| } |
| return util.isSameEntry(fileSystem1.root, fileSystem2.root); |
| }; |
| |
| /** |
| * Checks if given two entries are in the same directory. |
| * @param {!Entry} entry1 |
| * @param {!Entry} entry2 |
| * @return {boolean} True if given entries are in the same directory. |
| */ |
| util.isSiblingEntry = (entry1, entry2) => { |
| const path1 = entry1.fullPath.split('/'); |
| const path2 = entry2.fullPath.split('/'); |
| if (path1.length != path2.length) { |
| return false; |
| } |
| for (let i = 0; i < path1.length - 1; i++) { |
| if (path1[i] != path2[i]) { |
| return false; |
| } |
| } |
| return true; |
| }; |
| |
| /** |
| * Collator for sorting. |
| * @type {Intl.Collator} |
| */ |
| util.collator = |
| new Intl.Collator([], {usage: 'sort', numeric: true, sensitivity: 'base'}); |
| |
| /** |
| * Compare by name. The 2 entries must be in same directory. |
| * @param {Entry|FilesAppEntry} entry1 First entry. |
| * @param {Entry|FilesAppEntry} entry2 Second entry. |
| * @return {number} Compare result. |
| */ |
| util.compareName = (entry1, entry2) => { |
| return util.collator.compare(entry1.name, entry2.name); |
| }; |
| |
| /** |
| * Compare by label (i18n name). The 2 entries must be in same directory. |
| * @param {EntryLocation} locationInfo |
| * @param {!Entry|!FilesAppEntry} entry1 First entry. |
| * @param {!Entry|!FilesAppEntry} entry2 Second entry. |
| * @return {number} Compare result. |
| */ |
| util.compareLabel = (locationInfo, entry1, entry2) => { |
| return util.collator.compare( |
| util.getEntryLabel(locationInfo, entry1), |
| util.getEntryLabel(locationInfo, entry2)); |
| }; |
| |
| /** |
| * Compare by path. |
| * @param {Entry|FilesAppEntry} entry1 First entry. |
| * @param {Entry|FilesAppEntry} entry2 Second entry. |
| * @return {number} Compare result. |
| */ |
| util.comparePath = (entry1, entry2) => { |
| return util.collator.compare(entry1.fullPath, entry2.fullPath); |
| }; |
| |
| /** |
| * @param {EntryLocation} locationInfo |
| * @param {!Array<Entry|FilesAppEntry>} bottomEntries entries that should be |
| * grouped in the bottom, used for sorting Linux and Play files entries after |
| * other folders in MyFiles. |
| * return {function(Entry|FilesAppEntry, Entry|FilesAppEntry) to compare entries |
| * by name. |
| */ |
| util.compareLabelAndGroupBottomEntries = (locationInfo, bottomEntries) => { |
| const childrenMap = new Map(); |
| bottomEntries.forEach((entry) => { |
| childrenMap.set(entry.toURL(), entry); |
| }); |
| |
| /** |
| * Compare entries putting entries from |bottomEntries| in the bottom and |
| * sort by name within entries that are the same type in regards to |
| * |bottomEntries|. |
| * @param {Entry|FilesAppEntry} entry1 First entry. |
| * @param {Entry|FilesAppEntry} entry2 First entry. |
| */ |
| function compare_(entry1, entry2) { |
| // Bottom entry here means Linux or Play files, which should appear after |
| // all native entries. |
| const isBottomlEntry1 = childrenMap.has(entry1.toURL()) ? 1 : 0; |
| const isBottomlEntry2 = childrenMap.has(entry2.toURL()) ? 1 : 0; |
| |
| // When there are the same type, just compare by label. |
| if (isBottomlEntry1 === isBottomlEntry2) { |
| return util.compareLabel(locationInfo, entry1, entry2); |
| } |
| |
| return isBottomlEntry1 - isBottomlEntry2; |
| } |
| |
| return compare_; |
| }; |
| |
| /** |
| * Checks if {@code entry} is an immediate child of {@code directory}. |
| * |
| * @param {Entry} entry The presumptive child. |
| * @param {DirectoryEntry|FilesAppEntry} directory The presumptive |
| * parent. |
| * @return {!Promise<boolean>} Resolves with true if {@code directory} is |
| * parent of {@code entry}. |
| */ |
| util.isChildEntry = (entry, directory) => { |
| return new Promise((resolve, reject) => { |
| if (!entry || !directory) { |
| resolve(false); |
| } |
| |
| entry.getParent(parent => { |
| resolve(util.isSameEntry(parent, directory)); |
| }, reject); |
| }); |
| }; |
| |
| /** |
| * Checks if the child entry is a descendant of another entry. If the entries |
| * point to the same file or directory, then returns false. |
| * |
| * @param {!DirectoryEntry|!FilesAppEntry} ancestorEntry The ancestor |
| * directory entry. Can be a fake. |
| * @param {!Entry|!FilesAppEntry} childEntry The child entry. Can be a fake. |
| * @return {boolean} True if the child entry is contained in the ancestor path. |
| */ |
| util.isDescendantEntry = (ancestorEntry, childEntry) => { |
| if (!ancestorEntry.isDirectory) { |
| return false; |
| } |
| |
| // For EntryList and VolumeEntry they can contain entries from different |
| // files systems, so we should check its getUIChildren. |
| const entryList = util.toEntryList(ancestorEntry); |
| if (entryList.getUIChildren) { |
| // VolumeEntry has to check to root entry descendant entry. |
| const nativeEntry = entryList.getNativeEntry(); |
| if (nativeEntry && |
| util.isSameFileSystem(nativeEntry.filesystem, childEntry.filesystem)) { |
| return util.isDescendantEntry( |
| /** @type {!DirectoryEntry} */ (nativeEntry), childEntry); |
| } |
| |
| return entryList.getUIChildren().some(ancestorChild => { |
| if (util.isSameEntry(ancestorChild, childEntry)) { |
| return true; |
| } |
| |
| // root entry might not be resolved yet. |
| const volumeEntry = |
| /** @type {DirectoryEntry} */ (ancestorChild.getNativeEntry()); |
| return volumeEntry && |
| (util.isSameEntry(volumeEntry, childEntry) || |
| util.isDescendantEntry(volumeEntry, childEntry)); |
| }); |
| } |
| |
| if (!util.isSameFileSystem(ancestorEntry.filesystem, childEntry.filesystem)) { |
| return false; |
| } |
| if (util.isSameEntry(ancestorEntry, childEntry)) { |
| return false; |
| } |
| if (util.isFakeEntry(ancestorEntry) || util.isFakeEntry(childEntry)) { |
| return false; |
| } |
| |
| // Check if the ancestor's path with trailing slash is a prefix of child's |
| // path. |
| let ancestorPath = ancestorEntry.fullPath; |
| if (ancestorPath.slice(-1) !== '/') { |
| ancestorPath += '/'; |
| } |
| return childEntry.fullPath.indexOf(ancestorPath) === 0; |
| }; |
| |
| /** |
| * The last URL with visitURL(). |
| * |
| * @type {string} |
| * @private |
| */ |
| util.lastVisitedURL; |
| |
| /** |
| * Visit the URL. |
| * |
| * If the browser is opening, the url is opened in a new tag, otherwise the url |
| * is opened in a new window. |
| * |
| * @param {!string} url URL to visit. |
| */ |
| util.visitURL = url => { |
| util.lastVisitedURL = url; |
| window.open(url); |
| }; |
| |
| /** |
| * Return the last URL visited with visitURL(). |
| * |
| * @return {string} The last URL visited. |
| */ |
| util.getLastVisitedURL = () => { |
| return util.lastVisitedURL; |
| }; |
| |
| |
| /** |
| * Returns normalized current locale, or default locale - 'en'. |
| * @return {string} Current locale |
| */ |
| util.getCurrentLocaleOrDefault = () => { |
| // chrome.i18n.getMessage('@@ui_locale') can't be used in packed app. |
| // Instead, we pass it from C++-side with strings. |
| return str('UI_LOCALE') || 'en'; |
| }; |
| |
| /** |
| * Converts array of entries to an array of corresponding URLs. |
| * @param {Array<Entry>} entries Input array of entries. |
| * @return {!Array<string>} Output array of URLs. |
| */ |
| util.entriesToURLs = entries => { |
| return entries.map(entry => { |
| // When building background.js, cachedUrl is not refered other than here. |
| // Thus closure compiler raises an error if we refer the property like |
| // entry.cachedUrl. |
| return entry['cachedUrl'] || entry.toURL(); |
| }); |
| }; |
| |
| /** |
| * Converts array of URLs to an array of corresponding Entries. |
| * |
| * @param {Array<string>} urls Input array of URLs. |
| * @param {function(!Array<!Entry>, !Array<!URL>)=} opt_callback Completion |
| * callback with array of success Entries and failure URLs. |
| * @return {Promise} Promise fulfilled with the object that has entries property |
| * and failureUrls property. The promise is never rejected. |
| */ |
| util.URLsToEntries = (urls, opt_callback) => { |
| const promises = urls.map(url => { |
| return new Promise(window.webkitResolveLocalFileSystemURL.bind(null, url)) |
| .then( |
| entry => { |
| return {entry: entry}; |
| }, |
| failureUrl => { |
| // Not an error. Possibly, the file is not accessible anymore. |
| console.warn('Failed to resolve the file with url: ' + url + '.'); |
| return {failureUrl: url}; |
| }); |
| }); |
| const resultPromise = Promise.all(promises).then(results => { |
| const entries = []; |
| const failureUrls = []; |
| for (let i = 0; i < results.length; i++) { |
| if ('entry' in results[i]) { |
| entries.push(results[i].entry); |
| } |
| if ('failureUrl' in results[i]) { |
| failureUrls.push(results[i].failureUrl); |
| } |
| } |
| return { |
| entries: entries, |
| failureUrls: failureUrls, |
| }; |
| }); |
| |
| // Invoke the callback. If opt_callback is specified, resultPromise is still |
| // returned and fulfilled with a result. |
| if (opt_callback) { |
| resultPromise |
| .then(result => { |
| opt_callback(result.entries, result.failureUrls); |
| }) |
| .catch(error => { |
| console.error( |
| 'util.URLsToEntries is failed.', |
| error.stack ? error.stack : error); |
| }); |
| } |
| |
| return resultPromise; |
| }; |
| |
| /** |
| * Converts a url into an {!Entry}, if possible. |
| * |
| * @param {string} url |
| * |
| * @return {!Promise<!Entry>} Promise Resolves with the corresponding |
| * {!Entry} if possible, else rejects. |
| */ |
| util.urlToEntry = url => { |
| return new Promise(window.webkitResolveLocalFileSystemURL.bind(null, url)); |
| }; |
| |
| /** |
| * Returns whether the window is teleported or not. |
| * @param {Window} window Window. |
| * @return {Promise<boolean>} Whether the window is teleported or not. |
| */ |
| util.isTeleported = window => { |
| return new Promise(onFulfilled => { |
| window.chrome.fileManagerPrivate.getProfiles( |
| (profiles, currentId, displayedId) => { |
| onFulfilled(currentId !== displayedId); |
| }); |
| }); |
| }; |
| |
| /** |
| * Runs chrome.test.sendMessage in test environment. Does nothing if running |
| * in production environment. |
| * |
| * @param {string} message Test message to send. |
| */ |
| util.testSendMessage = message => { |
| const test = chrome.test || window.top.chrome.test; |
| if (test) { |
| test.sendMessage(message); |
| } |
| }; |
| |
| /** |
| * Extracts the extension of the path. |
| * |
| * Examples: |
| * util.splitExtension('abc.ext') -> ['abc', '.ext'] |
| * util.splitExtension('a/b/abc.ext') -> ['a/b/abc', '.ext'] |
| * util.splitExtension('a/b') -> ['a/b', ''] |
| * util.splitExtension('.cshrc') -> ['', '.cshrc'] |
| * util.splitExtension('a/b.backup/hoge') -> ['a/b.backup/hoge', ''] |
| * |
| * @param {string} path Path to be extracted. |
| * @return {Array<string>} Filename and extension of the given path. |
| */ |
| util.splitExtension = path => { |
| let dotPosition = path.lastIndexOf('.'); |
| if (dotPosition <= path.lastIndexOf('/')) { |
| dotPosition = -1; |
| } |
| |
| const filename = dotPosition != -1 ? path.substr(0, dotPosition) : path; |
| const extension = dotPosition != -1 ? path.substr(dotPosition) : ''; |
| return [filename, extension]; |
| }; |
| |
| /** |
| * Returns the localized name of the root type. |
| * @param {!EntryLocation} locationInfo Location info. |
| * @return {string} The localized name. |
| */ |
| util.getRootTypeLabel = locationInfo => { |
| switch (locationInfo.rootType) { |
| case VolumeManagerCommon.RootType.DOWNLOADS: |
| return locationInfo.volumeInfo.label; |
| case VolumeManagerCommon.RootType.DRIVE: |
| return str('DRIVE_MY_DRIVE_LABEL'); |
| case VolumeManagerCommon.RootType.SHARED_DRIVE: |
| // |locationInfo| points to either the root directory of an individual Team |
| // Drive or subdirectory under it, but not the Shared Drives grand |
| // directory. Every Shared Drive and its subdirectories always have |
| // individual names (locationInfo.hasFixedLabel is false). So |
| // getRootTypeLabel() is only used by LocationLine.show() to display the |
| // ancestor name in the location line like this: |
| // Shared Drives > ABC Shared Drive > Folder1 |
| // ^^^^^^^^^^^ |
| // By this reason, we return the label of the Shared Drives grand root here. |
| case VolumeManagerCommon.RootType.SHARED_DRIVES_GRAND_ROOT: |
| return str('DRIVE_SHARED_DRIVES_LABEL'); |
| case VolumeManagerCommon.RootType.COMPUTER: |
| case VolumeManagerCommon.RootType.COMPUTERS_GRAND_ROOT: |
| return str('DRIVE_COMPUTERS_LABEL'); |
| case VolumeManagerCommon.RootType.DRIVE_OFFLINE: |
| return str('DRIVE_OFFLINE_COLLECTION_LABEL'); |
| case VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME: |
| return str('DRIVE_SHARED_WITH_ME_COLLECTION_LABEL'); |
| case VolumeManagerCommon.RootType.DRIVE_RECENT: |
| return str('DRIVE_RECENT_COLLECTION_LABEL'); |
| case VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT: |
| return str('DRIVE_DIRECTORY_LABEL'); |
| case VolumeManagerCommon.RootType.RECENT: |
| return str('RECENT_ROOT_LABEL'); |
| case VolumeManagerCommon.RootType.CROSTINI: |
| return str('LINUX_FILES_ROOT_LABEL'); |
| case VolumeManagerCommon.RootType.MY_FILES: |
| return str('MY_FILES_ROOT_LABEL'); |
| case VolumeManagerCommon.RootType.MEDIA_VIEW: |
| const mediaViewRootType = |
| VolumeManagerCommon.getMediaViewRootTypeFromVolumeId( |
| locationInfo.volumeInfo.volumeId); |
| switch (mediaViewRootType) { |
| case VolumeManagerCommon.MediaViewRootType.IMAGES: |
| return str('MEDIA_VIEW_IMAGES_ROOT_LABEL'); |
| case VolumeManagerCommon.MediaViewRootType.VIDEOS: |
| return str('MEDIA_VIEW_VIDEOS_ROOT_LABEL'); |
| case VolumeManagerCommon.MediaViewRootType.AUDIO: |
| return str('MEDIA_VIEW_AUDIO_ROOT_LABEL'); |
| } |
| console.error('Unsupported media view root type: ' + mediaViewRootType); |
| return locationInfo.volumeInfo.label; |
| case VolumeManagerCommon.RootType.DRIVE_OTHER: |
| case VolumeManagerCommon.RootType.ARCHIVE: |
| case VolumeManagerCommon.RootType.REMOVABLE: |
| case VolumeManagerCommon.RootType.MTP: |
| case VolumeManagerCommon.RootType.PROVIDED: |
| case VolumeManagerCommon.RootType.ANDROID_FILES: |
| case VolumeManagerCommon.RootType.DOCUMENTS_PROVIDER: |
| return locationInfo.volumeInfo.label; |
| default: |
| console.error('Unsupported root type: ' + locationInfo.rootType); |
| return locationInfo.volumeInfo.label; |
| } |
| }; |
| |
| /** |
| * Returns the localized/i18n name of the entry. |
| * |
| * @param {EntryLocation} locationInfo |
| * @param {!Entry|!FilesAppEntry} entry The entry to be retrieve the name of. |
| * @return {string} The localized name. |
| */ |
| util.getEntryLabel = (locationInfo, entry) => { |
| if (locationInfo && locationInfo.hasFixedLabel) { |
| return util.getRootTypeLabel(locationInfo); |
| } |
| |
| // Special case for MyFiles/Downloads and MyFiles/PvmDefault. |
| if (locationInfo && |
| locationInfo.rootType == VolumeManagerCommon.RootType.DOWNLOADS) { |
| if (util.isMyFilesVolumeEnabled() && entry.fullPath == '/Downloads') { |
| return str('DOWNLOADS_DIRECTORY_LABEL'); |
| } |
| if (util.isPluginVmEnabled() && entry.fullPath == '/PvmDefault') { |
| return str('PLUGIN_VM_DIRECTORY_LABEL'); |
| } |
| } |
| |
| return entry.name; |
| }; |
| |
| /** |
| * Returns true if specified entry is a special entry such as MyFiles/Downloads, |
| * MyFiles/PvmDefault or Linux files root which cannot be modified such as |
| * deleted/cut or renamed. |
| * |
| * @param {!VolumeManager} volumeManager |
| * @param {(Entry|FakeEntry)} entry Entry or a fake entry. |
| * @return {boolean} |
| */ |
| util.isNonModifiable = (volumeManager, entry) => { |
| if (!entry) { |
| return false; |
| } |
| if (util.isFakeEntry(entry)) { |
| return true; |
| } |
| |
| // If the entry is not a valid entry. |
| if (!volumeManager) { |
| return false; |
| } |
| |
| const volumeInfo = volumeManager.getVolumeInfo(entry); |
| if (!volumeInfo) { |
| return false; |
| } |
| |
| if (volumeInfo.volumeType === VolumeManagerCommon.RootType.DOWNLOADS) { |
| if (util.isMyFilesVolumeEnabled() && entry.fullPath === '/Downloads') { |
| return true; |
| } |
| if (util.isPluginVmEnabled() && entry.fullPath === '/PvmDefault') { |
| return true; |
| } |
| } |
| |
| if (volumeInfo.volumeType === VolumeManagerCommon.RootType.CROSTINI && |
| entry.fullPath === '/') { |
| return true; |
| } |
| |
| return false; |
| }; |
| |
| /** |
| * Checks if the specified set of allowed effects contains the given effect. |
| * See: http://www.w3.org/TR/html5/editing.html#the-datatransfer-interface |
| * |
| * @param {string} effectAllowed The string denoting the set of allowed effects. |
| * @param {string} dropEffect The effect to be checked. |
| * @return {boolean} True if |dropEffect| is included in |effectAllowed|. |
| */ |
| util.isDropEffectAllowed = (effectAllowed, dropEffect) => { |
| return effectAllowed === 'all' || |
| effectAllowed.toLowerCase().indexOf(dropEffect) !== -1; |
| }; |
| |
| /** |
| * Checks if the specified character is printable ASCII. |
| * |
| * @param {string} character The input character. |
| * @return {boolean} True if |character| is printable ASCII, else false. |
| */ |
| util.isPrintable = character => { |
| if (character.length != 1) { |
| return false; |
| } |
| |
| const charCode = character.charCodeAt(0); |
| return charCode >= 32 && charCode <= 126; |
| }; |
| |
| /** |
| * Verifies the user entered name for file or folder to be created or |
| * renamed to. Name restrictions must correspond to File API restrictions |
| * (see DOMFilePath::isValidPath). Curernt WebKit implementation is |
| * out of date (spec is |
| * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to |
| * be fixed. Shows message box if the name is invalid. |
| * |
| * It also verifies if the name length is in the limit of the filesystem. |
| * |
| * @param {!DirectoryEntry} parentEntry The entry of the parent directory. |
| * @param {string} name New file or folder name. |
| * @param {boolean} filterHiddenOn Whether to report the hidden file name error |
| * or not. |
| * @return {Promise} Promise fulfilled on success, or rejected with the error |
| * message. |
| */ |
| util.validateFileName = (parentEntry, name, filterHiddenOn) => { |
| const testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name); |
| let msg; |
| if (testResult) { |
| return Promise.reject(strf('ERROR_INVALID_CHARACTER', testResult[0])); |
| } else if (/^\s*$/i.test(name)) { |
| return Promise.reject(str('ERROR_WHITESPACE_NAME')); |
| } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) { |
| return Promise.reject(str('ERROR_RESERVED_NAME')); |
| } else if (filterHiddenOn && /\.crdownload$/i.test(name)) { |
| return Promise.reject(str('ERROR_RESERVED_NAME')); |
| } else if (filterHiddenOn && name[0] == '.') { |
| return Promise.reject(str('ERROR_HIDDEN_NAME')); |
| } |
| |
| return new Promise((fulfill, reject) => { |
| chrome.fileManagerPrivate.validatePathNameLength( |
| parentEntry, name, valid => { |
| if (valid) { |
| fulfill(null); |
| } else { |
| reject(str('ERROR_LONG_NAME')); |
| } |
| }); |
| }); |
| }; |
| |
| /** |
| * Verifies the user entered name for external drive to be |
| * renamed to. Name restrictions must correspond to the target filesystem |
| * restrictions. |
| * |
| * It also verifies that name length is in the limits of the filesystem. |
| * |
| * @param {string} name New external drive name. |
| * @param {!VolumeInfo} volumeInfo |
| * @return {Promise} Promise fulfilled on success, or rejected with the error |
| * message. |
| */ |
| util.validateExternalDriveName = (name, volumeInfo) => { |
| // Verify if entered name for external drive respects restrictions provided by |
| // the target filesystem |
| |
| const fileSystem = volumeInfo.diskFileSystemType; |
| const nameLength = name.length; |
| const lengthLimit = VolumeManagerCommon.FileSystemTypeVolumeNameLengthLimit; |
| |
| // Verify length for the target file system type |
| if (lengthLimit.hasOwnProperty(fileSystem) && |
| nameLength > lengthLimit[fileSystem]) { |
| return Promise.reject( |
| strf('ERROR_EXTERNAL_DRIVE_LONG_NAME', lengthLimit[fileSystem])); |
| } |
| |
| // Checks if name contains only printable ASCII (from ' ' to '~') |
| for (let i = 0; i < nameLength; i++) { |
| if (!util.isPrintable(name[i])) { |
| return Promise.reject( |
| strf('ERROR_EXTERNAL_DRIVE_INVALID_CHARACTER', name[i])); |
| } |
| } |
| |
| const containsForbiddenCharacters = |
| /[\*\?\.\,\;\:\/\\\|\+\=\<\>\[\]\"\'\t]/.exec(name); |
| if (containsForbiddenCharacters) { |
| return Promise.reject(strf( |
| 'ERROR_EXTERNAL_DRIVE_INVALID_CHARACTER', |
| containsForbiddenCharacters[0])); |
| } |
| |
| return Promise.resolve(); |
| }; |
| |
| /** |
| * Adds a foreground listener to the background page components. |
| * The listener will be removed when the foreground window is closed. |
| * @param {!EventTarget} target |
| * @param {string} type |
| * @param {Function} handler |
| */ |
| util.addEventListenerToBackgroundComponent = (target, type, handler) => { |
| target.addEventListener(type, handler); |
| window.addEventListener('pagehide', () => { |
| target.removeEventListener(type, handler); |
| }); |
| }; |
| |
| /** |
| * Checks if an API call returned an error, and if yes then prints it. |
| */ |
| util.checkAPIError = () => { |
| if (chrome.runtime.lastError) { |
| console.error(chrome.runtime.lastError.message); |
| } |
| }; |
| |
| /** |
| * Makes a promise which will be fulfilled |ms| milliseconds later. |
| * @param {number} ms The delay in milliseconds. |
| * @return {!Promise} |
| */ |
| util.delay = ms => { |
| return new Promise(resolve => { |
| setTimeout(resolve, ms); |
| }); |
| }; |
| |
| /** |
| * Makes a promise which will be rejected if the given |promise| is not resolved |
| * or rejected for |ms| milliseconds. |
| * @param {!Promise} promise A promise which needs to be timed out. |
| * @param {number} ms Delay for the timeout in milliseconds. |
| * @param {string=} opt_message Error message for the timeout. |
| * @return {!Promise} A promise which can be rejected by timeout. |
| */ |
| util.timeoutPromise = (promise, ms, opt_message) => { |
| return Promise.race([ |
| promise, util.delay(ms).then(() => { |
| throw new Error(opt_message || 'Operation timed out.'); |
| }) |
| ]); |
| }; |
| |
| /** |
| * Examines whether the feedback panel mode is enabled. |
| * @return {boolean} True if the feedback panel UI mode is enabled. |
| */ |
| util.isFeedbackPanelEnabled = () => { |
| return loadTimeData.getBoolean('FEEDBACK_PANEL_ENABLED'); |
| }; |
| |
| /** |
| * Retrieves all entries inside the given |rootEntry|. |
| * @param {!DirectoryEntry} rootEntry |
| * @param {function(!Array<!Entry>)} entriesCallback Called when some chunk of |
| * entries are read. This can be called a couple of times until the |
| * completion. |
| * @param {function()} successCallback Called when the read is completed. |
| * @param {function(DOMError)} errorCallback Called when an error occurs. |
| * @param {function():boolean} shouldStop Callback to check if the read process |
| * should stop or not. When this callback is called and it returns true, |
| * the remaining recursive reads will be aborted. |
| * @param {number=} opt_maxDepth Max depth to delve directories recursively. |
| * If 0 is specified, only the rootEntry will be read. If -1 is specified |
| * or opt_maxDepth is unspecified, the depth of recursion is unlimited. |
| */ |
| util.readEntriesRecursively = |
| (rootEntry, entriesCallback, successCallback, errorCallback, shouldStop, |
| opt_maxDepth) => { |
| let numRunningTasks = 0; |
| let error = null; |
| const maxDepth = opt_maxDepth === undefined ? -1 : opt_maxDepth; |
| const maybeRunCallback = () => { |
| if (numRunningTasks === 0) { |
| if (shouldStop()) { |
| errorCallback(util.createDOMError(util.FileError.ABORT_ERR)); |
| } else if (error) { |
| errorCallback(error); |
| } else { |
| successCallback(); |
| } |
| } |
| }; |
| const processEntry = (entry, depth) => { |
| const onError = fileError => { |
| if (!error) { |
| error = fileError; |
| } |
| numRunningTasks--; |
| maybeRunCallback(); |
| }; |
| const onSuccess = entries => { |
| if (shouldStop() || error || entries.length === 0) { |
| numRunningTasks--; |
| maybeRunCallback(); |
| return; |
| } |
| entriesCallback(entries); |
| for (let i = 0; i < entries.length; i++) { |
| if (entries[i].isDirectory && |
| (maxDepth === -1 || depth < maxDepth)) { |
| processEntry(entries[i], depth + 1); |
| } |
| } |
| // Read remaining entries. |
| reader.readEntries(onSuccess, onError); |
| }; |
| |
| numRunningTasks++; |
| const reader = entry.createReader(); |
| reader.readEntries(onSuccess, onError); |
| }; |
| |
| processEntry(rootEntry, 0); |
| }; |
| |
| /** |
| * Do not remove or modify. Used in vm.CrostiniFiles tast tests at: |
| * https://chromium.googlesource.com/chromiumos/platform/tast-tests |
| * |
| * Get all entries for the given volume. |
| * @param {!VolumeInfo} volumeInfo |
| * @return {!Promise<Object<Entry>>} all entries keyed by fullPath. |
| */ |
| util.getEntries = volumeInfo => { |
| const root = volumeInfo.fileSystem.root; |
| return new Promise((resolve, reject) => { |
| const allEntries = {'/': root}; |
| function entriesCallback(someEntries) { |
| someEntries.forEach(entry => { |
| allEntries[entry.fullPath] = entry; |
| }); |
| } |
| function successCallback() { |
| resolve(allEntries); |
| } |
| util.readEntriesRecursively( |
| root, entriesCallback, successCallback, reject, () => false); |
| }); |
| }; |
| |
| /** |
| * Executes a functions only when the context is not the incognito one in a |
| * regular session. |
| * @param {function()} callback |
| */ |
| util.doIfPrimaryContext = callback => { |
| chrome.fileManagerPrivate.getProfiles((profiles) => { |
| if ((profiles[0] && profiles[0].profileId == '$guest') || |
| !chrome.extension.inIncognitoContext) { |
| callback(); |
| } |
| }); |
| }; |
| |
| /** |
| * Casts an Entry to a FilesAppEntry, to access a FilesAppEntry-specific |
| * property without Closure compiler complaining. |
| * TODO(lucmult): Wrap Entry in a FilesAppEntry derived class and remove |
| * this function. https://crbug.com/835203. |
| * @param {Entry|FilesAppEntry} entry |
| * @return {FilesAppEntry} |
| */ |
| util.toFilesAppEntry = entry => { |
| return /** @type {FilesAppEntry} */ (entry); |
| }; |
| |
| /** |
| * Casts an Entry to a EntryList, to access a FilesAppEntry-specific |
| * property without Closure compiler complaining. |
| * @param {Entry|FilesAppEntry} entry |
| * @return {EntryList} |
| */ |
| util.toEntryList = entry => { |
| return /** @type {EntryList} */ (entry); |
| }; |
| |
| /** |
| * Returns true if entry is FileSystemEntry or FileSystemDirectoryEntry, it |
| * returns false if it's FakeEntry or any one of the FilesAppEntry types. |
| * TODO(lucmult): Wrap Entry in a FilesAppEntry derived class and remove |
| * this function. https://crbug.com/835203. |
| * @param {Entry|FilesAppEntry} entry |
| * @return {boolean} |
| */ |
| util.isNativeEntry = entry => { |
| entry = util.toFilesAppEntry(entry); |
| // Only FilesAppEntry types has |type_name| attribute. |
| return entry.type_name === undefined; |
| }; |
| |
| /** |
| * For FilesAppEntry types that wraps a native entry, returns the native entry |
| * to be able to send to fileManagerPrivate API. |
| * @param {Entry|FilesAppEntry} entry |
| * @return {Entry|FilesAppEntry} |
| */ |
| util.unwrapEntry = entry => { |
| if (!entry) { |
| return entry; |
| } |
| |
| const nativeEntry = entry.getNativeEntry && entry.getNativeEntry(); |
| if (nativeEntry) { |
| return nativeEntry; |
| } |
| |
| return entry; |
| }; |
| |
| /** @return {boolean} */ |
| util.isArcUsbStorageUIEnabled = () => { |
| return loadTimeData.valueExists('ARC_USB_STORAGE_UI_ENABLED') && |
| loadTimeData.getBoolean('ARC_USB_STORAGE_UI_ENABLED'); |
| }; |
| |
| /** @return {boolean} */ |
| util.isMyFilesVolumeEnabled = () => { |
| return loadTimeData.valueExists('MY_FILES_VOLUME_ENABLED') && |
| loadTimeData.getBoolean('MY_FILES_VOLUME_ENABLED'); |
| }; |
| |
| /** @return {boolean} */ |
| util.isPluginVmEnabled = () => { |
| return loadTimeData.valueExists('PLUGIN_VM_ENABLED') && |
| loadTimeData.getBoolean('PLUGIN_VM_ENABLED'); |
| }; |
| |
| /** |
| * Used for logs and debugging. It tries to tell what type is the entry, its |
| * path and URL. |
| * |
| * @param {Entry|FilesAppEntry} entry |
| * @return {string} |
| */ |
| util.entryDebugString = (entry) => { |
| if (entry === null) { |
| return 'entry is null'; |
| } |
| if (entry === undefined) { |
| return 'entry is undefined'; |
| } |
| let typeName = ''; |
| if (entry.constructor && entry.constructor.name) { |
| typeName = entry.constructor.name; |
| } else { |
| typeName = Object.prototype.toString.call(entry); |
| } |
| let entryDescription = '(' + typeName + ') '; |
| if (entry.fullPath) { |
| entryDescription = entryDescription + entry.fullPath + ' '; |
| } |
| if (entry.toURL) { |
| entryDescription = entryDescription + entry.toURL(); |
| } |
| return entryDescription; |
| }; |
| |
| /** |
| * Returns true if all entries belong to the same volume. If there are no |
| * entries it also returns false. |
| * |
| * @param {!Array<Entry|FilesAppEntry>} entries |
| * @param {!VolumeManager} volumeManager |
| * @return boolean |
| */ |
| util.isSameVolume = (entries, volumeManager) => { |
| if (!entries.length) { |
| return false; |
| } |
| |
| const firstEntry = entries[0]; |
| if (!firstEntry) { |
| return false; |
| } |
| const volumeInfo = volumeManager.getVolumeInfo(firstEntry); |
| |
| for (let i = 1; i < entries.length; i++) { |
| if (!entries[i]) { |
| return false; |
| } |
| const volumeInfoToCompare = volumeManager.getVolumeInfo(assert(entries[i])); |
| if (!volumeInfoToCompare || |
| volumeInfoToCompare.volumeId !== volumeInfo.volumeId) { |
| return false; |
| } |
| } |
| |
| return true; |
| }; |