playbook/outfitter-agents/plugins/outfitter-stack/skills/stack-patterns/references/file-ops.md

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