Lesson 18-Deno File System Operations

Deno’s file system operation API design reflects the security and asynchronous characteristics of modern JavaScript. Unlike Node.js, all file operations in Deno require explicit permissions by default, and it adopts a Promise-based asynchronous API design. This tutorial will provide an in-depth analysis of the core functionalities of Deno’s file system operations, demonstrating applications from basic to advanced scenarios through code examples.

Basic File Operations

Fundamentals of File Reading and Writing

​Synchronous vs. Asynchronous Comparison​
Deno provides both synchronous and asynchronous methods for file operations, but it is recommended to prioritize asynchronous APIs to avoid blocking the event loop:

// Asynchronous file reading (recommended)
const text = await Deno.readTextFile("example.txt");
console.log(text);

// Synchronous reading (not recommended)
const syncText = Deno.readTextFileSync("example.txt");

​Binary File Operations​
For handling non-text files, use readFile and writeFile:

// Asynchronous reading and writing of binary files
const binaryData = await Deno.readFile("image.png");
await Deno.writeFile("copy.png", binaryData);

File System Permission Control

​Permission Flag Descriptions​
Deno’s file operations require explicit declaration of permissions:

Permission FlagPurpose
--allow-readAllows file reading operations
--allow-writeAllows file writing operations
--allow-netAllows network access (some file operations may require this)

​Minimal Permission Example​

# Only allow reading from a specific directory
deno run --allow-read=/path/to/dir script.ts

# Allow writing to a specific file
deno run --allow-write=/path/to/file script.ts

Advanced File Operations

File System Status Checks

​Retrieving File Metadata​

const fileInfo = await Deno.stat("example.txt");
console.log({
  size: fileInfo.size,          // File size in bytes
  isFile: fileInfo.isFile,      // Whether it is a file
  isDirectory: fileInfo.isDirectory, // Whether it is a directory
  modified: fileInfo.mtime,     // Modification time
  created: fileInfo.birthtime   // Creation time
});

​Checking Path Existence​

const exists = await Deno.exists("example.txt");
console.log("Does the file exist:", exists);

Directory Operations

​Recursive Directory Traversal​

async function walkDir(dirPath: string) {
  const entries = await Deno.readDir(dirPath);
  for await (const entry of entries) {
    const fullPath = `${dirPath}/${entry.name}`;
    if (entry.isDirectory) {
      await walkDir(fullPath); // Recursively traverse subdirectories
    } else {
      console.log("File:", fullPath);
    }
  }
}

// Usage example
await walkDir("./documents");

​Directory Creation and Deletion​

// Create a directory (including parent directories)
await Deno.mkdir("nested/dir", { recursive: true });

// Delete an empty directory
await Deno.remove("empty_dir");

// Recursively delete a directory
await Deno.remove("nested_dir", { recursive: true });

Advanced File System Features

File Locking and Concurrency Control

​Implementing File Locks​
Deno does not provide a file lock API, but a simple lock mechanism can be implemented as follows:

async function withFileLock<T>(
  filePath: string,
  callback: () => Promise<T>
): Promise<T> {
  const lockFile = `${filePath}.lock`;
  try {
    // Attempt to create the lock file (atomic operation)
    await Deno.open(lockFile, { create: true, write: true });

    // Execute critical code
    return await callback();
  } finally {
    // Ensure the lock is released
    await Deno.remove(lockFile);
  }
}

// Usage example
await withFileLock("data.json", async () => {
  const data = JSON.parse(await Deno.readTextFile("data.json"));
  data.counter++;
  await Deno.writeTextFile("data.json", JSON.stringify(data));
});

File System Monitoring

​Listening for File Changes​

const watcher = Deno.watchFs(["./config"]);

(async () => {
  for await (const event of watcher) {
    console.log("File system event:", {
      kind: event.kind,     // create|modify|remove
      paths: event.paths,   // Affected paths
      flag: event.flag      // (Only supported on some systems)
    });

    if (event.kind === "modify" && event.paths[0].endsWith("config.json")) {
      console.log("Configuration file updated, reloading...");
      const config = JSON.parse(await Deno.readTextFile("config.json"));
      // Handle configuration update...
    }
  }
})();

// Note: Requires --allow-read permission

Performance Optimization Techniques

Handling Large Files

​Streaming File Reading​

async function processLargeFile(filePath: string) {
  const file = await Deno.open(filePath, { read: true });
  const reader = new Deno.Buffer();

  // Read in chunks (to avoid memory overflow)
  while (true) {
    const bytesRead = await file.read(reader);
    if (bytesRead === null) break; // End of file

    const chunk = reader.bytes();
    console.log("Processing data chunk:", chunk.length);
    // Process the data chunk...

    reader.reset(); // Reset the buffer
  }

  file.close();
}

// Usage example
await processLargeFile("large_data.bin");

Optimizing Batch Operations

​Batch File Processing Pattern​

