File System β€” 75-Minute Interview Guide

Quick Start

What is it? In-memory hierarchical file system supporting directories and files with CRUD operations, pluggable storage strategies (plain/compressed), reversible commands (undo/redo), snapshot/restore, and event observability.

Key Classes: - FileSystem (Singleton): Centralized coordinator - FsNode (Abstract): Base for File and Directory - File: Leaf node with versioned content - Directory: Composite container for files/subdirectories - StorageStrategy (ABC): Pluggable encode/decode (plain vs compressed) - Command: Reversible operations (create, write, move, delete) - Snapshot: Memento for state recovery

Core Flows: 1. Create File/Directory: Resolve parent path β†’ Add node β†’ Emit event 2. Write Content: Fetch file β†’ Apply storage strategy β†’ Increment version β†’ Emit event 3. Move Node: Resolve source β†’ Detach from parent β†’ Attach to destination β†’ Emit event 4. Delete Node: Capture snapshot of subtree β†’ Remove from parent β†’ Enable undo 5. Snapshot/Restore: Deep-clone entire tree β†’ Restore replaces root β†’ Reversible via command

5 Design Patterns: - Composite: Uniform File/Directory traversal and operations - Strategy: Pluggable storage (plain vs compressed) independent of file logic - Command: Reversible CRUD with undo/redo stacks - Observer: Events (created, written, moved, deleted, strategy_swapped) for instrumentation - Memento: Snapshot captures entire tree state for restore - Singleton: One FileSystem instance manages all state


System Overview

In-memory hierarchical file system for directories and files with content transformation, reversible operations, and lifecycle observability.

Requirements

Functional: - Create/read/write/delete files and directories - Hierarchical path resolution ("/dir/sub/file.txt") - Move nodes between directories - Undo/redo operations (redo cleared after new execute) - Pluggable storage strategies (plain, compressed, extensible) - Snapshot entire tree state and restore - Emit events for lifecycle tracking - Track file versions and sizes

Non-Functional: - O(path_segments) path resolution - O(1) file content access - Extensible strategy pattern for future encryption/permissions - Observable via event listeners - Memory-efficient when using compression

Constraints: - Single-process, in-memory only (no persistence by default) - Deep copy acceptable (small dataset assumption) - Strategy swap reencodes all files transparently - Snapshot restore is reversible command (enable undo of restore)


Architecture Diagram (ASCII UML)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ FileSystem (Singleton)               β”‚
β”‚ - root: Directory                    β”‚
β”‚ - storage: StorageStrategy           β”‚
β”‚ - undo_stack: [Command]              β”‚
β”‚ - redo_stack: [Command]              β”‚
β”‚ - listeners: [Callable]              β”‚
β”‚ + execute(cmd), undo(), redo()       β”‚
β”‚ + take_snapshot(), restore_snapshot()β”‚
β”‚ + set_storage_strategy(s)            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β”‚ manages
             β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ FsNode(ABC) β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
               β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  File  β”‚      β”‚  Directory    β”‚
   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€      β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
   β”‚ _data  β”‚      β”‚ children:     β”‚
   β”‚version β”‚      β”‚ {name:Node}   β”‚
   β”‚        β”‚      β”‚               β”‚
   β”‚read()  β”‚      β”‚add(node)      β”‚
   β”‚write() β”‚      β”‚remove(name)   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚get(name)      β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚ StorageStrategy(ABC) β”‚
       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
       β”‚ + encode(str)->bytes β”‚
       β”‚ + decode(bytes)->str β”‚
       β”‚ + name()->str        β”‚
       β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚       β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”  β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚ Plain   β”‚  β”‚ Compressed     β”‚
       β”‚Strategy β”‚  β”‚ Strategy(zlib) β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚ Command(ABC)         β”‚
       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
       β”‚ + execute(fs)        β”‚
       β”‚ + undo(fs)           β”‚
       β”‚ + describe()->str    β”‚
       β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚   β”‚   β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”‚   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚         β”‚   β”‚   β”‚              β”‚
    β–Ό         β–Ό   β–Ό   β–Ό              β–Ό
