Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
406ea42
feat(backend): tier-based workspace file storage limits
ntindle Apr 14, 2026
5df958d
fix(backend): address PR review β€” scan bug, quota overwrite, test cov…
ntindle Apr 14, 2026
a81f305
fix(backend): address second-round review comments
ntindle Apr 15, 2026
ad97169
feat(frontend): show workspace storage usage in usage limits panel
ntindle Apr 15, 2026
c068804
refactor(frontend): use generated hook for workspace storage usage
ntindle Apr 15, 2026
9aa72fb
fix(platform): address third-round review comments
ntindle Apr 15, 2026
6794908
fix(platform): address human review β€” asyncio.gather, formatBytes tests
ntindle Apr 15, 2026
0af8f23
feat(frontend): update credits page copy for tier-based file storage
ntindle Apr 17, 2026
6905038
dx: remove accidentally committed scheduled_tasks.lock
ntindle Apr 17, 2026
81e4403
fix(platform): resolve merge conflicts with dev, gather sequential aw…
ntindle Apr 17, 2026
6f80a10
fix(frontend): fix lint, add storage bar test coverage
ntindle Apr 17, 2026
5c5dd37
Merge branch 'dev' into branch10
ntindle Apr 20, 2026
7351fba
fix(backend): address autogpt-pr-reviewer-in-dev blockers
ntindle Apr 20, 2026
64b7560
Merge branch 'branch10' of https://github.com/Significant-Gravitas/Au…
ntindle Apr 20, 2026
f3b4b0e
fix(platform): human-readable storage quota error messages
ntindle Apr 23, 2026
4d85a68
fix(platform): resolve merge conflicts with dev, update tier names
ntindle Apr 24, 2026
1fed970
fix(backend): move mid-file import to top, add tier coverage guard test
ntindle Apr 24, 2026
938cf9e
test(backend): add guard test for tier multiplier enum completeness
ntindle Apr 24, 2026
33eb9e9
fix(platform): address review β€” format_bytes rollup, storage bar visi…
ntindle Apr 24, 2026
bf044a2
Merge branch 'dev' into pr/12780/branch10
ntindle Apr 29, 2026
db66f34
feat(backend): pull workspace storage limits from LaunchDarkly
ntindle Apr 29, 2026
489ccf9
fix(frontend): isort fix and additional StorageBar test coverage
ntindle Apr 29, 2026
dbb4090
fix(backend): reject zero LD storage values, fix prettier formatting
ntindle Apr 29, 2026
ea4c617
ci: lower frontend patch coverage target from 80% to 70%
ntindle Apr 29, 2026
bb46059
refactor(backend): drop noisy 80% storage warning + document write_fi…
majdyz Apr 30, 2026
c93dac9
fix(backend/copilot): self-recovery hint on storage-full + close TOCT…
majdyz Apr 30, 2026
d3954e2
Merge branch 'dev' into branch10
majdyz Apr 30, 2026
5ee52ea
Merge branch 'dev' into branch10
ntindle Apr 30, 2026
6e4354a
dx(backend): update .env.default REDIS_PORT for cluster (6379 β†’ 17000)
ntindle Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 29 additions & 46 deletions autogpt_platform/backend/backend/api/features/workspace/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Workspace API routes for managing user file storage.
"""

import asyncio
import logging
import os
import re
Expand All @@ -14,18 +15,18 @@
from fastapi.responses import Response
from pydantic import BaseModel, Field

from backend.api.features.store.exceptions import VirusDetectedError, VirusScanError
from backend.copilot.rate_limit import get_workspace_storage_limit_bytes
from backend.data.workspace import (
WorkspaceFile,
count_workspace_files,
get_or_create_workspace,
get_workspace,
get_workspace_file,
get_workspace_total_size,
soft_delete_workspace_file,
)
from backend.util.settings import Config
from backend.util.virus_scanner import scan_content_safe
from backend.util.workspace import WorkspaceManager
from backend.util.workspace import WorkspaceManager, format_bytes
from backend.util.workspace_storage import get_workspace_storage


Expand Down Expand Up @@ -249,66 +250,47 @@ async def upload_file(
# Get or create workspace
workspace = await get_or_create_workspace(user_id)

# Pre-write storage cap check (soft check β€” final enforcement is post-write)
storage_limit_bytes = config.max_workspace_storage_mb * 1024 * 1024
current_usage = await get_workspace_total_size(workspace.id)
if storage_limit_bytes and current_usage + len(content) > storage_limit_bytes:
used_percent = (current_usage / storage_limit_bytes) * 100
raise fastapi.HTTPException(
status_code=413,
detail={
"message": "Storage limit exceeded",
"used_bytes": current_usage,
"limit_bytes": storage_limit_bytes,
"used_percent": round(used_percent, 1),
},
)

# Warn at 80% usage
if (
Comment thread
cursor[bot] marked this conversation as resolved.
storage_limit_bytes
and (usage_ratio := (current_usage + len(content)) / storage_limit_bytes) >= 0.8
):
logger.warning(
f"User {user_id} workspace storage at {usage_ratio * 100:.1f}% "
f"({current_usage + len(content)} / {storage_limit_bytes} bytes)"
)

Comment thread
cursor[bot] marked this conversation as resolved.
# Virus scan
await scan_content_safe(content, filename=filename)

# Write file via WorkspaceManager
# Write file via WorkspaceManager (handles virus scan, per-file size,
# and per-user tier-based storage quota internally).
manager = WorkspaceManager(user_id, workspace.id, session_id)
try:
workspace_file = await manager.write_file(
content, filename, overwrite=overwrite, metadata={"origin": "user-upload"}
)
Comment thread
ntindle marked this conversation as resolved.
except VirusDetectedError as e:
raise fastapi.HTTPException(status_code=400, detail=str(e)) from e
except VirusScanError as e:
raise fastapi.HTTPException(status_code=500, detail=str(e)) from e
except ValueError as e:
# write_file raises ValueError for both path-conflict and size-limit
# cases; map each to its correct HTTP status.
# write_file raises ValueError for path-conflict, size-limit, and
# storage-quota cases; map each to its correct HTTP status.
message = str(e)
if message.startswith("File too large"):
if message.startswith(("File too large", "Storage limit exceeded")):
raise fastapi.HTTPException(status_code=413, detail=message) from e
Comment thread
ntindle marked this conversation as resolved.
raise fastapi.HTTPException(status_code=409, detail=message) from e

# Post-write storage check β€” eliminates TOCTOU race on the quota.
# If a concurrent upload pushed us over the limit, undo this write.
storage_limit_bytes = await get_workspace_storage_limit_bytes(user_id)
new_total = await get_workspace_total_size(workspace.id)
if storage_limit_bytes and new_total > storage_limit_bytes:
try:
Comment thread
ntindle marked this conversation as resolved.
await soft_delete_workspace_file(workspace_file.id, workspace.id)
# Route through WorkspaceManager so the storage backend blob is
# removed too β€” soft_delete_workspace_file alone leaks the blob.
await manager.delete_file(workspace_file.id)
except Exception as e:
logger.warning(
f"Failed to soft-delete over-quota file {workspace_file.id} "
f"Failed to delete over-quota file {workspace_file.id} "
f"in workspace {workspace.id}: {e}"
)
raise fastapi.HTTPException(
status_code=413,
detail={
"message": "Storage limit exceeded (concurrent upload)",
"used_bytes": new_total,
"limit_bytes": storage_limit_bytes,
},
detail=(
f"Storage limit exceeded. "
f"You've used {format_bytes(new_total)} of your "
f"{format_bytes(storage_limit_bytes)} quota. "
f"Delete some files or upgrade your plan for more storage."
),
)

return UploadFileResponse(
Expand All @@ -331,12 +313,13 @@ async def get_storage_usage(
"""
Get storage usage information for the user's workspace.
"""
config = Config()
workspace = await get_or_create_workspace(user_id)

used_bytes = await get_workspace_total_size(workspace.id)
file_count = await count_workspace_files(workspace.id)
limit_bytes = config.max_workspace_storage_mb * 1024 * 1024
used_bytes, file_count, limit_bytes = await asyncio.gather(
get_workspace_total_size(workspace.id),
count_workspace_files(workspace.id),
get_workspace_storage_limit_bytes(user_id),
)

return StorageUsageResponse(
used_bytes=used_bytes,
Expand Down
Loading
Loading