Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite result and fs modules #208

Merged
merged 10 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler-wasm/build/src/worker_init.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function __initWorker(HANDLERS) {
if (msgId === "file") {
// ["file", 0, path, [true, data]]
// ["file", 0, path, [false]]
// ["file", 1, path, error]
// ["file", 1, path, FsError]
if (!pendingFiles[args]) {
return;
}
Expand Down
7 changes: 6 additions & 1 deletion compiler-wasm/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use std::cell::RefCell;

use js_sys::{Array, Function, Uint8Array};
use js_sys::{Array, Function, Reflect, Uint8Array};
use log::info;
use wasm_bindgen::prelude::*;

Expand Down Expand Up @@ -139,6 +139,11 @@ async fn load_file_internal(path: &str, check_changed: bool) -> ResResult<LoadFi
match result {
Ok(output) => Ok(output),
Err(e) => {
if let Ok(value) = Reflect::get(&e, &JsValue::from("message")) {
if let Some(s) = value.as_string() {
return Err(ResError::FailToLoadFile(path.to_string(), s));
}
}
logger::raw_error(&e);
Err(ResError::FailToLoadFile(
path.to_string(),
Expand Down
38 changes: 38 additions & 0 deletions libs/package-lock.json

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

13 changes: 13 additions & 0 deletions libs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "libs",
"private": true,
"version": "0.0.0",
"type": "module",
"dependencies": {
"denque": "^2.1.0",
"file-saver": "2.0.5"
},
"devDependencies": {
"@types/file-saver": "^2.0.7"
}
}
15 changes: 15 additions & 0 deletions libs/pure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 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 :)

## Libraries
- `fs`: Browser File System Integration
- `log`: Logging library with log export ability
- `result`: Rust-list result handling library
- `utils`: I don't know where to put the stuff
54 changes: 54 additions & 0 deletions libs/pure/src/fs/FsFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { FsResult, FsVoid } 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(): 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(): Promise<FsResult<string>>;

/// Get the content of the file
getBytes(): 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(): Promise<FsVoid>;

/// 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(): Promise<FsVoid>;

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

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

52 changes: 52 additions & 0 deletions libs/pure/src/fs/FsFileSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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(): 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: (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[];

}
102 changes: 102 additions & 0 deletions libs/pure/src/fs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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.

## 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 { 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 fsOpenReadWriteFrom(item);
if (result.err) {
console.error(result.err);
return;
}

const fs = result.val;
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.

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

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

const result = await fsOpenReadWrite(shouldRetry);
```



48 changes: 48 additions & 0 deletions libs/pure/src/fs/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Result, Void } from "pure/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,
/// The operation does not apply to a directory
IsDirectory: 5,
} 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 FsVoid = Void<FsError>;
29 changes: 29 additions & 0 deletions libs/pure/src/fs/impl/FsFileMgr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { FsFile } from "../FsFile";
import { FsFileSystemInternal } from "./FsFileSystemInternal";
import { fsFile } from "./file";

/// Internal class to track opened files
export class FsFileMgr {
private opened: { [path: string]: FsFile };

public constructor() {
this.opened = {};
}

public get(fs: FsFileSystemInternal, path: string): FsFile {
let file = this.opened[path];
if (!file) {
file = fsFile(fs, path);
this.opened[path] = file;
}
return file;
}

public close(path: string): void {
delete this.opened[path];
}

public getOpenedPaths(): string[] {
return Object.keys(this.opened);
}
}
Loading
Loading