Skip to main content

In-Memory File System

A directory tree in memory - mkdir, touch, ls, cat - where a directory and a file answer to the same interface for everything except "what's actually inside you." This is Composite, played completely straight.

Requirements

Functional

  • Create a file with content at a given path, creating any missing parent directories.
  • Create a directory at a given path.
  • List the immediate children of a directory.
  • Read a file's content by path; read a directory's total size as the sum of everything under it.
  • Delete a file or a directory (and everything inside it).

Non-functional

  • Path resolution (/a/b/c) should walk the tree node by node, not string-match against a flat table of every path ever created.
  • Computing a directory's size must not require a separate "keep this in sync" step scattered across every mutation - it should fall out of composing the sizes of its children.

Design

FileSystemNode is the interface both File and Directory implement; anything that walks the tree (size, listing, deletion) calls the same methods on whichever kind of node it's holding and never checks "is this a file or a directory" itself. Directory holds a map of child name to FileSystemNode, so a subdirectory is stored exactly like a file - just a child that happens to have children of its own.

CallerFileSystemDirectoryFilecreateFile("/a/b/c.txt", data)1resolveOrCreate("a")2addChild("c.txt", file)3size()4size()5
  1. 1The caller gives a full path - the filesystem walks it, the caller never touches a node directly.
  2. 2Each missing segment along the path becomes a new Directory, created on demand.
  3. 3The final directory in the path gets the new File added as a named child.
  4. 4Asking a directory for its size and asking a file for its size use the exact same method name.
  5. 5A directory sums the size() of every child without knowing whether that child is a file or another directory.

Path resolution is a fold over path segments: start at the root, and for each segment ask the current directory for that named child, moving one level deeper each time - there's no separate index of "every path that exists."

Class diagram

«interface»FileSystemNode+ name(): string+ size(): longFile- name: string- content: byte[]+ size(): long+ read(): byte[]+ write(data): voidDirectory- name: string- children: Map<string, FileSystemNode>+ size(): long+ addChild(node): void+ removeChild(name): void+ list(): List<string>FileSystem- root: Directory+ createFile(path, data): void+ createDirectory(path): void+ read(path): byte[]+ delete(path): void
implements
File and Directory both implement FileSystemNode; a Directory composes other FileSystemNodes, so size() and delete() recurse without ever checking node type.

Code

import java.util.*;
 
interface FileSystemNode {
String name();
long size();
}
 
class File implements FileSystemNode {
private final String name;
private byte[] content;
 
File(String name, byte[] content) {
this.name = name;
this.content = content;
}
 
public String name() { return name; }
public long size() { return content.length; }
byte[] read() { return content; }
void write(byte[] data) { this.content = data; }
}
 
class Directory implements FileSystemNode {
private final String name;
private final Map<String, FileSystemNode> children = new LinkedHashMap<>();
 
Directory(String name) {
this.name = name;
}
 
public String name() { return name; }
 
public long size() {
long total = 0;
for (FileSystemNode child : children.values()) total += child.size();
return total;
}
 
void addChild(FileSystemNode node) {
children.put(node.name(), node);
}
 
void removeChild(String name) {
children.remove(name);
}
 
FileSystemNode getChild(String name) {
return children.get(name);
}
 
List<String> list() {
return new ArrayList<>(children.keySet());
}
}
 
class FileSystem {
private final Directory root = new Directory("/");
 
private Directory resolveParent(String path, boolean createMissing) {
String[] segments = path.split("/");
Directory current = root;
for (int i = 1; i < segments.length - 1; i++) {
FileSystemNode next = current.getChild(segments[i]);
if (next == null) {
if (!createMissing) throw new NoSuchElementException("No such directory: " + segments[i]);
Directory created = new Directory(segments[i]);
current.addChild(created);
next = created;
}
current = (Directory) next;
}
return current;
}
 
void createFile(String path, byte[] data) {
String[] segments = path.split("/");
String fileName = segments[segments.length - 1];
Directory parent = resolveParent(path, true);
parent.addChild(new File(fileName, data));
}
 
void createDirectory(String path) {
String[] segments = path.split("/");
String dirName = segments[segments.length - 1];
Directory parent = resolveParent(path, true);
parent.addChild(new Directory(dirName));
}
 
byte[] read(String path) {
String[] segments = path.split("/");
String fileName = segments[segments.length - 1];
Directory parent = resolveParent(path, false);
File file = (File) parent.getChild(fileName);
return file.read();
}
 
void delete(String path) {
String[] segments = path.split("/");
String name = segments[segments.length - 1];
Directory parent = resolveParent(path, false);
parent.removeChild(name);
}
}

Design decisions

  • FileSystemNode is the Composite interface - File is a leaf, Directory composes more nodes. size() on a file returns its content length; size() on a directory sums its children's size() calls, whatever those children are. Neither implementation needs to know the other exists, which is the entire point of Composite: the recursive structure lives in the interface's contract, not in a type-switching helper function.
  • Directory stores children in a name-keyed map, not a list scanned by name on every lookup. Path resolution touches one map lookup per segment instead of a linear scan per level, which matters once a directory has more than a handful of entries.
  • Path resolution walks the tree, it never consults a separate path-to-node table. A flat table would need updating on every create, move, and delete and could drift from the tree it's supposed to describe; walking the tree from the root means there is exactly one source of truth for what exists.
  • What's missing for a real system: creating missing parent directories on touch is handled here as an eager mkdir -p; a real filesystem would also need move/rename (relinking a node under a new parent without recreating it, to keep it cheap regardless of subtree size) and permission checks per node, both of which are out of scope for the composite structure itself.
0%0 of 122 pages studied