merge: personal user filesystem minio integration
This commit is contained in:
@ -36,6 +36,19 @@ from beaver.integrations.mcp import MCPConnectionManager
|
||||
from beaver.services.agent_service import NOTIFICATION_SESSION_ID, AgentService
|
||||
from beaver.services.cron_service import CronService, schedule_from_api
|
||||
from beaver.services.skillhub_service import SkillHubService
|
||||
from beaver.services.user_files import (
|
||||
USER_FILE_ROOTS,
|
||||
UserFileError,
|
||||
UserFileNotFoundError,
|
||||
UserFilePathError,
|
||||
UserFileSizeError,
|
||||
UserFileService,
|
||||
)
|
||||
from beaver.services.user_file_resolver import (
|
||||
UserFileConfigurationError,
|
||||
UserFileStorageResolver,
|
||||
build_file_auth_context,
|
||||
)
|
||||
from beaver.skills.learning import SkillLearningWorker, SkillLearningWorkerConfig
|
||||
from beaver.skills.catalog.utils import parse_frontmatter
|
||||
|
||||
@ -485,6 +498,28 @@ def create_app(
|
||||
app.state.handoff_codes = {}
|
||||
app.state.auth_file = Path(os.getenv("BEAVER_AUTH_FILE") or "")
|
||||
max_file_size = 50 * 1024 * 1024
|
||||
max_user_file_upload_size = _int_env("BEAVER_USER_FILES_MAX_UPLOAD_BYTES", 5 * 1024 * 1024 * 1024)
|
||||
user_file_upload_part_size = _int_env("BEAVER_USER_FILES_UPLOAD_PART_SIZE", 10 * 1024 * 1024)
|
||||
|
||||
def _user_file_resolver(request: Request, authorization: str | None) -> UserFileStorageResolver:
|
||||
username = _require_web_user(app, authorization)
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
auth_context = build_file_auth_context(username=username, config=loaded.config)
|
||||
return UserFileStorageResolver(config=loaded.config, workspace=loaded.workspace, auth_context=auth_context)
|
||||
|
||||
async def _user_file_service(request: Request, authorization: str | None) -> UserFileService:
|
||||
return await _user_file_resolver(request, authorization).service()
|
||||
|
||||
def _user_file_http_error(exc: UserFileError) -> HTTPException:
|
||||
if isinstance(exc, UserFileNotFoundError):
|
||||
return HTTPException(status_code=404, detail=str(exc) or "File not found")
|
||||
if isinstance(exc, UserFilePathError):
|
||||
return HTTPException(status_code=400, detail=str(exc) or "Invalid path")
|
||||
if isinstance(exc, UserFileSizeError):
|
||||
return HTTPException(status_code=413, detail=str(exc) or "File too large")
|
||||
if isinstance(exc, UserFileConfigurationError):
|
||||
return HTTPException(status_code=503, detail=str(exc) or "User file storage is not configured")
|
||||
return HTTPException(status_code=400, detail=str(exc) or "User file operation failed")
|
||||
|
||||
@app.get("/api/ping", response_model=WebStatusResponse)
|
||||
async def ping(request: Request) -> WebStatusResponse:
|
||||
@ -1279,6 +1314,101 @@ def create_app(
|
||||
return {"ok": True}
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
@app.get("/api/user-files/status")
|
||||
async def user_files_status(
|
||||
request: Request,
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> dict[str, Any]:
|
||||
return (await _user_file_resolver(request, authorization).status()).to_dict()
|
||||
|
||||
@app.get("/api/user-files/browse")
|
||||
async def browse_user_files(
|
||||
request: Request,
|
||||
path: str = "",
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await (await _user_file_service(request, authorization)).browse(path)
|
||||
except UserFileError as exc:
|
||||
raise _user_file_http_error(exc) from exc
|
||||
|
||||
@app.get("/api/user-files/download")
|
||||
async def download_user_file(
|
||||
path: str,
|
||||
request: Request,
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> Response:
|
||||
try:
|
||||
content = await (await _user_file_service(request, authorization)).download(path)
|
||||
except UserFileError as exc:
|
||||
raise _user_file_http_error(exc) from exc
|
||||
disposition = "inline" if content.content_type.startswith("image/") else "attachment"
|
||||
return Response(
|
||||
content=content.content,
|
||||
media_type=content.content_type,
|
||||
headers={"Content-Disposition": content_disposition(disposition, content.name)},
|
||||
)
|
||||
|
||||
@app.get("/api/user-files/preview")
|
||||
async def preview_user_file(
|
||||
path: str,
|
||||
request: Request,
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await (await _user_file_service(request, authorization)).preview(path)
|
||||
except UserFileError as exc:
|
||||
raise _user_file_http_error(exc) from exc
|
||||
|
||||
@app.post("/api/user-files/upload")
|
||||
async def upload_user_file(
|
||||
request: Request,
|
||||
file: UploadFile = File(...),
|
||||
path: str = Form("uploads"),
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> dict[str, Any]:
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No filename provided")
|
||||
file_size = getattr(file, "size", None)
|
||||
if isinstance(file_size, int) and file_size > max_user_file_upload_size:
|
||||
raise HTTPException(status_code=413, detail=f"File too large (max {_human_upload_size(max_user_file_upload_size)})")
|
||||
try:
|
||||
return await (await _user_file_service(request, authorization)).upload_stream(
|
||||
path,
|
||||
file.filename,
|
||||
file.file,
|
||||
content_type=file.content_type or "application/octet-stream",
|
||||
max_bytes=max_user_file_upload_size,
|
||||
part_size=user_file_upload_part_size,
|
||||
)
|
||||
except UserFileError as exc:
|
||||
raise _user_file_http_error(exc) from exc
|
||||
|
||||
@app.delete("/api/user-files/delete")
|
||||
async def delete_user_file(
|
||||
path: str,
|
||||
request: Request,
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> dict[str, bool]:
|
||||
try:
|
||||
removed = await (await _user_file_service(request, authorization)).delete(path)
|
||||
except UserFileError as exc:
|
||||
raise _user_file_http_error(exc) from exc
|
||||
if removed:
|
||||
return {"ok": True}
|
||||
raise HTTPException(status_code=404, detail="Path not found")
|
||||
|
||||
@app.post("/api/user-files/mkdir")
|
||||
async def create_user_file_directory(
|
||||
path: str,
|
||||
request: Request,
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await (await _user_file_service(request, authorization)).mkdir(path)
|
||||
except UserFileError as exc:
|
||||
raise _user_file_http_error(exc) from exc
|
||||
|
||||
@app.get("/api/workspace/browse")
|
||||
async def browse_workspace_dir(request: Request, path: str = "") -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
@ -3165,6 +3295,27 @@ def _handoff_replay_window_seconds() -> int:
|
||||
return 15
|
||||
|
||||
|
||||
def _int_env(name: str, default: int) -> int:
|
||||
raw = os.getenv(name, "").strip()
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
value = int(raw)
|
||||
except ValueError:
|
||||
return default
|
||||
return value if value > 0 else default
|
||||
|
||||
|
||||
def _human_upload_size(size: int) -> str:
|
||||
units = ("B", "KB", "MB", "GB", "TB")
|
||||
value = float(size)
|
||||
for unit in units:
|
||||
if value < 1024 or unit == units[-1]:
|
||||
return f"{value:.0f}{unit}" if unit == "B" else f"{value:.1f}{unit}"
|
||||
value /= 1024
|
||||
return f"{size}B"
|
||||
|
||||
|
||||
def _prune_handoff_codes(app: FastAPI) -> None:
|
||||
now = time.time()
|
||||
replay_window = _handoff_replay_window_seconds()
|
||||
|
||||
Reference in New Issue
Block a user