CreateFile WriteFile Move Delete RestoreSnapshot
Command    Command    Node   Node  Command
           Command    Cmd    Cmd

Lifecycle:
ROOT (/)
  β”œβ”€β”€ File(name, _data, version)
  └── Directory(name, children)
         β”‚
         β”œβ”€β”€ File(name, _data, version)
         └── Directory(name, children)

COMMAND FLOW:
RECORD β†’ STRATEGY_ENCODE β†’ EXECUTE β†’ PUSH_UNDO β†’ CLEAR_REDO β†’ EMIT_EVENT
                                       ↓
                                  [UNDO]β†’REDO_STACK
                                       ↓
                                  [REDO]β†’UNDO_STACK

SNAPSHOT:
take_snapshot() β†’ deep_clone(root) β†’ Snapshot(root_clone)
restore_snapshot(snap) β†’ execute(RestoreSnapshotCommand(old,new))

Interview Q&A (12 Questions)

Basic Level

Q1: How do you represent files and directories? A: FsNode base class with File (leaf, stores _data bytes) and Directory (composite, stores children dict). File/Directory inherit path() method to resolve hierarchical location.

Q2: How do path resolution work? A: Split path by '/', traverse from root. Example: "/docs/spec.md" → root→get("docs")→get("spec.md"). Return None if path invalid. O(segments).

Q3: What's the storage strategy pattern for? A: Decouple content encoding from file logic. PlainStorageStrategy encodes/decodes UTF-8. CompressedStorageStrategy wraps zlib. Swap strategies transparently without rewriting file logic.

Q4: How do undo/redo stacks work? A: Execute: push command to undo_stack, clear redo_stack. Undo: pop from undo_stack, call cmd.undo(), push to redo_stack. Redo: pop from redo_stack, call cmd.execute(), push to undo_stack.

Q5: Why use Composite pattern? A: Uniform traversal for files and directories. Snapshot deep-clones entire tree without special-casing. Enables extensibility (new node types inherit FsNode).

Intermediate Level

Q6: How does write capture old content for undo? A: WriteFileCommand stores old_text before execute. On undo, restore old_text. File.version increments on each write for tracking.

Q7: How do you move a node safely? A: MoveNodeCommand captures original_parent. Execute: detach from parent, attach to dest_dir. Undo: detach from dest_dir, reattach to original_parent. Path updates automatically.

Q8: What's the snapshot/restore trade-off? A: Snapshot deep-copies entire tree (memory-intensive but simple). Alternative: incremental diffs or journaling (complex but space-efficient). For interview: snapshot sufficient for correctness demo.

Q9: How does strategy swap preserve file content? A: Read all files with old strategy β†’ decode to logical text. Switch strategy β†’ re-encode with new strategy. Transparent to callers, metrics reflect new physical size.

Q10: How do you delete with undo? A: DeleteNodeCommand captures snapshot of node+subtree before removal. On undo, deep-clone snapshot and reattach to original parent. Subtree fully restored.

Advanced Level

Q11: How to prevent memory explosion with many snapshots? A: Each snapshot is independent deep-copy (expensive for large trees). Alternative: copy-on-write (shared structure until mutation), or incremental diffs (replay from base+deltas). For production: journal WAL (write-ahead log) of commands.

Q12: How do metrics stay accurate across operations? A: _refresh_metrics() recalculates file_count, dir_count, total_bytes (logical), compressed_bytes (physical) via tree walk. Called after create/write/delete/strategy_swap. Expensive for large trees; alternative: delta tracking.


Scaling Q&A (12 Questions)

Q1: Can you support 1M files? A: In-memory hash lookups O(1) per child. Path resolution O(segments, typically 5-10). Total traversal ~10ΞΌs. 1M files: 100MB metadata + content. Memory bottleneck. Alternative: external storage (RocksDB, FUSE filesystem).

Q2: How to handle concurrent modifications? A: Single-process: simple. Multi-process: add threading.Lock around all state mutations. Readers can be lock-free with snapshot isolation (read old root while mutations on new root).

