"Design an in-memory file system supporting files and directories. Support creating, deleting, moving, reading, and writing files; listing directory contents; searching for files by name or extension; and computing disk usage for any directory."
B. Difficulty
Intermediate → Advanced. The tree structure itself is not hard — the difficulty is entirely in what a naive candidate bolts onto the base node type (every possible read-only operation, permission logic, locking) versus what a mature candidate keeps separate via Composite + Visitor + a disciplined locking protocol.
C. What This Problem Tests
Recognizing files and directories as a classic Composite — a directory holds children uniformly, whether they're files or more directories
ISP judgment: resisting the urge to add size(), search(), countFiles() etc. directly to the base node interface, and reaching for Visitor instead
Iterator design trade-offs when a directory's contents can change mid-traversal (fail-fast vs. snapshot)
Lock-ordering discipline for a cross-directory move() — the same multi-resource deadlock shape as Splitwise's multi-pair settle-up, applied to a tree instead of a ledger
D. Requirements
Functional
Create/delete files and directories (deleting a non-empty directory requires an explicit recursive flag)
Read/write file contents
Move/rename a file or directory, including across different parent directories
List a directory's immediate contents
Search for files by name, extension, or size under a given subtree
Compute total disk usage of any directory (recursive size)
Non-Functional
Concurrent reads must be safe and non-blocking against each other; a write must be exclusive against both other writes and reads to the same node
A move() must never deadlock against a concurrent move() elsewhere in the tree
A directory cannot be moved into its own subtree (no cycles)
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
In-memory only, or must it eventually persist to real disk?
Determines whether a pluggable StorageBackend boundary is needed now or can be deferred
Scope of X (Persistence)
2
Is per-user permission enforcement required, or just structural correctness?
Determines whether a Proxy access-control layer is in scope
Presence of SecureFileSystemProxy
3
Will this be accessed by multiple threads concurrently?
Determines whether locking is a core requirement or a stretch extension
Scope of W (Concurrency)
4
Are symbolic or hard links in scope?
Links reintroduce exactly the cycle risk that plain moves must already prevent, at a much larger scale
Excluded by assumption (see F)
5
Does search need wildcard/regex matching, or is exact/substring matching enough?
Affects whether search is a linear Visitor pass or needs a name index
Scope of SearchVisitor vs. an indexed alternative
F. Assumptions
Confirmed: in-memory tree of files and directories; CRUD, move, list, search, disk usage.
Assumption: single-process, multi-threaded access (not a distributed filesystem) — GFS/HDFS-style metadata services and chunk replication are explicitly out of scope (see Y).
Assumption: no symbolic or hard links — every node has exactly one parent, which is what makes "can't move a directory into its own descendant" a simple ancestry check rather than general cycle detection on an arbitrary graph.
Assumption: permissions, if present at all, are a simple owner-based check rather than full POSIX-style ACLs.
Composite (headline):File and Directory both extend FileSystemNode; a Directory holds a uniform collection of FileSystemNode — client code walking the tree never needs an instanceof check or an isDirectory() boolean scattered through every call site. This is the direct cure for the most common naive design in this problem (see AA).
Why Visitor over methods on FileSystemNode: disk usage, search-by-extension, and count-files-under-subtree are all read-only aggregate operations, and there will always be another one an interviewer asks for next. Adding each as a new method on FileSystemNode (and its File/Directory overrides) violates ISP — every node is forced to implement every operation whether it's relevant or not, and the base interface grows without bound. A NodeVisitor with visitFile()/visitDirectory(), dispatched via a single stable accept(visitor) method, lets new read-only operations (DiskUsageVisitor, SearchVisitor) be added as entirely new classes with zero changes to FileSystemNode, File, or Directory — a direct OCP payoff.
Iterator:DirectoryIterator decouples "how children are traversed" from Directory's internal storage — see AB for the fail-fast-vs-snapshot policy trade-off this pattern forces into the open.
Why Proxy, not Decorator, for permission checks (if in scope): a permission-checking wrapper around FileSystemNode looks structurally identical to a Decorator, but the intent is different — Decorator composes additional behavior onto a base object (e.g. Problem 5's surge-pricing decorator adds a computation), while a permission check is access control: it decides whether the real call happens at all, not how to embellish its result. That's exactly what Proxy names. Calling it a Decorator would describe the shape correctly but the intent wrongly — worth stating explicitly if asked.
J. Class Diagram
View Mermaid source
classDiagram
class FileSystemNode {
name
parent
+accept(visitor)
}
class File {
content
size
+read()
+write(data)
}
class Directory {
children
+addChild()
+removeChild()
+getChild(name)
}
class NodeVisitor {
<<interface>>
+visitFile(f)
+visitDirectory(d)
}
class FileSystem {
root
+createFile()
+move()
+search()
+diskUsage()
}
FileSystemNode <|-- File
FileSystemNode <|-- Directory
Directory --> "*" FileSystemNode : children
FileSystem --> Directory : root
NodeVisitor <|.. DiskUsageVisitor
NodeVisitor <|.. SearchVisitor
FileSystemNode --> NodeVisitor : accept
File owns content; Directory owns children; visitors own aggregate logic
One Node class with an isDirectory flag, content field, and a children list, all always present, most always unused
OCP
New read-only operation = new NodeVisitor implementation, zero changes to node classes
Adding countImages() as yet another method on FileSystemNode, forcing every existing node subclass to be touched again
LSP
File and Directory both honestly implement accept(visitor); a File visited by visitFile() never needs to fake directory behavior
Calling listChildren() on a File node and having it silently return an empty list instead of the operation being structurally unavailable
ISP
NodeVisitor stays a two-method interface; new capabilities are new visitor classes, not new methods forced onto FileSystemNode
A fat FileSystemNode interface with size(), search(), countFiles(), checksum() all bolted on directly
DIP
FileSystem depends on FileSystemNode/NodeVisitor abstractions, not concrete File/Directory branches
FileSystem.diskUsage() doing its own instanceof Directory recursion inline instead of delegating to a visitor
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
Composite
Files and directories need uniform treatment despite structural differences
FileSystemNode/File/Directory
Skip only if the filesystem is provably flat forever (unrealistic for this prompt)
Visitor
New read-only aggregate operations keep arriving, and shouldn't force changes to node classes
NodeVisitor, DiskUsageVisitor, SearchVisitor
Skip if the operation set is genuinely closed and small — then a couple of direct methods are simpler and Visitor is overkill
Iterator
Directory traversal shouldn't leak the internal children storage structure
DirectoryIterator
Skip if listing is always "materialize the whole list," with no need to abstract traversal itself
Proxy (optional)
Permission checks or lazy content loading should sit in front of a node without changing its interface
An optional SecureFileSystemProxy wrapping FileSystem
Skip if the round has no permission requirement at all
Explicitly avoided: Decorator for permission checks — see I for why Proxy is the semantically correct pattern (access control) even though the class shape looks similar to a Decorator (behavior augmentation).
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Storage backend
In-memory map-based tree
Real disk-backed or cloud blob-store persistence
StorageBackend interface behind File.read()/write(), same DIP boundary as elsewhere in this document
Permission model
None, or simple owner check
Full ACL / role-based access
Proxy layer, not baked into FileSystemNode itself
Search
Linear SearchVisitor pass
Indexed search (e.g. trie by name, or an inverted index by extension)
Search stays behind the same top-level FileSystem.search() signature regardless of internal implementation
Links
Strict one-parent tree
Symbolic links (a node can be reachable from multiple paths)
Would require revisiting the "single parent = no cycles" assumption entirely — flagged explicitly as a scope-expanding change, not a drop-in one
O. Design Evolution
V1: plain tree, no locking, isDirectory boolean and inline recursion for size/search.
V2: replace the boolean flag with real File/Directory subclasses under Composite once the inline instanceof branching starts spreading across call sites.
V3: extract NodeVisitor once a second read-only operation (search, after disk-usage) is requested — the point where hardcoding each as a new node method would start hurting.
V4: add DirectoryIterator with an explicit fail-fast-vs-snapshot decision.
V5: add per-node readers-writer locks and a canonical-path lock-ordering protocol for move().
V6: optional Proxy layer for permissions; pluggable StorageBackend for real persistence.
What stays stable:FileSystem's top-level API signatures. New aggregate operations are pure additions (new visitor classes); persistence and permissions slot in behind existing interfaces without touching File/Directory.
Ownership/validation notes:move() resolves both the source and destination parent directories, verifies the destination is not a descendant of the source (cycle check), then acquires both parents' locks in canonical-path order before mutating either — never partially detach a node without knowing the reattachment will succeed.
Q. Pseudocode — Critical Logic
FileSystem.move(source, destination):
srcParent = resolveParent(source)
destParent = resolveParent(destination)
node = srcParent.getChild(source.lastSegment)
if node is Directory and destination.isDescendantOf(source):
throw IllegalMoveException("cannot move a directory into its own subtree")
// lock ordering: always acquire in canonical-path order to prevent
// an ABBA deadlock against a concurrent move() going the other direction
(first, second) = orderByCanonicalPath(srcParent, destParent)
lock(first); lock(second)
try:
srcParent.removeChild(node.name)
node.parent = destParent
destParent.addChild(node)
finally:
unlock(second); unlock(first)
DiskUsageVisitor.visitDirectory(d):
total = 0
for child in d.children.values():
child.accept(this) // recurses; visitFile adds file.size directly
total += this.lastSize
this.lastSize = total
void move(Path source, Path destination) throws IllegalMoveException {
Directory srcParent = resolveParent(source);
Directory destParent = resolveParent(destination);
FileSystemNode node = srcParent.getChild(source.lastSegment());
if (node instanceof Directory && destination.isDescendantOf(source)) {
throw new IllegalMoveException("cannot move a directory into its own subtree");
}
// lock ordering: always acquire in canonical-path order to prevent
// an ABBA deadlock against a concurrent move() going the other direction
Directory[] ordered = orderByCanonicalPath(srcParent, destParent);
Directory first = ordered[0], second = ordered[1];
lock(first); lock(second);
try {
srcParent.removeChild(node.getName());
node.setParent(destParent);
destParent.addChild(node);
} finally {
unlock(second); unlock(first);
}
}
void visitDirectory(Directory d) {
long total = 0;
for (FileSystemNode child : d.getChildren().values()) {
child.accept(this); // recurses; visitFile adds file.size directly
total += this.lastSize;
}
this.lastSize = total;
}
void FileSystem::move(const Path& source, const Path& destination) {
auto srcParent = resolveParent(source);
auto destParent = resolveParent(destination);
auto node = srcParent->getChild(source.lastSegment());
if (std::dynamic_pointer_cast<Directory>(node) && destination.isDescendantOf(source)) {
throw IllegalMoveException("cannot move a directory into its own subtree"); // throws IllegalMoveException
}
// lock ordering: always acquire in canonical-path order to prevent
// an ABBA deadlock against a concurrent move() going the other direction
auto [first, second] = orderByCanonicalPath(srcParent, destParent);
lock(first); lock(second);
try {
srcParent->removeChild(node->name);
node->parent = destParent;
destParent->addChild(node);
} catch (...) {
unlock(second); unlock(first);
throw;
}
unlock(second); unlock(first);
}
void DiskUsageVisitor::visitDirectory(const Directory& d) {
long total = 0;
for (auto& [name, child] : d.children()) {
child->accept(*this); // recurses; visitFile adds file.size directly
total += lastSize_;
}
lastSize_ = total;
}
move(source, destination) {
const srcParent = this.resolveParent(source);
const destParent = this.resolveParent(destination);
const node = srcParent.getChild(source.lastSegment);
if (node instanceof Directory && destination.isDescendantOf(source)) {
throw new Error("cannot move a directory into its own subtree"); // IllegalMoveException
}
// lock ordering: always acquire in canonical-path order to prevent
// an ABBA deadlock against a concurrent move() going the other direction
const [first, second] = orderByCanonicalPath(srcParent, destParent);
lock(first); lock(second);
try {
srcParent.removeChild(node.name);
node.parent = destParent;
destParent.addChild(node);
} finally {
unlock(second); unlock(first);
}
}
visitDirectory(d) {
let total = 0;
for (const child of d.children.values()) {
child.accept(this); // recurses; visitFile adds file.size directly
total += this.lastSize;
}
this.lastSize = total;
}
move(source: Path, destination: Path): void {
const srcParent: Directory = this.resolveParent(source);
const destParent: Directory = this.resolveParent(destination);
const node: FileSystemNode = srcParent.getChild(source.lastSegment);
if (node instanceof Directory && destination.isDescendantOf(source)) {
throw new Error("cannot move a directory into its own subtree"); // IllegalMoveException
}
// lock ordering: always acquire in canonical-path order to prevent
// an ABBA deadlock against a concurrent move() going the other direction
const [first, second] = orderByCanonicalPath(srcParent, destParent);
lock(first); lock(second);
try {
srcParent.removeChild(node.name);
node.parent = destParent;
destParent.addChild(node);
} finally {
unlock(second); unlock(first);
}
}
visitDirectory(d: Directory): void {
let total = 0;
for (const child of d.children.values()) {
child.accept(this); // recurses; visitFile adds file.size directly
total += this.lastSize;
}
this.lastSize = total;
}
R. End-to-End Use Case
createDirectory("/docs")
createFile("/docs/report.txt"); write("/docs/report.txt", data)
createDirectory("/archive")
│
▼
move("/docs/report.txt", "/archive/report.txt")
│ resolveParent("/docs") and resolveParent("/archive")
│ cycle check: "/archive" is not a descendant of "/docs/report.txt" ✓
│ lock both parents in canonical order → detach → reattach → unlock
▼
diskUsage("/")
│ DiskUsageVisitor recurses: "/docs" (0 bytes now) + "/archive" (report.txt's size)
▼
Total returned
S. Data Structures
Structure
Used For
Why
Alternative & Trade-off
Map<String, FileSystemNode> per Directory
Child lookup by name
O(1) resolution of each path segment
A sorted structure (e.g. TreeMap) would give ordered listing for free at a small insert/lookup cost
List<String> segments in Path
Normalized, comparable location
Enables the canonical-path ordering used for deadlock-free locking in move()
A raw string path works but requires re-normalizing (resolving ../.) on every comparison
Matches the actual access pattern — reads (list, read content) vastly outnumber writes in most filesystems
A single global lock is simpler but serializes unrelated subtrees needlessly at scale
T. Complexity Analysis
Operation
Time
Space
Path resolution (create/delete/read/write)
O(depth of path)
O(1) extra
diskUsage() / search()
O(n) nodes in the subtree — no shortcut without caching or indexing
O(depth) for recursion stack
move()
O(depth of source) + O(depth of destination) for resolution and the cycle check
O(1)
U. Testing Strategy
Happy path: create → write → read round-trips content exactly; list() reflects exactly the current children.
Edge cases: deleting a non-empty directory without the recursive flag is rejected; deleting an empty directory succeeds without it.
Cycle prevention: attempting to move a directory into its own descendant is rejected before any locks are acquired or any node is mutated.
Concurrency tests: two concurrent move() calls that would lock the same two directories in opposite orders must not deadlock (verifies the canonical-order fix actually works); many concurrent readers of the same file never block each other; a writer blocks and is blocked by both readers and other writers on that same node.
Iterator semantics: a directory mutated mid-iteration behaves according to whichever policy was chosen (fail-fast throws cleanly; snapshot silently reflects the pre-mutation state) — the test asserts the chosen policy, not "some" behavior.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"Add a `find files larger than X modified in the last week` query."
Recognizing this as another Visitor, not a new node method
New SearchVisitor with richer SearchCriteria
New class only
Validates the Visitor investment from M
"This needs to run across a cluster of machines, not one process."
LLD/HLD boundary awareness
N/A at the LLD layer — candidate should name GFS/HDFS-style metadata-service + chunk-replication concerns and explicitly place them at HLD
N/A
Recognizing the boundary is the actual signal, not attempting to design a distributed FS live
"Two clients try to move overlapping subtrees at the same time — walk me through what happens."
Deadlock and lock-ordering reasoning
None if canonical ordering is already in place — candidate should trace through it
N/A
Directly exercises the W/Q lock-ordering design
"Add symbolic links."
Testing whether the candidate recognizes this breaks the single-parent assumption
Would require general cycle detection instead of a simple ancestry check, and reference-counted targets
Significant — new node type, revisit move/delete semantics
A strong candidate flags this as a real scope expansion, not a small add-on
W. Concurrency
Shared mutable state: every Directory's children map; every File's content.
Readers-writer lock per node: concurrent read()/list() calls proceed without blocking each other; a write(), addChild(), or removeChild() takes an exclusive lock.
Lock ordering for cross-directory move(): moving a node touches two directories (source parent, destination parent). Locking them in call order (source first, then destination) is exactly how two concurrent moves going in opposite directions deadlock — the same ABBA shape as Splitwise's multi-pair settle-up. The fix is identical in spirit: always acquire locks in one fixed global order (e.g. canonical path string comparison), regardless of which one is logically the "source" for a given call.
Cycle check happens before any lock is taken: validating that the destination isn't a descendant of the source is a pure read against the current tree shape; doing it first avoids holding locks during a check that might reject the whole operation anyway.
X. Persistence
The prompt is explicitly in-memory, but the design should not hardcode that assumption into File itself: a StorageBackend interface behind read()/write() (in-memory byte array today, real disk or blob-store tomorrow) keeps persistence swappable without touching the tree structure, Composite hierarchy, or any visitor — the same DIP boundary used for payment gateways and cache backends elsewhere in this document.
Distributed metadata service, chunk/block servers, replication, consistency model across machines (GFS/HDFS/S3-style)
Distributed Systems
Explicitly named but out of scope for this round unless the interviewer pivots there directly — a strong candidate draws this line rather than either ignoring scale entirely or over-designing a distributed FS unprompted
Z. Failure Scenarios
A node is deleted while a visitor is mid-traversal of that subtree: the fail-fast-vs-snapshot iterator policy (see AB) determines whether this throws cleanly or completes against a consistent pre-deletion view — either is acceptable, but it must be a deliberate choice, not undefined behavior.
`move()` fails partway (e.g. destination lock acquisition fails after the cycle check passed): the pseudocode in Q never detaches the node from its source until both locks are held, so a failure before that point leaves the tree untouched — no orphaned nodes.
Delete while a file handle is still open (POSIX-style unlink semantics): a reference-counted handle model means delete() unlinks the node from the directory tree immediately (it disappears from list()) but the underlying content isn't actually freed until the last open handle closes — the same "logically gone, physically pending" split real filesystems use.
AA. Common Bad Designs
A single Node class with an isDirectory boolean and scattered if/instanceof checks at every call site instead of Composite polymorphism — the single most common naive shape for this problem.
Every new query (size, search, count, checksum) added as a direct method on the base node interface — grows the interface without bound and violates ISP; this is exactly what Visitor exists to prevent.
A single global filesystem-wide lock — correct but needlessly serializes completely unrelated subtrees under any real load.
Locking move()'s two directories in call order instead of a fixed canonical order — passes every single-threaded test and deadlocks the first time two moves cross paths concurrently.
AB. Design Trade-Offs
Decision
Option A
Option B
Recommendation
Directory iteration semantics
Fail-fast (throw if the directory changes mid-iteration)
Snapshot (iterate a point-in-time copy)
Fail-fast surfaces bugs immediately and matches Java's own collection convention; snapshot is friendlier for long-running reports (like disk-usage scans) that shouldn't abort because of unrelated concurrent writes — state the choice explicitly rather than leaving it implicit
Directory size
Cached, invalidated bottom-up on writes
Recomputed on every diskUsage() call
Cache only if diskUsage() is called far more often than the tree is mutated; otherwise the invalidation bookkeeping costs more than it saves
Locking granularity
Per-node locks
Per-subtree (coarser) locks
Per-node for realistic concurrent workloads; coarser locks are simpler to reason about but reintroduce the "unrelated operations block each other" problem the global lock has
AC. Interviewer Evaluation
Strong signal: reaching for Composite immediately and unprompted for the file/directory split; independently proposing Visitor once asked for a second read-only operation, with a clear ISP-based justification for why it's not just another node method; naming the lock-ordering fix for move() without being walked into the deadlock first.
Red flag: an isDirectory boolean surviving past the first design pass; adding every new query directly to the node interface; proposing a single global lock as the final answer rather than a deliberately simple starting point; not noticing the cycle risk in move() until explicitly prompted.
AD. Interview Simulation
Interviewer: Design an in-memory file system.
Candidate: Files and directories need to be treated uniformly by anything walking the tree, but a directory has children and a file doesn't — that's a textbook Composite: both extend a common FileSystemNode, and client code never needs to check which one it has.
Interviewer: Now add a way to compute total disk usage under any directory.
Candidate: I wouldn't add a size() method to every node type for this — there will always be another aggregate query after this one. I'd add a NodeVisitor interface and a DiskUsageVisitor implementation, dispatched through one stable accept() method on the nodes.
Interviewer: What about moving a file between two directories while other operations are happening concurrently?
Candidate: That needs locks on both the source and destination parent directories. If I lock them in call order, two concurrent moves crossing in opposite directions can deadlock — so I'd always acquire locks in a fixed canonical order, like comparing normalized path strings, regardless of which is logically the source.
Interviewer: What stops someone from moving a directory into its own subtree?
Candidate: An explicit ancestry check — is the destination a descendant of the source — done before any locks are taken, since it's a pure read that can reject the operation for free.
Composite-based File/Directory hierarchy with a shared abstract node; recursive size computation walked directly rather than via a separate visitor object
No concurrency handling at all (single-threaded assumption throughout) and no Visitor abstraction — every new aggregate query would mean touching the node classes directly, which this document explicitly treats as an ISP violation
The base Composite shape itself; the concurrency and Visitor gaps are exactly what this document adds on top
Not independently verifiable — the page is paywalled/premium content behind a login, so only the course structure and pattern-name outline is publicly visible
Publicly visible outline lists Composite as the headline pattern, consistent with the approach here
Cannot verify locking strategy, Visitor usage, or move-cycle handling beyond the outline
Nothing beyond a weak cross-check that Composite is the expected headline pattern — cited honestly rather than fabricating unseen content
AF. Interview Cheat Sheet
Problem
File System
Difficulty
Intermediate → Advanced
Core requirements
Create/delete/move files and directories, read/write content, list, search, disk usage
Key clarifications
In-memory only? Permissions in scope? Multi-threaded? Symlinks? Search matching style?
isDirectory boolean instead of Composite; growing the base node interface per query instead of Visitor; call-order (not canonical-order) locking in move()
What NOT to over-engineer
Don't design a distributed metadata/chunk-server system when the prompt says in-memory; don't add symlink/ACL complexity unless asked
AG. Full Implementation
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// File System -- full LLD implementation with demo.
// Single-file Java translation of the Mermaid class diagram in section J.
// No concurrency badge on this problem -- kept single-threaded; the lock-ordering idea in the
// move() pseudocode is preserved only as commentary, not as real mutexes.
import java.util.*;
public class p9 {
// ---------- Path (value object) ----------
/** Normalized location, independent of the live tree. Says nothing about whether it exists. */
static final class Path {
final List<String> segments;
Path(List<String> segments) { this.segments = List.copyOf(segments); }
static Path of(String slashSeparated) {
List<String> parts = new ArrayList<>();
for (String s : slashSeparated.split("/")) if (!s.isEmpty()) parts.add(s);
return new Path(parts);
}
Path parent() { return new Path(segments.subList(0, Math.max(0, segments.size() - 1))); }
String lastSegment() { return segments.get(segments.size() - 1); }
Path resolve(String childName) {
List<String> next = new ArrayList<>(segments);
next.add(childName);
return new Path(next);
}
boolean isDescendantOf(Path other) {
if (other.segments.size() >= segments.size()) return false;
for (int i = 0; i < other.segments.size(); i++) {
if (!other.segments.get(i).equals(segments.get(i))) return false;
}
return true;
}
public String toString() { return "/" + String.join("/", segments); }
}
// ---------- NodeVisitor (interface) ----------
interface NodeVisitor {
void visitFile(File f);
void visitDirectory(Directory d);
}
// ---------- FileSystemNode (abstract) ----------
/** Common identity for anything in the tree. Aggregate ops are delegated to a NodeVisitor. */
static abstract class FileSystemNode {
String name;
Directory parent;
FileSystemNode(String name, Directory parent) { this.name = name; this.parent = parent; }
abstract void accept(NodeVisitor visitor);
}
// ---------- File (leaf) ----------
static final class File extends FileSystemNode {
byte[] content = new byte[0];
File(String name, Directory parent) { super(name, parent); }
int size() { return content.length; }
byte[] read() { return content; }
void write(byte[] data) { content = data; }
void accept(NodeVisitor visitor) { visitor.visitFile(this); }
}
// ---------- Directory (composite) ----------
static final class Directory extends FileSystemNode {
final Map<String, FileSystemNode> children = new LinkedHashMap<>();
Directory(String name, Directory parent) { super(name, parent); }
void addChild(FileSystemNode node) { children.put(node.name, node); }
void removeChild(String name) { children.remove(name); }
FileSystemNode getChild(String name) { return children.get(name); }
void accept(NodeVisitor visitor) { visitor.visitDirectory(this); }
}
// ---------- DirectoryIterator ----------
/**
* Snapshot iterator: copies the child list at creation time, so a concurrent addChild/removeChild
* on the underlying Directory during iteration cannot throw ConcurrentModificationException. This
* is a deliberate policy choice (snapshot over fail-fast) since the directory is a live, mutable map.
*/
static final class DirectoryIterator implements Iterator<FileSystemNode> {
private final List<FileSystemNode> snapshot;
private int index = 0;
DirectoryIterator(Directory directory) { this.snapshot = new ArrayList<>(directory.children.values()); }
public boolean hasNext() { return index < snapshot.size(); }
public FileSystemNode next() {
if (!hasNext()) throw new NoSuchElementException();
return snapshot.get(index++);
}
}
// ---------- NodeVisitor implementations ----------
static final class DiskUsageVisitor implements NodeVisitor {
private long lastSize;
long compute(FileSystemNode node) { node.accept(this); return lastSize; }
public void visitFile(File f) { lastSize = f.size(); }
public void visitDirectory(Directory d) {
long total = 0;
DirectoryIterator it = new DirectoryIterator(d);
while (it.hasNext()) {
FileSystemNode child = it.next();
child.accept(this); // recurses; visitFile sets lastSize directly for leaves
total += lastSize;
}
lastSize = total;
}
}
interface SearchCriteria { boolean matches(FileSystemNode node); }
static final class SearchVisitor implements NodeVisitor {
private final SearchCriteria criteria;
private final Path currentBase;
private final List<Path> matches = new ArrayList<>();
SearchVisitor(SearchCriteria criteria, Path base) { this.criteria = criteria; this.currentBase = base; }
List<Path> search(FileSystemNode node) { walk(node, currentBase); return matches; }
private void walk(FileSystemNode node, Path path) {
if (criteria.matches(node)) matches.add(path);
if (node instanceof Directory) {
Directory d = (Directory) node;
DirectoryIterator it = new DirectoryIterator(d);
while (it.hasNext()) {
FileSystemNode child = it.next();
walk(child, path.resolve(child.name));
}
}
}
public void visitFile(File f) { /* traversal handled by walk(); visitor methods unused here */ }
public void visitDirectory(Directory d) { /* see walk() */ }
}
// ---------- FileSystem (facade) ----------
static final class FileSystem {
final Directory root = new Directory("", null);
private Directory resolveParent(Path path) {
Directory current = root;
for (int i = 0; i < path.segments.size() - 1; i++) {
FileSystemNode next = current.getChild(path.segments.get(i));
if (!(next instanceof Directory)) throw new IllegalArgumentException("No such directory: " + path);
current = (Directory) next;
}
return current;
}
private FileSystemNode resolve(Path path) {
if (path.segments.isEmpty()) return root;
Directory parentDir = resolveParent(path);
FileSystemNode node = parentDir.getChild(path.lastSegment());
if (node == null) throw new IllegalArgumentException("No such path: " + path);
return node;
}
void createFile(Path path) {
Directory parentDir = resolveParent(path);
parentDir.addChild(new File(path.lastSegment(), parentDir));
}
void createDirectory(Path path) {
Directory parentDir = resolveParent(path);
parentDir.addChild(new Directory(path.lastSegment(), parentDir));
}
void delete(Path path, boolean recursive) {
Directory parentDir = resolveParent(path);
FileSystemNode node = parentDir.getChild(path.lastSegment());
if (node instanceof Directory && !((Directory) node).children.isEmpty() && !recursive) {
throw new IllegalStateException("Directory not empty; pass recursive=true to delete: " + path);
}
parentDir.removeChild(path.lastSegment());
}
byte[] read(Path path) {
FileSystemNode node = resolve(path);
if (!(node instanceof File)) throw new IllegalArgumentException("Not a file: " + path);
return ((File) node).read();
}
void write(Path path, byte[] data) {
FileSystemNode node = resolve(path);
if (!(node instanceof File)) throw new IllegalArgumentException("Not a file: " + path);
((File) node).write(data);
}
List<String> list(Path directoryPath) {
FileSystemNode node = resolve(directoryPath);
if (!(node instanceof Directory)) throw new IllegalArgumentException("Not a directory: " + directoryPath);
List<String> names = new ArrayList<>();
DirectoryIterator it = new DirectoryIterator((Directory) node);
while (it.hasNext()) names.add(it.next().name);
return names;
}
/**
* Move source under destination's parent. Guards against moving a directory into its own
* subtree (would create a cycle). No real threads exist in this single-threaded translation,
* so the "lock in canonical-path order" step from the pseudocode is left as a comment only --
* this is where a two-mutex ABBA-safe acquisition would go under a concurrent implementation.
*/
void move(Path source, Path destination) {
Directory srcParent = resolveParent(source);
Directory destParent = resolveParent(destination);
FileSystemNode node = srcParent.getChild(source.lastSegment());
if (node == null) throw new IllegalArgumentException("No such path: " + source);
if (node instanceof Directory && destination.isDescendantOf(source)) {
throw new IllegalArgumentException("cannot move a directory into its own subtree");
}
// lock ordering would be established here by canonical path in a concurrent version
srcParent.removeChild(node.name);
node.name = destination.lastSegment();
node.parent = destParent;
destParent.addChild(node);
}
List<Path> search(Path root, SearchCriteria criteria) {
FileSystemNode startNode = resolve(root);
return new SearchVisitor(criteria, root).search(startNode);
}
long diskUsage(Path root) {
FileSystemNode startNode = resolve(root);
return new DiskUsageVisitor().compute(startNode);
}
}
// ---------- Demo ----------
public static void main(String[] args) {
FileSystem fs = new FileSystem();
fs.createDirectory(Path.of("/docs"));
fs.createDirectory(Path.of("/docs/reports"));
fs.createFile(Path.of("/docs/readme.txt"));
fs.createFile(Path.of("/docs/reports/q1.csv"));
fs.createFile(Path.of("/docs/reports/q2.csv"));
fs.createDirectory(Path.of("/media"));
fs.write(Path.of("/docs/readme.txt"), "hello world".getBytes());
fs.write(Path.of("/docs/reports/q1.csv"), "a,b,c\n1,2,3".getBytes());
fs.write(Path.of("/docs/reports/q2.csv"), "x,y,z\n4,5,6\n7,8,9".getBytes());
System.out.println("Listing /docs: " + fs.list(Path.of("/docs")));
System.out.println("Listing /docs/reports: " + fs.list(Path.of("/docs/reports")));
System.out.println("Read /docs/readme.txt: " + new String(fs.read(Path.of("/docs/readme.txt"))));
System.out.println("Disk usage of /docs: " + fs.diskUsage(Path.of("/docs")) + " bytes");
List<Path> csvFiles = fs.search(Path.of("/docs"), node -> node instanceof File && node.name.endsWith(".csv"));
System.out.println("Search *.csv under /docs: " + csvFiles);
System.out.println("\nMoving /docs/reports -> /media/reports");
fs.move(Path.of("/docs/reports"), Path.of("/media/reports"));
System.out.println("Listing /media: " + fs.list(Path.of("/media")));
System.out.println("Listing /docs after move: " + fs.list(Path.of("/docs")));
try {
fs.move(Path.of("/media"), Path.of("/media/reports/media"));
} catch (IllegalArgumentException e) {
System.out.println("\nExpected rejection moving a directory into its own subtree: " + e.getMessage());
}
try {
fs.delete(Path.of("/media"), false);
} catch (IllegalStateException e) {
System.out.println("Expected rejection deleting non-empty dir without recursive: " + e.getMessage());
}
fs.delete(Path.of("/media"), true);
System.out.println("Root listing after recursive delete of /media: " + fs.list(Path.of("")));
}
}
// File System -- full class-diagram implementation, single-threaded.
// No concurrency badge on this problem -- kept single-threaded; the lock-ordering idea in the
// move() pseudocode is preserved only as commentary, not as real mutexes.
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <functional>
// ---------- Path (value object) ----------
class Path {
public:
std::vector<std::string> segments;
explicit Path(std::vector<std::string> segs = {}) : segments(std::move(segs)) {}
static Path of(const std::string& slashSeparated) {
std::vector<std::string> parts;
std::stringstream ss(slashSeparated);
std::string part;
while (std::getline(ss, part, '/')) if (!part.empty()) parts.push_back(part);
return Path(parts);
}
Path parentPath() const {
if (segments.empty()) return Path();
return Path(std::vector<std::string>(segments.begin(), segments.end() - 1));
}
std::string lastSegment() const { return segments.back(); }
Path resolveChild(const std::string& childName) const {
std::vector<std::string> next = segments;
next.push_back(childName);
return Path(next);
}
bool isDescendantOf(const Path& other) const {
if (other.segments.size() >= segments.size()) return false;
for (size_t i = 0; i < other.segments.size(); i++) {
if (other.segments[i] != segments[i]) return false;
}
return true;
}
std::string toString() const {
std::string out = "/";
for (size_t i = 0; i < segments.size(); i++) { out += segments[i]; if (i + 1 < segments.size()) out += "/"; }
return out;
}
};
class File;
class Directory;
// ---------- NodeVisitor (interface) ----------
struct NodeVisitor {
virtual ~NodeVisitor() = default;
virtual void visitFile(File& f) = 0;
virtual void visitDirectory(Directory& d) = 0;
};
// ---------- FileSystemNode (abstract) ----------
// Common identity for anything in the tree. Aggregate ops are delegated to a NodeVisitor.
class FileSystemNode {
public:
std::string name;
Directory* parent;
FileSystemNode(std::string name_, Directory* parent_) : name(std::move(name_)), parent(parent_) {}
virtual ~FileSystemNode() = default;
virtual void accept(NodeVisitor& visitor) = 0;
};
// ---------- File (leaf) ----------
class File : public FileSystemNode {
public:
std::string content;
File(std::string name_, Directory* parent_) : FileSystemNode(std::move(name_), parent_) {}
size_t size() const { return content.size(); }
std::string read() const { return content; }
void write(const std::string& data) { content = data; }
void accept(NodeVisitor& visitor) override;
};
// ---------- Directory (composite) ----------
class Directory : public FileSystemNode {
public:
std::map<std::string, std::shared_ptr<FileSystemNode>> children; // ordered by name
Directory(std::string name_, Directory* parent_) : FileSystemNode(std::move(name_), parent_) {}
void addChild(std::shared_ptr<FileSystemNode> node) { children[node->name] = node; }
void removeChild(const std::string& name) { children.erase(name); }
std::shared_ptr<FileSystemNode> getChild(const std::string& name) const {
auto it = children.find(name);
return it == children.end() ? nullptr : it->second;
}
void accept(NodeVisitor& visitor) override;
};
void File::accept(NodeVisitor& visitor) { visitor.visitFile(*this); }
void Directory::accept(NodeVisitor& visitor) { visitor.visitDirectory(*this); }
// ---------- DirectoryIterator ----------
// Snapshot iterator: copies the child list at creation time so a concurrent mutation of the
// underlying Directory during iteration can't invalidate iterators mid-walk (policy choice: snapshot).
class DirectoryIterator {
public:
explicit DirectoryIterator(const Directory& dir) {
for (auto& kv : dir.children) snapshot_.push_back(kv.second);
}
bool hasNext() const { return index_ < snapshot_.size(); }
std::shared_ptr<FileSystemNode> next() { return snapshot_[index_++]; }
private:
std::vector<std::shared_ptr<FileSystemNode>> snapshot_;
size_t index_ = 0;
};
// ---------- NodeVisitor implementations ----------
class DiskUsageVisitor : public NodeVisitor {
public:
long long compute(std::shared_ptr<FileSystemNode> node) { node->accept(*this); return lastSize_; }
void visitFile(File& f) override { lastSize_ = (long long)f.size(); }
void visitDirectory(Directory& d) override {
long long total = 0;
DirectoryIterator it(d);
while (it.hasNext()) {
auto child = it.next();
child->accept(*this); // recurses; visitFile sets lastSize_ directly for leaves
total += lastSize_;
}
lastSize_ = total;
}
private:
long long lastSize_ = 0;
};
using SearchCriteria = std::function<bool(const FileSystemNode&)>;
class SearchVisitor : public NodeVisitor {
public:
SearchVisitor(SearchCriteria criteria, Path base) : criteria_(std::move(criteria)), base_(std::move(base)) {}
std::vector<Path> search(std::shared_ptr<FileSystemNode> node) {
walk(node, base_);
return matches_;
}
void visitFile(File&) override {} // traversal is driven by walk(), not the visitor dispatch
void visitDirectory(Directory&) override {}
private:
SearchCriteria criteria_;
Path base_;
std::vector<Path> matches_;
void walk(std::shared_ptr<FileSystemNode> node, const Path& path) {
if (criteria_(*node)) matches_.push_back(path);
auto dir = std::dynamic_pointer_cast<Directory>(node);
if (dir) {
DirectoryIterator it(*dir);
while (it.hasNext()) {
auto child = it.next();
walk(child, path.resolveChild(child->name));
}
}
}
};
// ---------- FileSystem (facade) ----------
class FileSystem {
public:
std::shared_ptr<Directory> root = std::make_shared<Directory>("", nullptr);
void createFile(const Path& path) {
auto parentDir = resolveParent(path);
parentDir->addChild(std::make_shared<File>(path.lastSegment(), parentDir.get()));
}
void createDirectory(const Path& path) {
auto parentDir = resolveParent(path);
parentDir->addChild(std::make_shared<Directory>(path.lastSegment(), parentDir.get()));
}
void deleteNode(const Path& path, bool recursive) {
auto parentDir = resolveParent(path);
auto node = parentDir->getChild(path.lastSegment());
auto dir = std::dynamic_pointer_cast<Directory>(node);
if (dir && !dir->children.empty() && !recursive) {
throw std::runtime_error("Directory not empty; pass recursive=true to delete: " + path.toString());
}
parentDir->removeChild(path.lastSegment());
}
std::string read(const Path& path) {
auto node = resolve(path);
auto file = std::dynamic_pointer_cast<File>(node);
if (!file) throw std::runtime_error("Not a file: " + path.toString());
return file->read();
}
void write(const Path& path, const std::string& data) {
auto node = resolve(path);
auto file = std::dynamic_pointer_cast<File>(node);
if (!file) throw std::runtime_error("Not a file: " + path.toString());
file->write(data);
}
std::vector<std::string> list(const Path& directoryPath) {
auto node = resolve(directoryPath);
auto dir = std::dynamic_pointer_cast<Directory>(node);
if (!dir) throw std::runtime_error("Not a directory: " + directoryPath.toString());
std::vector<std::string> names;
DirectoryIterator it(*dir);
while (it.hasNext()) names.push_back(it.next()->name);
return names;
}
// Move source under destination's parent (destination's last segment becomes the new name).
// Guards against moving a directory into its own subtree. No real threads exist in this
// single-threaded translation, so canonical-path lock ordering is left as commentary only --
// this is where a two-mutex ABBA-safe acquisition would go under a concurrent implementation.
void move(const Path& source, const Path& destination) {
auto srcParent = resolveParent(source);
auto destParent = resolveParent(destination);
auto node = srcParent->getChild(source.lastSegment());
if (!node) throw std::runtime_error("No such path: " + source.toString());
auto asDir = std::dynamic_pointer_cast<Directory>(node);
if (asDir && destination.isDescendantOf(source)) {
throw std::runtime_error("cannot move a directory into its own subtree");
}
// lock ordering would be established here by canonical path in a concurrent version
srcParent->removeChild(node->name);
node->name = destination.lastSegment();
node->parent = destParent.get();
destParent->addChild(node);
}
std::vector<Path> search(const Path& startPath, const SearchCriteria& criteria) {
auto startNode = resolve(startPath);
return SearchVisitor(criteria, startPath).search(startNode);
}
long long diskUsage(const Path& startPath) {
auto startNode = resolve(startPath);
return DiskUsageVisitor().compute(startNode);
}
private:
std::shared_ptr<Directory> resolveParent(const Path& path) {
std::shared_ptr<Directory> current = root;
for (size_t i = 0; i + 1 < path.segments.size(); i++) {
auto next = std::dynamic_pointer_cast<Directory>(current->getChild(path.segments[i]));
if (!next) throw std::runtime_error("No such directory in path: " + path.toString());
current = next;
}
return current;
}
std::shared_ptr<FileSystemNode> resolve(const Path& path) {
if (path.segments.empty()) return root;
auto parentDir = resolveParent(path);
auto node = parentDir->getChild(path.lastSegment());
if (!node) throw std::runtime_error("No such path: " + path.toString());
return node;
}
};
static void printVec(const std::string& label, const std::vector<std::string>& v) {
std::cout << label << ": [";
for (size_t i = 0; i < v.size(); i++) { std::cout << v[i]; if (i + 1 < v.size()) std::cout << ", "; }
std::cout << "]\n";
}
int main() {
FileSystem fs;
fs.createDirectory(Path::of("/docs"));
fs.createDirectory(Path::of("/docs/reports"));
fs.createFile(Path::of("/docs/readme.txt"));
fs.createFile(Path::of("/docs/reports/q1.csv"));
fs.createFile(Path::of("/docs/reports/q2.csv"));
fs.createDirectory(Path::of("/media"));
fs.write(Path::of("/docs/readme.txt"), "hello world");
fs.write(Path::of("/docs/reports/q1.csv"), "a,b,c\n1,2,3");
fs.write(Path::of("/docs/reports/q2.csv"), "x,y,z\n4,5,6\n7,8,9");
printVec("Listing /docs", fs.list(Path::of("/docs")));
printVec("Listing /docs/reports", fs.list(Path::of("/docs/reports")));
std::cout << "Read /docs/readme.txt: " << fs.read(Path::of("/docs/readme.txt")) << "\n";
std::cout << "Disk usage of /docs: " << fs.diskUsage(Path::of("/docs")) << " bytes\n";
auto csvFiles = fs.search(Path::of("/docs"), [](const FileSystemNode& n) {
return dynamic_cast<const File*>(&n) != nullptr && n.name.size() > 4 && n.name.substr(n.name.size() - 4) == ".csv";
});
std::cout << "Search *.csv under /docs: [";
for (size_t i = 0; i < csvFiles.size(); i++) { std::cout << csvFiles[i].toString(); if (i + 1 < csvFiles.size()) std::cout << ", "; }
std::cout << "]\n";
std::cout << "\nMoving /docs/reports -> /media/reports\n";
fs.move(Path::of("/docs/reports"), Path::of("/media/reports"));
printVec("Listing /media", fs.list(Path::of("/media")));
printVec("Listing /docs after move", fs.list(Path::of("/docs")));
try {
fs.move(Path::of("/media"), Path::of("/media/reports/media"));
} catch (const std::exception& e) {
std::cout << "\nExpected rejection moving a directory into its own subtree: " << e.what() << "\n";
}
try {
fs.deleteNode(Path::of("/media"), false);
} catch (const std::exception& e) {
std::cout << "Expected rejection deleting non-empty dir without recursive: " << e.what() << "\n";
}
fs.deleteNode(Path::of("/media"), true);
printVec("Root listing after recursive delete of /media", fs.list(Path::of("")));
return 0;
}
// File System -- full class-diagram implementation, single-threaded.
// No concurrency badge on this problem -- kept single-threaded; the lock-ordering idea in the
// move() pseudocode is preserved only as commentary, not as real mutexes.
// Node-style, plain classes, CommonJS.
// ---------- Path (value object) ----------
class Path {
constructor(segments = []) { this.segments = [...segments]; }
static of(slashSeparated) {
return new Path(slashSeparated.split('/').filter((s) => s.length > 0));
}
parentPath() { return new Path(this.segments.slice(0, -1)); }
lastSegment() { return this.segments[this.segments.length - 1]; }
resolveChild(childName) { return new Path([...this.segments, childName]); }
isDescendantOf(other) {
if (other.segments.length >= this.segments.length) return false;
return other.segments.every((seg, i) => seg === this.segments[i]);
}
toString() { return '/' + this.segments.join('/'); }
}
// ---------- NodeVisitor (interface) ----------
class NodeVisitor {
visitFile(_f) { throw new Error('not implemented'); }
visitDirectory(_d) { throw new Error('not implemented'); }
}
// ---------- FileSystemNode (abstract) ----------
// Common identity for anything in the tree. Aggregate ops are delegated to a NodeVisitor.
class FileSystemNode {
constructor(name, parent) {
if (new.target === FileSystemNode) throw new Error('FileSystemNode is abstract');
this.name = name;
this.parent = parent;
}
accept(_visitor) { throw new Error('accept() not implemented'); }
}
// ---------- File (leaf) ----------
class File extends FileSystemNode {
constructor(name, parent) { super(name, parent); this.content = Buffer.alloc(0); }
size() { return this.content.length; }
read() { return this.content; }
write(data) { this.content = Buffer.isBuffer(data) ? data : Buffer.from(data); }
accept(visitor) { visitor.visitFile(this); }
}
// ---------- Directory (composite) ----------
class Directory extends FileSystemNode {
constructor(name, parent) { super(name, parent); this.children = new Map(); }
addChild(node) { this.children.set(node.name, node); }
removeChild(name) { this.children.delete(name); }
getChild(name) { return this.children.get(name); }
accept(visitor) { visitor.visitDirectory(this); }
}
// ---------- DirectoryIterator ----------
// Snapshot iterator: copies the child list at creation time so a concurrent mutation of the
// underlying Directory during iteration can't invalidate iteration mid-walk (policy: snapshot).
class DirectoryIterator {
constructor(directory) {
this.snapshot = Array.from(directory.children.values());
this.index = 0;
}
hasNext() { return this.index < this.snapshot.length; }
next() {
if (!this.hasNext()) throw new Error('No more elements');
return this.snapshot[this.index++];
}
}
// ---------- NodeVisitor implementations ----------
class DiskUsageVisitor extends NodeVisitor {
compute(node) { node.accept(this); return this.lastSize; }
visitFile(f) { this.lastSize = f.size(); }
visitDirectory(d) {
let total = 0;
const it = new DirectoryIterator(d);
while (it.hasNext()) {
const child = it.next();
child.accept(this); // recurses; visitFile sets lastSize directly for leaves
total += this.lastSize;
}
this.lastSize = total;
}
}
class SearchVisitor extends NodeVisitor {
constructor(criteria, base) { super(); this.criteria = criteria; this.base = base; this.matches = []; }
search(node) { this._walk(node, this.base); return this.matches; }
visitFile(_f) {} // traversal is driven by _walk(), not the visitor dispatch
visitDirectory(_d) {}
_walk(node, path) {
if (this.criteria(node)) this.matches.push(path);
if (node instanceof Directory) {
const it = new DirectoryIterator(node);
while (it.hasNext()) {
const child = it.next();
this._walk(child, path.resolveChild(child.name));
}
}
}
}
// ---------- FileSystem (facade) ----------
class FileSystem {
constructor() { this.root = new Directory('', null); }
_resolveParent(path) {
let current = this.root;
for (let i = 0; i < path.segments.length - 1; i++) {
const next = current.getChild(path.segments[i]);
if (!(next instanceof Directory)) throw new Error(`No such directory: ${path}`);
current = next;
}
return current;
}
_resolve(path) {
if (path.segments.length === 0) return this.root;
const parentDir = this._resolveParent(path);
const node = parentDir.getChild(path.lastSegment());
if (!node) throw new Error(`No such path: ${path}`);
return node;
}
createFile(path) {
const parentDir = this._resolveParent(path);
parentDir.addChild(new File(path.lastSegment(), parentDir));
}
createDirectory(path) {
const parentDir = this._resolveParent(path);
parentDir.addChild(new Directory(path.lastSegment(), parentDir));
}
delete(path, recursive) {
const parentDir = this._resolveParent(path);
const node = parentDir.getChild(path.lastSegment());
if (node instanceof Directory && node.children.size > 0 && !recursive) {
throw new Error(`Directory not empty; pass recursive=true to delete: ${path}`);
}
parentDir.removeChild(path.lastSegment());
}
read(path) {
const node = this._resolve(path);
if (!(node instanceof File)) throw new Error(`Not a file: ${path}`);
return node.read();
}
write(path, data) {
const node = this._resolve(path);
if (!(node instanceof File)) throw new Error(`Not a file: ${path}`);
node.write(data);
}
list(directoryPath) {
const node = this._resolve(directoryPath);
if (!(node instanceof Directory)) throw new Error(`Not a directory: ${directoryPath}`);
const names = [];
const it = new DirectoryIterator(node);
while (it.hasNext()) names.push(it.next().name);
return names;
}
// Move source under destination's parent (destination's last segment becomes the new name).
// Guards against moving a directory into its own subtree. No real threads exist in this
// single-threaded translation, so canonical-path lock ordering is left as commentary only --
// this is where a two-mutex ABBA-safe acquisition would go under a concurrent implementation.
move(source, destination) {
const srcParent = this._resolveParent(source);
const destParent = this._resolveParent(destination);
const node = srcParent.getChild(source.lastSegment());
if (!node) throw new Error(`No such path: ${source}`);
if (node instanceof Directory && destination.isDescendantOf(source)) {
throw new Error('cannot move a directory into its own subtree');
}
// lock ordering would be established here by canonical path in a concurrent version
srcParent.removeChild(node.name);
node.name = destination.lastSegment();
node.parent = destParent;
destParent.addChild(node);
}
search(startPath, criteria) {
const startNode = this._resolve(startPath);
return new SearchVisitor(criteria, startPath).search(startNode);
}
diskUsage(startPath) {
const startNode = this._resolve(startPath);
return new DiskUsageVisitor().compute(startNode);
}
}
// ---------- Demo ----------
function main() {
const fs = new FileSystem();
fs.createDirectory(Path.of('/docs'));
fs.createDirectory(Path.of('/docs/reports'));
fs.createFile(Path.of('/docs/readme.txt'));
fs.createFile(Path.of('/docs/reports/q1.csv'));
fs.createFile(Path.of('/docs/reports/q2.csv'));
fs.createDirectory(Path.of('/media'));
fs.write(Path.of('/docs/readme.txt'), 'hello world');
fs.write(Path.of('/docs/reports/q1.csv'), 'a,b,c\n1,2,3');
fs.write(Path.of('/docs/reports/q2.csv'), 'x,y,z\n4,5,6\n7,8,9');
console.log('Listing /docs:', fs.list(Path.of('/docs')));
console.log('Listing /docs/reports:', fs.list(Path.of('/docs/reports')));
console.log('Read /docs/readme.txt:', fs.read(Path.of('/docs/readme.txt')).toString());
console.log('Disk usage of /docs:', fs.diskUsage(Path.of('/docs')), 'bytes');
const csvFiles = fs.search(Path.of('/docs'), (node) => node instanceof File && node.name.endsWith('.csv'));
console.log('Search *.csv under /docs:', csvFiles.map(String));
console.log('\nMoving /docs/reports -> /media/reports');
fs.move(Path.of('/docs/reports'), Path.of('/media/reports'));
console.log('Listing /media:', fs.list(Path.of('/media')));
console.log('Listing /docs after move:', fs.list(Path.of('/docs')));
try {
fs.move(Path.of('/media'), Path.of('/media/reports/media'));
} catch (e) {
console.log('\nExpected rejection moving a directory into its own subtree:', e.message);
}
try {
fs.delete(Path.of('/media'), false);
} catch (e) {
console.log('Expected rejection deleting non-empty dir without recursive:', e.message);
}
fs.delete(Path.of('/media'), true);
console.log('Root listing after recursive delete of /media:', fs.list(Path.of('')));
}
if (require.main === module) main();
module.exports = {
Path, NodeVisitor, FileSystemNode, File, Directory, DirectoryIterator,
DiskUsageVisitor, SearchVisitor, FileSystem,
};
// File System -- full class-diagram implementation, single-threaded.
// No concurrency badge on this problem -- kept single-threaded; the lock-ordering idea in the
// move() pseudocode is preserved only as commentary, not as real mutexes.
//
// Note: the diagram's `File` leaf class is named `FileNode`, and its `FileSystem` facade is
// named `VirtualFileSystem`, here because TypeScript's default lib set includes DOM, which
// declares global `File` and `FileSystem` types; this avoids those name collisions while
// keeping identical fields/behavior. `Directory` and the rest keep their diagram names.
// ---------- Path (value object) ----------
class Path {
readonly segments: string[];
constructor(segments: string[] = []) { this.segments = [...segments]; }
static of(slashSeparated: string): Path {
return new Path(slashSeparated.split('/').filter((s) => s.length > 0));
}
parentPath(): Path { return new Path(this.segments.slice(0, -1)); }
lastSegment(): string { return this.segments[this.segments.length - 1]; }
resolveChild(childName: string): Path { return new Path([...this.segments, childName]); }
isDescendantOf(other: Path): boolean {
if (other.segments.length >= this.segments.length) return false;
return other.segments.every((seg, i) => seg === this.segments[i]);
}
toString(): string { return '/' + this.segments.join('/'); }
}
// ---------- NodeVisitor (interface) ----------
interface NodeVisitor {
visitFile(f: FileNode): void;
visitDirectory(d: Directory): void;
}
// ---------- FileSystemNode (abstract) ----------
// Common identity for anything in the tree. Aggregate ops are delegated to a NodeVisitor.
abstract class FileSystemNode {
constructor(public name: string, public parent: Directory | null) {}
abstract accept(visitor: NodeVisitor): void;
}
// ---------- FileNode (leaf) ----------
class FileNode extends FileSystemNode {
content: string = '';
constructor(name: string, parent: Directory | null) { super(name, parent); }
size(): number { return this.content.length; }
read(): string { return this.content; }
write(data: string): void { this.content = data; }
accept(visitor: NodeVisitor): void { visitor.visitFile(this); }
}
// ---------- Directory (composite) ----------
class Directory extends FileSystemNode {
readonly children = new Map<string, FileSystemNode>();
constructor(name: string, parent: Directory | null) { super(name, parent); }
addChild(node: FileSystemNode): void { this.children.set(node.name, node); }
removeChild(name: string): void { this.children.delete(name); }
getChild(name: string): FileSystemNode | undefined { return this.children.get(name); }
accept(visitor: NodeVisitor): void { visitor.visitDirectory(this); }
}
// ---------- DirectoryIterator ----------
// Snapshot iterator: copies the child list at creation time so a concurrent mutation of the
// underlying Directory during iteration can't invalidate iteration mid-walk (policy: snapshot).
class DirectoryIterator {
private readonly snapshot: FileSystemNode[];
private index = 0;
constructor(directory: Directory) { this.snapshot = Array.from(directory.children.values()); }
hasNext(): boolean { return this.index < this.snapshot.length; }
next(): FileSystemNode {
if (!this.hasNext()) throw new Error('No more elements');
return this.snapshot[this.index++];
}
}
// ---------- NodeVisitor implementations ----------
class DiskUsageVisitor implements NodeVisitor {
private lastSize = 0;
compute(node: FileSystemNode): number { node.accept(this); return this.lastSize; }
visitFile(f: FileNode): void { this.lastSize = f.size(); }
visitDirectory(d: Directory): void {
let total = 0;
const it = new DirectoryIterator(d);
while (it.hasNext()) {
const child = it.next();
child.accept(this); // recurses; visitFile sets lastSize directly for leaves
total += this.lastSize;
}
this.lastSize = total;
}
}
type SearchCriteria = (node: FileSystemNode) => boolean;
class SearchVisitor implements NodeVisitor {
private readonly matches: Path[] = [];
constructor(private readonly criteria: SearchCriteria, private readonly base: Path) {}
search(node: FileSystemNode): Path[] { this.walk(node, this.base); return this.matches; }
visitFile(_f: FileNode): void {} // traversal is driven by walk(), not the visitor dispatch
visitDirectory(_d: Directory): void {}
private walk(node: FileSystemNode, path: Path): void {
if (this.criteria(node)) this.matches.push(path);
if (node instanceof Directory) {
const it = new DirectoryIterator(node);
while (it.hasNext()) {
const child = it.next();
this.walk(child, path.resolveChild(child.name));
}
}
}
}
// ---------- VirtualFileSystem (facade) ----------
class VirtualFileSystem {
readonly root = new Directory('', null);
private resolveParent(path: Path): Directory {
let current = this.root;
for (let i = 0; i < path.segments.length - 1; i++) {
const next = current.getChild(path.segments[i]);
if (!(next instanceof Directory)) throw new Error(`No such directory: ${path}`);
current = next;
}
return current;
}
private resolve(path: Path): FileSystemNode {
if (path.segments.length === 0) return this.root;
const parentDir = this.resolveParent(path);
const node = parentDir.getChild(path.lastSegment());
if (!node) throw new Error(`No such path: ${path}`);
return node;
}
createFile(path: Path): void {
const parentDir = this.resolveParent(path);
parentDir.addChild(new FileNode(path.lastSegment(), parentDir));
}
createDirectory(path: Path): void {
const parentDir = this.resolveParent(path);
parentDir.addChild(new Directory(path.lastSegment(), parentDir));
}
delete(path: Path, recursive: boolean): void {
const parentDir = this.resolveParent(path);
const node = parentDir.getChild(path.lastSegment());
if (node instanceof Directory && node.children.size > 0 && !recursive) {
throw new Error(`Directory not empty; pass recursive=true to delete: ${path}`);
}
parentDir.removeChild(path.lastSegment());
}
read(path: Path): string {
const node = this.resolve(path);
if (!(node instanceof FileNode)) throw new Error(`Not a file: ${path}`);
return node.read();
}
write(path: Path, data: string): void {
const node = this.resolve(path);
if (!(node instanceof FileNode)) throw new Error(`Not a file: ${path}`);
node.write(data);
}
list(directoryPath: Path): string[] {
const node = this.resolve(directoryPath);
if (!(node instanceof Directory)) throw new Error(`Not a directory: ${directoryPath}`);
const names: string[] = [];
const it = new DirectoryIterator(node);
while (it.hasNext()) names.push(it.next().name);
return names;
}
// Move source under destination's parent (destination's last segment becomes the new name).
// Guards against moving a directory into its own subtree. No real threads exist in this
// single-threaded translation, so canonical-path lock ordering is left as commentary only --
// this is where a two-mutex ABBA-safe acquisition would go under a concurrent implementation.
move(source: Path, destination: Path): void {
const srcParent = this.resolveParent(source);
const destParent = this.resolveParent(destination);
const node = srcParent.getChild(source.lastSegment());
if (!node) throw new Error(`No such path: ${source}`);
if (node instanceof Directory && destination.isDescendantOf(source)) {
throw new Error('cannot move a directory into its own subtree');
}
// lock ordering would be established here by canonical path in a concurrent version
srcParent.removeChild(node.name);
node.name = destination.lastSegment();
node.parent = destParent;
destParent.addChild(node);
}
search(startPath: Path, criteria: SearchCriteria): Path[] {
const startNode = this.resolve(startPath);
return new SearchVisitor(criteria, startPath).search(startNode);
}
diskUsage(startPath: Path): number {
const startNode = this.resolve(startPath);
return new DiskUsageVisitor().compute(startNode);
}
}
// ---------- Demo ----------
function main(): void {
const fs = new VirtualFileSystem();
fs.createDirectory(Path.of('/docs'));
fs.createDirectory(Path.of('/docs/reports'));
fs.createFile(Path.of('/docs/readme.txt'));
fs.createFile(Path.of('/docs/reports/q1.csv'));
fs.createFile(Path.of('/docs/reports/q2.csv'));
fs.createDirectory(Path.of('/media'));
fs.write(Path.of('/docs/readme.txt'), 'hello world');
fs.write(Path.of('/docs/reports/q1.csv'), 'a,b,c\n1,2,3');
fs.write(Path.of('/docs/reports/q2.csv'), 'x,y,z\n4,5,6\n7,8,9');
console.log('Listing /docs:', fs.list(Path.of('/docs')));
console.log('Listing /docs/reports:', fs.list(Path.of('/docs/reports')));
console.log('Read /docs/readme.txt:', fs.read(Path.of('/docs/readme.txt')));
console.log('Disk usage of /docs:', fs.diskUsage(Path.of('/docs')), 'bytes');
const csvFiles = fs.search(Path.of('/docs'), (node) => node instanceof FileNode && node.name.endsWith('.csv'));
console.log('Search *.csv under /docs:', csvFiles.map(String));
console.log('\nMoving /docs/reports -> /media/reports');
fs.move(Path.of('/docs/reports'), Path.of('/media/reports'));
console.log('Listing /media:', fs.list(Path.of('/media')));
console.log('Listing /docs after move:', fs.list(Path.of('/docs')));
try {
fs.move(Path.of('/media'), Path.of('/media/reports/media'));
} catch (e) {
console.log('\nExpected rejection moving a directory into its own subtree:', (e as Error).message);
}
try {
fs.delete(Path.of('/media'), false);
} catch (e) {
console.log('Expected rejection deleting non-empty dir without recursive:', (e as Error).message);
}
fs.delete(Path.of('/media'), true);
console.log('Root listing after recursive delete of /media:', fs.list(Path.of('')));
}
main();