mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
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.
282 lines
8.5 KiB
TypeScript
282 lines
8.5 KiB
TypeScript
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) 2022 The Pybricks Authors
|
|
|
|
import { fileOpen, fileSave } from 'browser-fs-access';
|
|
import JSZip from 'jszip';
|
|
import {
|
|
call,
|
|
put,
|
|
race,
|
|
select,
|
|
take,
|
|
takeEvery,
|
|
takeLatest,
|
|
} from 'typed-redux-saga/macro';
|
|
import { getPybricksMicroPythonFileTemplate } from '../editor/pybricksMicroPython';
|
|
import {
|
|
fileStorageDidDumpAllFiles,
|
|
fileStorageDidFailToDumpAllFiles,
|
|
fileStorageDidFailToReadFile,
|
|
fileStorageDidFailToRenameFile,
|
|
fileStorageDidFailToWriteFile,
|
|
fileStorageDidReadFile,
|
|
fileStorageDidRenameFile,
|
|
fileStorageDidWriteFile,
|
|
fileStorageDumpAllFiles,
|
|
fileStorageReadFile,
|
|
fileStorageRenameFile,
|
|
fileStorageWriteFile,
|
|
} from '../fileStorage/actions';
|
|
import {
|
|
FileNameValidationResult,
|
|
pythonFileExtension,
|
|
pythonFileExtensionRegex,
|
|
pythonFileMimeType,
|
|
validateFileName,
|
|
} from '../pybricksMicropython/lib';
|
|
import { RootState } from '../reducers';
|
|
import { defined, ensureError, timestamp } from '../utils';
|
|
import {
|
|
explorerArchiveAllFiles,
|
|
explorerCreateNewFile,
|
|
explorerDidArchiveAllFiles,
|
|
explorerDidCreateNewFile,
|
|
explorerDidExportFile,
|
|
explorerDidFailToArchiveAllFiles,
|
|
explorerDidFailToCreateNewFile,
|
|
explorerDidFailToExportFile,
|
|
explorerDidFailToImportFiles,
|
|
explorerDidFailToRenameFile,
|
|
explorerDidImportFiles,
|
|
explorerDidRenameFile,
|
|
explorerExportFile,
|
|
explorerImportFiles,
|
|
explorerRenameFile,
|
|
} from './actions';
|
|
import {
|
|
renameFileDialogDidAccept,
|
|
renameFileDialogDidCancel,
|
|
renameFileDialogShow,
|
|
} from './renameFileDialog/actions';
|
|
|
|
function* handleExplorerArchiveAllFiles(): Generator {
|
|
try {
|
|
yield* put(fileStorageDumpAllFiles());
|
|
|
|
const { didDump, didFailToDump } = yield* race({
|
|
didDump: take(fileStorageDidDumpAllFiles),
|
|
didFailToDump: take(fileStorageDidFailToDumpAllFiles),
|
|
});
|
|
|
|
if (didFailToDump) {
|
|
throw didFailToDump.error;
|
|
}
|
|
|
|
defined(didDump);
|
|
|
|
const zip = new JSZip();
|
|
|
|
for (const f of didDump.files) {
|
|
yield* call(() => zip.file(f.path, f.contents));
|
|
}
|
|
|
|
const zipData = yield* call(() => zip.generateAsync({ type: 'blob' }));
|
|
|
|
const fileName = `pybricks-backup-${timestamp()}.zip`;
|
|
|
|
yield* call(() =>
|
|
fileSave(zipData, {
|
|
id: 'pybricksCodeFileStorageArchive',
|
|
fileName,
|
|
extensions: ['.zip'],
|
|
mimeTypes: ['application/zip'],
|
|
// TODO: translate description
|
|
description: 'Zip Files',
|
|
}),
|
|
);
|
|
|
|
yield* put(explorerDidArchiveAllFiles());
|
|
} catch (err) {
|
|
yield* put(explorerDidFailToArchiveAllFiles(ensureError(err)));
|
|
}
|
|
}
|
|
|
|
function* handleExplorerImportFiles(): Generator {
|
|
try {
|
|
const selectedFiles = yield* call(() =>
|
|
fileOpen({
|
|
id: 'pybricks-code-explorer-import',
|
|
mimeTypes: [pythonFileMimeType],
|
|
extensions: [pythonFileExtension],
|
|
// TODO: translate description
|
|
description: 'Python Files',
|
|
multiple: true,
|
|
excludeAcceptAllOption: true,
|
|
}),
|
|
);
|
|
|
|
for (const file of selectedFiles) {
|
|
// getting the text now to catch possible error *before* user interaction
|
|
const text = yield* call(() => file.text());
|
|
|
|
const [baseName] = file.name.split(pythonFileExtensionRegex);
|
|
const existingFiles = yield* select((s: RootState) => s.fileStorage.files);
|
|
|
|
const result = validateFileName(
|
|
baseName,
|
|
pythonFileExtension,
|
|
existingFiles.map((f) => f.path),
|
|
);
|
|
|
|
if (result != FileNameValidationResult.IsOk) {
|
|
// TODO: validate file name and allow user to rename or skip
|
|
console.error(
|
|
'skipping file',
|
|
file.name,
|
|
FileNameValidationResult[result],
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const fileName = `${baseName}${pythonFileExtension}`;
|
|
|
|
yield* put(fileStorageWriteFile(fileName, text));
|
|
|
|
const { didFailToWrite } = yield* race({
|
|
didWrite: take(
|
|
fileStorageDidWriteFile.when((a) => a.path === fileName),
|
|
),
|
|
didFailToWrite: take(
|
|
fileStorageDidFailToWriteFile.when((a) => a.path === fileName),
|
|
),
|
|
});
|
|
|
|
if (didFailToWrite) {
|
|
throw didFailToWrite.error;
|
|
}
|
|
}
|
|
|
|
yield* put(explorerDidImportFiles());
|
|
} catch (err) {
|
|
yield* put(explorerDidFailToImportFiles(ensureError(err)));
|
|
}
|
|
}
|
|
|
|
function* handleExplorerCreateNewFile(
|
|
action: ReturnType<typeof explorerCreateNewFile>,
|
|
): Generator {
|
|
try {
|
|
const fileName = `${action.fileName}${action.fileExtension}`;
|
|
|
|
yield* put(
|
|
fileStorageWriteFile(
|
|
fileName,
|
|
getPybricksMicroPythonFileTemplate(action.hub) || '',
|
|
),
|
|
);
|
|
|
|
const { didFailToWrite } = yield* race({
|
|
didWrite: take(fileStorageDidWriteFile.when((a) => a.path === fileName)),
|
|
didFailToWrite: take(
|
|
fileStorageDidFailToWriteFile.when((a) => a.path === fileName),
|
|
),
|
|
});
|
|
|
|
if (didFailToWrite) {
|
|
throw didFailToWrite.error;
|
|
}
|
|
|
|
yield* put(explorerDidCreateNewFile());
|
|
} catch (err) {
|
|
yield* put(explorerDidFailToCreateNewFile(ensureError(err)));
|
|
}
|
|
}
|
|
|
|
/** Connects user initiate rename file actions to the rename file dialog. */
|
|
function* handleExplorerRenameFile(
|
|
action: ReturnType<typeof explorerRenameFile>,
|
|
): Generator {
|
|
yield* put(renameFileDialogShow(action.fileName));
|
|
|
|
const { accepted, canceled } = yield* race({
|
|
accepted: take(renameFileDialogDidAccept),
|
|
canceled: take(renameFileDialogDidCancel),
|
|
});
|
|
|
|
if (canceled) {
|
|
yield* put(explorerDidFailToRenameFile());
|
|
return;
|
|
}
|
|
|
|
defined(accepted);
|
|
|
|
yield* put(fileStorageRenameFile(action.fileName, accepted.newName));
|
|
|
|
const didRename = yield* race({
|
|
succeeded: take(
|
|
fileStorageDidRenameFile.when((a) => a.fileName === action.fileName),
|
|
),
|
|
failed: take(
|
|
fileStorageDidFailToRenameFile.when((a) => a.fileName === action.fileName),
|
|
),
|
|
});
|
|
|
|
if (didRename.failed) {
|
|
yield* put(explorerDidFailToRenameFile());
|
|
return;
|
|
}
|
|
|
|
yield* put(explorerDidRenameFile());
|
|
}
|
|
|
|
function* handleExplorerExportFile(
|
|
action: ReturnType<typeof explorerExportFile>,
|
|
): Generator {
|
|
try {
|
|
yield* put(fileStorageReadFile(action.fileName));
|
|
|
|
const { didRead, didFailToRead } = yield* race({
|
|
didRead: take(
|
|
fileStorageDidReadFile.when((a) => a.path === action.fileName),
|
|
),
|
|
didFailToRead: take(
|
|
fileStorageDidFailToReadFile.when((a) => a.path === action.fileName),
|
|
),
|
|
});
|
|
|
|
if (didFailToRead) {
|
|
throw didFailToRead.error;
|
|
}
|
|
|
|
defined(didRead);
|
|
|
|
const blob = new Blob([didRead.contents], { type: `${pythonFileMimeType}` });
|
|
|
|
yield* call(() =>
|
|
fileSave(blob, {
|
|
id: 'pybricksCodeFileStorageExport',
|
|
fileName: action.fileName,
|
|
extensions: [pythonFileExtension],
|
|
mimeTypes: [pythonFileMimeType],
|
|
// TODO: translate description
|
|
description: 'Python Files',
|
|
}),
|
|
);
|
|
|
|
yield* put(explorerDidExportFile(action.fileName));
|
|
} catch (err) {
|
|
yield* put(explorerDidFailToExportFile(action.fileName, ensureError(err)));
|
|
}
|
|
}
|
|
|
|
export default function* (): Generator {
|
|
yield* takeEvery(explorerArchiveAllFiles, handleExplorerArchiveAllFiles);
|
|
yield* takeEvery(explorerImportFiles, handleExplorerImportFiles);
|
|
yield* takeEvery(explorerCreateNewFile, handleExplorerCreateNewFile);
|
|
// takeLatest should ensure that if we trigger a new rename before the
|
|
// previous one is finished, the old one will be canceled. We don't expect
|
|
// this to happen in practice though.
|
|
yield* takeLatest(explorerRenameFile, handleExplorerRenameFile);
|
|
yield* takeLatest(explorerExportFile, handleExplorerExportFile);
|
|
}
|