mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
explorer: split archive saga from fileStorage
This splits the fileStorage archive into two sagas to separate concerns. fileStorage now just gives a dump of the database and explorer deals with the user interaction and zipping.
This commit is contained in:
+15
-11
@@ -326,24 +326,28 @@ export const fileStorageDidFailToRenameFile = createAction(
|
||||
);
|
||||
|
||||
/**
|
||||
* Request to archive (download) all files in the store.
|
||||
* Requests file storage to dump all file paths and contents currently in storage.
|
||||
*/
|
||||
export const fileStorageArchiveAllFiles = createAction(() => ({
|
||||
type: 'fileStorage.action.archiveAllFiles',
|
||||
export const fileStorageDumpAllFiles = createAction(() => ({
|
||||
type: 'fileStorage.action.dumpAllFiles',
|
||||
}));
|
||||
|
||||
/**
|
||||
* Indicates that fileStorageArchiveAllFiles() succeeded.
|
||||
* Indicates that {@link fileStorageDumpAllFiles} succeeded.
|
||||
* @param files: An array of all file paths and contents.
|
||||
*/
|
||||
export const fileStorageDidArchiveAllFiles = createAction(() => ({
|
||||
type: 'fileStorage.action.didArchiveAllFiles',
|
||||
}));
|
||||
export const fileStorageDidDumpAllFiles = createAction(
|
||||
(files: ReadonlyArray<Readonly<{ path: string; contents: string }>>) => ({
|
||||
type: 'fileStorage.action.didDumpAllFiles',
|
||||
files,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates that fileStorageArchiveAllFiles() failed.
|
||||
* @param error The error that was raised.
|
||||
* Indicates that {@link fileStorageDumpAllFiles} succeeded.
|
||||
* @param error The error.
|
||||
*/
|
||||
export const fileStorageDidFailToArchiveAllFiles = createAction((error: Error) => ({
|
||||
type: 'fileStorage.action.didFailToArchiveAllFiles',
|
||||
export const fileStorageDidFailToDumpAllFiles = createAction((error: Error) => ({
|
||||
type: 'fileStorage.action.didFailToDumpAllFiles',
|
||||
error,
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import * as browserFsAccess from 'browser-fs-access';
|
||||
import 'fake-indexeddb/auto';
|
||||
import Dexie from 'dexie';
|
||||
import 'dexie-observable';
|
||||
@@ -11,16 +10,15 @@ import {
|
||||
FD,
|
||||
FileMetadata,
|
||||
FileOpenMode,
|
||||
fileStorageArchiveAllFiles,
|
||||
fileStorageClose,
|
||||
fileStorageDeleteFile,
|
||||
fileStorageDidAddItem,
|
||||
fileStorageDidArchiveAllFiles,
|
||||
fileStorageDidChangeItem,
|
||||
fileStorageDidClose,
|
||||
fileStorageDidDeleteFile,
|
||||
fileStorageDidFailToArchiveAllFiles,
|
||||
fileStorageDidDumpAllFiles,
|
||||
fileStorageDidFailToDeleteFile,
|
||||
fileStorageDidFailToDumpAllFiles,
|
||||
fileStorageDidFailToInitialize,
|
||||
fileStorageDidFailToOpen,
|
||||
fileStorageDidFailToRead,
|
||||
@@ -36,6 +34,7 @@ import {
|
||||
fileStorageDidRenameFile,
|
||||
fileStorageDidWrite,
|
||||
fileStorageDidWriteFile,
|
||||
fileStorageDumpAllFiles,
|
||||
fileStorageOpen,
|
||||
fileStorageRead,
|
||||
fileStorageReadFile,
|
||||
@@ -45,8 +44,6 @@ import {
|
||||
} from './actions';
|
||||
import fileStorage from './sagas';
|
||||
|
||||
jest.mock('browser-fs-access');
|
||||
|
||||
/** SHA256 hash of '' */
|
||||
const emptyFileSha256 =
|
||||
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
||||
@@ -700,33 +697,39 @@ describe('renameFile', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('archive', () => {
|
||||
describe('dump all files', () => {
|
||||
let saga: AsyncSaga;
|
||||
let testFile: FileMetadata;
|
||||
let testFileContents: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
await setUpTestFile(saga);
|
||||
[testFile, testFileContents] = await setUpTestFile(saga);
|
||||
});
|
||||
|
||||
it('should archive file', async () => {
|
||||
jest.spyOn(browserFsAccess, 'fileSave');
|
||||
saga.put(fileStorageDumpAllFiles());
|
||||
|
||||
saga.put(fileStorageArchiveAllFiles());
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidArchiveAllFiles());
|
||||
expect(browserFsAccess.fileSave).toHaveBeenCalled();
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidDumpAllFiles([
|
||||
{ path: testFile.path, contents: testFileContents },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should catch error', async () => {
|
||||
const testError = new Error('test error');
|
||||
jest.spyOn(browserFsAccess, 'fileSave').mockRejectedValue(testError);
|
||||
|
||||
saga.put(fileStorageArchiveAllFiles());
|
||||
jest.spyOn(Dexie.prototype, 'transaction').mockImplementation(() => {
|
||||
throw testError;
|
||||
});
|
||||
|
||||
saga.put(fileStorageDumpAllFiles());
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidFailToArchiveAllFiles(testError),
|
||||
fileStorageDidFailToDumpAllFiles(testError),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+15
-24
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { fileSave } from 'browser-fs-access';
|
||||
import Dexie, { Table } from 'dexie';
|
||||
import {
|
||||
ICreateChange,
|
||||
@@ -10,10 +9,9 @@ import {
|
||||
IUpdateChange,
|
||||
} from 'dexie-observable/api';
|
||||
import 'dexie-observable';
|
||||
import JSZip from 'jszip';
|
||||
import { eventChannel } from 'redux-saga';
|
||||
import { call, fork, put, race, take, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { defined, ensureError, timestamp } from '../utils';
|
||||
import { defined, ensureError } from '../utils';
|
||||
import { sha256Digest } from '../utils/crypto';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
import {
|
||||
@@ -21,16 +19,15 @@ import {
|
||||
FileMetadata,
|
||||
FileOpenMode,
|
||||
UUID,
|
||||
fileStorageArchiveAllFiles,
|
||||
fileStorageClose,
|
||||
fileStorageDeleteFile,
|
||||
fileStorageDidAddItem,
|
||||
fileStorageDidArchiveAllFiles,
|
||||
fileStorageDidChangeItem,
|
||||
fileStorageDidClose,
|
||||
fileStorageDidDeleteFile,
|
||||
fileStorageDidFailToArchiveAllFiles,
|
||||
fileStorageDidDumpAllFiles,
|
||||
fileStorageDidFailToDeleteFile,
|
||||
fileStorageDidFailToDumpAllFiles,
|
||||
fileStorageDidFailToInitialize,
|
||||
fileStorageDidFailToOpen,
|
||||
fileStorageDidFailToRead,
|
||||
@@ -46,6 +43,7 @@ import {
|
||||
fileStorageDidRenameFile,
|
||||
fileStorageDidWrite,
|
||||
fileStorageDidWriteFile,
|
||||
fileStorageDumpAllFiles,
|
||||
fileStorageOpen,
|
||||
fileStorageRead,
|
||||
fileStorageReadFile,
|
||||
@@ -615,30 +613,23 @@ function* handleRenameFile(
|
||||
}
|
||||
}
|
||||
|
||||
function* handleArchiveAllFiles(db: FileStorageDb): Generator {
|
||||
function* handleDumpAllFiles(db: FileStorageDb): Generator {
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
const dump = new Array<{ path: string; contents: string }>();
|
||||
|
||||
yield* call(() => db._contents.each((f) => zip.file(f.path, f.contents)));
|
||||
|
||||
const zipData = yield* call(() => zip.generateAsync({ type: 'blob' }));
|
||||
|
||||
const fileName = `pybricks-backup-${timestamp()}.zip`;
|
||||
// REVISIT: consider using dexie-export-import addon if we want to do
|
||||
// a full backup instead of just the file contents
|
||||
// https://www.npmjs.com/package/dexie-export-import
|
||||
|
||||
yield* call(() =>
|
||||
fileSave(zipData, {
|
||||
id: 'pybricksCodeFileStorageArchive',
|
||||
fileName,
|
||||
extensions: ['.zip'],
|
||||
mimeTypes: ['application/zip'],
|
||||
// TODO: translate description
|
||||
description: 'Zip Files',
|
||||
}),
|
||||
db.transaction('r', db._contents, () =>
|
||||
db._contents.each((f) => dump.push(f)),
|
||||
),
|
||||
);
|
||||
|
||||
yield* put(fileStorageDidArchiveAllFiles());
|
||||
yield* put(fileStorageDidDumpAllFiles(dump));
|
||||
} catch (err) {
|
||||
yield* put(fileStorageDidFailToArchiveAllFiles(ensureError(err)));
|
||||
yield* put(fileStorageDidFailToDumpAllFiles(ensureError(err)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,7 +692,7 @@ function* initialize(): Generator {
|
||||
yield* takeEvery(fileStorageWriteFile, handleWriteFile);
|
||||
yield* takeEvery(fileStorageDeleteFile, handleDeleteFile, db);
|
||||
yield* takeEvery(fileStorageRenameFile, handleRenameFile, db);
|
||||
yield* takeEvery(fileStorageArchiveAllFiles, handleArchiveAllFiles, db);
|
||||
yield* takeEvery(fileStorageDumpAllFiles, handleDumpAllFiles, db);
|
||||
|
||||
const files = yield* call(() => db.metadata.toArray());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user