from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request, Query from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from sqlalchemy import or_, func from typing import List, Optional import math import re from .database import get_db from .storage import get_storage, S3Storage from .models import Project, Package, Artifact, Tag, Upload, Consumer from .schemas import ( ProjectCreate, ProjectResponse, PackageCreate, PackageResponse, ArtifactResponse, TagCreate, TagResponse, UploadResponse, ConsumerResponse, HealthResponse, PaginatedResponse, PaginationMeta, ) router = APIRouter() def get_user_id(request: Request) -> str: """Extract user ID from request (simplified for now)""" api_key = request.headers.get("X-Orchard-API-Key") if api_key: return "api-user" auth = request.headers.get("Authorization") if auth: return "bearer-user" return "anonymous" # Health check @router.get("/health", response_model=HealthResponse) def health_check(): return HealthResponse(status="ok") # Project routes @router.get("/api/v1/projects", response_model=PaginatedResponse[ProjectResponse]) def list_projects( request: Request, page: int = Query(default=1, ge=1, description="Page number"), limit: int = Query(default=20, ge=1, le=100, description="Items per page"), search: Optional[str] = Query(default=None, description="Search by project name"), db: Session = Depends(get_db), ): user_id = get_user_id(request) # Base query - filter by access query = db.query(Project).filter( or_(Project.is_public == True, Project.created_by == user_id) ) # Apply search filter (case-insensitive) if search: query = query.filter(func.lower(Project.name).contains(search.lower())) # Get total count before pagination total = query.count() # Apply pagination offset = (page - 1) * limit projects = query.order_by(Project.name).offset(offset).limit(limit).all() # Calculate total pages total_pages = math.ceil(total / limit) if total > 0 else 1 return PaginatedResponse( items=projects, pagination=PaginationMeta( page=page, limit=limit, total=total, total_pages=total_pages, ), ) @router.post("/api/v1/projects", response_model=ProjectResponse) def create_project(project: ProjectCreate, request: Request, db: Session = Depends(get_db)): user_id = get_user_id(request) existing = db.query(Project).filter(Project.name == project.name).first() if existing: raise HTTPException(status_code=400, detail="Project already exists") db_project = Project( name=project.name, description=project.description, is_public=project.is_public, created_by=user_id, ) db.add(db_project) db.commit() db.refresh(db_project) return db_project @router.get("/api/v1/projects/{project_name}", response_model=ProjectResponse) def get_project(project_name: str, db: Session = Depends(get_db)): project = db.query(Project).filter(Project.name == project_name).first() if not project: raise HTTPException(status_code=404, detail="Project not found") return project # Package routes @router.get("/api/v1/project/{project_name}/packages", response_model=List[PackageResponse]) def list_packages(project_name: str, db: Session = Depends(get_db)): project = db.query(Project).filter(Project.name == project_name).first() if not project: raise HTTPException(status_code=404, detail="Project not found") packages = db.query(Package).filter(Package.project_id == project.id).order_by(Package.name).all() return packages @router.post("/api/v1/project/{project_name}/packages", response_model=PackageResponse) def create_package(project_name: str, package: PackageCreate, db: Session = Depends(get_db)): project = db.query(Project).filter(Project.name == project_name).first() if not project: raise HTTPException(status_code=404, detail="Project not found") existing = db.query(Package).filter(Package.project_id == project.id, Package.name == package.name).first() if existing: raise HTTPException(status_code=400, detail="Package already exists in this project") db_package = Package( project_id=project.id, name=package.name, description=package.description, ) db.add(db_package) db.commit() db.refresh(db_package) return db_package # Upload artifact @router.post("/api/v1/project/{project_name}/{package_name}/upload", response_model=UploadResponse) def upload_artifact( project_name: str, package_name: str, request: Request, file: UploadFile = File(...), tag: Optional[str] = Form(None), db: Session = Depends(get_db), storage: S3Storage = Depends(get_storage), ): user_id = get_user_id(request) # Get project and package project = db.query(Project).filter(Project.name == project_name).first() if not project: raise HTTPException(status_code=404, detail="Project not found") package = db.query(Package).filter(Package.project_id == project.id, Package.name == package_name).first() if not package: raise HTTPException(status_code=404, detail="Package not found") # Store file sha256_hash, size, s3_key = storage.store(file.file) # Create or update artifact record artifact = db.query(Artifact).filter(Artifact.id == sha256_hash).first() if artifact: artifact.ref_count += 1 else: artifact = Artifact( id=sha256_hash, size=size, content_type=file.content_type, original_name=file.filename, created_by=user_id, s3_key=s3_key, ) db.add(artifact) # Record upload upload = Upload( artifact_id=sha256_hash, package_id=package.id, original_name=file.filename, uploaded_by=user_id, source_ip=request.client.host if request.client else None, ) db.add(upload) # Create tag if provided if tag: existing_tag = db.query(Tag).filter(Tag.package_id == package.id, Tag.name == tag).first() if existing_tag: existing_tag.artifact_id = sha256_hash existing_tag.created_by = user_id else: new_tag = Tag( package_id=package.id, name=tag, artifact_id=sha256_hash, created_by=user_id, ) db.add(new_tag) db.commit() return UploadResponse( artifact_id=sha256_hash, size=size, project=project_name, package=package_name, tag=tag, ) # Download artifact @router.get("/api/v1/project/{project_name}/{package_name}/+/{ref}") def download_artifact( project_name: str, package_name: str, ref: str, db: Session = Depends(get_db), storage: S3Storage = Depends(get_storage), ): # Get project and package project = db.query(Project).filter(Project.name == project_name).first() if not project: raise HTTPException(status_code=404, detail="Project not found") package = db.query(Package).filter(Package.project_id == project.id, Package.name == package_name).first() if not package: raise HTTPException(status_code=404, detail="Package not found") # Resolve reference to artifact artifact = None # Check for explicit prefixes if ref.startswith("artifact:"): artifact_id = ref[9:] artifact = db.query(Artifact).filter(Artifact.id == artifact_id).first() elif ref.startswith("tag:") or ref.startswith("version:"): tag_name = ref.split(":", 1)[1] tag = db.query(Tag).filter(Tag.package_id == package.id, Tag.name == tag_name).first() if tag: artifact = db.query(Artifact).filter(Artifact.id == tag.artifact_id).first() else: # Try as tag name first tag = db.query(Tag).filter(Tag.package_id == package.id, Tag.name == ref).first() if tag: artifact = db.query(Artifact).filter(Artifact.id == tag.artifact_id).first() else: # Try as direct artifact ID artifact = db.query(Artifact).filter(Artifact.id == ref).first() if not artifact: raise HTTPException(status_code=404, detail="Artifact not found") # Stream from S3 stream = storage.get_stream(artifact.s3_key) filename = artifact.original_name or f"{artifact.id}" return StreamingResponse( stream, media_type=artifact.content_type or "application/octet-stream", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) # Compatibility route @router.get("/project/{project_name}/{package_name}/+/{ref}") def download_artifact_compat( project_name: str, package_name: str, ref: str, db: Session = Depends(get_db), storage: S3Storage = Depends(get_storage), ): return download_artifact(project_name, package_name, ref, db, storage) # Tag routes @router.get("/api/v1/project/{project_name}/{package_name}/tags", response_model=List[TagResponse]) def list_tags(project_name: str, package_name: str, db: Session = Depends(get_db)): project = db.query(Project).filter(Project.name == project_name).first() if not project: raise HTTPException(status_code=404, detail="Project not found") package = db.query(Package).filter(Package.project_id == project.id, Package.name == package_name).first() if not package: raise HTTPException(status_code=404, detail="Package not found") tags = db.query(Tag).filter(Tag.package_id == package.id).order_by(Tag.name).all() return tags @router.post("/api/v1/project/{project_name}/{package_name}/tags", response_model=TagResponse) def create_tag( project_name: str, package_name: str, tag: TagCreate, request: Request, db: Session = Depends(get_db), ): user_id = get_user_id(request) project = db.query(Project).filter(Project.name == project_name).first() if not project: raise HTTPException(status_code=404, detail="Project not found") package = db.query(Package).filter(Package.project_id == project.id, Package.name == package_name).first() if not package: raise HTTPException(status_code=404, detail="Package not found") # Verify artifact exists artifact = db.query(Artifact).filter(Artifact.id == tag.artifact_id).first() if not artifact: raise HTTPException(status_code=404, detail="Artifact not found") # Create or update tag existing = db.query(Tag).filter(Tag.package_id == package.id, Tag.name == tag.name).first() if existing: existing.artifact_id = tag.artifact_id existing.created_by = user_id db.commit() db.refresh(existing) return existing db_tag = Tag( package_id=package.id, name=tag.name, artifact_id=tag.artifact_id, created_by=user_id, ) db.add(db_tag) db.commit() db.refresh(db_tag) return db_tag # Consumer routes @router.get("/api/v1/project/{project_name}/{package_name}/consumers", response_model=List[ConsumerResponse]) def get_consumers(project_name: str, package_name: str, db: Session = Depends(get_db)): project = db.query(Project).filter(Project.name == project_name).first() if not project: raise HTTPException(status_code=404, detail="Project not found") package = db.query(Package).filter(Package.project_id == project.id, Package.name == package_name).first() if not package: raise HTTPException(status_code=404, detail="Package not found") consumers = db.query(Consumer).filter(Consumer.package_id == package.id).order_by(Consumer.last_access.desc()).all() return consumers # Artifact by ID @router.get("/api/v1/artifact/{artifact_id}", response_model=ArtifactResponse) def get_artifact(artifact_id: str, db: Session = Depends(get_db)): artifact = db.query(Artifact).filter(Artifact.id == artifact_id).first() if not artifact: raise HTTPException(status_code=404, detail="Artifact not found") return artifact