Q3: How to persist tree to disk? A: Snapshot + journaling. Checkpoint: write JSON tree every N minutes. Log: append-only command log (create_file, write_file, move_node). On restart: load checkpoint + replay log.

Q4: How to optimize path resolution for deep paths? A: Cache frequently-accessed paths in dict. Or: flat storage with full path as key (trade memory for speed). Hybrid: LRU cache for top-accessed 1000 paths.

Q5: Can you support 100GB files? A: In-memory: no. Must stream from disk. File abstraction: offset + length pointers to disk blocks. Read returns content iterator, not full bytes. Storage strategy becomes streaming codec.

Q6: How to support incremental snapshots? A: Snapshot 1: full tree clone. Snapshot 2: only store deltas (new files, modified content, deleted nodes). Restore 2: apply delta to snapshot 1. Save memory but add complexity.

Q7: How to distribute filesystem across shards? A: Shard by path hash. Node /a/b/c hashes to shard_i. Each shard holds independent FileSystem instance. Distributed query: resolve path, route to shard, query locally. Trade: cross-shard moves complex.

Q8: How to handle rename without moving? A: Rename is file.name change + parent update. Command: RenameNodeCommand captures old_name. Execute: update name + parent's children dict key. Undo: restore old_name.

Q9: How to support lazy-loaded subtrees? A: Directory.children is dict of lazy-load proxies. On access, fetch subtree from storage. Storage returns hydrated Directory. Trade: latency spike on first access, memory saved.

Q10: How to implement efficient garbage collection? A: Track refcounts for deleted snapshots. When refcount=0, free. Alternative: MVCC (multi-version concurrency control) with timestamp-based cleanup. Remove versions older than min_active_snapshot_ts.

Q11: How to support atomic transactions across multiple operations? A: Batch commands into transaction. Execute each command. If any fails: rollback (undo each in reverse order). Or: pre-validate transaction before executing any command.

Q12: How to prevent infinite undo/redo growth? A: Cap undo_stack to last N commands. Discard oldest. Or: cleanup old redo_stack entries after new execute (prevents memory leak from redo chain). Metrics track undo_depth.


Demo Scenarios (5 Examples)

Demo 1: Setup & Create Hierarchy

- Create /docs directory
- Create /docs/spec.md file
- Write "API specification" to spec.md
- Create /docs/readme.md file
- Verify metrics: 2 files, 2 directories

Demo 2: Strategy Swap & Compression

- Write 1KB text to spec.md with PlainStorageStrategy
- Observe: physical_size = 1KB
- Swap to CompressedStorageStrategy
- Observe: physical_size β‰ˆ 150 bytes (zlib)
- Read file: still returns "API specification" (transparent)

Demo 3: Move Node Between Directories

- Create /docs/archive directory
- Move /docs/spec.md to /docs/archive/spec.md
- Verify: /docs/spec.md doesn't exist
- Verify: /docs/archive/spec.md exists with same content

Demo 4: Delete & Undo/Redo

- Delete /docs/archive/spec.md
- Verify: file gone
- Undo: file restored to /docs/archive/spec.md
- Redo: file deleted again

Demo 5: Snapshot & Restore

- Take snapshot of entire tree
- Create /temp directory and /temp/test.txt
- Write "temporary" to test.txt
- Restore snapshot
- Verify: /temp gone, tree reverted
- Check undo_stack: restore command can be undone

Complete Implementation

"""
πŸ—‚οΈ File System - Interview Implementation
Demonstrates:
1. Composite pattern (File/Directory hierarchy)
2. Strategy pattern (pluggable storage)
3. Command pattern (reversible CRUD with undo/redo)
4. Observer pattern (event emission)
5. Memento pattern (snapshot/restore)
6. Singleton pattern (centralized FileSystem)
"""

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable, Any, Tuple
import zlib
import threading

# ============================================================================
# STORAGE STRATEGIES
# ============================================================================

