Design (LLD) Google Drive - Machine Coding

Design (LLD) Google Drive - Machine Coding

Features Required:

  1. User Authentication and Authorization:

    • Description: Implement a user management system where users can register, log in, and have roles assigned (e.g., admin, regular user). Authorization controls access to files and operations based on user roles and permissions.
  2. File and Folder Management:

    • Description: Allow users to create, rename, delete, upload, and download files and folders. Files and folders are organized hierarchically, resembling a tree structure.
  3. File Sharing:

    • Description: Users can share files and folders with other users, assigning specific permissions such as read or write access.
  4. Version Control:

    • Description: Maintain versions of files whenever changes are made, allowing users to revert to previous versions if needed.
  5. Search Functionality:

    • Description: Enable users to search for files and folders by name within their accessible directories.
  6. Storage Quota:

    • Description: Implement storage limits per user, tracking the total storage used and preventing actions that exceed the quota.

Design Patterns Used:

  1. Singleton Pattern:

    • Usage: Ensure only one instance of the FileSystemManager exists to manage the file system globally.

    • Reason: Centralizes file system management, preventing inconsistencies and ensuring synchronized access.

  2. Factory Pattern:

    • Usage: Use a FileSystemFactory to create File and Folder objects.

    • Reason: Encapsulates the creation logic, making it easier to manage object creation and extensions.

  3. Composite Pattern:

    • Usage: Implement File and Folder classes that both extend a common FileSystemComponent interface.

    • Reason: Allows clients to treat individual objects and compositions uniformly, simplifying file system operations.

  4. Decorator Pattern:

    • Usage: Use PermissionsDecorator to add permissions dynamically to files and folders.

    • Reason: Enhances objects with additional responsibilities without modifying their structure.

  5. Observer Pattern:

    • Usage: Implement observers that notify users when shared files are updated.

    • Reason: Provides a mechanism for automatic updates, improving collaboration features.

  6. Strategy Pattern:

    • Usage: Define different storage strategies (e.g., local storage, cloud storage) via a StorageStrategy interface.

    • Reason: Enables switching between different storage implementations without affecting the system's core logic.

  7. Proxy Pattern:

    • Usage: Use FileSystemProxy to control access to files and folders based on user permissions.

    • Reason: Adds a layer of security by controlling object access, enforcing permission checks.

  8. Command Pattern:

    • Usage: Implement undo/redo functionality for file operations using command objects.

    • Reason: Encapsulates requests as objects, allowing for operation sequencing, undoing, and redoing actions.

  9. Template Method Pattern:

    • Usage: Define an abstract FileOperation class with a template method for file operations.

    • Reason: Allows subclasses to redefine certain steps of an algorithm without changing its structure.

Algorithms Involved:

  1. Tree Traversal Algorithms:

    • Used for navigating and managing the hierarchical file system (e.g., depth-first search for searching files).
  2. Hashing:

    • Utilized for quick lookup of files and folders using hash maps.
  3. Access Control Algorithms:

    • Implemented to check user permissions before performing operations on files and folders.
  4. Version Control Algorithms:

    • Manage file versions by keeping a history of changes and allowing retrieval of previous versions.

Diagrams

Code (Java):

// FileSystemComponent.java
public interface FileSystemComponent {
    String getName();
    void setName(String name);
    void add(FileSystemComponent component) throws UnsupportedOperationException;
    void remove(FileSystemComponent component) throws UnsupportedOperationException;
    FileSystemComponent getChild(String name) throws UnsupportedOperationException;
    void display(String indent);
}
// File.java
import java.util.ArrayList;
import java.util.List;

public class File implements FileSystemComponent {
    private String name;
    private String content;
    private List<FileVersion> versions;
    private PermissionsDecorator permissions;

    public File(String name) {
        this.name = name;
        this.content = "";
        this.versions = new ArrayList<>();
        this.permissions = new PermissionsDecorator();
    }

    // Composite Pattern Methods
    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void add(FileSystemComponent component) {
        throw new UnsupportedOperationException("Cannot add to a file.");
    }

    @Override
    public void remove(FileSystemComponent component) {
        throw new UnsupportedOperationException("Cannot remove from a file.");
    }

    @Override
    public FileSystemComponent getChild(String name) {
        throw new UnsupportedOperationException("Files do not contain children.");
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + "File: " + name);
    }

    // File-Specific Methods
    public void writeContent(String content) {
        this.content = content;
        versions.add(new FileVersion(content));
    }

    public String readContent() {
        return content;
    }

    public List<FileVersion> getVersions() {
        return versions;
    }

    public PermissionsDecorator getPermissions() {
        return permissions;
    }
}
// Folder.java
import java.util.HashMap;
import java.util.Map;

public class Folder implements FileSystemComponent {
    private String name;
    private Map<String, FileSystemComponent> children;
    private PermissionsDecorator permissions;

    public Folder(String name) {
        this.name = name;
        this.children = new HashMap<>();
        this.permissions = new PermissionsDecorator();
    }

