@firesystem/core
Version:
Virtual File System core interfaces and types
563 lines (433 loc) • 13.2 kB
Markdown
# /core
Virtual File System (VFS) core interfaces and types for JavaScript/TypeScript applications. This package provides a consistent API for implementing file system operations across different storage backends.
## Features
- 🎯 **Unified API** - Same interface for IndexedDB, Memory, S3, or any storage backend
- 📁 **Full FS Operations** - Read, write, mkdir, rmdir, rename, move, copy, and more
- 👀 **Watch System** - Real-time notifications with glob pattern support
- 🔍 **Glob Patterns** - Search files using familiar glob syntax
- 🚀 **Zero Dependencies** - Core package has no runtime dependencies
- 📝 **TypeScript First** - Written in TypeScript with complete type definitions
- ✅ **Battle Tested** - Comprehensive test suite ensuring reliability
## Installation
```bash
npm install /core
# or
yarn add /core
# or
pnpm add /core
```
## Available Implementations
- [`/indexeddb`](../indexeddb) - Browser storage using IndexedDB
- [`/memory`](../memory) - In-memory storage for testing
- [`/s3`](../s3) - AWS S3 storage backend
- [`/workspace`](../workspace) - Multi-source workspace manager
- More coming soon (Node.js fs, etc.)
## Creating Custom Implementations
VFS provides a `BaseFileSystem` abstract class that implements common functionality:
```typescript
import { BaseFileSystem } from "@firesystem/core";
export class MyCustomFileSystem extends BaseFileSystem {
// Define capabilities
readonly capabilities = {
readonly: false,
caseSensitive: true,
atomicRename: true,
supportsWatch: true,
supportsMetadata: true,
maxFileSize: 10 * 1024 * 1024, // 10MB
maxPathLength: 255,
};
// Implement required methods
async readFile(path: string): Promise<FileEntry> {
// Your implementation
}
async writeFile(
path: string,
content: any,
metadata?: FileMetadata,
): Promise<FileEntry> {
// Your implementation
}
// ... implement other required methods
// Override permission methods if needed
async canModify(path: string): Promise<boolean> {
// Custom permission logic
return super.canModify(path); // or your own logic
}
}
```
## API Overview
### IFileSystem Interface
The main interface that all VFS implementations must follow:
```typescript
interface IFileSystem {
// File operations
readFile(path: string): Promise<FileEntry>;
writeFile(
path: string,
content: any,
metadata?: FileMetadata,
): Promise<FileEntry>;
deleteFile(path: string): Promise<void>;
exists(path: string): Promise<boolean>;
// Directory operations
readDir(path: string): Promise<FileEntry[]>;
mkdir(path: string, recursive?: boolean): Promise<FileEntry>;
rmdir(path: string, recursive?: boolean): Promise<void>;
// File system operations
rename(oldPath: string, newPath: string): Promise<FileEntry>;
move(sourcePaths: string[], targetPath: string): Promise<void>;
copy(sourcePath: string, targetPath: string): Promise<FileEntry>;
// Watch operations
watch(pattern: string, callback: (event: FSEvent) => void): Disposable;
// Utility operations
stat(path: string): Promise<FileStat>;
glob(pattern: string): Promise<string[]>;
// Storage management
clear(): Promise<void>;
size(): Promise<number>;
// Permission checking
canModify(path: string): Promise<boolean>;
canCreateIn(parentPath: string): Promise<boolean>;
// Atomic operations (optional)
writeFileAtomic?(
path: string,
content: any,
metadata?: FileMetadata,
): Promise<FileEntry>;
// Capabilities (optional)
readonly capabilities?: IFileSystemCapabilities;
}
```
## Usage Examples
### Basic File Operations
```typescript
import { IndexedDBFileSystem } from "@firesystem/indexeddb";
const fs = new IndexedDBFileSystem({ dbName: "my-app" });
// Write a file
await fs.writeFile("/hello.txt", "Hello, World!");
// Read a file
const file = await fs.readFile("/hello.txt");
console.log(file.content); // "Hello, World!"
// Check if file exists
const exists = await fs.exists("/hello.txt"); // true
// Delete a file
await fs.deleteFile("/hello.txt");
```
### Working with Directories
```typescript
// Create a directory
await fs.mkdir("/src");
// Create nested directories
await fs.mkdir("/src/components/Button", true);
// List directory contents
const files = await fs.readDir("/src");
console.log(files); // [{ name: "components", type: "directory", ... }]
// Remove empty directory
await fs.rmdir("/src/components/Button");
// Remove directory and all contents
await fs.rmdir("/src", true);
```
### File Metadata
```typescript
// Write file with metadata
await fs.writeFile(
"/document.json",
{ title: "My Document" },
{
tags: ["important", "work"],
description: "Quarterly report",
},
);
// Read file with metadata
const doc = await fs.readFile("/document.json");
console.log(doc.metadata?.tags); // ["important", "work"]
// Get file statistics
const stats = await fs.stat("/document.json");
console.log(stats.size); // File size in bytes
console.log(stats.created); // Date created
console.log(stats.modified); // Date last modified
```
### Moving and Copying Files
```typescript
// Rename a file
await fs.rename("/old-name.txt", "/new-name.txt");
// Move files to a directory
await fs.mkdir("/archive");
await fs.move(["/file1.txt", "/file2.txt"], "/archive");
// Copy a file
await fs.copy("/template.html", "/index.html");
```
### Glob Patterns
```typescript
// Find all JavaScript files
const jsFiles = await fs.glob("**/*.js");
// Find all files in src directory
const srcFiles = await fs.glob("/src/**/*");
// Find all test files
const testFiles = await fs.glob("**/*.test.{js,ts}");
// Find all files in root directory only
const rootFiles = await fs.glob("*");
```
### Watch System
```typescript
// Watch all files
const watcher = fs.watch("**", (event) => {
console.log(`${event.type}: ${event.path}`);
});
// Watch specific file types
fs.watch("**/*.json", (event) => {
if (event.type === "updated") {
console.log(`JSON file updated: ${event.path}`);
}
});
// Watch specific directory
fs.watch("/src/**/*", (event) => {
console.log(`Change in src: ${event.type} ${event.path}`);
});
// Stop watching
watcher.dispose();
```
### Watch Event Types
```typescript
type FSEventType =
| "created" // File or directory was created
| "updated" // File content was modified
| "deleted" // File or directory was deleted
| "moved" // File or directory was moved (event.oldPath available)
| "renamed"; // File or directory was renamed (event.oldPath available)
```
### Storage Management
```typescript
// Get total storage size
const totalSize = await fs.size();
console.log(`Total storage: ${totalSize} bytes`);
// Clear all files (except root directory)
await fs.clear();
```
### Permission Checking
VFS now supports permission checking before operations:
```typescript
// Check if can modify a file
if (await fs.canModify("/protected.txt")) {
await fs.deleteFile("/protected.txt");
} else {
console.log("File is read-only or protected");
}
// Check if can create in directory
if (await fs.canCreateIn("/restricted")) {
await fs.writeFile("/restricted/new.txt", "content");
} else {
console.log("Cannot create files in this directory");
}
```
### Atomic Writing
For critical operations, use atomic writing when available:
```typescript
// Writes to temp file then renames (atomic)
// Falls back to regular write if not supported
if (fs.writeFileAtomic) {
await fs.writeFileAtomic("/important.json", {
data: "critical data",
});
} else {
await fs.writeFile("/important.json", {
data: "critical data",
});
}
```
### File System Capabilities
Check what a file system supports:
```typescript
// Check capabilities
if (fs.capabilities) {
console.log(`Read-only: ${fs.capabilities.readonly}`);
console.log(`Case sensitive: ${fs.capabilities.caseSensitive}`);
console.log(`Atomic rename: ${fs.capabilities.atomicRename}`);
console.log(`Supports watch: ${fs.capabilities.supportsWatch}`);
if (fs.capabilities.maxFileSize) {
console.log(`Max file size: ${fs.capabilities.maxFileSize} bytes`);
}
}
// Example: Check before large file operations
const largeData = new ArrayBuffer(100 * 1024 * 1024); // 100MB
if (
fs.capabilities?.maxFileSize &&
largeData.byteLength > fs.capabilities.maxFileSize
) {
throw new Error(
`File too large. Max size: ${fs.capabilities.maxFileSize} bytes`,
);
}
```
## Path Handling
All paths in VFS are normalized automatically:
```typescript
// These all resolve to the same path
await fs.writeFile("file.txt", "content"); // -> /file.txt
await fs.writeFile("/file.txt", "content"); // -> /file.txt
await fs.writeFile("//file.txt", "content"); // -> /file.txt
await fs.writeFile("/file.txt/", "content"); // -> /file.txt
```
## Error Handling
VFS follows POSIX error conventions:
```typescript
try {
await fs.readFile("/missing.txt");
} catch (error) {
// Error: ENOENT: no such file or directory, open '/missing.txt'
}
try {
await fs.rmdir("/has-files");
} catch (error) {
// Error: ENOTEMPTY: directory not empty, rmdir '/has-files'
}
try {
await fs.mkdir("/already/exists");
} catch (error) {
// Error: EEXIST: file already exists, mkdir '/already/exists'
}
```
## Type Definitions
### FileEntry
```typescript
interface FileEntry {
path: string;
name: string;
type: "file" | "directory";
size?: number;
created?: Date;
modified?: Date;
metadata?: FileMetadata;
content?: any; // Only present when reading files
}
```
### FileStat
```typescript
interface FileStat {
path: string;
size: number;
type: "file" | "directory";
created: Date;
modified: Date;
accessed?: Date;
readonly?: boolean; // Indicates if file/directory is read-only
}
```
### FileMetadata
```typescript
interface FileMetadata {
tags?: string[];
description?: string;
[key: string]: any; // Custom metadata
}
```
### FSEvent
```typescript
interface FSEvent {
type: FSEventType;
path: string;
oldPath?: string; // Present for move/rename events
timestamp: Date;
metadata?: Record<string, any>;
}
```
### Disposable
```typescript
interface Disposable {
dispose(): void;
}
```
### IFileSystemCapabilities
```typescript
interface IFileSystemCapabilities {
// Operation support
readonly: boolean; // Is the file system read-only?
caseSensitive: boolean; // Does it differentiate case?
atomicRename: boolean; // Is rename operation atomic?
// Limits
maxFileSize?: number; // Maximum file size in bytes
maxPathLength?: number; // Maximum path length
// Features
supportsWatch: boolean; // Does it support file watching?
supportsMetadata: boolean; // Does it support custom metadata?
}
```
## Glob Pattern Support
VFS supports standard glob patterns:
- `*` - Matches any characters except `/`
- `**` - Matches any number of directories
- `?` - Matches single character except `/`
- `{a,b}` - Matches either `a` or `b`
- `[abc]` - Matches any character in brackets
- `[!abc]` - Matches any character not in brackets
### Examples
```typescript
"*.js"; // All .js files in root
"**/*.js"; // All .js files recursively
"src/**/*.ts"; // All .ts files under src
"*.{js,ts}"; // All .js and .ts files in root
"test/**"; // Everything under test directory
"**/test/*"; // All files directly under any test directory
```
## Best Practices
### 1. Always use absolute paths internally
```typescript
// Good
await fs.writeFile("/config/app.json", config);
// Okay (will be normalized to /config/app.json)
await fs.writeFile("config/app.json", config);
```
### 2. Check existence before operations
```typescript
if (await fs.exists("/old-file.txt")) {
await fs.deleteFile("/old-file.txt");
}
```
### 3. Use recursive flag for nested directories
```typescript
// This will fail if parent doesn't exist
await fs.mkdir("/deep/nested/dir");
// This will create all parent directories
await fs.mkdir("/deep/nested/dir", true);
```
### 4. Dispose watchers when done
```typescript
const watcher = fs.watch("**/*.log", handler);
// Later...
watcher.dispose(); // Stop watching to free resources
```
### 5. Handle errors appropriately
```typescript
async function safeReadFile(path: string): Promise<string | null> {
try {
const file = await fs.readFile(path);
return file.content;
} catch (error) {
if (error.message.includes("ENOENT")) {
return null; // File doesn't exist
}
throw error; // Re-throw other errors
}
}
```
## Testing
For testing, use the in-memory implementation:
```typescript
import { MemoryFileSystem } from "@firesystem/memory";
describe("My App", () => {
let fs: IFileSystem;
beforeEach(() => {
fs = new MemoryFileSystem();
});
it("should save user data", async () => {
await saveUserData(fs, { name: "John" });
const file = await fs.readFile("/users/john.json");
expect(file.content.name).toBe("John");
});
});
```
## Contributing
Contributions are welcome! Please read our [Contributing Guide](../../CONTRIBUTING.md) for details.
## License
MIT © Anderson D. Rosa