-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
882438f
commit 7fad9f2
Showing
27 changed files
with
1,273 additions
and
54 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
``` | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
Oops, something went wrong.