    // Composite Pattern Methods
    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void add(FileSystemComponent component) {
        children.put(component.getName(), component);
    }

    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component.getName());
    }

    @Override
    public FileSystemComponent getChild(String name) {
        return children.get(name);
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + "Folder: " + name);
        for (FileSystemComponent component : children.values()) {
            component.display(indent + "    ");
        }
    }

    public Map<String, FileSystemComponent> getChildren() {
        return children;
    }

    public PermissionsDecorator getPermissions() {
        return permissions;
    }
}
// PermissionsDecorator.java
import java.util.HashMap;
import java.util.Map;

public class PermissionsDecorator {
    private Map<User, Permission> userPermissions;

    public PermissionsDecorator() {
        this.userPermissions = new HashMap<>();
    }

    public void setPermission(User user, Permission permission) {
        userPermissions.put(user, permission);
    }

    public Permission getPermission(User user) {
        return userPermissions.getOrDefault(user, Permission.NONE);
    }

    public enum Permission {
        READ, WRITE, NONE
    }
}
// User.java
public class User {
    private String username;
    private String password;
    private Role role;

    public enum Role {
        ADMIN, USER
    }

    public User(String username, String password, Role role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

    // Getters
    public String getUsername() {
        return username;
    }

    public Role getRole() {
        return role;
    }

    // Authentication Method
    public boolean authenticate(String password) {
        return this.password.equals(password);
    }
}
// FileSystemManager.java
public class FileSystemManager {
    private static FileSystemManager instance;
    private Folder root;
    private StorageStrategy storageStrategy;

    private FileSystemManager() {
        root = new Folder("root");
        storageStrategy = new LocalStorageStrategy(); // Default Strategy
    }

    public static synchronized FileSystemManager getInstance() {
        if (instance == null) {
            instance = new FileSystemManager();
        }
        return instance;
    }

    public Folder getRoot() {
        return root;
    }

    public void setStorageStrategy(StorageStrategy strategy) {
        this.storageStrategy = strategy;
    }

    public StorageStrategy getStorageStrategy() {
        return storageStrategy;
    }
}
// FileVersion.java
public class FileVersion {
    private String content;
    private long timestamp;

    public FileVersion(String content) {
        this.content = content;
        this.timestamp = System.currentTimeMillis();
    }

    public String getContent() {
        return content;
    }

    public long getTimestamp() {
        return timestamp;
    }
}
// Observer.java
public interface Observer {
    void update(String message);
}

// Subject.java
import java.util.ArrayList;
import java.util.List;

public class Subject {
    private List<Observer> observers = new ArrayList<>();

    public void attach(Observer observer) {
        observers.add(observer);
    }

    public void detach(Observer observer) {
        observers.remove(observer);
    }

    protected void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}
// StorageStrategy.java
public interface StorageStrategy {
    void saveFile(File file);
    File loadFile(String fileName);
}

// LocalStorageStrategy.java
public class LocalStorageStrategy implements StorageStrategy {
    @Override
    public void saveFile(File file) {
        // Implement local storage saving logic
        System.out.println("Saving file locally: " + file.getName());
    }

    @Override
    public File loadFile(String fileName) {
        // Implement local storage loading logic
        System.out.println("Loading file locally: " + fileName);
        return new File(fileName);
    }
}
// FileSystemProxy.java
public class FileSystemProxy implements FileSystemComponent {
    private FileSystemComponent realComponent;
    private User currentUser;

    public FileSystemProxy(FileSystemComponent component, User user) {
        this.realComponent = component;
        this.currentUser = user;
    }

    @Override
    public String getName() {
        return realComponent.getName();
    }

    @Override
    public void setName(String name) {
        if (hasWritePermission()) {
            realComponent.setName(name);
        } else {
            throw new SecurityException("No write permission for user: " + currentUser.getUsername());
        }
    }

    @Override
    public void add(FileSystemComponent component) {
        if (hasWritePermission()) {
            realComponent.add(component);
        } else {
            throw new SecurityException("No write permission for user: " + currentUser.getUsername());
        }
    }

    @Override
    public void remove(FileSystemComponent component) {
        if (hasWritePermission()) {
            realComponent.remove(component);
        } else {
            throw new SecurityException("No write permission for user: " + currentUser.getUsername());
        }
    }

    @Override
    public FileSystemComponent getChild(String name) {
        if (hasReadPermission()) {
            return realComponent.getChild(name);
        } else {
            throw new SecurityException("No read permission for user: " + currentUser.getUsername());
        }
    }

    @Override
    public void display(String indent) {
        if (hasReadPermission()) {
            realComponent.display(indent);
        } else {
            throw new SecurityException("No read permission for user: " + currentUser.getUsername());
        }
    }

    private boolean hasReadPermission() {
        PermissionsDecorator.Permission permission = realComponent.getPermissions().getPermission(currentUser);
        return permission == PermissionsDecorator.Permission.READ || permission == PermissionsDecorator.Permission.WRITE;
    }

