File System Access API: How VSCode.dev Edits Local Files in the Browser

File System Access API: How VSCode.dev Edits Local Files in the Browser

ยท

8 min read

Featured on Hashnode
Play this article

Have you ever given vscode.dev a try? It's the online version of the popular code editor, vscode, that so many people love. With vscode.dev, you get all the same capabilities as the offline version of the app. You can do things like version control, search through files, install extensions, lint and format your code and more.

One of the coolest things about vscode.dev is that you can easily open a project folder directly from your local machine. Once you've done that, you can seamlessly continue your development process right from your browser. You can add new files, make changes to existing ones, and do so much more, all while working within the browser. And all those changes you make are automatically reflected in your project's folder on your local file system.

vscode.dev running in the browser

But, how does vscode.dev manages to read and change your local files? Thanks to the File System Access API, which makes all of this possible!

Introducing File System Access API

The File System Access API allows online applications, like vscode.dev, to interact with your computer's files in a safe and controlled way. Without the need for any server-side intervention.

It's like a virtual bridge that connects the online world to your local files. With this API, vscode.dev can ask your permission to access specific folders on your computer. Once you give it the green light, it can read files from those folders, make changes to them, and even save new files there.

Reading A File

When you click "open file" in vscode.dev, a file picker appears, letting you choose any file from your computer. This is made possible by the window.showOpenFilePicker method. This method shows a file picker that allows the user to select one or more files from his local machine. And returns an array of FileSystemHandles (We will call it fileHandle for ease). The fileHandle object contains some metadata about the selected file as the file type and file name and it acts as a reference for the file. This fileHandle will be used later on to get the file itself and its contents.

const [fileHandle] = await window.showOpenFilePicker();

The showOpenFilePicker method also accepts an options parameter that can customize how it behaves, like allowing choosing multiple files, accepting certain file types only, etc. For example, you can allow selecting multiple files or restrict it to certain file types only. Giving you more control over the file picker.

const pickerOptions = {
    types: [
        {
          description: "Images",
          accept: {"image/*": [".png", ".gif", ".jpeg", ".jpg"]},
        },
    ],
    multiple: true,
}

const [fileHandle] = await window.showOpenFilePicker(pickerOptions);

Once you have the fileHandle for the file you picked. You can use it to get the file itself, and then you can read the contents of the file.

const [fileHandle] = await window.showOpenFilePicker(pickerOptions);
const file = await fileHandle.getFile();
const content = await file.text();

Reading Folder

Reading a folder is quite similar to reading files. Instead of using the showOpenFilePicker method, we use showDirectoryPicker. It shows a popup where users can choose a folder. Once they select a folder, it gives us a FileSystemDirectoryHandle (We will call it DirectoryHandle for ease). This directoryHandle is like the fileHandle we talked about earlier. It acts as a reference for the chosen folder and gives us some metadata about it, like its kind and its name.

const directoryHandle = await window.showDirectoryPicker();

To access the contents of the directory, we use the directoryHandle.values() method. This method returns an asynchronous iterator, which means we can go through the directory's contents one by one. The directory can contain both files and other folders. So, each item returned by the directoryHandle.values() method can be either a fileHandle or another folderHandle and we can distinguish between the two by checking the kind property.

const directoryHandle = await window.showDirectoryPicker();
for await (const entry of directoryHandle.values()) {
    if (entry.kind === 'file') {
        const fileHandle = entry; 
        // Do whatever we need with the file
    }

    if (entry.kind === 'directory') {
        const subdirectoryHandl = entry;
        // Do whatever we need with the folder 
    }
}

Saving Changes To Opened File

So, when we're done making all the changes to the file and want to save it back to our computer, we remember that special fileHandle we got when we first chose the file. It's like our key to access that file again.

Using that fileHandle, we create something called a writable stream on the file. Then, we use this stream to write our changes to the file.

The writable stream doesn't immediately change the original file. Instead, it writes everything to a temporary file first. Only when the stream is completely done, and we close it, does it swap the temporary file with our intended file. That's when our changes become permanent!

So, to make sure our changes happen, we need to close the stream after writing to the file. This way, our modifications take effect, and the file on our computer gets updated.

const writableStream = await fileHandle.createWritable();
await writableStream.write(newContent);
await writableStream.close();

Creating New File

