package handlers import ( "context" "fmt" "mime/multipart" "net/http" "path" "strconv" "time" "github.com/gin-gonic/gin" "github.com/minio/minio-go/v7" "gorm.io/gorm" "smoop-api/internal/dto" "smoop-api/internal/models" ) type RecordsHandler struct { db *gorm.DB minio *minio.Client recordsBucket string presignTTL time.Duration } func NewRecordsHandler(db *gorm.DB, mc *minio.Client, bucket string, ttl time.Duration) *RecordsHandler { return &RecordsHandler{db: db, minio: mc, recordsBucket: bucket, presignTTL: ttl} } func (h *RecordsHandler) Upload(c *gin.Context) { file, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "file required"}) return } guid := c.PostForm("guid") if guid == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "guid required"}) return } startedAt, _ := strconv.ParseInt(c.PostForm("startedAt"), 10, 64) stoppedAt, _ := strconv.ParseInt(c.PostForm("stoppedAt"), 10, 64) var dev models.Device if err := h.db.Where("guid = ?", guid).First(&dev).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "device not found"}) return } objKey := fmt.Sprintf("%s/%d_%s", guid, time.Now().UnixNano(), path.Base(file.Filename)) if err := h.putFile(c, file, h.recordsBucket, objKey); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "upload failed"}) return } rec := models.Record{ DeviceGUID: dev.GUID, StartedAt: startedAt, StoppedAt: stoppedAt, ObjectKey: objKey, } if err := h.db.Create(&rec).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db save failed"}) return } c.JSON(http.StatusCreated, dto.RecordDto{ID: rec.ID, StartedAt: rec.StartedAt, StoppedAt: rec.StoppedAt}) } func (h *RecordsHandler) putFile(c *gin.Context, fh *multipart.FileHeader, bucket, object string) error { src, err := fh.Open() if err != nil { return err } defer src.Close() _, err = h.minio.PutObject(c, bucket, object, src, fh.Size, minio.PutObjectOptions{ ContentType: fh.Header.Get("Content-Type"), ContentDisposition: "attachment; filename=" + fh.Filename, }) return err } func (h *RecordsHandler) List(c *gin.Context) { guid := c.Query("guid") if guid == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "guid is required"}) return } offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) if limit <= 0 || limit > 200 { limit = 50 } var total int64 h.db.Model(&models.Record{}).Where("device_guid = ?", guid).Count(&total) var recs []models.Record if err := h.db.Where("device_guid = ?", guid).Order("id desc").Offset(offset).Limit(limit).Find(&recs).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) return } out := make([]dto.RecordDto, 0, len(recs)) for _, r := range recs { out = append(out, dto.RecordDto{ID: r.ID, StartedAt: r.StartedAt, StoppedAt: r.StoppedAt}) } c.JSON(http.StatusOK, dto.RecordListDto{Records: out, Offset: offset, Limit: limit, Total: total}) } func (h *RecordsHandler) File(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var rec models.Record if err := h.db.First(&rec, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } u, err := h.minio.PresignedGetObject(context.Background(), h.recordsBucket, rec.ObjectKey, h.presignTTL, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "presign failed"}) return } c.Redirect(http.StatusFound, u.String()) }