diff --git a/server.py b/server.py
index fba56e6..f11778d 100644
--- a/server.py
+++ b/server.py
@@ -364,8 +364,26 @@ async def get_cached(content_hash: str):
@app.get("/ui/cache/{content_hash}", response_class=HTMLResponse)
-async def ui_cache_view(content_hash: str):
+async def ui_cache_view(content_hash: str, request: Request):
"""View cached content with appropriate display."""
+ current_user = get_user_from_cookie(request)
+ if not current_user:
+ return HTMLResponse(f'''
+
+
+
Login Required | Art DAG L1
+
+
+ Login to view cached content.
+
+
+''', status_code=401)
+
+ # Check user has access to this file
+ user_hashes = get_user_cache_hashes(current_user)
+ if content_hash not in user_hashes:
+ return HTMLResponse('Access denied
', status_code=403)
+
cache_path = CACHE_DIR / content_hash
if not cache_path.exists():
@@ -477,9 +495,55 @@ async def import_to_cache(path: str):
return {"content_hash": content_hash, "cached": True}
+def save_cache_meta(content_hash: str, uploader: str, filename: str = None):
+ """Save metadata for a cached file."""
+ meta_path = CACHE_DIR / f"{content_hash}.meta.json"
+ meta = {
+ "uploader": uploader,
+ "uploaded_at": datetime.now(timezone.utc).isoformat(),
+ "filename": filename
+ }
+ # Don't overwrite existing metadata (preserve original uploader)
+ if not meta_path.exists():
+ with open(meta_path, "w") as f:
+ json.dump(meta, f)
+
+
+def load_cache_meta(content_hash: str) -> dict:
+ """Load metadata for a cached file."""
+ meta_path = CACHE_DIR / f"{content_hash}.meta.json"
+ if meta_path.exists():
+ with open(meta_path) as f:
+ return json.load(f)
+ return {}
+
+
+def get_user_cache_hashes(username: str) -> set:
+ """Get all cache hashes owned by or associated with a user."""
+ actor_id = f"@{username}@{L2_DOMAIN}"
+ hashes = set()
+
+ # Files uploaded by user
+ if CACHE_DIR.exists():
+ for f in CACHE_DIR.iterdir():
+ if f.name.endswith('.meta.json'):
+ meta = load_cache_meta(f.name.replace('.meta.json', ''))
+ if meta.get("uploader") in (username, actor_id):
+ hashes.add(f.name.replace('.meta.json', ''))
+
+ # Files from user's runs (inputs and outputs)
+ for run in list_all_runs():
+ if run.username in (username, actor_id):
+ hashes.update(run.inputs)
+ if run.output_hash:
+ hashes.add(run.output_hash)
+
+ return hashes
+
+
@app.post("/cache/upload")
-async def upload_to_cache(file: UploadFile = File(...)):
- """Upload a file to cache."""
+async def upload_to_cache(file: UploadFile = File(...), username: str = Depends(get_required_user)):
+ """Upload a file to cache. Requires authentication."""
# Write to temp file first
import tempfile
with tempfile.NamedTemporaryFile(delete=False) as tmp:
@@ -497,6 +561,10 @@ async def upload_to_cache(file: UploadFile = File(...)):
else:
tmp_path.unlink()
+ # Save uploader metadata
+ actor_id = f"@{username}@{L2_DOMAIN}"
+ save_cache_meta(content_hash, actor_id, file.filename)
+
return {"content_hash": content_hash, "filename": file.filename, "size": len(content)}
@@ -927,23 +995,27 @@ async def ui_cache_list(request: Request):
if not current_user:
return 'Login to see cached content.
'
- # Get all cached files
+ # Get hashes owned by/associated with this user
+ user_hashes = get_user_cache_hashes(current_user)
+
+ # Get cache items that belong to the user
cache_items = []
if CACHE_DIR.exists():
for f in CACHE_DIR.iterdir():
- if f.is_file() and not f.name.endswith('.provenance.json'):
- stat = f.stat()
- cache_items.append({
- "hash": f.name,
- "size": stat.st_size,
- "mtime": stat.st_mtime
- })
+ if f.is_file() and not f.name.endswith('.provenance.json') and not f.name.endswith('.meta.json'):
+ if f.name in user_hashes:
+ stat = f.stat()
+ cache_items.append({
+ "hash": f.name,
+ "size": stat.st_size,
+ "mtime": stat.st_mtime
+ })
# Sort by modification time (newest first)
cache_items.sort(key=lambda x: x["mtime"], reverse=True)
if not cache_items:
- return 'Cache is empty.
'
+ return 'No cached files. Upload files or run effects to see them here.
'
html_parts = ['']
@@ -996,12 +1068,30 @@ async def ui_cache_list(request: Request):
@app.get("/ui/detail/{run_id}", response_class=HTMLResponse)
-async def ui_detail_page(run_id: str):
+async def ui_detail_page(run_id: str, request: Request):
"""Full detail page for a run."""
+ current_user = get_user_from_cookie(request)
+ if not current_user:
+ return HTMLResponse(f'''
+
+
+
Login Required | Art DAG L1
+
+
+
Login to view run details.
+
+
+''', status_code=401)
+
run = load_run(run_id)
if not run:
return HTMLResponse('
Run not found
', status_code=404)
+ # Check user owns this run
+ actor_id = f"@{current_user}@{L2_DOMAIN}"
+ if run.username not in (current_user, actor_id):
+ return HTMLResponse('
Access denied
', status_code=403)
+
# Check Celery task status if running
if run.status == "running" and run.celery_task_id:
task = celery_app.AsyncResult(run.celery_task_id)