What if we want to make a completely new file that isn't already there in our computer's files? no worries! We've learned about the showOpenFilePicker method, which helps us open files from our local system. Now, we have a similar method showSaveFilePicker that helps us in doing so. The difference is, Instead of opening a dialog to choose a file to open, showSaveFilePicker opens a dialog to save a brand-new file.

Using this method is straightforward! It opens the save dialog, and when you choose a location to save the file, it gives you the fileHandle. With that fileHandle, you can read the file or save changes to it, just like we did before.

const fileHandle = await window.showSaveFilePicker();

It also accepts an optional parameter. With this parameter, you can customize some things, like suggesting a name for the new file and setting its extension.

const options = {
    suggestedName: "NasserSpace.txt"
};
const fileHandle = await window.showSaveFilePicker(options);

Browser Permissions Needed!

The operating system won't let any website read or write to the user's disk without his permission. It's like when you install a new app on your mobile, and it asks for your permission to access things like files, camera, microphone, and so on.

In the same way, when a user visits your website, it needs his approval to access his local file system. The user is in control, and they have the final say on whether they want to grant your web app permission or not.

There are two types of file permissions: "read" and "readwrite". With "read" permission, you can only read the file's contents, while "readwrite" permission allows you to both read and write to the file.

Grant Permission Once

When the user picks a file with the showOpenFilePicker method, the browser automatically grants us "read" permission for that file. It makes sense since the user intentionally selected that file to read.

But when we read a whole folder using showDirectoryPicker, the browser requests the user's permission to access all the files within the folder with "read" permission.

This process only happens once. Once the user grants permission, we can read all the files without asking again.

In the same way, When you try to modify a file or folder for the first time, the browser will ask for the user's permission to do so. The permission that we need here is readwrite permission. The user only needs to grant this permission once. Once it's granted, you can freely modify and save new files to that folder and its subfolders without the user being asked again.

Getting permission is a one-time setup that makes future accessing or writing to the disk smooth and hassle-free!

Checking Users Permissions

In certain situations, we may need to check if the user has allowed us to access their local files. If they haven't granted permission yet, we can take the opportunity to explain to him why we need it and how we'll handle their files, to put their minds at ease. We can also guide the user on how to grant the permission, or if required, ask them again for permission.

To check permissions for a specific file, you can use the queryPermission method on the fileHandle you want to access.

The queryPermission method returns one of those values:

  • "granted": permission has already been granted by the user.

  • "denied": permission has been denied by the user.

  • "prompt": user has not explicitly granted or denied permission yet.

const options = { mode: 'readwrite' };
const permissionStatus = await fileHandle.queryPermission(options);

if(permissionStatus == 'denied') {
    // Show user why do we need his permission
}

if(permissionStatus == 'prompt') {
    // Request User Permission
}

Requesting User Permission

When we try to read or modify a file, the browser automatically shows a dialog asking for user permission. But sometimes, we might want to ask for permission explicitly. To do that, we can use a new function called requestPermission.

This method also accepts an optional parameter that allows us to specify whether we're asking for read permission only or readwrite permission. It returns the same result as the queryPermission method: either "granted", "denied", or "prompt".

const options = { mode: 'read' };
const permissionStatus = await fileHandle.requestPermission(options);
๐Ÿ’ก
If the user has already granted access to that file, requestPermission won't show the request permission dialog again. Instead, it will directly return "granted".

Browsers Compatibility

Supported: Chrome, Microsoft Edge, Opera, Brave, Arc, and other Chromium-based browsers.

Not Supported (yet): Firefox, Safari.

Opening Doors to Innovation ๐Ÿ’ก

Building VSCode on top of the File System Access API is just one awesome application, but there's so much more we can do with this powerful tool! Some cool examples:

  1. File Management Apps: We can create handy apps that help you organize your local files for better productivity. No more searching through cluttered folders!

  2. Videos and Photos Editors: A web-based media editor that uses web technologies like web assembly and WebGPU along with the File System Access API. It lets you edit videos and photos right on your computer without any hassle.

  3. File Type Conversion Apps: We can build a web app that handles file conversions from one format to another without sending them to a server. Quick and easy!

The possibilities are endless! With the File System Access API, we have a wide door open for creating and innovating all kinds of awesome applications. Let your imagination run wild!

ย