class StorageStrategy:
    """Abstract strategy for content encoding/decoding"""

    def name(self) -> str:
        return self.__class__.__name__

    def encode(self, text: str) -> bytes:
        raise NotImplementedError

    def decode(self, data: bytes) -> str:
        raise NotImplementedError

class PlainStorageStrategy(StorageStrategy):
    """Direct UTF-8 encoding"""

    def encode(self, text: str) -> bytes:
        return text.encode("utf-8")

    def decode(self, data: bytes) -> str:
        return data.decode("utf-8") if data else ""

class CompressedStorageStrategy(StorageStrategy):
    """Zlib compression for space efficiency"""

    def encode(self, text: str) -> bytes:
        return zlib.compress(text.encode("utf-8"))

    def decode(self, data: bytes) -> str:
        if not data:
            return ""
        return zlib.decompress(data).decode("utf-8")

# ============================================================================
# COMPOSITE NODES
# ============================================================================

@dataclass
class FsNode:
    """Base node for file system hierarchy"""
    name: str
    parent: Optional[Directory] = None

    def path(self) -> str:
        """Resolve full path by walking parent chain"""
        if self.parent is None:
            return '/' if self.name == '' else f'/{self.name}'
        base = self.parent.path()
        return base.rstrip('/') + ('/' + self.name if self.name else '')

@dataclass
class File(FsNode):
    """Leaf node: stores content with versioning"""
    _data: bytes = b''
    version: int = 0

    def write(self, text: str, storage: StorageStrategy) -> None:
        """Encode text via strategy and increment version"""
        self._data = storage.encode(text)
        self.version += 1

    def read(self, storage: StorageStrategy) -> str:
        """Decode content via strategy"""
        return storage.decode(self._data)

    def logical_size(self, storage: StorageStrategy) -> int:
        """Uncompressed size"""
        return len(self.read(storage))

    def physical_size(self) -> int:
        """Compressed size (bytes stored)"""
        return len(self._data)

@dataclass
class Directory(FsNode):
    """Composite node: contains child files/directories"""
    children: Dict[str, FsNode] = field(default_factory=dict)

    def add(self, node: FsNode) -> None:
        """Add child and update parent reference"""
        self.children[node.name] = node
        node.parent = self

    def remove(self, name: str) -> FsNode:
        """Remove child and clear parent reference"""
        node = self.children.pop(name)
        node.parent = None
        return node

    def get(self, name: str) -> Optional[FsNode]:
        """Lookup child by name"""
        return self.children.get(name)

# ============================================================================
# COMMANDS (REVERSIBLE OPERATIONS)
# ============================================================================

class Command:
    """Abstract reversible command"""

    def execute(self, fs: FileSystem) -> None:
        raise NotImplementedError

    def undo(self, fs: FileSystem) -> None:
        raise NotImplementedError

    def describe(self) -> str:
        return self.__class__.__name__

class CreateFileCommand(Command):
    """Create file at path"""

    def __init__(self, path: str) -> None:
        self.path = path
        self.created = False

    def execute(self, fs: FileSystem) -> None:
        if fs.exists(self.path):
            raise ValueError("File already exists")
        fs._create_file(self.path)
        self.created = True

    def undo(self, fs: FileSystem) -> None:
        if self.created:
            fs._delete_node(self.path)

    def describe(self) -> str:
        return f"CreateFile {self.path}"

class WriteFileCommand(Command):
    """Write content to file (captures old for undo)"""

    def __init__(self, path: str, new_text: str) -> None:
        self.path = path
        self.new_text = new_text
        self.old_text: Optional[str] = None

    def execute(self, fs: FileSystem) -> None:
        file = fs._get_file(self.path)
        self.old_text = file.read(fs.storage)
        file.write(self.new_text, fs.storage)

    def undo(self, fs: FileSystem) -> None:
        if self.old_text is None:
            return
        file = fs._get_file(self.path)
        file.write(self.old_text, fs.storage)

    def describe(self) -> str:
        return f"WriteFile {self.path}"

