Design and implement an in-memory cloud storage system that maps file paths to metadata and supports multi-user quotas. Use simple data structures; performance optimizations are not required.
Constraints and invariants:
-
The filesystem namespace is global: each file path is unique across all users.
-
Each file is owned by exactly one user.
-
For any non-admin user, the total size of their files must not exceed their capacity at any time.
-
The special user "admin" has unlimited capacity; Level 1 operations are performed as admin.
-
Sorting rule used anywhere it applies: by size descending, then by name in lexicographical (ASCII) order ascending as a tie-breaker.
Level 1 — basic file manipulation (admin by default):
-
add_file(self, name: str, size: int) -> bool: Add a new file. Fail if name already exists. Return True on success, False otherwise.
-
get_file_size(self, name: str) -> int | None: Return the size if the file exists, else None.
-
delete_file(self, name: str) -> int | None: Delete the file and return its size if it existed, else None.
Level 2 — top-N by size with name filtering (per-user):
-
get_n_largest(self, user_id: str, prefix: str | None, suffix: str | None, n: int) -> list[str]: Among files owned by user_id whose names start with prefix (if prefix is not None/empty) and end with suffix (if suffix is not None/empty), return the names of the top n files sorted by the rule above. If fewer than n match, return all. If none match, return [].
Level 3 — users, quotas, merging, and capacity adjustments:
-
add_user(self, user_id: str, capacity: int) -> bool: Create a user with the given capacity. Fail if user_id already exists. Return True on success, False otherwise.
-
add_file_by(self, user_id: str, name: str, size: int) -> int | None: Same behavior as add_file, but the file is owned by user_id. If adding the file would exceed the user’s capacity, the operation fails. On success, return the user’s remaining capacity (capacity − total_owned_size), else None.
-
adjust_capacity(self, user_id: str, new_capacity: int) -> int | None: Change the user’s capacity. If the new capacity is smaller than current usage, repeatedly delete the user’s largest files (using the sorting rule above to break ties) until total size ≤ new_capacity or no files remain. Return the number of files deleted, or None if user_id does not exist.
-
merge_users(self, target_id: str, source_id: str) -> int | None: Move all files owned by source_id into target_id. If a moved file name conflicts with an existing global name, skip that file. Ensure target_id’s capacity invariant is maintained; if necessary, delete target_id’s largest files (per the same deletion rule) until within capacity. After the merge, delete source_id and its backup (if any); target_id’s backup is not modified. Return the count of files successfully moved, or None if either user does not exist.
Level 4 — backup/restore and size transform operations:
-
backup_user(self, user_id: str) -> int | None: Snapshot the current set of files (names and sizes) owned by user_id into a separate backup store, overwriting any prior backup for that user. Return the number of files backed up, or None if user_id does not exist.
-
restore_user(self, user_id: str) -> int | None: Replace the user’s current files with the latest backup snapshot. If there is no backup, delete all of the user’s files. When restoring, if a file from the backup conflicts with an existing global name owned by another user, skip that file. Do not change the user’s capacity value as part of restore. Return the number of files restored, or None if user_id does not exist.
-
compress_user(self, user_id: str) -> int | None: For each file owned by user_id, set size := floor(size /
2). Return the number of files updated, or None if user_id does not exist.
-
decompress_user(self, user_id: str) -> int | None: For each file owned by user_id, set size := size * 2, then ensure the capacity invariant holds; if it does not, delete the user’s largest files (per the same deletion rule) until within capacity. Return the number of files remaining after decompression, or None if user_id does not exist.
Notes:
-
Implement exact lexicographical tie-breaking on names when sizes are equal.
-
Comparisons for prefix/suffix may be performed directly on strings; no trie or priority queue is required.
-
All method names, arguments, and return types should match exactly as specified above.