Commit 7f5a8b83 authored by Joel Hockey's avatar Joel Hockey Committed by Commit Bot

FilesApp: support being a file drop target to crostini apps

Add support for FilesApp to be a drop target for files which have
originated dragging from other apps such as crostini, plugin vm, or arc.

FilesApp seems to be using custom fs/* mime types such as fs/sources
for DataTransfer used in drag and drop.  This appears to pass around
lists of entry URLs as strings. The URLs are later resolved to FileEntry
using webkitResolveLocalFileSystemURL.

When files are provided from other systems besides FilesApp, the URLs
provided are not compatible, but DataTransferItem.webkitGetAsEntry() can
be used to get a FileEntry which can be used in the drop-copy operation.

This CL now allows DataTransfer with type 'Files', and refactors
FileTransferController.PastePlan to allow sourceEntries to be provided
as an alternative to sourceURLs.

Bug: 1144138
Change-Id: I6b160eef7140d824b816076c71f1bd3a455a4415
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2542938Reviewed-by: default avatarLuciano Pacheco <lucmult@chromium.org>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#829073}
parent 0fa6b72f
......@@ -9,7 +9,7 @@
// #import * as wrappedVolumeManagerFactory from './volume_manager_factory.m.js'; const {volumeManagerFactory} = wrappedVolumeManagerFactory;
// #import {VolumeManagerImpl} from './volume_manager_impl.m.js';
// #import * as wrappedVolumeManagerCommon from '../../../base/js/volume_manager_types.m.js'; const {VolumeManagerCommon} = wrappedVolumeManagerCommon;
// #import {MockFileSystem} from '../../common/js/mock_entry.m.js';
// #import {MockEntry, MockFileSystem} from '../../common/js/mock_entry.m.js';
// #import * as wrappedUtil from '../../common/js/util.m.js'; const {util} = wrappedUtil;
// #import {str} from '../../common/js/util.m.js';
// #import {EntryLocation} from '../../../externs/entry_location.m.js';
......@@ -258,3 +258,43 @@ MockVolumeManager.prototype.findByDevicePath =
/** @override */
MockVolumeManager.prototype.whenVolumeInfoReady =
VolumeManagerImpl.prototype.whenVolumeInfoReady;
/**
* Used to override window.webkitResolveLocalFileSystemURL for testing. This
* emulates the real function by parsing `url` and finding the matching entry
* in `volumeManager`. E.g. filesystem:downloads/dir/file.txt will look up the
* 'downloads' volume for /dir/file.txt.
*
* @param {VolumeManager} volumeManager VolumeManager to resolve URLs with.
* @param {string} url URL to resolve.
* @param {function(!MockEntry)} successCallback Success callback.
* @param {function(!FileError)=} errorCallback Error callback.
*/
MockVolumeManager.resolveLocalFileSystemURL =
(volumeManager, url, successCallback, errorCallback) => {
const match = url.match(/^filesystem:(\w+)(\/.*)/);
if (match) {
const volumeType =
/** @type {VolumeManagerCommon.VolumeType} */ (match[1]);
let path = match[2];
const volume = volumeManager.getCurrentProfileVolumeInfo(volumeType);
if (volume) {
// Decode URI in file paths.
path = path.split('/').map(decodeURIComponent).join('/');
const entry = volume.fileSystem.entries[path];
if (entry) {
setTimeout(successCallback, 0, entry);
return;
}
}
}
const message =
`MockVolumeManager.resolveLocalFileSystemURL not found: ${url}`;
console.warn(message);
const error = new DOMException(message, 'NotFoundError');
if (errorCallback) {
setTimeout(errorCallback, 0, error);
} else {
throw error;
}
};
......@@ -693,7 +693,7 @@ class FileTasks {
this.fileTransferController_.executePaste(
new FileTransferController.PastePlan(
this.entries_.map(e => e.toURL()), pvmDir,
this.entries_.map(e => e.toURL()), [], pvmDir,
assert(this.volumeManager_.getLocationInfo(pvmDir)),
toMove));
this.directoryModel_.changeDirectoryEntry(pvmDir);
......
......@@ -425,20 +425,38 @@ class FileTransferController {
* Collects parameters of paste operation by the given command and the current
* system clipboard.
*
* @param {!DataTransfer} clipboardData System data transfer object.
* @param {DirectoryEntry=} opt_destinationEntry Paste destination.
* @param {string=} opt_effect Desired drop/paste effect. Could be
* 'move'|'copy' (default is copy). Ignored if conflicts with
* |clipboardData.effectAllowed|.
* @return {!FileTransferController.PastePlan}
*/
preparePaste(clipboardData, opt_destinationEntry, opt_effect) {
// When FilesApp does drag and drop to itself, it uses fs/sources to
// populate sourceURLs, and it will resolve sourceEntries later using
// webkitResolveLocalFileSystemURL().
const sourceURLs = clipboardData.getData('fs/sources') ?
clipboardData.getData('fs/sources').split('\n') :
[];
// When FilesApp is the paste target for other apps such as crostini,
// the file URL is either not provided, or it is not compatible. We use
// DataTransferItem.webkitGetAsEntry() to get the entry now.
const sourceEntries = sourceURLs.length === 0 ?
Array.prototype.filter.call(clipboardData.items, i => i.kind === 'file')
.map(i => i.webkitGetAsEntry()) :
[];
// effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers
// work fine.
const effectAllowed = clipboardData.effectAllowed !== 'uninitialized' ?
clipboardData.effectAllowed :
clipboardData.getData('fs/effectallowed');
const destinationEntry = opt_destinationEntry ||
const destinationEntry = assert(
opt_destinationEntry ||
/** @type {DirectoryEntry} */
(this.directoryModel_.getCurrentDirEntry());
(this.directoryModel_.getCurrentDirEntry()));
const toMove = util.isDropEffectAllowed(effectAllowed, 'move') &&
(!util.isDropEffectAllowed(effectAllowed, 'copy') ||
opt_effect === 'move');
......@@ -447,11 +465,12 @@ class FileTransferController {
this.volumeManager_.getLocationInfo(destinationEntry);
if (!destinationLocationInfo) {
console.log(
'Failed to get destination location for ' + destinationEntry.title() +
'Failed to get destination location for ' + destinationEntry.toURL() +
' while attempting to paste files.');
}
return new FileTransferController.PastePlan(
sourceURLs, destinationEntry, assert(destinationLocationInfo), toMove);
sourceURLs, sourceEntries, destinationEntry,
assert(destinationLocationInfo), toMove);
}
/**
......@@ -469,10 +488,8 @@ class FileTransferController {
const pastePlan =
this.preparePaste(clipboardData, opt_destinationEntry, opt_effect);
return FileTransferController.URLsToEntriesWithAccess(pastePlan.sourceURLs)
.then(entriesResult => {
const sourceEntries = entriesResult.entries;
return pastePlan.resolveEntries().then(
sourceEntries => {
if (sourceEntries.length == 0) {
// This can happen when copied files were deleted before pasting
// them. We execute the plan as-is, so as to share the post-copy
......@@ -480,13 +497,12 @@ class FileTransferController {
// same-directory entries.
return Promise.resolve(this.executePaste(pastePlan));
}
const confirmationType = pastePlan.getConfirmationType(sourceEntries);
const confirmationType = pastePlan.getConfirmationType();
if (confirmationType ==
FileTransferController.ConfirmationType.NONE) {
return Promise.resolve(this.executePaste(pastePlan));
}
const messages = pastePlan.getConfirmationMessages(
confirmationType, sourceEntries);
const messages = pastePlan.getConfirmationMessages(confirmationType);
this.confirmationCallback_(pastePlan.isMove, messages)
.then(userApproved => {
if (userApproved) {
......@@ -508,21 +524,16 @@ class FileTransferController {
const destinationEntry = pastePlan.destinationEntry;
let entries = [];
let failureUrls;
let shareEntries;
const taskId = this.fileOperationManager_.generateTaskId();
FileTransferController.URLsToEntriesWithAccess(sourceURLs)
.then(/**
* @param {Object} result
*/
result => {
failureUrls = result.failureUrls;
// The promise is not rejected, so it's safe to not remove the
// early progress center item here.
return this.fileOperationManager_.filterSameDirectoryEntry(
result.entries, destinationEntry, toMove);
})
pastePlan.resolveEntries()
.then(sourceEntries => {
// The promise is not rejected, so it's safe to not remove the
// early progress center item here.
return this.fileOperationManager_.filterSameDirectoryEntry(
sourceEntries, destinationEntry, toMove);
})
.then(/**
* @param {!Array<Entry>} filteredEntries
*/
......@@ -579,7 +590,7 @@ class FileTransferController {
entries, destinationEntry, toMove, taskId);
this.pendingTaskIds.splice(
this.pendingTaskIds.indexOf(taskId), 1);
})
})
.catch(error => {
if (error !== 'ABORT') {
console.error(error.stack ? error.stack : error);
......@@ -587,9 +598,9 @@ class FileTransferController {
})
.finally(() => {
// Publish source not found error item.
for (let i = 0; i < failureUrls.length; i++) {
const fileName =
decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
for (let i = 0; i < pastePlan.failureUrls.length; i++) {
const fileName = decodeURIComponent(
pastePlan.failureUrls[i].replace(/^.+\//, ''));
const item = new ProgressCenterItem();
item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
if (toMove) {
......@@ -1338,7 +1349,10 @@ class FileTransferController {
return false;
}
if (!clipboardData.types || clipboardData.types.indexOf('fs/tag') === -1) {
// DataTransfer type will be 'fs/tag' when the source was FilesApp, or
// 'Files' when the source was any other app.
const types = clipboardData.types;
if (!types || !(types.includes('fs/tag') || types.includes('Files'))) {
return false; // Unsupported type of content.
}
......@@ -1640,18 +1654,32 @@ FileTransferController.ConfirmationType = {
FileTransferController.PastePlan = class {
/**
* @param {!Array<string>} sourceURLs URLs of source entries.
* @param {!Array<!Entry>} sourceEntries Entries of source entries.
* @param {!DirectoryEntry} destinationEntry Destination directory.
* @param {!EntryLocation} destinationLocationInfo Location info of the
* destination directory.
* @param {boolean} isMove true if move, false if copy.
*/
constructor(sourceURLs, destinationEntry, destinationLocationInfo, isMove) {
constructor(
sourceURLs, sourceEntries, destinationEntry, destinationLocationInfo,
isMove) {
/**
* @type {!Array<string>}
* @const
*/
this.sourceURLs = sourceURLs;
/**
* @type {!Array<!Entry>}
*/
this.sourceEntries = sourceEntries;
/**
* Any URLs from sourceURLs which failed resolving to into sourceEntries.
* @type {!Array<string>}
*/
this.failureUrls = [];
/**
* @type {!DirectoryEntry}
*/
......@@ -1669,20 +1697,34 @@ FileTransferController.PastePlan = class {
this.isMove = isMove;
}
/**
* Resolves sourceEntries from sourceURLs if needed and returns them.
*
* @return {!Promise<!Array<!Entry>>}
*/
async resolveEntries() {
if (!this.sourceEntries.length) {
const result =
await FileTransferController.URLsToEntriesWithAccess(this.sourceURLs);
this.sourceEntries = result.entries;
this.failureUrls = result.failureUrls;
}
return this.sourceEntries;
}
/**
* Obtains whether the planned operation requires user's confirmation, as well
* as its type.
*
* @param {!Array<!Entry>} sourceEntries
* @return {FileTransferController.ConfirmationType} type of the confirmation
* required for the operation. If no confirmation is needed,
* FileTransferController.ConfirmationType.NONE will be returned.
*/
getConfirmationType(sourceEntries) {
assert(sourceEntries.length != 0);
getConfirmationType() {
assert(this.sourceEntries.length != 0);
const source = {
isTeamDrive: util.isSharedDriveEntry(sourceEntries[0]),
teamDriveName: util.getTeamDriveName(sourceEntries[0])
isTeamDrive: util.isSharedDriveEntry(this.sourceEntries[0]),
teamDriveName: util.getTeamDriveName(this.sourceEntries[0])
};
const destination = {
isTeamDrive: util.isSharedDriveEntry(this.destinationEntry),
......@@ -1727,9 +1769,9 @@ FileTransferController.PastePlan = class {
* @param {FileTransferController.ConfirmationType} confirmationType
* @return {!Array<string>} sentences for a confirmation dialog box.
*/
getConfirmationMessages(confirmationType, sourceEntries) {
assert(sourceEntries.length != 0);
const sourceName = util.getTeamDriveName(sourceEntries[0]);
getConfirmationMessages(confirmationType) {
assert(this.sourceEntries.length != 0);
const sourceName = util.getTeamDriveName(this.sourceEntries[0]);
const destinationName = util.getTeamDriveName(this.destinationEntry);
switch (confirmationType) {
case FileTransferController.ConfirmationType.MOVE_BETWEEN_SHARED_DRIVES:
......
......@@ -65,7 +65,10 @@ function setUp() {
enableExternalFileScheme: () => {},
getProfiles: (callback) => {
setTimeout(callback, 0, [], '', '');
}
},
grantAccess: (entryURLs, callback) => {
setTimeout(callback, 0);
},
},
};
installMockChrome(mockChrome);
......@@ -91,8 +94,11 @@ function setUp() {
// Fake DirectoryModel.
const directoryModel = createFakeDirectoryModel();
// Fake VolumeManager.
// Create fake VolumeManager and install webkitResolveLocalFileSystemURL.
volumeManager = new MockVolumeManager();
window.webkitResolveLocalFileSystemURL =
MockVolumeManager.resolveLocalFileSystemURL.bind(null, volumeManager);
// Fake FileSelectionHandler.
selectionHandler = new FakeFileSelectionHandler();
......@@ -231,3 +237,55 @@ function testCanMoveDownloads() {
selectionHandler.updateSelection([otherFolderEntry], []);
assertTrue(fileTransferController.canCutOrDrag());
}
/**
* Tests preparePaste() with FilesApp fs/sources and standard DataTransfer.
*/
async function testPreparePaste(done) {
const myFilesVolume = volumeManager.volumeInfoList.item(1);
const myFilesMockFs =
/** @type {!MockFileSystem} */ (myFilesVolume.fileSystem);
myFilesMockFs.populate(['/testfile.txt', '/testdir/']);
const testFile = MockFileEntry.create(myFilesMockFs, '/testfile.txt');
const testDir = MockDirectoryEntry.create(myFilesMockFs, '/testdir');
// FilesApp internal drag and drop should populate sourceURLs at first, and
// only populate sourceEntries after calling resolveEntries().
const filesAppDataTransfer = new DataTransfer();
filesAppDataTransfer.setData('fs/sources', testFile.toURL());
const filesAppPastePlan =
fileTransferController.preparePaste(filesAppDataTransfer, testDir);
assertEquals(filesAppPastePlan.sourceURLs.length, 1);
assertEquals(filesAppPastePlan.sourceEntries.length, 0);
await filesAppPastePlan.resolveEntries();
assertEquals(filesAppPastePlan.sourceEntries.length, 1);
assertEquals(filesAppPastePlan.sourceEntries[0], testFile);
// Drag and drop from other apps will use DataTransfer.item with
// item.kind === 'file', and use webkitGetAsEntry() to populate sourceEntries.
const otherMockFs = new MockFileSystem('not-filesapp');
const otherFile = MockFileEntry.create(otherMockFs, '/otherfile.txt');
const otherDataTransfer = /** @type {!DataTransfer} */ ({
effectAllowed: 'copy',
getData: () => {
return '';
},
items: [{
kind: 'file',
webkitGetAsEntry: () => {
return otherFile;
},
}],
});
const otherPastePlan =
fileTransferController.preparePaste(otherDataTransfer, testDir);
assertEquals(otherPastePlan.sourceURLs.length, 0);
assertEquals(otherPastePlan.sourceEntries.length, 1);
assertEquals(otherPastePlan.sourceEntries[0], otherFile);
await otherPastePlan.resolveEntries();
assertEquals(otherPastePlan.sourceURLs.length, 0);
assertEquals(otherPastePlan.sourceEntries.length, 1);
assertEquals(otherPastePlan.sourceEntries[0], otherFile);
done();
}
......@@ -316,35 +316,5 @@ chrome.fileSystem = {
},
};
/**
* Override webkitResolveLocalFileSystemURL for testing.
* @param {string} url URL to resolve.
* @param {function(!MockEntry)} successCallback Success callback.
* @param {function(!Error)} errorCallback Error callback.
*/
// eslint-disable-next-line
var webkitResolveLocalFileSystemURL = (url, successCallback, errorCallback) => {
const match = url.match(/^filesystem:(\w+)(\/.*)/);
if (match) {
const volumeType = /** @type {VolumeManagerCommon.VolumeType} */ (match[1]);
let path = match[2];
const volume = mockVolumeManager.getCurrentProfileVolumeInfo(volumeType);
if (volume) {
// Decode URI in file paths.
path = path.split('/').map(decodeURIComponent).join('/');
const entry = volume.fileSystem.entries[path];
if (entry) {
setTimeout(successCallback, 0, entry);
return;
}
}
}
const message = `webkitResolveLocalFileSystemURL not found: ${url}`;
console.warn(message);
const error = new DOMException(message, 'NotFoundError');
if (errorCallback) {
setTimeout(errorCallback, 0, error);
} else {
throw error;
}
};
window.webkitResolveLocalFileSystemURL =
MockVolumeManager.resolveLocalFileSystemURL.bind(null, mockVolumeManager);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment