Skip to content

Folders

Martin Henz edited this page Apr 8, 2024 · 1 revision

Overview

The Source Academy makes use of BrowserFS, an in-browser file system that emulates the Node.js file system API and supports storing and retrieving files from various backends.

Accessing the File System

After the Redux store is initialised, so is the file system. At the root of the app src/index.tsx, we call createInBrowserFileSystem with the store passed in as parameter. The file system can then be accessed in other parts of the application via:

const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem);

Handling File System Updates

The information in this section is true as of 9 October 2022.

While BrowserFS emulates the Node.js file system API, it does not support the various file system listeners present in Node.js such as watch. More specifically, the interface exists but the operation is not supported in any of the backends:

BrowserFS watch is unsupported

There is an open issue in the BrowserFS repository requesting for file system listener support from November 2016, but it shows no signs of progress and is unlikely to ever be implemented.

The inability to watch for file system changes brings about complications when it comes to the handling of file system updates. For example, when a file is added to a directory in one of the React components, how do we tell another component that lists the files in the same directory to refresh its list of files from the file system? Let us call the component responsible for updating the file system Component U and the component responsible for reading from the file system Component R. Assuming we want to avoid implementing our own file system listeners (due to the complexity of doing so), we then have the following 4 cases:

  1. Component U and Component R are the same component.

    This is fairly trivial; we can trigger a read after the update is performed easily.

  2. Component R is an ancestor of Component U.

    In this case, we can pass the read function as a prop down the component tree. In Component U, we call the read function after the update is performed.

  3. Component U is an ancestor of Component R.

    After the update, we will need to force Component R to re-render so as to read the updated file system state. Unfortunately, this is not the idiomatic approach in React; child components re-render automatically when the props passed in change. While we pass the fileSystem around the components as a prop, it is merely an interface which does not reflect the state of the file system it describes. As such, there is no way for the child component to know that the underlying file system has updated.

    To get around this, we can pass in a key prop to Component R. When the value of key changes, Component R will be forced to re-render. Then in Component U, we keep track of some arbitrary state that we use as the value to key:

    const [key, setKey] = React.useState<number>(0);
    const forceRefresh = () => setKey((key + 1) % 2);

    When forceRefresh is called, Component R will be forcibly re-rendered. This should be used sparingly.

  4. Neither Component U nor Component R are ancestors of the other.

    The state of Component R will have to be managed in the lowest common ancestor component, which then simplifies this case to Case 2. However, this muddies the responsibilities of each component and results in tight coupling.

Currently, the above workarounds satisfy our needs without adding too much complexity (especially since we do not have any instances of Case 4). However, if our use cases evolve to be more complex, implementing file system watchers would probably be necessary.

Lack of Options

The information in this section is true as of 9 October 2022.

Despite being modelled after the Node.js file system API, most of the interfaces do not take in an options object. This results in certain file operations being impossible without us writing additional code.

An example of this can be seen in the rmdir function when attempting to recursively remove a directory and its contents. In Node.js, rmdir takes in recursive as an additional option to determine whether recursive directory removal should be performed. Because BrowserFS's rmdir implementation does not accept an options object, recursive directory removal is not available to us out of the box.

Additional file system behaviour that we need is implemented in src/commons/utils/FileSystemUtils.ts.

Other Things to Note

  • Not all backends support the synchronous versions of operations. When using a synchronous operation with a backend that does not support it, the error messages can be quite cryptic. The list of backends and their support of the various APIs can be found here.