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 { ...@@ -31,11 +31,16 @@ class TrashConfig {
* make comparisons simpler. * make comparisons simpler.
* @param {string} trashDir Trash directory. Must end with a slash to make * @param {string} trashDir Trash directory. Must end with a slash to make
* comparisons simpler. * 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.rootType = rootType;
this.topDir = topDir; this.topDir = topDir;
this.trashDir = trashDir; this.trashDir = trashDir;
this.prefixPathWithRemoteMount = prefixPathWithRemoteMount;
this.pathPrefix = '';
} }
} }
...@@ -44,14 +49,17 @@ class TrashConfig { ...@@ -44,14 +49,17 @@ class TrashConfig {
*/ */
class TrashItem { class TrashItem {
/** /**
* @param {string} name * @param {string} name Name of the file deleted.
* @param {!Entry} filesEntry * @param {!Entry} filesEntry Trash files entry.
* @param {!FileEntry} infoEntry * @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.name = name;
this.filesEntry = filesEntry; this.filesEntry = filesEntry;
this.infoEntry = infoEntry; this.infoEntry = infoEntry;
this.pathPrefix = pathPrefix;
} }
} }
...@@ -87,6 +95,9 @@ class Trash { ...@@ -87,6 +95,9 @@ class Trash {
for (const config of Trash.CONFIG) { for (const config of Trash.CONFIG) {
const entryInVolume = fullPathSlash.startsWith(config.topDir); const entryInVolume = fullPathSlash.startsWith(config.topDir);
if (config.rootType === info.rootType && entryInVolume) { if (config.rootType === info.rootType && entryInVolume) {
if (config.prefixPathWithRemoteMount) {
config.pathPrefix = info.volumeInfo.remoteMountPath;
}
const entryInTrash = fullPathSlash.startsWith(config.trashDir); const entryInTrash = fullPathSlash.startsWith(config.trashDir);
return entryInTrash ? null : config; return entryInTrash ? null : config;
} }
...@@ -239,10 +250,10 @@ class Trash { ...@@ -239,10 +250,10 @@ class Trash {
// If any step fails, the file will be unchanged, and any partial trashinfo // If any step fails, the file will be unchanged, and any partial trashinfo
// file created will be cleaned up when we remove old items. // file created will be cleaned up when we remove old items.
// TODO(crbug.com/953310): Remove old items. // TODO(crbug.com/953310): Remove old items.
const infoEntry = const infoEntry = await this.writeTrashInfoFile_(
await this.writeTrashInfoFile_(trashDirs.info, name, entry.fullPath); trashDirs.info, name, config.pathPrefix + entry.fullPath);
const filesEntry = await this.moveTo_(entry, trashDirs.files, name); 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 { ...@@ -257,13 +268,19 @@ class Trash {
const file = await new Promise( const file = await new Promise(
(resolve, reject) => trashItem.infoEntry.file(resolve, reject)); (resolve, reject) => trashItem.infoEntry.file(resolve, reject));
const text = await file.text(); const text = await file.text();
const found = text.match(/^Path=\/(.*)/m); const found = text.match(/^Path=(.*)/m);
if (!found) { if (!found) {
throw new DOMException(`No Path found to restore in ${ throw new DOMException(`No Path found to restore in ${
trashItem.infoEntry.fullPath}, text=${text}`); trashItem.infoEntry.fullPath}, text=${text}`);
} }
const path = found[1]; 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. // Move to last directory in path, making sure dirs are created if needed.
let dir = trashItem.filesEntry.filesystem.root; let dir = trashItem.filesEntry.filesystem.root;
...@@ -297,4 +314,7 @@ Trash.CONFIG = [ ...@@ -297,4 +314,7 @@ Trash.CONFIG = [
VolumeManagerCommon.RootType.DOWNLOADS, '/Downloads/', VolumeManagerCommon.RootType.DOWNLOADS, '/Downloads/',
'/Downloads/.Trash/'), '/Downloads/.Trash/'),
new TrashConfig(VolumeManagerCommon.RootType.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) { ...@@ -212,6 +212,61 @@ async function testDownloadsHasOwnTrash(done) {
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(). * 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