mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-15 02:54:07 +00:00
explorer: implement duplicate file feature
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user