class CreateDirectoryCommand(Command):
    """Create directory at path"""

    def __init__(self, path: str) -> None:
        self.path = path.rstrip('/')
        self.created = False

    def execute(self, fs: FileSystem) -> None:
        if fs.exists(self.path):
            raise ValueError("Directory already exists")
        fs._create_directory(self.path)
        self.created = True

    def undo(self, fs: FileSystem) -> None:
        if self.created:
            node = fs._resolve(self.path)
            if isinstance(node, Directory) and not node.children and node.parent is not None:
                node.parent.remove(node.name)

class MoveNodeCommand(Command):
    """Move node from src to destination directory"""

    def __init__(self, src_path: str, dest_dir_path: str) -> None:
        self.src_path = src_path
        self.dest_dir_path = dest_dir_path
        self.original_parent: Optional[Directory] = None
        self.node_name: Optional[str] = None

    def execute(self, fs: FileSystem) -> None:
        node = fs._resolve(self.src_path)
        if node is None:
            raise ValueError("Source not found")
        dest_dir = fs._resolve(self.dest_dir_path)
        if not isinstance(dest_dir, Directory):
            raise ValueError("Destination is not a directory")
        if node.parent is None:
            raise ValueError("Cannot move root")

        self.original_parent = node.parent
        self.node_name = node.name
        node.parent.remove(node.name)
        dest_dir.add(node)

    def undo(self, fs: FileSystem) -> None:
        if self.original_parent and self.node_name:
            node = fs._resolve(self.dest_dir_path + '/' + self.node_name)
            if node:
                node.parent.remove(node.name)
                self.original_parent.add(node)

    def describe(self) -> str:
        return f"MoveNode {self.src_path} β†’ {self.dest_dir_path}"

class DeleteNodeCommand(Command):
    """Delete node (captures subtree for undo)"""

    def __init__(self, path: str) -> None:
        self.path = path
        self.snapshot: Optional[FsNode] = None
        self.parent_path: Optional[str] = None

    def execute(self, fs: FileSystem) -> None:
        node = fs._resolve(self.path)
        if node is None or node.parent is None:
            raise ValueError("Cannot delete root or non-existent")
        self.snapshot = fs._clone_node(node)
        self.parent_path = node.parent.path()
        node.parent.remove(node.name)

    def undo(self, fs: FileSystem) -> None:
        if self.snapshot and self.parent_path:
            parent = fs._resolve(self.parent_path)
            if isinstance(parent, Directory):
                parent.add(fs._clone_node(self.snapshot))

    def describe(self) -> str:
        return f"DeleteNode {self.path}"

class RestoreSnapshotCommand(Command):
    """Restore tree from snapshot (reversible)"""

    def __init__(self, old_root: Directory, new_root: Directory) -> None:
        self.old_root = old_root
        self.new_root = new_root

    def execute(self, fs: FileSystem) -> None:
        fs.root = self.new_root

    def undo(self, fs: FileSystem) -> None:
        fs.root = self.old_root

    def describe(self) -> str:
        return "RestoreSnapshot"

# ============================================================================
# SNAPSHOT (MEMENTO)
# ============================================================================

@dataclass
class Snapshot:
    """Captures entire tree state"""
    root_clone: Directory

# ============================================================================
# FILE SYSTEM (SINGLETON)
# ============================================================================

