Skip to content

Commit

Permalink
start rewriting fs and lock
Browse files Browse the repository at this point in the history
  • Loading branch information
Pistonight committed Feb 23, 2024
1 parent 882438f commit 7fad9f2
Show file tree
Hide file tree
Showing 27 changed files with 1,273 additions and 54 deletions.
23 changes: 23 additions & 0 deletions libs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions libs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "libs",
"private": true,
"version": "0.0.0",
"type": "module",
"dependencies": {
"denque": "^2.1.0"
}
}
11 changes: 11 additions & 0 deletions libs/pure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# pure

Collection of javascript libraries used internally in my projects.

These are inherently unstable. However if you want, you can depend on it
either by copying them into your project or use other creative methods.

Some libraries have external dependencies, which you want to state in the `package.json`
of your project. This repo doesn't have one :)

All code are licensed under UNLICENSE unless otherwise stated.
56 changes: 56 additions & 0 deletions libs/pure/src/fs/FsFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ResultHandle } from "../result";
import { FsResult } from "./error";

/// Interface for operating on a file in the loaded file system
export interface FsFile {
/// Path of the file relative to the root of the file system (the uploaded directory)
readonly path: string;

/// Returns if the content of the file in memory is newer than the file on disk
isDirty(): boolean;

/// Get the last modified time. May load it from file system if needed
getLastModified(r: ResultHandle): Promise<FsResult<number>>;

/// Get the text content of the file
///
/// If the file is not loaded, it will load it.
///
/// If the file is not a text file, it will return InvalidEncoding
getText(r: ResultHandle): Promise<FsResult<string>>;

/// Get the content of the file
getBytes(r: ResultHandle): Promise<FsResult<Uint8Array>>

/// Set the content in memory. Does not save to disk.
/// Does nothing if file is closed
setText(content: string): void;

/// Set the content in memory. Does not save to disk.
/// Does nothing if file is closed
setBytes(content: Uint8Array): void;

/// Load the file's content if it's not newer than fs
///
/// Returns Ok if the file is newer than fs
loadIfNotDirty(r: ResultHandle): Promise<FsResult<void>>;

/// Load the file's content from FS.
///
/// Overwrites any unsaved changes in memory only if the file was modified
/// at a later time than the last in memory modification.
///
/// If it fails, the file's content in memory will not be changed
load(r: ResultHandle): Promise<FsResult<void>>;

/// Save the file's content to FS if it is dirty.
///
/// If not dirty, returns Ok
writeIfNewer(r: ResultHandle): Promise<FsResult<void>>;

/// Close the file. In memory content will be lost.
/// Further operations on the file will fail
close(): void;

}

79 changes: 79 additions & 0 deletions libs/pure/src/fs/FsFileSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ResultHandle } from "../result";
import { FsFile } from "./FsFile";
import { FsResult } from "./error";
import { FsCapabilities } from "./support";

/// File system before it is initialized
///
/// This is an internal type used inside fsOpen functions
export interface FsFileSystemUninit {
/// Initialize the file system
init(r: ResultHandle): Promise<FsResult<FsFileSystem>>;
}

/// Initialized file system
export interface FsFileSystem {

/// Get the root path of the file system for display
///
/// The returned string has no significance in the file system itself.
/// It should only be used as an indicator to the user.
readonly root: string;

/// Capabilities of this file system implementation
/// See README.md for more information
readonly capabilities: FsCapabilities;

/// List files in a directory
///
/// The input path should be relative to the root (of the uploaded directory).
///
/// Returns a list of file names in the directory (not full paths).
/// Directory names end with a slash.
///
/// Returns Fail if the underlying file system operation fails.
listDir: (r: ResultHandle, path: string) => Promise<FsResult<string[]>>;

/// Get a file object for operations
///
/// The returned object can store temporary state for the file, such
/// as newer content. Calling openFile with the same path will
/// return the same object.
///
/// Note that opening a file doesn't actually block the file
/// from being modified by programs other than the browser.
///
/// You can make the FsFileSystem forget about the file by
/// calling `close` on the file object.
getFile: (path: string) => FsFile;

/// Get all paths that `getFile` has been called with but not `close`d
getOpenedPaths: () => string[];

}

/// Internal APIs
export interface FsFileSystemInternal {
/// Read the file as a File object
///
/// Returns Fail if the underlying file system operation fails.
read: (r: ResultHandle, path: string) => Promise<FsResult<File>>;

/// Write content to a file
///
/// Writes the content to the path specified.
/// If the content is a string, UTF-8 encoding is used.
///
/// Will overwrite existing file.
///
/// Returns Fail if the underlying file system operation fails.
/// Returns NotSupported if the browser does not support this
write: (
r: ResultHandle,
path: string,
content: Uint8Array,
) => Promise<FsResult<void>>;

/// Forget about a file
closeFile: (path: string) => void;
}
106 changes: 106 additions & 0 deletions libs/pure/src/fs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# pure/fs

