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

4.0 KiB

File Operations Patterns

Deep dive into @outfitter/file-ops patterns for safe file handling.

Secure Paths

Prevent path traversal attacks with securePath:

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:

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:

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:

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

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
// 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

import { ensureDir } from "@outfitter/file-ops";

await ensureDir("/path/to/nested/dir");
// Creates all parent directories if needed

Safe Removal

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

import { createTempFile } from "@outfitter/file-ops";

const tempPath = await createTempFile("myapp", ".json");
// Returns path like /tmp/myapp-abc123.json

With Cleanup

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