    private boolean hasWritePermission() {
        PermissionsDecorator.Permission permission = realComponent.getPermissions().getPermission(currentUser);
        return permission == PermissionsDecorator.Permission.WRITE;
    }
}
// FileOperation.java
public abstract class FileOperation {
    public final void execute() {
        if (validate()) {
            performOperation();
            logOperation();
        } else {
            System.out.println("Validation failed. Operation aborted.");
        }
    }

    protected abstract boolean validate();
    protected abstract void performOperation();
    protected void logOperation() {
        System.out.println("Operation performed: " + this.getClass().getSimpleName());
    }
}
// CreateFileOperation.java
public class CreateFileOperation extends FileOperation {
    private Folder folder;
    private String fileName;

    public CreateFileOperation(Folder folder, String fileName) {
        this.folder = folder;
        this.fileName = fileName;
    }

    @Override
    protected boolean validate() {
        return folder.getChild(fileName) == null;
    }

    @Override
    protected void performOperation() {
        folder.add(new File(fileName));
    }
}
// FileSystemSearch.java
public class FileSystemSearch {
    public FileSystemComponent search(FileSystemComponent component, String name) {
        if (component.getName().equals(name)) {
            return component;
        }

        if (component instanceof Folder) {
            Folder folder = (Folder) component;
            for (FileSystemComponent child : folder.getChildren().values()) {
                FileSystemComponent found = search(child, name);
                if (found != null) {
                    return found;
                }
            }
        }
        return null;
    }
}
// Main.java
public class Main {
    public static void main(String[] args) {
        // Create Users
        User admin = new User("admin", "admin123", User.Role.ADMIN);
        User user1 = new User("user1", "password1", User.Role.USER);
        User user2 = new User("user2", "password2", User.Role.USER);

        // Get FileSystemManager Instance
        FileSystemManager fsManager = FileSystemManager.getInstance();

        // Create Folders and Files
        Folder root = fsManager.getRoot();
        root.getPermissions().setPermission(admin, PermissionsDecorator.Permission.WRITE);
        root.getPermissions().setPermission(user1, PermissionsDecorator.Permission.WRITE);

        // Create Files and Folders using Template Method Pattern
        FileOperation createFolderOperation = new CreateFileOperation(root, "Documents");
        createFolderOperation.execute();

        Folder documents = (Folder) root.getChild("Documents");
        documents.getPermissions().setPermission(user1, PermissionsDecorator.Permission.WRITE);

        FileOperation createFileOperation = new CreateFileOperation(documents, "Resume.docx");
        createFileOperation.execute();

        // Use Proxy Pattern for Access Control
        FileSystemProxy user1Proxy = new FileSystemProxy(documents, user1);
        user1Proxy.display("");

        // Version Control
        File resume = (File) documents.getChild("Resume.docx");
        resume.writeContent("Version 1 of Resume");
        resume.writeContent("Version 2 of Resume");

        // Observer Pattern for Notifications
        Subject subject = new Subject();
        Observer user2Observer = new Observer() {
            @Override
            public void update(String message) {
                System.out.println("User2 received notification: " + message);
            }
        };
        subject.attach(user2Observer);
        subject.notifyObservers("Resume.docx has been updated.");

        // Search Functionality
        FileSystemSearch search = new FileSystemSearch();
        FileSystemComponent found = search.search(root, "Resume.docx");
        if (found != null) {
            System.out.println("Found: " + found.getName());
        } else {
            System.out.println("File not found.");
        }

        // Storage Strategy
        fsManager.setStorageStrategy(new LocalStorageStrategy());
        fsManager.getStorageStrategy().saveFile(resume);
    }
}

Explanation of the Code:

  • Composite Pattern: File and Folder both implement FileSystemComponent, allowing them to be treated uniformly.

  • Singleton Pattern: FileSystemManager ensures only one instance manages the file system.

  • Decorator Pattern: PermissionsDecorator adds permissions to files and folders dynamically.

  • Observer Pattern: Subject and Observer enable notifications when files are updated.

  • Strategy Pattern: StorageStrategy allows for different storage mechanisms.

  • Proxy Pattern: FileSystemProxy controls access based on user permissions.

  • Command Pattern: FileOperation and its subclasses implement operations that can be undone/redone.

  • Template Method Pattern: FileOperation defines the steps for file operations, allowing subclasses to implement specifics.

  • Search Functionality: FileSystemSearch recursively searches the file system tree.

Missing Feature (Will be Covered in our Premium Course)

  1. In above code, permission is based on users only but we need more granular level control like having access at File Level

  2. Having different Access type like commenter, read, write like google drive have.

Multi-threading Support

Current design does not support multi-threading and distributed systems out of the box.The design and code provided earlier are intended for a single-threaded application running on a single machine. The focus was on demonstrating how to implement various features and design patterns in Java to simulate a simplified version of Google Drive. As such, the implementation lacks the necessary components to handle multi-threading and distribution across multiple systems.

To extend the current design to support multi-threading and distributed systems, several significant modifications and considerations are necessary. Covered in our Premium Course

Did you find this article valuable?

Support Low Level Design (LLD) Coding by becoming a sponsor. Any amount is appreciated!