{"record":{"id":"62630122458e5922","repo":"odysseus-dev/odysseus","slug":"image-file-not-found-626301","errorCode":null,"errorMessage":"Image file not found","messagePattern":"Image file not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"routes/gallery/gallery_routes.py","lineNumber":530,"sourceCode":"        try:\n            angle = int(data.get(\"angle\", 90))\n        except (TypeError, ValueError):\n            raise HTTPException(400, \"Invalid angle\")\n        if angle not in (90, -90, 180, 270):\n            raise HTTPException(400, \"Angle must be 90, -90, 180, or 270\")\n\n        user = get_current_user(request)\n        db = SessionLocal()\n        try:\n            img = db.query(GalleryImage).filter(GalleryImage.id == image_id).first()\n            if not img:\n                raise HTTPException(404, \"Image not found\")\n            if not user or img.owner != user:\n                raise HTTPException(403, \"Not your image\")\n\n            img_path = _gallery_image_path(img.filename)\n            if not img_path.exists():\n                raise HTTPException(404, \"Image file not found\")\n\n            # PIL rotates counter-clockwise; the API takes \"clockwise\"\n            # convention so we negate to match user expectation.\n            with Image.open(img_path) as pil:\n                rotated = pil.rotate(-angle, expand=True)\n                # Recompute hash so dedupe stays accurate.\n                buf = BytesIO()\n                ext = img.filename.rsplit(\".\", 1)[-1].lower()\n                save_kwargs = {}\n                if ext in (\"jpg\", \"jpeg\"):\n                    save_kwargs[\"quality\"] = 95\n                    fmt = \"JPEG\"\n                elif ext == \"webp\":\n                    fmt = \"WEBP\"\n                    save_kwargs[\"quality\"] = 95\n                else:\n                    fmt = \"PNG\"\n                rotated.save(buf, format=fmt, **save_kwargs)","sourceCodeStart":512,"sourceCodeEnd":548,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/gallery/gallery_routes.py#L512-L548","documentation":"HTTP 404 raised by POST /api/gallery/{image_id}/rotate when the DB row exists but the backing file is missing on disk (img_path.exists() is false). This indicates DB/filesystem desync: the record survived while the file under GALLERY_IMAGE_DIR was deleted, moved, or never written.","triggerScenarios":"Manual cleanup of the gallery image directory; a crashed write in a previous operation; volume/container restarts losing ephemeral storage; the filename column containing a stale path.","commonSituations":"Docker deployments without persistent volumes; rsync/cron jobs pruning 'orphan' files; partial restore from backup that recovered the DB but not the files.","solutions":["Check GALLERY_IMAGE_DIR for the expected filename from img.filename.","Restore the file from backup, or delete the orphan DB row so the gallery stays consistent.","Mount the gallery directory on persistent storage in containerized deployments."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await rotateImage(id, angle); } catch (e) {\n  if (e.status === 404) {\n    const meta = await getImage(id);\n    if (meta) reportDesync(id, 'DB row exists but file missing'); // ops alert\n  }\n}","preventionTips":["Persist the gallery image directory (volumes in containers)","Exclude gallery dirs from cleanup jobs","Restore DB and files together from backup","Reconcile orphan rows periodically"],"tags":["http","filesystem","gallery","rotate","data-integrity"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}