| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assert} from 'chrome://resources/js/assert.m.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; |
| import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js'; |
| |
| import {MockDirectoryEntry, MockFileEntry, MockFileSystem} from '../../common/js/mock_entry.js'; |
| import {TrashDirs} from '../../common/js/trash.js'; |
| import {VolumeManagerCommon} from '../../common/js/volume_manager_types.js'; |
| |
| import {MockVolumeManager} from './mock_volume_manager.js'; |
| import {Trash} from './trash.js'; |
| |
| /** @type {!MockVolumeManager} */ |
| let volumeManager; |
| |
| /** |
| * State for the feature flags for faking in loadTimeData. |
| * @type {Object<string, boolean>} |
| * */ |
| let flags = {}; |
| |
| // Set up the test components. |
| export function setUp() { |
| // Mock LoadTimeData strings. |
| flags = { |
| 'FILES_TRASH_ENABLED': true, |
| }; |
| loadTimeData.getBoolean = id => flags[id]; |
| loadTimeData.getString = id => id; |
| |
| volumeManager = new MockVolumeManager(); |
| } |
| |
| /** |
| * Call removeFileOrDirectory with the supplied settings and validate that |
| * we correctly either permanently delete, or move to trash. |
| * |
| * @suppress {accessControls} Access private functions |
| * permanentlyDeleteFileOrDirectory_() and trashFileOrDirectory_(). |
| */ |
| function checkRemoveFileOrDirectory( |
| filesTrashEnabled, rootType, path, deletePermanently, |
| expectPermanentlyDelete) { |
| flags['FILES_TRASH_ENABLED'] = filesTrashEnabled; |
| const volumeInfo = |
| volumeManager.createVolumeInfo(rootType, 'volumeId', 'label'); |
| const f = MockFileEntry.create(volumeInfo.fileSystem, path); |
| |
| const trash = new Trash(); |
| // Detect whether permanentlyDelete..., or trash... is called. |
| let permanentlyDeleteCalled = false; |
| let trashCalled = false; |
| trash.permanentlyDeleteFileOrDirectory_ = () => { |
| permanentlyDeleteCalled = true; |
| return Promise.resolve(); |
| }; |
| trash.trashFileOrDirectory_ = (volumeManager, entry) => { |
| trashCalled = true; |
| return Promise.resolve(); |
| }; |
| |
| trash.removeFileOrDirectory(volumeManager, f, deletePermanently); |
| assertEquals(expectPermanentlyDelete, permanentlyDeleteCalled); |
| assertEquals(!expectPermanentlyDelete, trashCalled); |
| } |
| |
| /** |
| * Test that removeFileOrDirectory() correctly moves to trash, or permanently |
| * deletes. |
| */ |
| export function testRemoveFileOrDirectory() { |
| // Only use trash if flag is enabled, entry is in 'downloads' volume, but not |
| // in /.Trash. |
| |
| // enabled, rootType, path, deletePermanently, expectPermanentlyDelete. |
| checkRemoveFileOrDirectory(false, 'removable', '/f', false, true); |
| checkRemoveFileOrDirectory(false, 'removable', '/f', true, true); |
| checkRemoveFileOrDirectory(false, 'downloads', '/f', false, true); |
| checkRemoveFileOrDirectory(false, 'downloads', '/f', true, true); |
| checkRemoveFileOrDirectory(true, 'removable', '/f', false, true); |
| checkRemoveFileOrDirectory(true, 'removable', '/f', true, true); |
| checkRemoveFileOrDirectory(true, 'downloads', '/f', false, false); |
| checkRemoveFileOrDirectory(true, 'downloads', '/.Trash/f', false, true); |
| checkRemoveFileOrDirectory(true, 'downloads', '/f', true, true); |
| } |
| |
| /** |
| * Test permanentlyDeleteFileOrDirectory_(). |
| * |
| * @suppress {accessControls} Access permanentlyDeleteFileOrDirectory_(). |
| */ |
| export async function testPermanentlyDeleteFileOrDirectory(done) { |
| const trash = new Trash(); |
| const fs = new MockFileSystem('volumeId'); |
| const dir = MockDirectoryEntry.create(fs, '/dir'); |
| const file1 = MockFileEntry.create(fs, '/dir/file1'); |
| MockFileEntry.create(fs, '/dir/file2'); |
| MockFileEntry.create(fs, '/dir/file3'); |
| |
| // Deleted file should be removed and no new files in FileSystem. |
| assertEquals(5, Object.keys(fs.entries).length); |
| assertTrue(!!fs.entries['/dir/file1']); |
| await trash.permanentlyDeleteFileOrDirectory_(file1); |
| assertFalse(!!fs.entries['/dir/file1']); |
| assertEquals(4, Object.keys(fs.entries).length); |
| |
| // Deleted dir should also delete all children. |
| assertTrue(!!fs.entries['/dir']); |
| await trash.permanentlyDeleteFileOrDirectory_(dir); |
| assertFalse(!!fs.entries['/dir']); |
| assertFalse(!!fs.entries['/dir/file2']); |
| assertFalse(!!fs.entries['/dir/file3']); |
| assertEquals(1, Object.keys(fs.entries).length); |
| |
| done(); |
| } |
| |
| /** |
| * Test trash in MyFiles. |
| */ |
| export async function testMyFilesTrash(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'])); |
| |
| // Trashed file should be moved to /.Trash/files and new file added in |
| // /.Trash/info. |
| assertEquals(5, Object.keys(fs.entries).length); |
| assertTrue(!!fs.entries['/dir/file1']); |
| await trash.removeFileOrDirectory(volumeManager, file1, deletePermanently); |
| assertFalse(!!fs.entries['/dir/file1']); |
| assertTrue(fs.entries['/.Trash/files'].isDirectory); |
| assertTrue(fs.entries['/.Trash/info'].isDirectory); |
| assertTrue(fs.entries['/.Trash/files/file1'].isFile); |
| assertTrue(fs.entries['/.Trash/info/file1.trashinfo'].isFile); |
| let text = await fs.entries['/.Trash/files/file1'].content.text(); |
| assertEquals('f1', text); |
| text = await fs.entries['/.Trash/info/file1.trashinfo'].content.text(); |
| assertTrue(text.startsWith('[Trash Info]\nPath=/dir/file1\nDeletionDate=')); |
| assertEquals(9, Object.keys(fs.entries).length); |
| |
| // Trashed dir should also move children files into /.Trash/files. |
| assertTrue(!!fs.entries['/dir']); |
| await trash.removeFileOrDirectory(volumeManager, dir, deletePermanently); |
| assertFalse(!!fs.entries['/dir']); |
| assertFalse(!!fs.entries['/dir/file2']); |
| assertFalse(!!fs.entries['/dir/file3']); |
| assertTrue(fs.entries['/.Trash/files'].isDirectory); |
| assertTrue(fs.entries['/.Trash/info'].isDirectory); |
| assertTrue(fs.entries['/.Trash/files/dir'].isDirectory); |
| assertTrue(fs.entries['/.Trash/files/dir/file2'].isFile); |
| assertTrue(fs.entries['/.Trash/files/dir/file3'].isFile); |
| text = await fs.entries['/.Trash/files/dir/file2'].content.text(); |
| assertEquals('f2', text); |
| text = await fs.entries['/.Trash/files/dir/file3'].content.text(); |
| assertEquals('f3', text); |
| assertTrue(fs.entries['/.Trash/info/dir.trashinfo'].isFile); |
| text = await fs.entries['/.Trash/info/dir.trashinfo'].content.text(); |
| assertTrue(text.startsWith('[Trash Info]\nPath=/dir\nDeletionDate=')); |
| assertEquals(10, Object.keys(fs.entries).length); |
| |
| done(); |
| } |
| |
| /** |
| * Test that Downloads has its own /Downloads/.Trash since it is a separate |
| * mount on a device and we don't want move to trash to be a copy operation. |
| */ |
| export async function testDownloadsHasOwnTrash(done) { |
| const trash = new Trash(); |
| const deletePermanently = false; |
| const downloads = volumeManager.getCurrentProfileVolumeInfo( |
| VolumeManagerCommon.VolumeType.DOWNLOADS); |
| const fs = downloads.fileSystem; |
| const file1 = MockFileEntry.create(fs, '/file1', null, new Blob(['f1'])); |
| const dir2 = MockDirectoryEntry.create(fs, '/Downloads'); |
| const file2 = |
| MockFileEntry.create(fs, '/Downloads/file2', null, new Blob(['f2'])); |
| const file3 = |
| MockFileEntry.create(fs, '/Downloads/file3', null, new Blob(['f3'])); |
| assertEquals(5, Object.keys(fs.entries).length); |
| |
| // Move /file1 to trash. |
| await trash.removeFileOrDirectory(volumeManager, file1, deletePermanently); |
| assertTrue(fs.entries['/.Trash'].isDirectory); |
| assertTrue(fs.entries['/.Trash/files'].isDirectory); |
| assertTrue(fs.entries['/.Trash/info'].isDirectory); |
| assertTrue(fs.entries['/.Trash/files/file1'].isFile); |
| assertTrue(fs.entries['/.Trash/info/file1.trashinfo'].isFile); |
| assertEquals(9, Object.keys(fs.entries).length); |
| |
| // Move /Downloads/file2 to trash. |
| await trash.removeFileOrDirectory(volumeManager, file2, deletePermanently); |
| assertTrue(fs.entries['/Downloads/.Trash'].isDirectory); |
| assertTrue(fs.entries['/Downloads/.Trash/files'].isDirectory); |
| assertTrue(fs.entries['/Downloads/.Trash/info'].isDirectory); |
| assertTrue(fs.entries['/Downloads/.Trash/files/file2'].isFile); |
| assertTrue(fs.entries['/Downloads/.Trash/info/file2.trashinfo'].isFile); |
| assertEquals(13, Object.keys(fs.entries).length); |
| |
| // Delete /Downloads/.Trash/files/file2. |
| const file2Trashed = fs.entries['/Downloads/.Trash/files/file2']; |
| assertFalse(!!trash.shouldMoveToTrash(volumeManager, file2Trashed)); |
| await trash.removeFileOrDirectory( |
| volumeManager, file2Trashed, deletePermanently); |
| assertEquals(12, Object.keys(fs.entries).length); |
| |
| // Delete /Downloads/.Trash. |
| const downloadsTrash = fs.entries['/Downloads/.Trash']; |
| assertFalse(!!trash.shouldMoveToTrash(volumeManager, downloadsTrash)); |
| await trash.removeFileOrDirectory( |
| volumeManager, downloadsTrash, deletePermanently); |
| assertFalse(!!fs.entries['/Downloads/.Trash']); |
| assertEquals(8, Object.keys(fs.entries).length); |
| |
| // Move /Downloads/file3 to trash, should recreate /Downloads/.Trash. |
| await trash.removeFileOrDirectory(volumeManager, file3, deletePermanently); |
| assertTrue(fs.entries['/Downloads/.Trash'].isDirectory); |
| assertTrue(fs.entries['/Downloads/.Trash/files'].isDirectory); |
| assertTrue(fs.entries['/Downloads/.Trash/info'].isDirectory); |
| assertTrue(fs.entries['/Downloads/.Trash/files/file3'].isFile); |
| assertTrue(fs.entries['/Downloads/.Trash/info/file3.trashinfo'].isFile); |
| assertEquals(12, Object.keys(fs.entries).length); |
| done(); |
| } |
| |
| /** |
| * Test crostini trash in .local/share/Trash. |
| */ |
| export 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 file1TrashEntry = 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(file1TrashEntry)); |
| 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(). |
| */ |
| export async function testRestore(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'])); |
| |
| // Move /dir/file1 to trash. |
| const file1TrashEntry = await trash.removeFileOrDirectory( |
| volumeManager, file1, deletePermanently); |
| assertEquals(9, Object.keys(fs.entries).length); |
| assertFalse(!!fs.entries['/dir/file1']); |
| assertEquals('file1', file1TrashEntry.name); |
| assertEquals(fs.entries['/.Trash/files/file1'], file1TrashEntry.filesEntry); |
| assertEquals( |
| fs.entries['/.Trash/info/file1.trashinfo'], file1TrashEntry.infoEntry); |
| |
| // Restore it. |
| await trash.restore(volumeManager, assert(file1TrashEntry)); |
| assertEquals(8, Object.keys(fs.entries).length); |
| assertTrue(!!fs.entries['/dir/file1']); |
| |
| // Move /dir/file2 to trash, recreate a new /dir/file2, |
| // original should restore to '/dir/file2 (1)'. |
| const file2TrashEntry = await trash.removeFileOrDirectory( |
| volumeManager, file2, deletePermanently); |
| assertFalse(!!fs.entries['/dir/file2']); |
| assertEquals(9, Object.keys(fs.entries).length); |
| MockFileEntry.create(fs, '/dir/file2', null, new Blob(['f2v2'])); |
| assertEquals(10, Object.keys(fs.entries).length); |
| await trash.restore(volumeManager, assert(file2TrashEntry)); |
| assertEquals(9, Object.keys(fs.entries).length); |
| assertTrue(!!fs.entries['/dir/file2 (1)']); |
| let text = await fs.entries['/dir/file2'].content.text(); |
| assertEquals('f2v2', text); |
| text = await fs.entries['/dir/file2 (1)'].content.text(); |
| assertEquals('f2', text); |
| |
| done(); |
| } |
| |
| /** |
| * Test removeOldEntries_(). |
| * |
| * @suppress {accessControls} Access removeOldItems_() and inProgress_. |
| */ |
| export 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'])); |
| const file6 = MockFileEntry.create(fs, '/dir/file6', null, new Blob(['f6'])); |
| |
| // Get TrashConfig. |
| const config = trash.shouldMoveToTrash(volumeManager, fs.root); |
| assert(config); |
| |
| // Move files to trash. |
| for (const f of [file1, file2, file3, file4, file5, file6]) { |
| await trash.removeFileOrDirectory(volumeManager, f, deletePermanently); |
| } |
| assertEquals(17, 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 that are write-in-progress with no DeletionDate should be ignored. |
| fs.entries['/.Trash/info/file1.trashinfo'].content = |
| new Blob(['no-deletion-date']); |
| trash.inProgress_.set('downloads-/', new Set(['file1.trashinfo'])); |
| delete fs.entries['/.Trash/files/file1']; |
| // Files without a matching file in .Trash/files should be deleted. |
| delete fs.entries['/.Trash/files/file2']; |
| // Files with no DeletionDate should be deleted. |
| fs.entries['/.Trash/info/file3.trashinfo'].content = |
| new Blob(['no-deletion-date']); |
| // Files with DeletionDate which cannot be parsed should be deleted. |
| fs.entries['/.Trash/info/file4.trashinfo'].content = |
| new Blob(['DeletionDate=abc']); |
| // Files with no matching trashinfo should be deleted. |
| delete fs.entries['/.Trash/info/file5.trashinfo']; |
| |
| const trashDirs = |
| new TrashDirs(fs.entries['/.Trash/files'], fs.entries['/.Trash/info']); |
| await trash.removeOldItems_(trashDirs, config, now); |
| assertTrue(!!fs.entries['/']); |
| assertTrue(!!fs.entries['/.Trash']); |
| assertTrue(!!fs.entries['/.Trash/files']); |
| assertTrue(!!fs.entries['/.Trash/files/file6']); |
| assertTrue(!!fs.entries['/.Trash/info']); |
| assertTrue(!!fs.entries['/.Trash/info/file1.trashinfo']); |
| assertTrue(!!fs.entries['/.Trash/info/file6.trashinfo']); |
| assertTrue(!!fs.entries['/dir']); |
| assertEquals(8, Object.keys(fs.entries).length); |
| |
| // Items older than 30d should be deleted. |
| const daysAgo29 = now + (29 * 24 * 60 * 60 * 1000); |
| const daysAgo31 = now + (31 * 24 * 60 * 60 * 1000); |
| await trash.removeOldItems_(trashDirs, config, daysAgo29); |
| assertEquals(8, Object.keys(fs.entries).length); |
| |
| await trash.removeOldItems_(trashDirs, config, daysAgo31); |
| assertTrue(!!fs.entries['/.Trash/info/file1.trashinfo']); |
| assertFalse(!!fs.entries['/.Trash/info/file5.trashinfo']); |
| assertFalse(!!fs.entries['/.Trash/files/file5']); |
| assertEquals(6, Object.keys(fs.entries).length); |
| |
| // trashinfo with no matching file, and not in-progress should be deleted. |
| trash.inProgress_.get('downloads-/').delete('file1.trashinfo'); |
| await trash.removeOldItems_(trashDirs, config, daysAgo31); |
| assertFalse(!!fs.entries['/.Trash/info/file1.trashinfo']); |
| assertEquals(5, Object.keys(fs.entries).length); |
| |
| done(); |
| } |