class FileSystem:
    """Centralized file system coordinator"""

    _instance: Optional[FileSystem] = None
    _lock = threading.Lock()

    def __new__(cls) -> FileSystem:
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self) -> None:
        if hasattr(self, '_initialized'):
            return

        self._initialized = True
        self.root = Directory(name='')
        self.storage: StorageStrategy = PlainStorageStrategy()
        self.undo_stack: List[Command] = []
        self.redo_stack: List[Command] = []
        self.listeners: List[Callable[[str, Dict[str, Any]], None]] = []
        self.metrics: Dict[str, int] = {
            'file_count': 0,
            'dir_count': 1,
            'total_bytes': 0,
            'compressed_bytes': 0,
            'command_executed': 0,
            'command_undone': 0
        }
        print("πŸ—‚οΈ FileSystem initialized")

    def register(self, fn: Callable[[str, Dict[str, Any]], None]) -> None:
        """Subscribe to events"""
        self.listeners.append(fn)

    def _emit(self, event: str, payload: Dict[str, Any]) -> None:
        """Emit event to all listeners"""
        for listener_fn in self.listeners:
            try:
                listener_fn(event, payload)
            except Exception as e:
                print(f"  [WARN] Listener error: {e}")

    # ---- PATH RESOLUTION ----

    def _resolve(self, path: str) -> Optional[FsNode]:
        """Resolve path to node, return None if not found"""
        if path == '/' or path == '':
            return self.root

        parts = [p for p in path.strip('/').split('/') if p]
        node: FsNode = self.root

        for part in parts:
            if not isinstance(node, Directory):
                return None
            node = node.get(part)
            if node is None:
                return None

        return node

    def exists(self, path: str) -> bool:
        """Check if path exists"""
        return self._resolve(path) is not None

    def _get_file(self, path: str) -> File:
        """Resolve path to file, raise if not a file"""
        node = self._resolve(path)
        if not isinstance(node, File):
            raise ValueError(f"Not a file: {path}")
        return node

    # ---- PRIMITIVE OPERATIONS ----

    def _create_file(self, path: str) -> None:
        """Create file at path"""
        if path.endswith('/'):
            raise ValueError("File path cannot end with /")

        parts = path.rstrip('/').split('/')
        parent_path = '/'.join(parts[:-1]) or '/'
        name = parts[-1]

        parent = self._resolve(parent_path)
        if not isinstance(parent, Directory):
            raise ValueError("Parent directory missing")

        parent.add(File(name=name))
        self._emit("node_created", {"path": path, "type": "file"})
        self._refresh_metrics()

    def _create_directory(self, path: str) -> None:
        """Create directory at path"""
        path = path.rstrip('/')
        parts = path.split('/')
        parent_path = '/'.join(parts[:-1]) or '/'
        name = parts[-1]

        parent = self._resolve(parent_path)
        if not isinstance(parent, Directory):
            raise ValueError("Parent directory missing")

        parent.add(Directory(name=name))
        self._emit("node_created", {"path": path, "type": "directory"})
        self._refresh_metrics()

    def _delete_node(self, path: str) -> None:
        """Delete node at path"""
        node = self._resolve(path)
        if node is None or node.parent is None:
            raise ValueError("Cannot delete root or non-existent")

        node.parent.remove(node.name)
        self._emit("node_deleted", {"path": path})
        self._refresh_metrics()

    def _clone_node(self, node: FsNode) -> FsNode:
        """Deep clone node and subtree"""
        if isinstance(node, File):
            return File(name=node.name, _data=node._data, version=node.version)

        if isinstance(node, Directory):
            new_dir = Directory(name=node.name)
            for child in node.children.values():
                child_clone = self._clone_node(child)
                new_dir.add(child_clone)
            return new_dir

        raise ValueError("Unknown node type")

    # ---- COMMAND API ----

    def execute(self, cmd: Command) -> None:
        """Execute command and manage stacks"""
        cmd.execute(self)
        self.undo_stack.append(cmd)
        self.redo_stack.clear()
        self.metrics['command_executed'] += 1
        self._emit("command_executed", {"cmd": cmd.describe()})

    def undo(self) -> None:
        """Undo last command"""
        if not self.undo_stack:
            return

        cmd = self.undo_stack.pop()
        cmd.undo(self)
        self.redo_stack.append(cmd)
        self.metrics['command_undone'] += 1
        self._emit("command_undone", {"cmd": cmd.describe()})
        self._refresh_metrics()

    def redo(self) -> None:
        """Redo last undone command"""
        if not self.redo_stack:
            return

        cmd = self.redo_stack.pop()
        cmd.execute(self)
        self.undo_stack.append(cmd)
        self.metrics['command_executed'] += 1
        self._emit("command_executed", {"cmd": cmd.describe(), "redo": True})
        self._refresh_metrics()

    def set_storage_strategy(self, strategy: StorageStrategy) -> None:
        """Swap storage strategy (retranscode all files)"""
        old_name = self.storage.name()

        # Read all files with old strategy
        all_files = self._all_files()
        contents: List[Tuple[File, str]] = [(f, f.read(self.storage)) for f in all_files]

        # Switch strategy
        self.storage = strategy

        # Retranscode with new strategy
        for f, text in contents:
            f.write(text, self.storage)

        self._emit("strategy_swapped", {"old": old_name, "new": strategy.name()})
        self._refresh_metrics()

    def take_snapshot(self) -> Snapshot:
        """Create snapshot of entire tree"""
        snap = Snapshot(root_clone=self._clone_node(self.root))  # type: ignore
        self._emit("snapshot_taken", {"file_count": self.metrics['file_count']})
        return snap

    def restore_snapshot(self, snap: Snapshot) -> None:
        """Restore tree from snapshot (reversible via command)"""
        old_root = self.root
        new_root = self._clone_node(snap.root_clone)  # type: ignore
        self.execute(RestoreSnapshotCommand(old_root, new_root))
        self._emit("snapshot_restored", {})

    def _all_files(self) -> List[File]:
        """Collect all files in tree"""
        result: List[File] = []

        def walk(node: FsNode) -> None:
            if isinstance(node, File):
                result.append(node)
            elif isinstance(node, Directory):
                for child in node.children.values():
                    walk(child)

        walk(self.root)
        return result

    def _all_dirs(self) -> List[Directory]:
        """Collect all directories in tree"""
        result: List[Directory] = []

        def walk(d: Directory) -> None:
            result.append(d)
            for child in d.children.values():
                if isinstance(child, Directory):
                    walk(child)

        walk(self.root)
        return result

    def _refresh_metrics(self) -> None:
        """Recalculate all metrics"""
        files = self._all_files()
        dirs = self._all_dirs()

        logical_total = sum(f.logical_size(self.storage) for f in files)
        physical_total = sum(f.physical_size() for f in files)

        self.metrics.update({
            'file_count': len(files),
            'dir_count': len(dirs),
            'total_bytes': logical_total,
            'compressed_bytes': physical_total
        })
        self._emit("metrics_updated", self.metrics.copy())

    def get_metrics(self) -> Dict[str, int]:
        """Return current metrics"""
        return self.metrics.copy()

