Files
pybricks-code/src/explorer/sagas.ts
T
David Lechner c6cf0608f4 explorer: move files state from fileStorage
This state should be exclusivly used to for displaying files in the
explorer, so the state should be in the explorer branch of the
reducer tree.
2022-04-04 18:59:13 -05:00

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.explorer.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);
}