Commit 51af6f2e authored by Joel Hockey's avatar Joel Hockey Committed by Commit Bot

FilesApp trash race fix

I just noticed another flake in
https://ci.chromium.org/p/chromium/builders/ci/linux-chromeos-rel/42616.

1/ The in-progress map is changed to store a globally unique identifier
so that multiple deletes across volumes cannot collide.

2/ The in-progress map was being cleared after the *.trashinfo files was
written, but before the file is moved. This is now changed so that we
only clear the map once the file is moved.

Bug: 953310
Change-Id: I49c9c75878288f2f1a0be4447c1885a07c873bc6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2521981
Auto-Submit: Joel Hockey <joelhockey@chromium.org>
Reviewed-by: default avatarNoel Gordon <noel@chromium.org>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#824750}
parent 40351207
...@@ -77,10 +77,14 @@ class Trash { ...@@ -77,10 +77,14 @@ class Trash {
this.trashDirs_ = {}; this.trashDirs_ = {};
/** /**
* Set of in-progress deletes. Items in this list are ignored by
* removeOldItems_(). Use getInProgressKey_() to create a globally unique
* key.
*
* @private {!Set<string>} * @private {!Set<string>}
* @const * @const
*/ */
this.infoWritesInProgress_ = new Set(); this.inProgress_ = new Set();
} }
/** /**
...@@ -193,19 +197,16 @@ class Trash { ...@@ -193,19 +197,16 @@ class Trash {
* removeOldItems_() if it is running concurrently. * removeOldItems_() if it is running concurrently.
* *
* @param {!DirectoryEntry} trashInfoDir /.Trash/info directory. * @param {!DirectoryEntry} trashInfoDir /.Trash/info directory.
* @param {string} name name for <name>.trashinfo file. * @param {string} trashInfoName name of the *.trashinfo file.
* @param {string} path path to use in .trashinfo file. * @param {string} path path to use in *.trashinfo file.
* @return {!Promise<!FileEntry>} * @return {!Promise<!FileEntry>}
* @private * @private
*/ */
async writeTrashInfoFile_(trashInfoDir, name, path) { async writeTrashInfoFile_(trashInfoDir, trashInfoName, path) {
const filename = `${name}.trashinfo`;
this.infoWritesInProgress_.add(filename);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
trashInfoDir.getFile(filename, {create: true}, infoFile => { trashInfoDir.getFile(trashInfoName, {create: true}, infoFile => {
infoFile.createWriter(writer => { infoFile.createWriter(writer => {
writer.onwriteend = () => { writer.onwriteend = () => {
this.infoWritesInProgress_.delete(filename);
resolve(infoFile); resolve(infoFile);
}; };
writer.onerror = reject; writer.onerror = reject;
...@@ -248,6 +249,16 @@ class Trash { ...@@ -248,6 +249,16 @@ class Trash {
}); });
} }
/**
* Gets a globally unique key to use in the in-progress set.
*
* @param {!DirectoryEntry} trashInfoDir /.Trash/info directory.
* @param {string} trashInfoName name of the *.trashinfo file.
*/
getInProgressKey_(trashInfoDir, trashInfoName) {
return `${trashInfoDir.toURL()}/${trashInfoName}`;
}
/** /**
* Move a file or a directory to the trash. * Move a file or a directory to the trash.
* *
...@@ -260,13 +271,17 @@ class Trash { ...@@ -260,13 +271,17 @@ class Trash {
const trashDirs = await this.getTrashDirs_(entry, config); const trashDirs = await this.getTrashDirs_(entry, config);
const name = const name =
await fileOperationUtil.deduplicatePath(trashDirs.files, entry.name); await fileOperationUtil.deduplicatePath(trashDirs.files, entry.name);
const trashInfoName = `${name}.trashinfo`;
// Write trashinfo first, then only move file if info write succeeds. // Write trashinfo first, then only move file if info write succeeds.
// 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.
const inProgressKey = this.getInProgressKey_(trashDirs.info, trashInfoName);
this.inProgress_.add(inProgressKey);
const infoEntry = await this.writeTrashInfoFile_( const infoEntry = await this.writeTrashInfoFile_(
trashDirs.info, name, config.pathPrefix + entry.fullPath); trashDirs.info, trashInfoName, config.pathPrefix + entry.fullPath);
const filesEntry = await this.moveTo_(entry, trashDirs.files, name); const filesEntry = await this.moveTo_(entry, trashDirs.files, name);
this.inProgress_.delete(inProgressKey);
return new TrashItem(entry.name, filesEntry, infoEntry, config.pathPrefix); return new TrashItem(entry.name, filesEntry, infoEntry, config.pathPrefix);
} }
...@@ -369,8 +384,9 @@ class Trash { ...@@ -369,8 +384,9 @@ class Trash {
continue; continue;
} }
// Ignore any write-in-progress files. // Ignore any in-progress files.
if (this.infoWritesInProgress_.has(entry.name)) { if (this.inProgress_.has(
this.getInProgressKey_(trashDirs.info, entry.name))) {
console.log(`Ignoring write in progress ${entry.toURL()}`); console.log(`Ignoring write in progress ${entry.toURL()}`);
continue; continue;
} }
......
...@@ -326,8 +326,7 @@ async function testRestore(done) { ...@@ -326,8 +326,7 @@ async function testRestore(done) {
/** /**
* Test removeOldEntries_(). * Test removeOldEntries_().
* *
* @suppress {accessControls} Access removeOldItems_() and * @suppress {accessControls} Access removeOldItems_() and inProgress_().
* infoWritesInProgress_.
*/ */
async function testRemoveOldItems_(done) { async function testRemoveOldItems_(done) {
const trash = new Trash(); const trash = new Trash();
...@@ -358,7 +357,8 @@ async function testRemoveOldItems_(done) { ...@@ -358,7 +357,8 @@ async function testRemoveOldItems_(done) {
// Files that are write-in-progress with no DeletionDate should be ignored. // Files that are write-in-progress with no DeletionDate should be ignored.
fs.entries['/.Trash/info/file1.trashinfo'].content = fs.entries['/.Trash/info/file1.trashinfo'].content =
new Blob(['no-deletion-date']); new Blob(['no-deletion-date']);
trash.infoWritesInProgress_.add('file1.trashinfo'); trash.inProgress_.add(
`${fs.entries['/.Trash/info'].toURL()}/file1.trashinfo`);
delete fs.entries['/.Trash/files/file1']; delete fs.entries['/.Trash/files/file1'];
// Files without a matching file in .Trash/files should be deleted. // Files without a matching file in .Trash/files should be deleted.
delete fs.entries['/.Trash/files/file2']; delete fs.entries['/.Trash/files/file2'];
...@@ -396,7 +396,8 @@ async function testRemoveOldItems_(done) { ...@@ -396,7 +396,8 @@ async function testRemoveOldItems_(done) {
assertFalse(!!fs.entries['/.Trash/files/file5']); assertFalse(!!fs.entries['/.Trash/files/file5']);
assertEquals(6, Object.keys(fs.entries).length); assertEquals(6, Object.keys(fs.entries).length);
trash.infoWritesInProgress_.delete('file1.trashinfo'); trash.inProgress_.delete(
`${fs.entries['/.Trash/info'].toURL()}/file1.trashinfo`);
await trash.removeOldItems_(trashDirs, daysAgo31); await trash.removeOldItems_(trashDirs, daysAgo31);
assertFalse(!!fs.entries['/.Trash/info/file1.trashinfo']); assertFalse(!!fs.entries['/.Trash/info/file1.trashinfo']);
assertEquals(5, Object.keys(fs.entries).length); assertEquals(5, Object.keys(fs.entries).length);
......
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