# ============================================================================
# DEMO SCENARIOS
# ============================================================================

def print_section(title: str) -> None:
    print(f"\n{'='*70}")
    print(f"  {title}")
    print(f"{'='*70}")

def simple_listener(event: str, payload: Dict[str, Any]) -> None:
    if event in ['node_created', 'node_deleted', 'strategy_swapped', 'snapshot_taken']:
        print(f"  [EVENT] {event}: {payload}")

def demo_1_setup() -> None:
    print_section("DEMO 1: SETUP & CREATE HIERARCHY")

    fs = FileSystem.instance()
    fs.register(simple_listener)

    fs.execute(CreateDirectoryCommand("/docs"))
    fs.execute(CreateFileCommand("/docs/spec.md"))
    fs.execute(WriteFileCommand("/docs/spec.md", "API specification document"))
    fs.execute(CreateFileCommand("/docs/readme.md"))
    fs.execute(WriteFileCommand("/docs/readme.md", "Project README"))

    print(f"\n  Content of /docs/spec.md: {fs._get_file('/docs/spec.md').read(fs.storage)}")
    print(f"  Metrics: {fs.get_metrics()}")

def demo_2_strategy_swap() -> None:
    print_section("DEMO 2: STORAGE STRATEGY SWAP")

    fs = FileSystem.instance()

    before = fs.get_metrics()
    print(f"  Before compression: logical={before['total_bytes']}, physical={before['compressed_bytes']}")

    fs.set_storage_strategy(CompressedStorageStrategy())

    after = fs.get_metrics()
    print(f"  After compression:  logical={after['total_bytes']}, physical={after['compressed_bytes']}")
    print(f"  Compression ratio: {100 * after['compressed_bytes'] / after['total_bytes']:.1f}%")

