From 47a629c7db6b6d41d45c153a030738a88d031930 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 8 Sep 2021 08:17:39 -0700 Subject: [PATCH] Add FileSystemHandle::move() and FileSystemHandle::rename() methods Currently, it is not possible to move or rename a file or directory without creating a new file/directory, copying over data (recursively, in the case of a directory), and removing the original. This CL allows for the atomic moving of a file or directory on the local file system without needing to duplicate data. Moves to non-local file systems will are not guaranteed to be atomic and will involve duplicating data. PR: https://github.com/WICG/file-system-access/pull/317 Bug: 1140805 Change-Id: I774ed1d9616249b6ecc80783db48a7bfee915aab --- ...ileSystemBaseHandle-move-manual.https.html | 10 + ...xed_FileSystemBaseHandle-move.https.any.js | 3 + .../script-tests/FileSystemBaseHandle-move.js | 304 ++++++++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 file-system-access/local_FileSystemBaseHandle-move-manual.https.html create mode 100644 file-system-access/sandboxed_FileSystemBaseHandle-move.https.any.js create mode 100644 file-system-access/script-tests/FileSystemBaseHandle-move.js diff --git a/file-system-access/local_FileSystemBaseHandle-move-manual.https.html b/file-system-access/local_FileSystemBaseHandle-move-manual.https.html new file mode 100644 index 00000000000000..2290a3144dae31 --- /dev/null +++ b/file-system-access/local_FileSystemBaseHandle-move-manual.https.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/file-system-access/sandboxed_FileSystemBaseHandle-move.https.any.js b/file-system-access/sandboxed_FileSystemBaseHandle-move.https.any.js new file mode 100644 index 00000000000000..46ed52039bd0d5 --- /dev/null +++ b/file-system-access/sandboxed_FileSystemBaseHandle-move.https.any.js @@ -0,0 +1,3 @@ +// META: script=resources/test-helpers.js +// META: script=resources/sandboxed-fs-test-helpers.js +// META: script=script-tests/FileSystemBaseHandle-move.js diff --git a/file-system-access/script-tests/FileSystemBaseHandle-move.js b/file-system-access/script-tests/FileSystemBaseHandle-move.js new file mode 100644 index 00000000000000..2273a3594e77f3 --- /dev/null +++ b/file-system-access/script-tests/FileSystemBaseHandle-move.js @@ -0,0 +1,304 @@ +// META: script=resources/test-helpers.js + +'use strict'; + +directory_test(async (t, root) => { + const handle = await createFileWithContents(t, 'file-before', 'foo', root); + await handle.move(root, 'file-after'); + + assert_array_equals(await getSortedDirectoryEntries(root), ['file-after']); + assert_equals(await getFileContents(handle), 'foo'); + assert_equals(await getFileSize(handle), 3); +}, 'move(dir, name) to rename a file'); + +directory_test(async (t, root) => { + const handle = await createFileWithContents(t, 'file-before', 'foo', root); + await handle.move(root, 'file-before'); + + assert_array_equals(await getSortedDirectoryEntries(root), ['file-before']); + assert_equals(await getFileContents(handle), 'foo'); + assert_equals(await getFileSize(handle), 3); +}, 'move(dir, name) to rename a file the same name'); + +directory_test(async (t, root) => { + const dir = await root.getDirectoryHandle('dir-before', {create: true}); + await dir.move(root, 'dir-after'); + + assert_array_equals(await getSortedDirectoryEntries(root), ['dir-after/']); + assert_array_equals(await getSortedDirectoryEntries(dir), []); +}, 'move(dir, name) to rename an empty directory'); + +directory_test(async (t, root) => { + const dir = await root.getDirectoryHandle('dir-before', {create: true}); + await createFileWithContents(t, 'file-in-dir', 'abc', dir); + await dir.move(root, 'dir-after'); + + assert_array_equals(await getSortedDirectoryEntries(root), ['dir-after/']); + assert_array_equals(await getSortedDirectoryEntries(dir), ['file-in-dir']); +}, 'move(dir, name) to rename a non-empty directory'); + +directory_test(async (t, root) => { + const dir_src = await root.getDirectoryHandle('dir-src', {create: true}); + const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true}); + const file = await createFileWithContents(t, 'file', 'abc', dir_src); + await file.move(dir_dest); + + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']); + assert_array_equals(await getSortedDirectoryEntries(dir_src), []); + assert_array_equals(await getSortedDirectoryEntries(dir_dest), ['file']); + assert_equals(await getFileContents(file), 'abc'); + assert_equals(await getFileSize(file), 3); +}, 'move(dir) to move a file to a new directory'); + +directory_test(async (t, root) => { + const dir_src = await root.getDirectoryHandle('dir-src', {create: true}); + const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true}); + const file = await createFileWithContents(t, 'file', 'abc', dir_src); + await file.move(dir_dest, ''); + + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']); + assert_array_equals(await getSortedDirectoryEntries(dir_src), []); + assert_array_equals(await getSortedDirectoryEntries(dir_dest), ['file']); + assert_equals(await getFileContents(file), 'abc'); + assert_equals(await getFileSize(file), 3); +}, 'move(dir, "") to move a file to a new directory'); + +directory_test(async (t, root) => { + const dir_src = await root.getDirectoryHandle('dir-src', {create: true}); + const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true}); + const file = + await createFileWithContents(t, 'file-in-dir-src', 'abc', dir_src); + await file.move(dir_dest, 'file-in-dir-dest'); + + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']); + assert_array_equals(await getSortedDirectoryEntries(dir_src), []); + assert_array_equals( + await getSortedDirectoryEntries(dir_dest), ['file-in-dir-dest']); + assert_equals(await getFileContents(file), 'abc'); + assert_equals(await getFileSize(file), 3); +}, 'move(dir, name) to move a file to a new directory'); + +directory_test(async (t, root) => { + const dir_src = await root.getDirectoryHandle('dir-src', {create: true}); + const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true}); + const dir_in_dir = + await dir_src.getDirectoryHandle('dir-in-dir', {create: true}); + await dir_in_dir.move(dir_dest); + + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']); + assert_array_equals(await getSortedDirectoryEntries(dir_src), []); + assert_array_equals( + await getSortedDirectoryEntries(dir_dest), ['dir-in-dir/']); + assert_array_equals(await getSortedDirectoryEntries(dir_in_dir), []); +}, 'move(dir) to move an empty directory to a new directory'); + +directory_test(async (t, root) => { + const dir_src = await root.getDirectoryHandle('dir-src', {create: true}); + const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true}); + const dir_in_dir = + await dir_src.getDirectoryHandle('dir-in-dir', {create: true}); + await dir_in_dir.move(dir_dest, ""); + + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']); + assert_array_equals(await getSortedDirectoryEntries(dir_src), []); + assert_array_equals( + await getSortedDirectoryEntries(dir_dest), ['dir-in-dir/']); + assert_array_equals(await getSortedDirectoryEntries(dir_in_dir), []); +}, 'move(dir, "") to move an empty directory to a new directory'); + +directory_test(async (t, root) => { + const dir_src = await root.getDirectoryHandle('dir-src', {create: true}); + const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true}); + const dir_in_dir = + await dir_src.getDirectoryHandle('dir-in-dir', {create: true}); + await dir_in_dir.move(dir_dest, 'dir-in-dir'); + + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']); + assert_array_equals(await getSortedDirectoryEntries(dir_src), []); + assert_array_equals( + await getSortedDirectoryEntries(dir_dest), ['dir-in-dir/']); + assert_array_equals(await getSortedDirectoryEntries(dir_in_dir), []); +}, 'move(dir, name) to move an empty directory to a new directory'); + +directory_test(async (t, root) => { + const dir_src = await root.getDirectoryHandle('dir-src', {create: true}); + const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true}); + const dir_in_dir = + await dir_src.getDirectoryHandle('dir-in-dir', {create: true}); + const file = + await createFileWithContents(t, 'file-in-dir', 'abc', dir_in_dir); + await dir_in_dir.move(dir_dest, ""); + + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']); + assert_array_equals(await getSortedDirectoryEntries(dir_src), []); + assert_array_equals( + await getSortedDirectoryEntries(dir_dest), ['dir-in-dir/']); + assert_array_equals( + await getSortedDirectoryEntries(dir_in_dir), ['file-in-dir']); + // `file` should be invalidated after moving directories. + await promise_rejects_dom(t, 'NotFoundError', getFileContents(file)); +}, 'move(dir, "") to move a non-empty directory to a new directory'); + +directory_test(async (t, root) => { + const dir_src = await root.getDirectoryHandle('dir-src', {create: true}); + const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true}); + const dir_in_dir = + await dir_src.getDirectoryHandle('dir-in-dir', {create: true}); + const file = + await createFileWithContents(t, 'file-in-dir', 'abc', dir_in_dir); + await dir_in_dir.move(dir_dest, 'dir-in-dir'); + + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']); + assert_array_equals(await getSortedDirectoryEntries(dir_src), []); + assert_array_equals( + await getSortedDirectoryEntries(dir_dest), ['dir-in-dir/']); + assert_array_equals( + await getSortedDirectoryEntries(dir_in_dir), ['file-in-dir']); + // `file` should be invalidated after moving directories. + await promise_rejects_dom(t, 'NotFoundError', getFileContents(file)); +}, 'move(dir, name) to move a non-empty directory to a new directory'); + +directory_test(async (t, root) => { + const dir1 = await root.getDirectoryHandle('dir1', {create: true}); + const dir2 = await root.getDirectoryHandle('dir2', {create: true}); + const handle = await createFileWithContents(t, 'file', 'foo', root); + + await handle.move(dir1); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']); + assert_array_equals(await getSortedDirectoryEntries(dir1), ['file']); + assert_array_equals(await getSortedDirectoryEntries(dir2), []); + assert_equals(await getFileContents(handle), 'foo'); + + await handle.move(dir2); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']); + assert_array_equals(await getSortedDirectoryEntries(dir1), []); + assert_array_equals(await getSortedDirectoryEntries(dir2), ['file']); + assert_equals(await getFileContents(handle), 'foo'); + + await handle.move(root); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/', 'file']); + assert_array_equals(await getSortedDirectoryEntries(dir1), []); + assert_array_equals(await getSortedDirectoryEntries(dir2), []); + assert_equals(await getFileContents(handle), 'foo'); +}, 'move(dir) can be called multiple times'); + +directory_test(async (t, root) => { + const dir1 = await root.getDirectoryHandle('dir1', {create: true}); + const dir2 = await root.getDirectoryHandle('dir2', {create: true}); + const handle = await createFileWithContents(t, 'file', 'foo', root); + + await handle.move(dir1, ""); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']); + assert_array_equals(await getSortedDirectoryEntries(dir1), ['file']); + assert_array_equals(await getSortedDirectoryEntries(dir2), []); + assert_equals(await getFileContents(handle), 'foo'); + + await handle.move(dir2, ""); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']); + assert_array_equals(await getSortedDirectoryEntries(dir1), []); + assert_array_equals(await getSortedDirectoryEntries(dir2), ['file']); + assert_equals(await getFileContents(handle), 'foo'); + + await handle.move(root, ""); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/', 'file']); + assert_array_equals(await getSortedDirectoryEntries(dir1), []); + assert_array_equals(await getSortedDirectoryEntries(dir2), []); + assert_equals(await getFileContents(handle), 'foo'); +}, 'move(dir, "") can be called multiple times'); + +directory_test(async (t, root) => { + const dir1 = await root.getDirectoryHandle('dir1', {create: true}); + const dir2 = await root.getDirectoryHandle('dir2', {create: true}); + const handle = await createFileWithContents(t, 'file', 'foo', root); + + await handle.move(dir1, 'file-1'); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']); + assert_array_equals(await getSortedDirectoryEntries(dir1), ['file-1']); + assert_array_equals(await getSortedDirectoryEntries(dir2), []); + assert_equals(await getFileContents(handle), 'foo'); + + await handle.move(dir2, 'file-2'); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']); + assert_array_equals(await getSortedDirectoryEntries(dir1), []); + assert_array_equals(await getSortedDirectoryEntries(dir2), ['file-2']); + assert_equals(await getFileContents(handle), 'foo'); + + await handle.move(root, 'file-3'); + assert_array_equals( + await getSortedDirectoryEntries(root), ['dir1/', 'dir2/', 'file-3']); + assert_array_equals(await getSortedDirectoryEntries(dir1), []); + assert_array_equals(await getSortedDirectoryEntries(dir2), []); + assert_equals(await getFileContents(handle), 'foo'); +}, 'move(dir, name) can be called multiple times'); + +directory_test(async (t, root) => { + const handle = await createFileWithContents(t, 'file-before', 'foo', root); + await promise_rejects_js( + t, TypeError, handle.move(root, '#$23423@352^*3243')); + + assert_array_equals(await getSortedDirectoryEntries(root), ['file-before']); + assert_equals(await getFileContents(handle), 'foo'); + assert_equals(await getFileSize(handle), 3); +}, 'move(dir, name) with a name with invalid characters should fail'); + +directory_test(async (t, root) => { + const dir = await root.getDirectoryHandle('dir', {create: true}); + await promise_rejects_dom( + t, 'InvalidModificationError', dir.move(dir)); + + assert_array_equals(await getSortedDirectoryEntries(root), ['dir/']); + assert_array_equals(await getSortedDirectoryEntries(dir), []); +}, 'move(dir, name) to move a directory within itself fails'); + +directory_test(async (t, root) => { + const dir = await root.getDirectoryHandle('dir', {create: true}); + await promise_rejects_dom( + t, 'InvalidModificationError', dir.move(dir, 'dir-fail')); + + assert_array_equals(await getSortedDirectoryEntries(root), ['dir/']); + assert_array_equals(await getSortedDirectoryEntries(dir), []); +}, 'move(dir, name) to move a directory within itself and rename fails'); + +directory_test(async (t, root) => { + const parent_dir = + await root.getDirectoryHandle('parent-dir', {create: true}); + const child_dir = + await parent_dir.getDirectoryHandle('child-dir', {create: true}); + await promise_rejects_dom( + t, 'InvalidModificationError', parent_dir.move(child_dir)); + + assert_array_equals(await getSortedDirectoryEntries(root), ['parent-dir/']); + assert_array_equals( + await getSortedDirectoryEntries(parent_dir), ['child-dir/']); + assert_array_equals(await getSortedDirectoryEntries(child_dir), []); +}, 'move(dir) to move a directory within a descendent fails'); + +directory_test(async (t, root) => { + const parent_dir = + await root.getDirectoryHandle('parent-dir', {create: true}); + const child_dir = + await parent_dir.getDirectoryHandle('child-dir', {create: true}); + await promise_rejects_dom( + t, 'InvalidModificationError', parent_dir.move(child_dir, 'dir')); + + assert_array_equals(await getSortedDirectoryEntries(root), ['parent-dir/']); + assert_array_equals( + await getSortedDirectoryEntries(parent_dir), ['child-dir/']); + assert_array_equals(await getSortedDirectoryEntries(child_dir), []); +}, 'move(dir, name) to move a directory within a descendent fails');