From 33974c52b343ed83c2b067f8c7fe539f024db8c9 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 12 Mar 2022 14:18:02 -0600 Subject: [PATCH] fileStorage: add rename file actions --- src/fileStorage/actions.ts | 41 +++++++++++++++++++++++++++++++++++ src/fileStorage/sagas.test.ts | 25 +++++++++++++++++++++ src/fileStorage/sagas.ts | 36 ++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/src/fileStorage/actions.ts b/src/fileStorage/actions.ts index 2d3f575a..3b04136b 100644 --- a/src/fileStorage/actions.ts +++ b/src/fileStorage/actions.ts @@ -109,6 +109,47 @@ export const fileStorageDidFailToDeleteFile = createAction( }), ); +/** + * Requests for a file to be renamed. + * @param oldName The name of a file that exists in storage. + * @param newName The new name for the file. + */ +export const fileStorageRenameFile = createAction( + (oldName: string, newName: string) => ({ + type: 'fileStorage.action.renameFile', + oldName, + newName, + }), +); + +/** + * Indicates that fileStorageRenameFile(oldName, newName) succeeded. + * @param oldName The previous file name. + * @param newName The current file name. + */ +export const fileStorageDidRenameFile = createAction( + (oldName: string, newName: string) => ({ + type: 'fileStorage.action.didRenameFile', + oldName, + newName, + }), +); + +/** + * Indicates that fileStorageRenameFile(oldName, newName) failed. + * @param oldName The current file name. + * @param newName The requested new file name. + * @param error The error. + */ +export const fileStorageDidFailToRenameFile = createAction( + (oldName: string, newName: string, error: Error) => ({ + type: 'fileStorage.action.didRenameFile', + oldName, + newName, + error, + }), +); + /** * Request to export (download) a file. * @param fileName The name of the file. diff --git a/src/fileStorage/sagas.test.ts b/src/fileStorage/sagas.test.ts index 5c789611..4c8e8d3b 100644 --- a/src/fileStorage/sagas.test.ts +++ b/src/fileStorage/sagas.test.ts @@ -17,9 +17,11 @@ import { fileStorageDidInitialize, fileStorageDidReadFile, fileStorageDidRemoveItem, + fileStorageDidRenameFile, fileStorageDidWriteFile, fileStorageExportFile, fileStorageReadFile, + fileStorageRenameFile, fileStorageWriteFile, } from './actions'; import fileStorage from './sagas'; @@ -136,6 +138,29 @@ it('should delete files', async () => { await saga.end(); }); +describe('rename', () => { + it('should rename files', async () => { + const newName = 'new.file'; + + const saga = new AsyncSaga(fileStorage); + + const [testFileName] = await setUpTestFile(saga); + + saga.put(fileStorageRenameFile(testFileName, newName)); + + const action = await saga.take(); + expect(action).toEqual(fileStorageDidChangeItem(newName)); + + const action2 = await saga.take(); + expect(action2).toEqual(fileStorageDidRemoveItem(testFileName)); + + const action3 = await saga.take(); + expect(action3).toEqual(fileStorageDidRenameFile(testFileName, newName)); + + await saga.end(); + }); +}); + describe('export', () => { it('should fail if file does not exist', async () => { const testFileName = 'test.file'; diff --git a/src/fileStorage/sagas.ts b/src/fileStorage/sagas.ts index 058c8548..9bced53c 100644 --- a/src/fileStorage/sagas.ts +++ b/src/fileStorage/sagas.ts @@ -23,13 +23,16 @@ import { fileStorageDidFailToExportFile, fileStorageDidFailToInitialize, fileStorageDidFailToReadFile, + fileStorageDidFailToRenameFile, fileStorageDidFailToWriteFile, fileStorageDidInitialize, fileStorageDidReadFile, fileStorageDidRemoveItem, + fileStorageDidRenameFile, fileStorageDidWriteFile, fileStorageExportFile, fileStorageReadFile, + fileStorageRenameFile, fileStorageWriteFile, } from './actions'; @@ -167,6 +170,38 @@ function* handleDeleteFile( } } +/** + * Renames a file in storage. + * @param files The localForage instance. + * @param action The action that triggered this saga. + */ +function* handleRenameFile( + files: LocalForage, + action: ReturnType, +) { + try { + yield* call(async () => { + // There is no move/rename API, so we have to make a copy with the + // new name and delete the old one. + // FIXME: This should be an atomic operation, e.g. if removing the + // old file fails, the new file should be removed. + const contents = await files.getItem(action.oldName); + await files.setItem(action.newName, contents); + await files.removeItem(action.oldName); + }); + + yield* put(fileStorageDidRenameFile(action.oldName, action.newName)); + } catch (err) { + yield* put( + fileStorageDidFailToRenameFile( + action.oldName, + action.newName, + ensureError(err), + ), + ); + } +} + function* handleArchiveAllFiles(files: LocalForage): Generator { try { const zip = new JSZip(); @@ -264,6 +299,7 @@ function* initialize(): Generator { yield* takeEvery(fileStorageReadFile, handleReadFile, files); yield* takeEvery(fileStorageWriteFile, handleWriteFile, files); yield* takeEvery(fileStorageDeleteFile, handleDeleteFile, files); + yield* takeEvery(fileStorageRenameFile, handleRenameFile, files); yield* takeEvery(fileStorageExportFile, handleExportFile, files); yield* takeEvery(fileStorageArchiveAllFiles, handleArchiveAllFiles, files);