Commit 271b3a17 authored by Joel Hockey's avatar Joel Hockey Committed by Commit Bot

FilesApp: support .local/share/Trash in crostini

In order to interoperate with other crostini file managers, we will use
$HOME/.local/share/Trash as the crostini trashdir, and also prefix all
Paths with the $HOME remote mount path.

Bug: 953310
Change-Id: I6148dfcc5d240ae1959d0bf00698b882822b97e4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2477706
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Reviewed-by: default avatarNoel Gordon <noel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#819693}
parent dd2ed7a2
......@@ -31,11 +31,16 @@ class TrashConfig {
* make comparisons simpler.
* @param {string} trashDir Trash directory. Must end with a slash to make
* comparisons simpler.
* @param {boolean=} prefixPathWithRemoteMount Optional, if true, 'Path=' in
* *.trashinfo is prefixed with the volume.remoteMountPath. For crostini,
* this is the user's homedir (/home/<username>).
*/
constructor(rootType, topDir, trashDir) {
constructor(rootType, topDir, trashDir, prefixPathWithRemoteMount = false) {
this.rootType = rootType;
this.topDir = topDir;
this.trashDir = trashDir;
this.prefixPathWithRemoteMount = prefixPathWithRemoteMount;
this.pathPrefix = '';
}
}
......@@ -44,14 +49,17 @@ class TrashConfig {
*/
class TrashItem {
/**
* @param {string} name
* @param {!Entry} filesEntry
* @param {!FileEntry} infoEntry
* @param {string} name Name of the file deleted.
* @param {!Entry} filesEntry Trash files entry.
* @param {!FileEntry} infoEntry Trash info entry.
* @param {string=} pathPrefix Optional prefix for 'Path=' in *.trashinfo. For
* crostini, this is the user's homedir (/home/<username>).
*/
constructor(name, filesEntry, infoEntry) {
constructor(name, filesEntry, infoEntry, pathPrefix = '') {
this.name = name;
this.filesEntry = filesEntry;
this.infoEntry = infoEntry;
this.pathPrefix = pathPrefix;
}
}
......@@ -87,6 +95,9 @@ class Trash {
for (const config of Trash.CONFIG) {
const entryInVolume = fullPathSlash.startsWith(config.topDir);
if (config.rootType === info.rootType && entryInVolume) {
if (config.prefixPathWithRemoteMount) {
config.pathPrefix = info.volumeInfo.remoteMountPath;
}
const entryInTrash = fullPathSlash.startsWith(config.trashDir);
return entryInTrash ? null : config;
}
......@@ -239,10 +250,10 @@ class Trash {
// If any step fails, the file will be unchanged, and any partial trashinfo
// file created will be cleaned up when we remove old items.
// TODO(crbug.com/953310): Remove old items.
const infoEntry =
await this.writeTrashInfoFile_(trashDirs.info, name, entry.fullPath);
const infoEntry = await this.writeTrashInfoFile_(
trashDirs.info, name, config.pathPrefix + entry.fullPath);
const filesEntry = await this.moveTo_(entry, trashDirs.files, name);
return new TrashItem(entry.name, filesEntry, infoEntry);
return new TrashItem(entry.name, filesEntry, infoEntry, config.pathPrefix);
}
/**
......@@ -257,13 +268,19 @@ class Trash {
const file = await new Promise(
(resolve, reject) => trashItem.infoEntry.file(resolve, reject));
const text = await file.text();
const found = text.match(/^Path=\/(.*)/m);
const found = text.match(/^Path=(.*)/m);
if (!found) {
throw new DOMException(`No Path found to restore in ${
trashItem.infoEntry.fullPath}, text=${text}`);
}
const path = found[1];
const parts = path.split('/');
if (!path.startsWith(trashItem.pathPrefix)) {
throw new DOMException(`Path does not match expected prefix in ${
trashItem.infoEntry.fullPath}, prefix=${trashItem.pathPrefix}, text=${
text}`);
}
const pathNoLeadingSlash = path.substring(trashItem.pathPrefix.length + 1);
const parts = pathNoLeadingSlash.split('/');
// Move to last directory in path, making sure dirs are created if needed.
let dir = trashItem.filesEntry.filesystem.root;
......@@ -297,4 +314,7 @@ Trash.CONFIG = [
VolumeManagerCommon.RootType.DOWNLOADS, '/Downloads/',
'/Downloads/.Trash/'),
new TrashConfig(VolumeManagerCommon.RootType.DOWNLOADS, '/', '/.Trash/'),
new TrashConfig(
VolumeManagerCommon.RootType.CROSTINI, '/', '/.local/share/Trash/',
/*prefixPathWithRemoteMount=*/ true),
];
......@@ -212,6 +212,61 @@ async function testDownloadsHasOwnTrash(done) {
done();
}
/**
* Test crostini trash in .local/share/Trash.
*
* @suppress {accessControls} Access shouldMoveToTrash_().
*/
async function testCrostiniTrash(done) {
const trash = new Trash();
const deletePermanently = false;
const crostini = volumeManager.createVolumeInfo(
VolumeManagerCommon.VolumeType.CROSTINI, 'crostini', 'Linux files', '',
'/home/testuser');
const fs = crostini.fileSystem;
const file1 = MockFileEntry.create(fs, '/file1', null, new Blob(['f1']));
const file2 = MockFileEntry.create(fs, '/file2', null, new Blob(['f1']));
assertEquals(3, Object.keys(fs.entries).length);
// Move /file1 to trash.
const file1TrashItem = await trash.removeFileOrDirectory(
volumeManager, file1, deletePermanently);
assertFalse(!!fs.entries['/file1']);
assertTrue(fs.entries['/.local/share/Trash'].isDirectory);
assertTrue(fs.entries['/.local/share/Trash/files'].isDirectory);
assertTrue(fs.entries['/.local/share/Trash/info'].isDirectory);
assertTrue(fs.entries['/.local/share/Trash/files/file1'].isFile);
assertTrue(fs.entries['/.local/share/Trash/info/file1.trashinfo'].isFile);
const text = await fs.entries['/.local/share/Trash/info/file1.trashinfo']
.content.text();
assertTrue(
text.startsWith('[Trash Info]\nPath=/home/testuser/file1\nDeletionDate='),
`${text} must have Path=/home/test/user/file1`);
assertEquals(9, Object.keys(fs.entries).length);
// Restore /file1
await trash.restore(volumeManager, assert(file1TrashItem));
assertEquals(8, Object.keys(fs.entries).length);
assertTrue(!!fs.entries['/file1']);
// Move /file2 to trash, then delete /.local/share/Trash/files/file2.
await trash.removeFileOrDirectory(volumeManager, file2, deletePermanently);
const file2Trashed = fs.entries['/.local/share/Trash/files/file2'];
assertFalse(!!trash.shouldMoveToTrash_(volumeManager, file2Trashed));
await trash.removeFileOrDirectory(
volumeManager, file2Trashed, deletePermanently);
assertEquals(8, Object.keys(fs.entries).length);
// Delete /.local/share/Trash.
const crostiniTrash = fs.entries['/.local/share/Trash'];
assertFalse(!!trash.shouldMoveToTrash_(volumeManager, crostiniTrash));
await trash.removeFileOrDirectory(
volumeManager, crostiniTrash, deletePermanently);
assertEquals(4, Object.keys(fs.entries).length);
done();
}
/**
* Test restore().
*/
......
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