Spaces:
Sleeping
Sleeping
Seth
commited on
Commit
·
b3f7679
1
Parent(s):
27fb040
update
Browse files- backend/app/main.py +82 -0
- frontend/src/pages/Repository.jsx +91 -6
backend/app/main.py
CHANGED
|
@@ -430,6 +430,88 @@ async def get_assets(
|
|
| 430 |
}
|
| 431 |
]
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
# ---- Post Management ----
|
| 434 |
|
| 435 |
@app.post("/api/posts", response_model=PostResponse)
|
|
|
|
| 430 |
}
|
| 431 |
]
|
| 432 |
|
| 433 |
+
@app.get("/api/assets/{asset_id}/download")
|
| 434 |
+
async def download_asset(asset_id: int, db: Session = Depends(get_db)):
|
| 435 |
+
"""Download or preview an asset file"""
|
| 436 |
+
try:
|
| 437 |
+
from app.models import Asset
|
| 438 |
+
|
| 439 |
+
# Try to get asset from database
|
| 440 |
+
try:
|
| 441 |
+
db_asset = db.query(Asset).filter(Asset.id == asset_id).first()
|
| 442 |
+
except Exception as orm_error:
|
| 443 |
+
# If ORM fails, use direct psycopg2
|
| 444 |
+
if "Could not determine version" in str(orm_error):
|
| 445 |
+
conn = get_direct_psycopg2_connection()
|
| 446 |
+
if conn:
|
| 447 |
+
try:
|
| 448 |
+
cursor = conn.cursor()
|
| 449 |
+
cursor.execute("""
|
| 450 |
+
SELECT id, name, file_path, file_type
|
| 451 |
+
FROM assets
|
| 452 |
+
WHERE id = %s
|
| 453 |
+
""", (asset_id,))
|
| 454 |
+
row = cursor.fetchone()
|
| 455 |
+
cursor.close()
|
| 456 |
+
conn.close()
|
| 457 |
+
if row:
|
| 458 |
+
file_path = Path(row[2])
|
| 459 |
+
if file_path.exists():
|
| 460 |
+
return FileResponse(
|
| 461 |
+
path=str(file_path),
|
| 462 |
+
filename=row[1],
|
| 463 |
+
media_type="application/octet-stream"
|
| 464 |
+
)
|
| 465 |
+
else:
|
| 466 |
+
raise HTTPException(status_code=404, detail="File not found on disk")
|
| 467 |
+
else:
|
| 468 |
+
raise HTTPException(status_code=404, detail="Asset not found")
|
| 469 |
+
except Exception as psycopg2_error:
|
| 470 |
+
if conn:
|
| 471 |
+
conn.close()
|
| 472 |
+
raise HTTPException(status_code=500, detail=str(psycopg2_error))
|
| 473 |
+
else:
|
| 474 |
+
raise HTTPException(status_code=500, detail=str(orm_error))
|
| 475 |
+
|
| 476 |
+
if not db_asset:
|
| 477 |
+
raise HTTPException(status_code=404, detail="Asset not found")
|
| 478 |
+
|
| 479 |
+
file_path = Path(db_asset.file_path)
|
| 480 |
+
if not file_path.exists():
|
| 481 |
+
raise HTTPException(status_code=404, detail="File not found on disk")
|
| 482 |
+
|
| 483 |
+
# Determine media type
|
| 484 |
+
media_type = "application/octet-stream"
|
| 485 |
+
if db_asset.file_type == "image":
|
| 486 |
+
if file_path.suffix.lower() in [".jpg", ".jpeg"]:
|
| 487 |
+
media_type = "image/jpeg"
|
| 488 |
+
elif file_path.suffix.lower() == ".png":
|
| 489 |
+
media_type = "image/png"
|
| 490 |
+
elif file_path.suffix.lower() == ".gif":
|
| 491 |
+
media_type = "image/gif"
|
| 492 |
+
elif file_path.suffix.lower() == ".webp":
|
| 493 |
+
media_type = "image/webp"
|
| 494 |
+
elif db_asset.file_type == "video":
|
| 495 |
+
if file_path.suffix.lower() == ".mp4":
|
| 496 |
+
media_type = "video/mp4"
|
| 497 |
+
elif file_path.suffix.lower() == ".webm":
|
| 498 |
+
media_type = "video/webm"
|
| 499 |
+
elif db_asset.file_type == "document":
|
| 500 |
+
if file_path.suffix.lower() == ".pdf":
|
| 501 |
+
media_type = "application/pdf"
|
| 502 |
+
elif file_path.suffix.lower() in [".doc", ".docx"]:
|
| 503 |
+
media_type = "application/msword"
|
| 504 |
+
|
| 505 |
+
return FileResponse(
|
| 506 |
+
path=str(file_path),
|
| 507 |
+
filename=db_asset.name,
|
| 508 |
+
media_type=media_type
|
| 509 |
+
)
|
| 510 |
+
except HTTPException:
|
| 511 |
+
raise
|
| 512 |
+
except Exception as e:
|
| 513 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 514 |
+
|
| 515 |
# ---- Post Management ----
|
| 516 |
|
| 517 |
@app.post("/api/posts", response_model=PostResponse)
|
frontend/src/pages/Repository.jsx
CHANGED
|
@@ -106,6 +106,8 @@ export default function Repository() {
|
|
| 106 |
const [isUploading, setIsUploading] = useState(false);
|
| 107 |
const [assets, setAssets] = useState(mockAssets);
|
| 108 |
const [isLoadingAssets, setIsLoadingAssets] = useState(false);
|
|
|
|
|
|
|
| 109 |
const fileInputRef = useRef(null);
|
| 110 |
|
| 111 |
const toggleProduct = (productId) => {
|
|
@@ -566,10 +568,27 @@ export default function Repository() {
|
|
| 566 |
)}
|
| 567 |
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
|
| 568 |
<div className="flex gap-2">
|
| 569 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
<Eye className="w-4 h-4" />
|
| 571 |
</Button>
|
| 572 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
<Download className="w-4 h-4" />
|
| 574 |
</Button>
|
| 575 |
</div>
|
|
@@ -595,10 +614,15 @@ export default function Repository() {
|
|
| 595 |
</Button>
|
| 596 |
</DropdownMenuTrigger>
|
| 597 |
<DropdownMenuContent align="end">
|
| 598 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
|
|
|
| 599 |
<Eye className="w-4 h-4 mr-2" /> Preview
|
| 600 |
</DropdownMenuItem>
|
| 601 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
| 602 |
<Download className="w-4 h-4 mr-2" /> Download
|
| 603 |
</DropdownMenuItem>
|
| 604 |
<DropdownMenuItem className="text-red-600">
|
|
@@ -662,10 +686,15 @@ export default function Repository() {
|
|
| 662 |
</Button>
|
| 663 |
</DropdownMenuTrigger>
|
| 664 |
<DropdownMenuContent align="end">
|
| 665 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
|
|
|
| 666 |
<Eye className="w-4 h-4 mr-2" /> Preview
|
| 667 |
</DropdownMenuItem>
|
| 668 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
| 669 |
<Download className="w-4 h-4 mr-2" /> Download
|
| 670 |
</DropdownMenuItem>
|
| 671 |
<DropdownMenuItem className="text-red-600">
|
|
@@ -682,6 +711,62 @@ export default function Repository() {
|
|
| 682 |
</motion.div>
|
| 683 |
</div>
|
| 684 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
</div>
|
| 686 |
);
|
| 687 |
}
|
|
|
|
| 106 |
const [isUploading, setIsUploading] = useState(false);
|
| 107 |
const [assets, setAssets] = useState(mockAssets);
|
| 108 |
const [isLoadingAssets, setIsLoadingAssets] = useState(false);
|
| 109 |
+
const [previewAsset, setPreviewAsset] = useState(null);
|
| 110 |
+
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
| 111 |
const fileInputRef = useRef(null);
|
| 112 |
|
| 113 |
const toggleProduct = (productId) => {
|
|
|
|
| 568 |
)}
|
| 569 |
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
|
| 570 |
<div className="flex gap-2">
|
| 571 |
+
<Button
|
| 572 |
+
size="icon"
|
| 573 |
+
variant="secondary"
|
| 574 |
+
className="h-9 w-9"
|
| 575 |
+
onClick={(e) => {
|
| 576 |
+
e.stopPropagation();
|
| 577 |
+
setPreviewAsset(asset);
|
| 578 |
+
setPreviewDialogOpen(true);
|
| 579 |
+
}}
|
| 580 |
+
>
|
| 581 |
<Eye className="w-4 h-4" />
|
| 582 |
</Button>
|
| 583 |
+
<Button
|
| 584 |
+
size="icon"
|
| 585 |
+
variant="secondary"
|
| 586 |
+
className="h-9 w-9"
|
| 587 |
+
onClick={(e) => {
|
| 588 |
+
e.stopPropagation();
|
| 589 |
+
window.open(`/api/assets/${asset.id}/download`, '_blank');
|
| 590 |
+
}}
|
| 591 |
+
>
|
| 592 |
<Download className="w-4 h-4" />
|
| 593 |
</Button>
|
| 594 |
</div>
|
|
|
|
| 614 |
</Button>
|
| 615 |
</DropdownMenuTrigger>
|
| 616 |
<DropdownMenuContent align="end">
|
| 617 |
+
<DropdownMenuItem onClick={() => {
|
| 618 |
+
setPreviewAsset(asset);
|
| 619 |
+
setPreviewDialogOpen(true);
|
| 620 |
+
}}>
|
| 621 |
<Eye className="w-4 h-4 mr-2" /> Preview
|
| 622 |
</DropdownMenuItem>
|
| 623 |
+
<DropdownMenuItem onClick={() => {
|
| 624 |
+
window.open(`/api/assets/${asset.id}/download`, '_blank');
|
| 625 |
+
}}>
|
| 626 |
<Download className="w-4 h-4 mr-2" /> Download
|
| 627 |
</DropdownMenuItem>
|
| 628 |
<DropdownMenuItem className="text-red-600">
|
|
|
|
| 686 |
</Button>
|
| 687 |
</DropdownMenuTrigger>
|
| 688 |
<DropdownMenuContent align="end">
|
| 689 |
+
<DropdownMenuItem onClick={() => {
|
| 690 |
+
setPreviewAsset(asset);
|
| 691 |
+
setPreviewDialogOpen(true);
|
| 692 |
+
}}>
|
| 693 |
<Eye className="w-4 h-4 mr-2" /> Preview
|
| 694 |
</DropdownMenuItem>
|
| 695 |
+
<DropdownMenuItem onClick={() => {
|
| 696 |
+
window.open(`/api/assets/${asset.id}/download`, '_blank');
|
| 697 |
+
}}>
|
| 698 |
<Download className="w-4 h-4 mr-2" /> Download
|
| 699 |
</DropdownMenuItem>
|
| 700 |
<DropdownMenuItem className="text-red-600">
|
|
|
|
| 711 |
</motion.div>
|
| 712 |
</div>
|
| 713 |
</div>
|
| 714 |
+
|
| 715 |
+
{/* Preview Dialog */}
|
| 716 |
+
<Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}>
|
| 717 |
+
<DialogContent className="max-w-4xl max-h-[90vh] overflow-auto">
|
| 718 |
+
<DialogHeader>
|
| 719 |
+
<DialogTitle>{previewAsset?.name || 'Preview'}</DialogTitle>
|
| 720 |
+
</DialogHeader>
|
| 721 |
+
<div className="mt-4">
|
| 722 |
+
{previewAsset && (
|
| 723 |
+
<div className="space-y-4">
|
| 724 |
+
{previewAsset.type === 'image' ? (
|
| 725 |
+
<img
|
| 726 |
+
src={`/api/assets/${previewAsset.id}/download`}
|
| 727 |
+
alt={previewAsset.name}
|
| 728 |
+
className="max-w-full h-auto rounded-lg"
|
| 729 |
+
/>
|
| 730 |
+
) : previewAsset.type === 'video' ? (
|
| 731 |
+
<video
|
| 732 |
+
src={`/api/assets/${previewAsset.id}/download`}
|
| 733 |
+
controls
|
| 734 |
+
className="max-w-full rounded-lg"
|
| 735 |
+
>
|
| 736 |
+
Your browser does not support the video tag.
|
| 737 |
+
</video>
|
| 738 |
+
) : (
|
| 739 |
+
<div className="flex flex-col items-center justify-center p-12 bg-slate-50 rounded-lg">
|
| 740 |
+
<FileText className="w-16 h-16 text-slate-400 mb-4" />
|
| 741 |
+
<p className="text-slate-600 mb-4">Preview not available for this file type</p>
|
| 742 |
+
<Button
|
| 743 |
+
onClick={() => window.open(`/api/assets/${previewAsset.id}/download`, '_blank')}
|
| 744 |
+
variant="outline"
|
| 745 |
+
>
|
| 746 |
+
<Download className="w-4 h-4 mr-2" />
|
| 747 |
+
Download to view
|
| 748 |
+
</Button>
|
| 749 |
+
</div>
|
| 750 |
+
)}
|
| 751 |
+
<div className="flex items-center justify-between pt-4 border-t">
|
| 752 |
+
<div className="text-sm text-slate-600">
|
| 753 |
+
<p><strong>Type:</strong> {previewAsset.type}</p>
|
| 754 |
+
<p><strong>Size:</strong> {previewAsset.size}</p>
|
| 755 |
+
<p><strong>Date:</strong> {previewAsset.date}</p>
|
| 756 |
+
</div>
|
| 757 |
+
<Button
|
| 758 |
+
onClick={() => window.open(`/api/assets/${previewAsset.id}/download`, '_blank')}
|
| 759 |
+
variant="outline"
|
| 760 |
+
>
|
| 761 |
+
<Download className="w-4 h-4 mr-2" />
|
| 762 |
+
Download
|
| 763 |
+
</Button>
|
| 764 |
+
</div>
|
| 765 |
+
</div>
|
| 766 |
+
)}
|
| 767 |
+
</div>
|
| 768 |
+
</DialogContent>
|
| 769 |
+
</Dialog>
|
| 770 |
</div>
|
| 771 |
);
|
| 772 |
}
|