Commit 546f1932 authored by Joel Hockey's avatar Joel Hockey Committed by Commit Bot

Remove items in FilesApp trash older than 30d

On the first time in each FilesApp session that a file is deleted, the
current contents of the related trash are checked and any files older
than 30d are permanently deleted.

Bug: 953310
Change-Id: I8d34c3333152bc690652d070bff0d2f34b6ef1c8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2490960
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Reviewed-by: default avatarNoel Gordon <noel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#819694}
parent 271b3a17
......@@ -168,6 +168,8 @@ class Trash {
const trashFiles = await this.getDirectory_(trashRoot, 'files');
const trashInfo = await this.getDirectory_(trashRoot, 'info');
trashDirs = new TrashDirs(trashFiles, trashInfo);
// Check and remove old items max once per session.
this.removeOldItems_(trashDirs, Date.now());
this.trashDirs_[key] = trashDirs;
return trashDirs;
}
......@@ -249,7 +251,6 @@ class Trash {
// Write trashinfo first, then only move file if info write succeeds.
// 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, config.pathPrefix + entry.fullPath);
const filesEntry = await this.moveTo_(entry, trashDirs.files, name);
......@@ -292,14 +293,115 @@ class Trash {
// If any step fails, then either we still have the file in trash with a
// valid trashinfo, or file is restored and trashinfo will be cleaned up
// when we remove old items.
// TODO(crbug.com/953310): Remove old items.
const name =
await fileOperationUtil.deduplicatePath(dir, parts[parts.length - 1]);
await this.moveTo_(trashItem.filesEntry, dir, name);
await this.permanentlyDeleteFileOrDirectory_(trashItem.infoEntry);
}
/**
* Remove any items from trash older than 30d.
* @param {!TrashDirs} trashDirs
* @param {number} now Current time in milliseconds from epoch.
*/
async removeOldItems_(trashDirs, now) {
const ls = (reader) => {
return new Promise((resolve, reject) => {
reader.readEntries(results => resolve(results), error => reject(error));
});
};
const rm = (entry, log, desc) => {
if (entry) {
log(`Deleting ${entry.toURL()}: ${desc}`);
return this.permanentlyDeleteFileOrDirectory_(entry).catch(
e => console.error(`Error deleting ${entry.toURL()}: ${desc}`, e));
}
};
// Get all entries in trash/files. Read files first before info in case
// trash or restore operations happen during this.
const filesEntries = {};
const filesReader = trashDirs.files.createReader();
try {
while (true) {
const entries = await ls(filesReader);
if (!entries.length) {
break;
}
entries.forEach(entry => filesEntries[entry.name] = entry);
}
} catch (e) {
console.error('Error reading old files entries', e);
return;
}
// Check entries in trash/info and delete items older than 30d.
const infoReader = trashDirs.info.createReader();
try {
while (true) {
const entries = await ls(infoReader);
if (!entries.length) {
break;
}
for (const entry of entries) {
if (!entry.isFile) {
rm(entry, console.error, 'Unexpected trash info directory');
continue;
}
if (!entry.name.endsWith('.trashinfo')) {
rm(entry, console.error, 'Unexpected trash info file');
continue;
}
const name = entry.name.substring(0, entry.name.length - 10);
const filesEntry = filesEntries[name];
delete filesEntries[name];
const file = await new Promise(
(resolve, reject) => entry.file(resolve, reject));
const text = await file.text();
const found = text.match(/^DeletionDate=(.*)/m);
if (!found) {
rm(entry, console.error, 'Could not find DeletionDate in ' + text);
rm(filesEntry, console.error, 'Invalid matching trashinfo');
continue;
}
const d = Date.parse(found[1]);
if (!d) {
rm(entry, console.error, 'Could not parse DeletionDate in ' + text);
rm(filesEntry, console.error, 'Invalid matching trashinfo');
continue;
}
const ago30d = now - Trash.AUTO_DELETE_INTERVAL_MS;
const ago30dStr = new Date(ago30d).toISOString();
if (d < ago30d) {
const msg = `Older than ${ago30dStr}, DeletionDate=${found[1]}`;
rm(entry, console.log, msg);
rm(filesEntry, console.log, msg);
}
}
}
} catch (e) {
console.error('Error reading old info entries', e);
return;
}
// Any entries left in filesEntries have no matching *.trashinfo file.
for (const entry of Object.values(filesEntries)) {
rm(entry, console.error, 'No matching *.trashinfo file');
}
}
}
/**
* Interval (ms) until items in trash are permanently deleted. 30 days.
* @const
*/
Trash.AUTO_DELETE_INTERVAL_MS = 30 * 24 * 60 * 60 * 1000;
/**
* Volumes supported for Trash, and location of Trash dir. Items will be
* searched in order.
......
......@@ -315,3 +315,68 @@ async function testRestore(done) {
done();
}
/**
* Test removeOldEntries_().
*
* @suppress {accessControls} Access removeOldItems_().
*/
async function testRemoveOldItems_(done) {
const trash = new Trash();
const deletePermanently = false;
const downloads = volumeManager.getCurrentProfileVolumeInfo(
VolumeManagerCommon.VolumeType.DOWNLOADS);
const fs = downloads.fileSystem;
const dir = MockDirectoryEntry.create(fs, '/dir');
const file1 = MockFileEntry.create(fs, '/dir/file1', null, new Blob(['f1']));
const file2 = MockFileEntry.create(fs, '/dir/file2', null, new Blob(['f2']));
const file3 = MockFileEntry.create(fs, '/dir/file3', null, new Blob(['f3']));
const file4 = MockFileEntry.create(fs, '/dir/file4', null, new Blob(['f4']));
const file5 = MockFileEntry.create(fs, '/dir/file5', null, new Blob(['f5']));
// Move files to trash.
for (const f of [file1, file2, file3, file4, file5]) {
await trash.removeFileOrDirectory(volumeManager, f, deletePermanently);
}
assertEquals(15, Object.keys(fs.entries).length);
const now = Date.now();
// Directories inside info should be deleted.
MockDirectoryEntry.create(fs, '/.Trash/info/baddir.trashinfo');
// Files that do not end with .trashinfo should be deleted.
MockFileEntry.create(fs, '/.Trash/info/f', null, new Blob(['f']));
// Files without a matching file in .Trash/files are left.
delete fs.entries['/.Trash/files/file1'];
// Files with no DeletionDate should be deleted.
fs.entries['/.Trash/info/file2.trashinfo'].content =
new Blob(['no-deletion-date']);
// Files with DeletionDate which cannot be parsed should be deleted.
fs.entries['/.Trash/info/file3.trashinfo'].content =
new Blob(['DeletionDate=abc']);
// Files with no matching trashinfo should be deleted.
delete fs.entries['/.Trash/info/file4.trashinfo'];
const trashDirs =
new TrashDirs(fs.entries['/.Trash/files'], fs.entries['/.Trash/info']);
await trash.removeOldItems_(trashDirs, now);
assertTrue(!!fs.entries['/']);
assertTrue(!!fs.entries['/.Trash']);
assertTrue(!!fs.entries['/.Trash/files']);
assertTrue(!!fs.entries['/.Trash/files/file5']);
assertTrue(!!fs.entries['/.Trash/info']);
assertTrue(!!fs.entries['/.Trash/info/file1.trashinfo']);
assertTrue(!!fs.entries['/.Trash/info/file5.trashinfo']);
assertTrue(!!fs.entries['/dir']);
assertEquals(8, Object.keys(fs.entries).length);
// Items older than 30d should be deleted.
await trash.removeOldItems_(trashDirs, now + (29 * 24 * 60 * 60 * 1000));
assertEquals(8, Object.keys(fs.entries).length);
await trash.removeOldItems_(trashDirs, now + (31 * 24 * 60 * 60 * 1000));
assertFalse(!!fs.entries['/.Trash/info/file5.trashinfo']);
assertFalse(!!fs.entries['/.Trash/files/file5']);
assertEquals(5, Object.keys(fs.entries).length);
done();
}
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