explorer: implement duplicate file feature

This commit is contained in:
David Lechner
2022-04-08 18:11:09 -05:00
parent 30741b13ef
commit e0a6926d01
25 changed files with 693 additions and 37 deletions
+33
View File
@@ -261,6 +261,39 @@ export const fileStorageDidFailToWriteFile = createAction(
}),
);
/**
* Request to copy a file from storage.
* @param path: The path of the file to be copied.
* @param newPath: The path of the new file to be created.
*/
export const fileStorageCopyFile = createAction((path: string, newPath: string) => ({
type: 'fileStorage.action.copyFile',
path,
newPath,
}));
/**
* Indicates that {@link fileStorageCopyFile} succeeded.
* @param path: The file path.
*/
export const fileStorageDidCopyFile = createAction((path: string) => ({
type: 'fileStorage.action.didCopyFile',
path,
}));
/**
* Indicates that {@link fileStorageCopyFile} failed.
* @param path: The file path.
* @param error The error.
*/
export const fileStorageDidFailToCopyFile = createAction(
(path: string, error: Error) => ({
type: 'fileStorage.action.didFailToCopyFile',
path,
error,
}),
);
/**
* Request to delete a file from storage.
* @param path: The file path.
+75
View File
@@ -11,12 +11,15 @@ import {
FileMetadata,
FileOpenMode,
fileStorageClose,
fileStorageCopyFile,
fileStorageDeleteFile,
fileStorageDidAddItem,
fileStorageDidChangeItem,
fileStorageDidClose,
fileStorageDidCopyFile,
fileStorageDidDeleteFile,
fileStorageDidDumpAllFiles,
fileStorageDidFailToCopyFile,
fileStorageDidFailToDeleteFile,
fileStorageDidFailToDumpAllFiles,
fileStorageDidFailToInitialize,
@@ -544,6 +547,78 @@ describe('writeFile', () => {
});
});
describe('copyFile', () => {
let saga: AsyncSaga;
let testFile: FileMetadata;
beforeEach(async () => {
saga = new AsyncSaga(fileStorage);
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
[testFile] = await setUpTestFile(saga);
});
it('should fail if file does not exist', async () => {
saga.put(fileStorageCopyFile('other.file', 'new.file'));
await expect(saga.take()).resolves.toEqual(
fileStorageDidFailToCopyFile(
'other.file',
new Error("file 'other.file' does not exist"),
),
);
});
it('should fail if new file is open', async () => {
saga.put(fileStorageOpen('new.file', 'w'));
await expect(saga.take()).resolves.toEqual(
fileStorageDidOpen('new.file', 1 as FD),
);
saga.put(fileStorageCopyFile('test.file', 'new.file'));
await expect(saga.take()).resolves.toEqual(
fileStorageDidFailToCopyFile(
'test.file',
new Error("file 'new.file' is in use"),
),
);
});
it('should fail if new file exists', async () => {
saga.put(fileStorageOpen('new.file', 'w'));
await expect(saga.take()).resolves.toEqual(
fileStorageDidOpen('new.file', 1 as FD),
);
saga.put(fileStorageClose(1 as FD));
await expect(saga.take()).resolves.toEqual(fileStorageDidClose(1 as FD));
saga.put(fileStorageCopyFile('test.file', 'new.file'));
await expect(saga.take()).resolves.toEqual(
fileStorageDidFailToCopyFile(
'test.file',
new Error("file 'new.file' already exists"),
),
);
});
it('should copy file', async () => {
saga.put(fileStorageCopyFile('test.file', 'new.file'));
await expect(saga.take()).resolves.toEqual(fileStorageDidCopyFile('test.file'));
await expect(saga.take()).resolves.toEqual(
fileStorageDidAddItem({ ...testFile, uuid: uuid(1), path: 'new.file' }),
);
});
afterEach(async () => {
await saga.end();
});
});
describe('deleteFile', () => {
let saga: AsyncSaga;
let testFile: FileMetadata;
+64
View File
@@ -20,12 +20,15 @@ import {
FileOpenMode,
UUID,
fileStorageClose,
fileStorageCopyFile,
fileStorageDeleteFile,
fileStorageDidAddItem,
fileStorageDidChangeItem,
fileStorageDidClose,
fileStorageDidCopyFile,
fileStorageDidDeleteFile,
fileStorageDidDumpAllFiles,
fileStorageDidFailToCopyFile,
fileStorageDidFailToDeleteFile,
fileStorageDidFailToDumpAllFiles,
fileStorageDidFailToInitialize,
@@ -481,6 +484,66 @@ function* handleWriteFile(action: ReturnType<typeof fileStorageWriteFile>): Gene
}
}
function* handleCopyFile(
db: FileStorageDb,
action: ReturnType<typeof fileStorageCopyFile>,
): Generator {
try {
yield* call(() =>
navigator.locks.request(
lockNameForPath(action.newPath),
{ ifAvailable: true },
async (lock) => {
if (lock === null) {
throw new Error(`file '${action.newPath}' is in use`);
}
await db.transaction('rw', db.metadata, db._contents, async () => {
const metadata = await db.metadata
.where('path')
.equals(action.path)
.first();
if (!metadata) {
throw new Error(`file '${action.path}' does not exist`);
}
if (
await db.metadata
.where('path')
.equals(action.newPath)
.first()
) {
throw new Error(`file '${action.newPath}' already exists`);
}
await db.metadata.add((<Omit<FileMetadata, 'uuid'>>{
...metadata,
uuid: undefined,
path: action.newPath,
}) as FileMetadata);
const contents = await db._contents.get(metadata.path);
// istanbul ignore if: should not be reachable
if (!contents) {
throw new Error(
`bug: missing file content for ${metadata.path}`,
);
}
await db._contents.add({ ...contents, path: action.newPath });
});
},
),
);
yield* put(fileStorageDidCopyFile(action.path));
} catch (err) {
yield* put(fileStorageDidFailToCopyFile(action.path, ensureError(err)));
}
}
/**
* Deletes a file from storage.
* @param db The database instance.
@@ -690,6 +753,7 @@ function* initialize(): Generator {
yield* takeEvery(fileStorageWrite, handleWrite, db, openFds);
yield* takeEvery(fileStorageReadFile, handleReadFile);
yield* takeEvery(fileStorageWriteFile, handleWriteFile);
yield* takeEvery(fileStorageCopyFile, handleCopyFile, db);
yield* takeEvery(fileStorageDeleteFile, handleDeleteFile, db);
yield* takeEvery(fileStorageRenameFile, handleRenameFile, db);
yield* takeEvery(fileStorageDumpAllFiles, handleDumpAllFiles, db);