Seth commited on
Commit
b3f7679
·
1 Parent(s): 27fb040
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 size="icon" variant="secondary" className="h-9 w-9">
 
 
 
 
 
 
 
 
 
570
  <Eye className="w-4 h-4" />
571
  </Button>
572
- <Button size="icon" variant="secondary" className="h-9 w-9">
 
 
 
 
 
 
 
 
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
  }