148 lines
4.0 KiB
Markdown
148 lines
4.0 KiB
Markdown
# File Operations Patterns
|
|
|
|
Deep dive into @outfitter/file-ops patterns for safe file handling.
|
|
|
|
## Secure Paths
|
|
|
|
Prevent path traversal attacks with `securePath`:
|
|
|
|
```typescript
|
|
import { securePath } from "@outfitter/file-ops";
|
|
|
|
const path = securePath("/data", userInput);
|
|
// Throws if userInput tries to escape /data via ../
|
|
```
|
|
|
|
## Atomic Writes
|
|
|
|
Write files atomically to prevent corruption:
|
|
|
|
```typescript
|
|
import { writeFileAtomic } from "@outfitter/file-ops";
|
|
|
|
await writeFileAtomic("/path/to/file.json", JSON.stringify(data, null, 2));
|
|
// Writes to temp file, then renames (atomic on POSIX)
|
|
```
|
|
|
|
## File Locking
|
|
|
|
### Exclusive Lock
|
|
|
|
For write operations that need exclusive access:
|
|
|
|
```typescript
|
|
import { withExclusiveLock } from "@outfitter/file-ops";
|
|
|
|
const result = await withExclusiveLock("/path/to/file.lock", async () => {
|
|
const data = await Bun.file("/path/to/data.json").json();
|
|
data.counter += 1;
|
|
await writeFileAtomic("/path/to/data.json", JSON.stringify(data));
|
|
return data;
|
|
});
|
|
|
|
if (result.isErr()) {
|
|
// Lock acquisition failed or operation threw
|
|
}
|
|
```
|
|
|
|
### Shared Lock (Reader-Writer)
|
|
|
|
Use `withSharedLock()` for read operations that can run concurrently:
|
|
|
|
```typescript
|
|
import { withSharedLock, withExclusiveLock } from "@outfitter/file-ops";
|
|
|
|
// Multiple readers can hold shared locks simultaneously
|
|
const readResult = await withSharedLock("/path/to/data.lock", async () => {
|
|
return await Bun.file("/path/to/data.json").json();
|
|
});
|
|
|
|
// Writers need exclusive lock (blocks readers)
|
|
const writeResult = await withExclusiveLock("/path/to/data.lock", async () => {
|
|
const data = await Bun.file("/path/to/data.json").json();
|
|
data.updated = Date.now();
|
|
await writeFileAtomic("/path/to/data.json", JSON.stringify(data));
|
|
return data;
|
|
});
|
|
```
|
|
|
|
**Lock fairness note:** Reader-writer locks can cause starvation. With many concurrent readers, writers may wait indefinitely (and vice versa). For high-contention scenarios, consider using exclusive locks only or implementing application-level queuing.
|
|
|
|
### Lock Options
|
|
|
|
```typescript
|
|
await withExclusiveLock("/path/to/file.lock", operation, {
|
|
timeout: 5000, // Max wait time in ms (default: 10000)
|
|
retryDelay: 100, // Delay between retries (default: 50)
|
|
staleThreshold: 60000, // Consider lock stale after this many ms
|
|
});
|
|
```
|
|
|
|
### Lock File Conventions
|
|
|
|
- Use `.lock` extension for lock files
|
|
- Place lock files alongside the protected resource
|
|
- Use consistent lock file paths across all accessors
|
|
|
|
```typescript
|
|
// Good: Lock file next to data file
|
|
const dataPath = "/data/users.json";
|
|
const lockPath = "/data/users.json.lock";
|
|
|
|
// Good: Named lock in XDG state
|
|
import { getStatePath } from "@outfitter/config";
|
|
const lockPath = getStatePath("myapp", "db.lock");
|
|
```
|
|
|
|
## Safe Directory Operations
|
|
|
|
### Ensure Directory Exists
|
|
|
|
```typescript
|
|
import { ensureDir } from "@outfitter/file-ops";
|
|
|
|
await ensureDir("/path/to/nested/dir");
|
|
// Creates all parent directories if needed
|
|
```
|
|
|
|
### Safe Removal
|
|
|
|
```typescript
|
|
import { safeRemove } from "@outfitter/file-ops";
|
|
|
|
await safeRemove("/path/to/file-or-dir");
|
|
// No error if doesn't exist, removes recursively if dir
|
|
```
|
|
|
|
## Temp Files
|
|
|
|
### Create Temp File
|
|
|
|
```typescript
|
|
import { createTempFile } from "@outfitter/file-ops";
|
|
|
|
const tempPath = await createTempFile("myapp", ".json");
|
|
// Returns path like /tmp/myapp-abc123.json
|
|
```
|
|
|
|
### With Cleanup
|
|
|
|
```typescript
|
|
import { withTempFile } from "@outfitter/file-ops";
|
|
|
|
const result = await withTempFile("myapp", ".json", async (tempPath) => {
|
|
await Bun.write(tempPath, JSON.stringify(data));
|
|
return await processFile(tempPath);
|
|
});
|
|
// Temp file automatically cleaned up
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Always use atomic writes** for critical data
|
|
2. **Lock before read-modify-write** operations
|
|
3. **Use shared locks** for read-only operations to improve concurrency
|
|
4. **Validate paths** with `securePath` before using user input
|
|
5. **Clean up temp files** with `withTempFile` pattern
|
|
6. **Use XDG paths** from `@outfitter/config` for state/cache files
|