High level browser to file system integration library.

This library integrates the `File`, `FileEntry` and `FileSystemAccess` API
to provide different levels of integration with file system in web apps.

Basically, user can select a directory as a mount point, and browser can access
read and sometimes write in the directory.

## Dependency
This library depends on `pure/result`

## Support
Use `fsGetSupportStatus()` to inspect which implementation will be used.

```typescript
import { fsGetSupportStatus } from "pure/fs";

const { implementation, isSecureContext } = fsGetSupportStatus();
```

`implementation` can be 3 values:
1. `FileSystemAccess`: This is used for Google Chrome and Edge, and possibly other browsers, under secure context.
2. `FileEntry`: This is used for Firefox when the FS is mounted through a drag-and-drop interface.
3. `File`: This is used for Firefox when the FS is mounted by a directory picker dialog

The implementation is also chosen in this order and the first supported one is selected. If you are on Chrome/Edge and `FileSystemAccess` is not used, you can use `isSecureContext` to narrow down the reason.

If you are wondering why Safari is not mentioned, it's because Apple made it so I have to buy a Mac to test, which I didn't.

After you get an instance of `FsFileSystem`, you can use `capabilities` to inspect
what is and is not supported.

See `FsCapabilities` for more info. This is the support matrix:
|Implementation|`write`?|`live`?|
|--------------|--------|-------|
|`FileSystemAccess`|Yes*|Yes |
|`FileEntry` |No |Yes |
|`File` |No |No |

* - Need to request permission from user.


## Usage
First you need to get an instance of `FsFileSystem`. You can:
1. Call `fsOpenRead()` or `fsOpenReadWrite()` to show a directory picker,
2. Call `fsOpenReadFrom` or `fsOpenReadWriteFrom()` and pass in a `DataTransferItem` from a drag-and-drop interface.

NOTE: `fsOpenReadWrite` does not guarantee the implementation supports writing. You should check
with `capabilities` afterward.

This is an example drop zone implementation in TypeScript
```typescript
import { tryInvokeAsync } from "pure/result";
import { fsOpenReadWriteFrom } from "pure/fs";

const div = document.createElement("div");

div.addEventListener("dragover", (e) => {
if (e.dataTransfer) {
// setting this will allow dropping
e.dataTransfer.dropEffect = "link";
}
});

div.addEventListener("drop", async (e) => {
const item = e.dataTransfer?.items[0];
if (!item) {
console.error("no item");
return;
}

const result = await tryInvokeAsync((r) => fsOpenReadWriteFrom(r, item));
if (result.isErr()) {
console.error(result.error);
return;
}

const fs = result.value;
const { write, live } = fs.capabilities;
// check capabilities and use fs
// ...
});
```

## Retry open
You can pass in a retry handler and return true to retry, when opening fails.
The handler is async so you can ask user. It uses the `pure/result` library.

```typescript
import { ResultHandle, tryInvokeAsync } from "pure/result";
import { FsError, FsResult } from "pure/fs";

async function shouldRetry(r: ResultHandle, error: FsError, attempt: number): Promise<FsResult<boolean>> {
if (retryCount < 10 && error === FsError.PermissionDenied) {
alert("you must give permission to use this feature!");
return r.putOk(true);
}
return r.putOk(false);
}

const result = await tryInvokeAsync((r) => fsOpenReadWrite(r, shouldRetry));
```


46 changes: 46 additions & 0 deletions libs/pure/src/fs/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Result, StableResult } from "../result";

/// Result type for file system operations
export const FsErr = {
/// Generic error
Fail: 1,
/// The operation does not apply to the root directory
IsRoot: 2,
/// Invalid encoding
InvalidEncoding: 3,
/// Not supported
NotSupported: 4,
/// The operation does not apply to a file
IsFile: 5,
/// The file was not modified since the last check
NotModified: 6,
/// Permission error
PermissionDenied: 7,
/// User abort
UserAbort: 8,
/// Not found
NotFound: 9,
/// Trying to do stuff to a closed file
IsClosed: 10,
/// If the path is invalid, for example trying to get the parent of root
InvalidPath: 11,
/// Trying to operate on a file that has been closed
Closed: 12,
} as const;

export type FsErr = (typeof FsErr)[keyof typeof FsErr];
export type FsError = {
readonly code: FsErr;
readonly message: string;
}

export function fsErr(code: FsErr, message: string): FsError {
return { code, message };
}

export function fsFail(message: string): FsError {
return fsErr(FsErr.Fail, message);
}

export type FsResult<T> = Result<T, FsError>;
export type FsStableResult<T> = StableResult<T, FsError>;
Loading

0 comments on commit 7fad9f2

Please sign in to comment.