async function batchProcessFiles(filePaths: string[]) {
  // Control concurrency
  const concurrency = 3;
  const semaphore = new Semaphore(concurrency);

  await Promise.all(
    filePaths.map(async (filePath) => {
      await semaphore.acquire();
      try {
        await processSingleFile(filePath);
      } finally {
        semaphore.release();
      }
    })
  );
}

// Simple semaphore implementation
class Semaphore {
  private queue: Array<() => void> = [];
  constructor(private count: number) {}

  acquire() {
    return new Promise<void>((resolve) => {
      if (this.count > 0) {
        this.count--;
        resolve();
      } else {
        this.queue.push(resolve);
      }
    });
  }

  release() {
    if (this.queue.length > 0) {
      const next = this.queue.shift()!;
      next();
    } else {
      this.count++;
    }
  }
}

// Usage example
await batchProcessFiles(["file1.txt", "file2.txt", /* ... */]);

Security Best Practices

Principle of Least Privilege

​Example of Precise Permission Control​

# Only allow reading a specific file
deno run --allow-read=/path/to/specific/file script.ts

# Allow writing to a specific directory but not reading
deno run --allow-write=/path/to/write/dir --allow-read=/path/to/read/dir script.ts

​Dynamic Permission Requests​

async function safeFileRead(filePath: string) {
  // Check current permissions
  const readStatus = await Deno.permissions.query({ name: "read" });

  if (readStatus.state === "granted") {
    return Deno.readTextFile(filePath);
  }

  // Request permission to read a specific file
  const granted = await Deno.permissions.request({
    name: "read",
    path: filePath
  });

  if (granted) {
    return Deno.readTextFile(filePath);
  }

  throw new Error("File read permission denied");
}

Security Protection Measures

​Protection Against Path Traversal Attacks​

function safeJoin(basePath: string, relativePath: string): string {
  const resolvedBase = new URL(basePath).pathname;
  const resolvedPath = new URL(relativePath, `file://${resolvedBase}/`).pathname;

  // Prevent directory traversal attacks
  if (!resolvedPath.startsWith(resolvedBase)) {
    throw new Error("Illegal path access");
  }

  return resolvedPath;
}

// Usage example
const safePath = safeJoin("/safe/dir", "../malicious.txt"); // Throws an error

​Handling Sensitive Files​

async function handleSensitiveFile() {
  // 1. Get the path from an environment variable (rather than hardcoding)
  const filePath = Deno.env.get("SENSITIVE_FILE_PATH");
  if (!filePath) throw new Error("Sensitive file path not configured");

  // 2. Temporarily request permissions
  const hasAccess = await Deno.permissions.request({ name: "read", path: filePath });
  if (!hasAccess) throw new Error("Insufficient permissions");

  try {
    // 3. Process immediately and clear from memory
    const content = await Deno.readTextFile(filePath);
    const processed = processContent(content);

    // 4. Clear sensitive data from memory promptly
    content.replace(/[a-zA-Z0-9]/g, "*");

    return processed;
  } finally {
    // 5. Explicitly release resources
    // (Deno does not have explicit memory management, but ensure file handles are closed)
  }
}

Practical Examples

Configuration File Hot Reload System

class ConfigManager {
  private config: any;
  private watcher: Deno.FsWatcher;
  private lastModified = 0;

  constructor(private filePath: string) {
    this.loadConfig();
    this.setupWatcher();
  }

  private async loadConfig() {
    const stats = await Deno.stat(this.filePath);
    if (stats.mtime?.getTime() <= this.lastModified) return;

    this.lastModified = stats.mtime.getTime();
    const content = await Deno.readTextFile(this.filePath);
    this.config = JSON.parse(content);
    console.log("Configuration updated:", this.config);
  }

  private setupWatcher() {
    this.watcher = Deno.watchFs(this.filePath);
    (async () => {
      for await (const event of this.watcher) {
        if (event.kind === "modify") {
          await this.loadConfig();
        }
      }
    })();
  }

  getConfig() {
    return this.config;
  }
}

// Usage example
const configManager = new ConfigManager("./config.json");
setInterval(() => {
  console.log("Current configuration:", configManager.getConfig());
}, 5000);

High-Performance Logging System

class AsyncLogger {
  private writeQueue: string[] = [];
  private isWriting = false;
  private filePath: string;

  constructor(filePath: string) {
    this.filePath = filePath;
  }

  async log(message: string) {
    this.writeQueue.push(`${new Date().toISOString()} - ${message}\n`);
    if (!this.isWriting) {
      this.processQueue();
    }
  }

  private async processQueue() {
    if (this.writeQueue.length === 0) {
      this.isWriting = false;
      return;
    }

    this.isWriting = true;
    const batch = this.writeQueue.splice(0, 100); // Write 100 entries in a batch
    const content = batch.join("");

    try {
      await Deno.writeTextFile(this.filePath, content, { append: true });
    } catch (error) {
      console.error("Log write failed:", error);
      // Retry logic...
    } finally {
      this.processQueue(); // Process the next batch
    }
  }
}

// Usage example
const logger = new AsyncLogger("./app.log");
setInterval(() => {
  logger.log(`System status update: ${Math.random()}`);
}, 100);
Share your love