mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-15 11:04:49 +00:00
fileStorage: implement file locks
This breaks down reading and writing into low-level and high level sagas. Opening now takes a lock on the file so that it can only have one writer or have multiple readers. The low-level read and write sagas use the file descriptor to determine if the file is still open before actually performing the action. Since we are using the web locks api, these locks should work across browser tabs/windows. The high-level read/write sagas perform the low-level open, read/write, close operations in a single call. The high-level delete and rename sagas are also updated to take locks on the files while performing the respective operations and will fail if the file is already being used somewhere else.
This commit is contained in:
+19
-47
@@ -4,16 +4,13 @@
|
||||
import * as browserFsAccess from 'browser-fs-access';
|
||||
import { FileWithHandle } from 'browser-fs-access';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { AsyncSaga, uuid } from '../../test';
|
||||
import { AsyncSaga } from '../../test';
|
||||
import {
|
||||
fileStorageDidFailToOpenFile,
|
||||
fileStorageDidFailToReadFile,
|
||||
fileStorageDidFailToRenameFile,
|
||||
fileStorageDidOpenFile,
|
||||
fileStorageDidReadFile,
|
||||
fileStorageDidRenameFile,
|
||||
fileStorageDidWriteFile,
|
||||
fileStorageOpenFile,
|
||||
fileStorageReadFile,
|
||||
fileStorageRenameFile,
|
||||
fileStorageWriteFile,
|
||||
@@ -56,15 +53,11 @@ describe('handleExplorerImportFiles', () => {
|
||||
|
||||
saga.put(explorerImportFiles());
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageOpenFile(testFileName));
|
||||
|
||||
saga.put(fileStorageDidOpenFile(testFileName, uuid(0)));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageWriteFile(uuid(0), testFileContents),
|
||||
fileStorageWriteFile(testFileName, testFileContents),
|
||||
);
|
||||
|
||||
saga.put(fileStorageDidWriteFile(uuid(0)));
|
||||
saga.put(fileStorageDidWriteFile(testFileName));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(explorerDidImportFiles());
|
||||
|
||||
@@ -93,10 +86,6 @@ describe('handleExplorerCreateNewFile', () => {
|
||||
|
||||
saga.put(explorerCreateNewFile('test', pythonFileExtension, Hub.Technic));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageOpenFile('test.py'));
|
||||
|
||||
saga.put(fileStorageDidOpenFile('test.py', uuid(0)));
|
||||
|
||||
await expect(saga.take()).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"contents": "from pybricks.hubs import TechnicHub
|
||||
@@ -108,12 +97,12 @@ describe('handleExplorerCreateNewFile', () => {
|
||||
hub = TechnicHub()
|
||||
|
||||
",
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"path": "test.py",
|
||||
"type": "fileStorage.action.writeFile",
|
||||
}
|
||||
`);
|
||||
|
||||
saga.put(fileStorageDidWriteFile(uuid(0)));
|
||||
saga.put(fileStorageDidWriteFile('test.py'));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(explorerDidCreateNewFile());
|
||||
|
||||
@@ -170,7 +159,6 @@ describe('handleExplorerRenameFile', () => {
|
||||
describe('handleExplorerExportFile', () => {
|
||||
let saga: AsyncSaga;
|
||||
const testFile = 'test.file';
|
||||
const testFileId = uuid(0);
|
||||
const testFileContents = '# test file contents';
|
||||
const testError = new Error('test error');
|
||||
|
||||
@@ -179,11 +167,11 @@ describe('handleExplorerExportFile', () => {
|
||||
|
||||
saga.put(explorerExportFile(testFile));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageOpenFile(testFile));
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageReadFile(testFile));
|
||||
});
|
||||
|
||||
it('should fail if file does not exist', async () => {
|
||||
saga.put(fileStorageDidFailToOpenFile(testFile, testError));
|
||||
saga.put(fileStorageDidFailToReadFile(testFile, testError));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
explorerDidFailToExportFile(testFile, testError),
|
||||
@@ -191,42 +179,26 @@ describe('handleExplorerExportFile', () => {
|
||||
});
|
||||
|
||||
describe('should read file', () => {
|
||||
beforeEach(async () => {
|
||||
saga.put(fileStorageDidOpenFile(testFile, testFileId));
|
||||
it('should export file', async () => {
|
||||
// NB: resolved value doesn't matter since it is not used
|
||||
jest.spyOn(browserFsAccess, 'fileSave').mockResolvedValue(null);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageReadFile(testFileId));
|
||||
saga.put(fileStorageDidReadFile(testFile, testFileContents));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(explorerDidExportFile(testFile));
|
||||
|
||||
expect(browserFsAccess.fileSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should catch read error', async () => {
|
||||
saga.put(fileStorageDidFailToReadFile(testFileId, testError));
|
||||
it('should catch error', async () => {
|
||||
jest.spyOn(browserFsAccess, 'fileSave').mockRejectedValue(testError);
|
||||
|
||||
saga.put(fileStorageDidReadFile(testFile, testFileContents));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
explorerDidFailToExportFile(testFile, testError),
|
||||
);
|
||||
});
|
||||
|
||||
describe('should read file', () => {
|
||||
it('should export file', async () => {
|
||||
jest.spyOn(browserFsAccess, 'fileSave').mockResolvedValue(null);
|
||||
|
||||
saga.put(fileStorageDidReadFile(testFileId, testFileContents));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
explorerDidExportFile('test.file'),
|
||||
);
|
||||
expect(browserFsAccess.fileSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should catch error', async () => {
|
||||
jest.spyOn(browserFsAccess, 'fileSave').mockRejectedValue(testError);
|
||||
|
||||
saga.put(fileStorageDidReadFile(testFileId, testFileContents));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
explorerDidFailToExportFile('test.file', testError),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+12
-60
@@ -13,15 +13,12 @@ import {
|
||||
} from 'typed-redux-saga/macro';
|
||||
import { getPybricksMicroPythonFileTemplate } from '../editor/pybricksMicroPython';
|
||||
import {
|
||||
fileStorageDidFailToOpenFile,
|
||||
fileStorageDidFailToReadFile,
|
||||
fileStorageDidFailToRenameFile,
|
||||
fileStorageDidFailToWriteFile,
|
||||
fileStorageDidOpenFile,
|
||||
fileStorageDidReadFile,
|
||||
fileStorageDidRenameFile,
|
||||
fileStorageDidWriteFile,
|
||||
fileStorageOpenFile,
|
||||
fileStorageReadFile,
|
||||
fileStorageRenameFile,
|
||||
fileStorageWriteFile,
|
||||
@@ -94,29 +91,14 @@ function* handleExplorerImportFiles(): Generator {
|
||||
|
||||
const fileName = `${baseName}${pythonFileExtension}`;
|
||||
|
||||
yield* put(fileStorageOpenFile(fileName));
|
||||
|
||||
const { didOpen, didFailToOpen } = yield* race({
|
||||
didOpen: take(fileStorageDidOpenFile.when((a) => a.path === fileName)),
|
||||
didFailToOpen: take(
|
||||
fileStorageDidFailToOpenFile.when((a) => a.path === fileName),
|
||||
),
|
||||
});
|
||||
|
||||
if (didFailToOpen) {
|
||||
throw didFailToOpen.error;
|
||||
}
|
||||
|
||||
defined(didOpen);
|
||||
|
||||
yield* put(fileStorageWriteFile(didOpen.id, text));
|
||||
yield* put(fileStorageWriteFile(fileName, text));
|
||||
|
||||
const { didFailToWrite } = yield* race({
|
||||
didWrite: take(
|
||||
fileStorageDidWriteFile.when((a) => a.id === didOpen.id),
|
||||
fileStorageDidWriteFile.when((a) => a.path === fileName),
|
||||
),
|
||||
didFailToWrite: take(
|
||||
fileStorageDidFailToWriteFile.when((a) => a.id === didOpen.id),
|
||||
fileStorageDidFailToWriteFile.when((a) => a.path === fileName),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -137,32 +119,17 @@ function* handleExplorerCreateNewFile(
|
||||
try {
|
||||
const fileName = `${action.fileName}${action.fileExtension}`;
|
||||
|
||||
yield* put(fileStorageOpenFile(fileName));
|
||||
|
||||
const { didOpen, didFailToOpen } = yield* race({
|
||||
didOpen: take(fileStorageDidOpenFile.when((a) => a.path === fileName)),
|
||||
didFailToOpen: take(
|
||||
fileStorageDidFailToOpenFile.when((a) => a.path === fileName),
|
||||
),
|
||||
});
|
||||
|
||||
if (didFailToOpen) {
|
||||
throw didFailToOpen.error;
|
||||
}
|
||||
|
||||
defined(didOpen);
|
||||
|
||||
yield* put(
|
||||
fileStorageWriteFile(
|
||||
didOpen.id,
|
||||
fileName,
|
||||
getPybricksMicroPythonFileTemplate(action.hub) || '',
|
||||
),
|
||||
);
|
||||
|
||||
const { didFailToWrite } = yield* race({
|
||||
didWrite: take(fileStorageDidWriteFile.when((a) => a.id === didOpen.id)),
|
||||
didWrite: take(fileStorageDidWriteFile.when((a) => a.path === fileName)),
|
||||
didFailToWrite: take(
|
||||
fileStorageDidFailToWriteFile.when((a) => a.id === didOpen.id),
|
||||
fileStorageDidFailToWriteFile.when((a) => a.path === fileName),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -217,29 +184,14 @@ function* handleExplorerExportFile(
|
||||
action: ReturnType<typeof explorerExportFile>,
|
||||
): Generator {
|
||||
try {
|
||||
yield* put(fileStorageOpenFile(action.fileName));
|
||||
|
||||
const { didOpen, didFailToOpen } = yield* race({
|
||||
didOpen: take(
|
||||
fileStorageDidOpenFile.when((a) => a.path === action.fileName),
|
||||
),
|
||||
didFailToOpen: take(
|
||||
fileStorageDidFailToOpenFile.when((a) => a.path === action.fileName),
|
||||
),
|
||||
});
|
||||
|
||||
if (didFailToOpen) {
|
||||
throw didFailToOpen.error;
|
||||
}
|
||||
|
||||
defined(didOpen);
|
||||
|
||||
yield* put(fileStorageReadFile(didOpen.id));
|
||||
yield* put(fileStorageReadFile(action.fileName));
|
||||
|
||||
const { didRead, didFailToRead } = yield* race({
|
||||
didRead: take(fileStorageDidReadFile.when((a) => a.id === didOpen.id)),
|
||||
didRead: take(
|
||||
fileStorageDidReadFile.when((a) => a.path === action.fileName),
|
||||
),
|
||||
didFailToRead: take(
|
||||
fileStorageDidFailToReadFile.when((a) => a.id === didOpen.id),
|
||||
fileStorageDidFailToReadFile.when((a) => a.path === action.fileName),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -254,7 +206,7 @@ function* handleExplorerExportFile(
|
||||
yield* call(() =>
|
||||
fileSave(blob, {
|
||||
id: 'pybricksCodeFileStorageExport',
|
||||
fileName: didOpen.path,
|
||||
fileName: action.fileName,
|
||||
extensions: [pythonFileExtension],
|
||||
mimeTypes: [pythonFileMimeType],
|
||||
// TODO: translate description
|
||||
|
||||
Reference in New Issue
Block a user