Commit 889950e6 authored by David's avatar David Committed by Commit Bot

Make renaming a file not reload the file in chrome://media-app.

Previously we called `launchWithDirectory()` to load in the renamed
file. That blocked the renaming IPC finishing on the entire directory
loading. This change takes a similar approach to SAVE_AS updating
the file in `tokenMap` & `currentFiles` relying on the client app
to reload itself (done in `handleChromiumIPC`) so we don't need to
load the file again!

This change unblocks b/163002455, b/163422302 & b/163432652 as well
as adds a return value for invalid tokens.

It also prevents losing our edit stack when we rename a file!

Bug: b/163422302, b/162281131, b/162281681, 996088
Change-Id: I690963870d1346d31cb77a0c44aeb95802216ef5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2352586
Commit-Queue: David Lei <dlei@google.com>
Reviewed-by: default avatarTrent Apted <tapted@chromium.org>
Cr-Commit-Position: refs/heads/master@{#799001}
parent b1b9c938
...@@ -154,6 +154,13 @@ guestMessagePipe.registerHandler(Message.RENAME_FILE, async (message) => { ...@@ -154,6 +154,13 @@ guestMessagePipe.registerHandler(Message.RENAME_FILE, async (message) => {
} }
const originalFile = await handle.getFile(); const originalFile = await handle.getFile();
let originalFileIndex =
currentFiles.findIndex(fd => fd.token === renameMsg.token);
if (originalFileIndex < 0) {
return {renameResult: RenameResult.FILE_NO_LONGER_IN_LAST_OPENED_DIRECTORY};
}
const renamedFileHandle = const renamedFileHandle =
await directory.getFileHandle(renameMsg.newFilename, {create: true}); await directory.getFileHandle(renameMsg.newFilename, {create: true});
// Copy file data over to the new file. // Copy file data over to the new file.
...@@ -171,9 +178,30 @@ guestMessagePipe.registerHandler(Message.RENAME_FILE, async (message) => { ...@@ -171,9 +178,30 @@ guestMessagePipe.registerHandler(Message.RENAME_FILE, async (message) => {
await directory.removeEntry(originalFile.name); await directory.removeEntry(originalFile.name);
} }
// Reload current file so it is in an editable state, this is done before // Replace the old file in our internal representation. There is no harm using
// removing the old file so the relaunch starts sooner. // the old file's token since the old file is removed.
await launchWithDirectory(directory, renamedFileHandle); tokenMap.set(renameMsg.token, renamedFileHandle);
// Remove the entry for `originalFile` in current files, replace it with a
// FileDescriptor for the renamed file.
const renamedFile = await renamedFileHandle.getFile();
// Ensure the file is still in `currentFiles` after all the above `awaits`. If
// missing it means either new files have loaded (or tried to), see
// b/164985809.
originalFileIndex =
currentFiles.findIndex(fd => fd.token === renameMsg.token);
if (originalFileIndex < 0) {
// Can't navigate to the renamed file so don't add it to `currentFiles`.
return {renameResult: RenameResult.SUCCESS};
}
currentFiles.splice(originalFileIndex, 1, {
token: renameMsg.token,
file: renamedFile,
handle: renamedFileHandle,
inCurrentDirectory: true
});
return {renameResult: RenameResult.SUCCESS}; return {renameResult: RenameResult.SUCCESS};
}); });
...@@ -614,6 +642,8 @@ async function processOtherFilesInDirectory( ...@@ -614,6 +642,8 @@ async function processOtherFilesInDirectory(
// Only allow traversal of related file types. // Only allow traversal of related file types.
if (entry && isFileRelated(focusFile, entry.file)) { if (entry && isFileRelated(focusFile, entry.file)) {
// Note: The focus file will be processed here again but will be skipped
// over when added to `currentFiles`.
relatedFiles.push({ relatedFiles.push({
token: generateToken(entry.handle), token: generateToken(entry.handle),
file: entry.file, file: entry.file,
...@@ -730,6 +760,9 @@ async function launchWithDirectory(directory, handle) { ...@@ -730,6 +760,9 @@ async function launchWithDirectory(directory, handle) {
// The app is operable with the first file now. // The app is operable with the first file now.
// Process other files in directory. // Process other files in directory.
// TODO(https://github.com/WICG/native-file-system/issues/215): Don't process
// other files if there is only 1 file which is already loaded by
// `sendSnapshotToGuest()` above.
await loadOtherRelatedFiles( await loadOtherRelatedFiles(
directory, asFile.file, asFile.handle, localLaunchNumber); directory, asFile.file, asFile.handle, localLaunchNumber);
} }
......
...@@ -103,6 +103,7 @@ let NavigateMessage; ...@@ -103,6 +103,7 @@ let NavigateMessage;
* @enum {number} * @enum {number}
*/ */
const RenameResult = { const RenameResult = {
FILE_NO_LONGER_IN_LAST_OPENED_DIRECTORY: -1,
SUCCESS: 0, SUCCESS: 0,
FILE_EXISTS: 1, FILE_EXISTS: 1,
}; };
......
...@@ -82,6 +82,11 @@ async function runTestQuery(data) { ...@@ -82,6 +82,11 @@ async function runTestQuery(data) {
.call(firstReceivedItem(), data.renameLastFile); .call(firstReceivedItem(), data.renameLastFile);
if (renameResult === RenameResult.FILE_EXISTS) { if (renameResult === RenameResult.FILE_EXISTS) {
result = 'renameOriginalFile resolved file exists'; result = 'renameOriginalFile resolved file exists';
} else if (
renameResult ===
RenameResult.FILE_NO_LONGER_IN_LAST_OPENED_DIRECTORY) {
result = 'renameOriginalFile resolved ' +
'FILE_NO_LONGER_IN_LAST_OPENED_DIRECTORY';
} else { } else {
result = 'renameOriginalFile resolved success'; result = 'renameOriginalFile resolved success';
} }
......
...@@ -284,12 +284,9 @@ TEST_F('MediaAppUIBrowserTest', 'NonLaunchableIpcAfterFastLoad', async () => { ...@@ -284,12 +284,9 @@ TEST_F('MediaAppUIBrowserTest', 'NonLaunchableIpcAfterFastLoad', async () => {
testDone(); testDone();
}); });
// Tests that we can launch the MediaApp with the selected (first) file, // Tests that we can launch the MediaApp with the selected (first) file,
// interact with it by invoking IPC (rename) that re-launches the // and re-launch it before all files from the first launch are loaded in.
// MediaApp (calls `launchWithDirectory`), then the rest of the files TEST_F('MediaAppUIBrowserTest', 'ReLaunchableAfterFastLoad', async () => {
// in the current directory are loaded in.
TEST_F('MediaAppUIBrowserTest', 'ReLaunchableIpcAfterFastLoad', async () => {
const files = const files =
await createMultipleImageFiles(['file1', 'file2', 'file3', 'file4']); await createMultipleImageFiles(['file1', 'file2', 'file3', 'file4']);
const directory = await createMockTestDirectory(files); const directory = await createMockTestDirectory(files);
...@@ -303,48 +300,42 @@ TEST_F('MediaAppUIBrowserTest', 'ReLaunchableIpcAfterFastLoad', async () => { ...@@ -303,48 +300,42 @@ TEST_F('MediaAppUIBrowserTest', 'ReLaunchableIpcAfterFastLoad', async () => {
await assertSingleFileLaunch(directory, files.length); await assertSingleFileLaunch(directory, files.length);
// Invoke Rename IPC that relaunches the app, this calls // Mutate the second file.
// `launchWithDirectory()` which increments globalLaunchNumber. directory.files[1].name = 'changed.png';
const messageRename = {renameLastFile: 'new_file_name.png'}; // Relaunch the app with the second file.
const testResponse = await sendTestMessage(messageRename); await launchWithDirectory(directory, directory.files[1]);
assertEquals(
testResponse.testQueryResult, 'renameOriginalFile resolved success');
// Ensure rename relaunches the app incremented the `globalLaunchNumber`. // Ensure second launch incremented the `globalLaunchNumber`.
assertEquals(1, globalLaunchNumber); assertEquals(1, globalLaunchNumber);
// The renamed file (also the focused file) is at the end of the
// `FileSystemDirectoryHandle.files` since it was deleted and a new file
// with the same contents and a different name was created.
assertEquals(directory.files[3].name, 'new_file_name.png');
// The call to `launchWithDirectory()` from rename relaunching the app loads // Second launch loads other files into `currentFiles`.
// other files into `currentFiles`.
await assertFilesLoaded( await assertFilesLoaded(
directory, ['new_file_name.png', 'file2.png', 'file3.png', 'file4.png'], directory, ['changed.png', 'file3.png', 'file4.png', 'file1.png'],
'fast files: check files after renaming'); 'fast files: check files after relaunching');
const currentFilesAfterRenameLaunch = [...currentFiles]; const currentFilesAfterSecondLaunch = [...currentFiles];
const loadedFilesAfterRename = await getLoadedFiles(); const loadedFilesSecondLaunch = await getLoadedFiles();
// Try to load with previous launch number, has no effect as it is aborted // Try to load with previous launch number simulating the first launch
// early due to different launch numbers. // completing after the second launch. Has no effect as it is aborted early
// due to different launch numbers.
const previousLaunchNumber = 0; const previousLaunchNumber = 0;
await loadOtherRelatedFiles( await loadOtherRelatedFiles(
directory, focusFile.file, focusFile.handle, previousLaunchNumber); directory, focusFile.file, focusFile.handle, previousLaunchNumber);
// Ensure same files as before the call to `loadOtherRelatedFiles()` by for // Ensure `currentFiles is the same as the file state at the end of the second
// equality equality. // launch before the call to `loadOtherRelatedFiles()`.
currentFilesAfterRenameLaunch.map( currentFilesAfterSecondLaunch.map(
(fd, index) => assertEquals( (fd, index) => assertEquals(
fd, currentFiles[index], fd, currentFiles[index],
`Equality check for file ${ `Equality check for file ${
JSON.stringify(fd)} in currentFiles filed`)); JSON.stringify(fd)} in currentFiles filed`));
// Focus file stays index 0. // Focus file (file that the directory was launched with) stays index 0.
const lastLoadedFiles = await getLoadedFiles(); const lastLoadedFiles = await getLoadedFiles();
assertEquals('new_file_name.png', lastLoadedFiles[0].name); assertEquals('changed.png', lastLoadedFiles[0].name);
assertEquals(loadedFilesAfterRename[0].name, lastLoadedFiles[0].name); assertEquals(loadedFilesSecondLaunch[0].name, lastLoadedFiles[0].name);
// Focus file in the `FileSystemDirectoryHandle` is at index 3. // Focus file in the `FileSystemDirectoryHandle` is at index 1.
assertEquals(directory.files[3].name, lastLoadedFiles[0].name); assertEquals(directory.files[1].name, lastLoadedFiles[0].name);
testDone(); testDone();
}); });
...@@ -764,49 +755,94 @@ TEST_F('MediaAppUIBrowserTest', 'NavigateIPC', async () => { ...@@ -764,49 +755,94 @@ TEST_F('MediaAppUIBrowserTest', 'NavigateIPC', async () => {
}); });
// Tests the IPC behind the implementation of ReceivedFile.renameOriginalFile() // Tests the IPC behind the implementation of ReceivedFile.renameOriginalFile()
// in the untrusted context. // in the untrusted context. This test is integration-y making sure we rename
// the focus file and that gets inserted in the right place in `currentFiles`
// preserving navigation order.
TEST_F('MediaAppUIBrowserTest', 'RenameOriginalIPC', async () => { TEST_F('MediaAppUIBrowserTest', 'RenameOriginalIPC', async () => {
const directory = await launchWithFiles([await createTestImageFile()]); const directory = await launchWithFiles([
const firstFileHandle = directory.files[0]; await createTestImageFile(1, 1, 'file1.png'),
const firstFile = firstFileHandle.getFileSync(); await createTestImageFile(1, 1, 'file2.png')
]);
let testResponse;
// Nothing should be deleted initially. // Nothing should be deleted initially.
assertEquals(null, directory.lastDeleted); assertEquals(null, directory.lastDeleted);
// Navigate to second file "file2.png".
await advance(1);
// Test normal rename flow. // Test normal rename flow.
const messageRename = {renameLastFile: 'new_file_name.png'}; const file2Handle = directory.files[entryIndex];
const file2File = file2Handle.getFileSync();
const file2Token = currentFiles[entryIndex].token;
let messageRename = {renameLastFile: 'new_file_name.png'};
let testResponse;
testResponse = await sendTestMessage(messageRename); testResponse = await sendTestMessage(messageRename);
assertEquals( assertEquals(
testResponse.testQueryResult, 'renameOriginalFile resolved success'); testResponse.testQueryResult, 'renameOriginalFile resolved success');
// The original file that was renamed got deleted. // The original file that was renamed got deleted.
assertEquals(firstFileHandle, directory.lastDeleted); assertEquals(file2Handle, directory.lastDeleted);
// There is still one file which is the renamed version of the original file. assertEquals(directory.files.length, 2);
assertEquals(directory.files.length, 1); assertEquals(directory.files[entryIndex].name, 'new_file_name.png');
assertEquals(directory.files[0].name, 'new_file_name.png'); // The new file uses the same token as the old file.
assertEquals(currentFiles[entryIndex].token, file2Token);
// Check the new file written has the correct data. // Check the new file written has the correct data.
const newHandle = directory.files[0]; const renamedHandle = directory.files[entryIndex];
const newFile = await newHandle.getFile(); const renamedFile = await renamedHandle.getFile();
assertEquals(newFile.size, firstFile.size); assertEquals(renamedFile.size, file2File.size);
assertEquals(await newFile.text(), await firstFile.text()); assertEquals(await renamedFile.text(), await file2File.text());
// Check the internal representation (token map & currentFiles) is updated.
assertEquals(tokenMap.get(file2Token), renamedHandle);
assertEquals(currentFiles[entryIndex].handle, renamedHandle);
// Check navigation order is preserved.
assertEquals(entryIndex, 1);
assertEquals(currentFiles[entryIndex].handle.name, 'new_file_name.png');
assertEquals(currentFiles[0].handle.name, 'file1.png');
// Test renaming when a file with the new name already exists. // Advancing wraps around back to the first file.
await advance(1);
assertEquals(entryIndex, 0);
assertEquals(currentFiles[entryIndex].handle.name, 'file1.png');
assertEquals(currentFiles[1].handle.name, 'new_file_name.png');
// Test renaming when a file with the new name already exists, tries to rename
// `file1.png` to `new_file_name.png` which already exists.
const messageRenameExists = {renameLastFile: 'new_file_name.png'}; const messageRenameExists = {renameLastFile: 'new_file_name.png'};
testResponse = await sendTestMessage(messageRenameExists); testResponse = await sendTestMessage(messageRenameExists);
assertEquals( assertEquals(
testResponse.testQueryResult, 'renameOriginalFile resolved file exists'); testResponse.testQueryResult, 'renameOriginalFile resolved file exists');
// No change to the existing file. // No change to the existing file.
assertEquals(directory.files.length, 1); assertEquals(directory.files.length, 2);
assertEquals(directory.files[0].name, 'new_file_name.png'); assertEquals(directory.files[entryIndex].name, 'file1.png');
assertEquals(directory.files[1].name, 'new_file_name.png');
// Test renaming when something is out of sync with `currentFiles` and has an
// expired token.
const expiredToken = tokenGenerator.next().value;
currentFiles[entryIndex].token = expiredToken;
messageRename = {renameLastFile: 'another_name.png'};
testResponse = await sendTestMessage(messageRename);
// Fails silently, nothing changes.
assertEquals(
testResponse.testQueryResult,
'renameOriginalFile resolved FILE_NO_LONGER_IN_LAST_OPENED_DIRECTORY');
assertEquals(currentFiles[entryIndex].handle.name, 'file1.png');
assertEquals(currentFiles.length, 2);
assertEquals(directory.files.length, 2);
// Test it throws an error by simulating a failed directory change.
simulateLosingAccessToDirectory();
// Prevent the trusted context throwing errors resulting JS errors. // Prevent the trusted context throwing errors resulting JS errors.
guestMessagePipe.logClientError = error => console.log(JSON.stringify(error)); guestMessagePipe.logClientError = error => console.log(JSON.stringify(error));
guestMessagePipe.rethrowErrors = false; guestMessagePipe.rethrowErrors = false;
// Test it throws an error by simulating a failed directory change.
simulateLosingAccessToDirectory();
const messageRenameNoOp = {renameLastFile: 'new_file_name_2.png'}; const messageRenameNoOp = {renameLastFile: 'new_file_name_2.png'};
testResponse = await sendTestMessage(messageRenameNoOp); testResponse = await sendTestMessage(messageRenameNoOp);
...@@ -815,6 +851,7 @@ TEST_F('MediaAppUIBrowserTest', 'RenameOriginalIPC', async () => { ...@@ -815,6 +851,7 @@ TEST_F('MediaAppUIBrowserTest', 'RenameOriginalIPC', async () => {
testResponse.testQueryResult, testResponse.testQueryResult,
'renameOriginalFile failed Error: Error: rename-file: Rename failed. ' + 'renameOriginalFile failed Error: Error: rename-file: Rename failed. ' +
'File without launch directory.'); 'File without launch directory.');
testDone(); testDone();
}); });
......
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