def demo_3_move_node() -> None:
    print_section("DEMO 3: MOVE NODE BETWEEN DIRECTORIES")

    fs = FileSystem.instance()

    fs.execute(CreateDirectoryCommand("/archive"))
    fs.execute(MoveNodeCommand("/docs/readme.md", "/archive"))

    print(f"  /docs/readme.md exists: {fs.exists('/docs/readme.md')}")
    print(f"  /archive/readme.md exists: {fs.exists('/archive/readme.md')}")
    print(f"  Content preserved: {fs._get_file('/archive/readme.md').read(fs.storage)}")

def demo_4_delete_undo_redo() -> None:
    print_section("DEMO 4: DELETE & UNDO/REDO")

    fs = FileSystem.instance()

    fs.execute(DeleteNodeCommand("/archive/readme.md"))
    print(f"  After delete: /archive/readme.md exists = {fs.exists('/archive/readme.md')}")

    fs.undo()
    print(f"  After undo:   /archive/readme.md exists = {fs.exists('/archive/readme.md')}")

    fs.redo()
    print(f"  After redo:   /archive/readme.md exists = {fs.exists('/archive/readme.md')}")

def demo_5_snapshot_restore() -> None:
    print_section("DEMO 5: SNAPSHOT & RESTORE")

    fs = FileSystem.instance()

    snap = fs.take_snapshot()
    before_metrics = fs.get_metrics()
    print(f"  Snapshot taken: file_count={before_metrics['file_count']}")

    fs.execute(CreateFileCommand("/temp/test.txt"))
    fs.execute(WriteFileCommand("/temp/test.txt", "Temporary data"))

    mutated_metrics = fs.get_metrics()
    print(f"  After mutation: file_count={mutated_metrics['file_count']}")
    print(f"  /temp/test.txt exists: {fs.exists('/temp/test.txt')}")

    fs.restore_snapshot(snap)
    restored_metrics = fs.get_metrics()
    print(f"  After restore:  file_count={restored_metrics['file_count']}")
    print(f"  /temp/test.txt exists: {fs.exists('/temp/test.txt')}")

# ============================================================================
# MAIN
# ============================================================================

if __name__ == "__main__":
    print("\n" + "="*70)
    print("πŸ—‚οΈ FILE SYSTEM - 5 DEMO SCENARIOS")
    print("="*70)

    demo_1_setup()
    demo_2_strategy_swap()
    demo_3_move_node()
    demo_4_delete_undo_redo()
    demo_5_snapshot_restore()

    print("\n" + "="*70)
    print("βœ… ALL DEMOS COMPLETED")
    print("="*70 + "\n")

Design Patterns

Pattern Usage Benefit
Composite File/Directory hierarchy Uniform traversal, extensible node types
Strategy Storage encode/decode Transparent content transformation (plain vs compressed)
Command Reversible operations Undo/redo with consistent semantics, extensible
Observer Event listeners Lifecycle observability, decoupled instrumentation
Memento Snapshot/restore Fast global rollback, state recovery
Singleton FileSystem instance Centralized state, no duplication

Summary

βœ… Hierarchical structure with paths, directories, and files βœ… CRUD operations (create, read, write, delete) βœ… Reversible commands with undo/redo stacks βœ… Pluggable storage (plain, compressed, extensible) βœ… Snapshot/restore for state recovery βœ… Event emission for lifecycle tracking βœ… Metrics tracking (file count, sizes, compression ratios) βœ… Composite pattern for uniform traversal βœ… Strategy pattern for content transformation βœ… Extensible design (new strategies, commands, node types)

Key Takeaway: File system demonstrates composite hierarchy (uniform File/Directory handling), strategy pattern (pluggable storage), command pattern (reversible operations with undo/redo), and observability via events. Focus: separation of concerns, extensibility narrative, and scaling trade-offs (in-memory vs external storage, deep copy snapshots vs incremental diffs).