package api import ( "bytes" "errors" "fmt" "io" "mime" "net/http" "path/filepath" "strings" "github.com/gin-gonic/gin" "github.com/go-sql-driver/mysql" "gitlab.celogeek.com/photos/api/internal/photos/models" "gitlab.celogeek.com/photos/api/internal/photoserrors" "gorm.io/gorm" ) var CHUNK_SIZE int64 = 4 << 20 type File struct { Name string `json:"name"` Checksum string `json:"checksum"` Chunks []string `json:"chunks"` } func (s *Service) FileCreate(c *gin.Context) { file := &File{} if err := c.ShouldBindJSON(file); err != nil { s.Error(c, http.StatusInternalServerError, err) return } if len(file.Name) < 1 { s.Error(c, http.StatusBadRequest, photoserrors.ErrStoreMissingName) return } if len(file.Checksum) != 40 { s.Error(c, http.StatusBadRequest, photoserrors.ErrStoreBadChecksum) return } if len(file.Chunks) == 0 { s.Error(c, http.StatusBadRequest, photoserrors.ErrStoreMissingChunks) return } for _, chunk := range file.Chunks { if len(chunk) != 40 { s.Error(c, http.StatusBadRequest, photoserrors.ErrStoreBadChecksum) return } } r, rs, err := s.Store.Combine(file.Chunks) if err != nil { if strings.HasPrefix(err.Error(), "chunk") && strings.HasSuffix(err.Error(), "doesn't exists") { s.Error(c, http.StatusBadRequest, err) } else { s.Error(c, http.StatusInternalServerError, err) } return } if r != file.Checksum { s.Error(c, http.StatusExpectationFailed, photoserrors.ErrStoreMismatchChecksum) return } sess := s.CurrentSession(c) f := &models.File{ Name: file.Name, Checksum: file.Checksum, Size: rs, AuthorId: &sess.AccountId, } err = s.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Create(f).Error; err != nil { return err } for i, chunk := range file.Chunks { fc := &models.FileChunk{ FileId: f.ID, Part: uint32(i + 1), Checksum: chunk, } if err := tx.Create(fc).Error; err != nil { return err } } return nil }) if nerr, ok := err.(*mysql.MySQLError); ok { if nerr.Number == 1062 { // duplicate error if strings.HasSuffix(nerr.Message, "for key 'checksum'") { err = nil } } } if err != nil { s.Error(c, http.StatusInternalServerError, err) return } c.JSON(http.StatusOK, gin.H{ "status": "success", "sum": file.Checksum, "nbChunks": len(file.Chunks), "size": rs, }) } func (s *Service) FileCreateChunk(c *gin.Context) { if c.Request.ContentLength > CHUNK_SIZE { s.Error(c, http.StatusBadRequest, photoserrors.ErrStoreBadChunkSize) return } b := bytes.NewBuffer([]byte{}) io.Copy(b, c.Request.Body) c.Request.Body.Close() sess := s.CurrentSession(c) chunk := s.Store.NewChunk(b.Bytes()) if err := chunk.Save(sess); err != nil { if errors.Is(err, photoserrors.ErrStoreChunkAlreadyExists) { c.JSON(http.StatusOK, gin.H{ "status": "success", "checksum": chunk.Sum, }) } else { s.Error(c, http.StatusBadRequest, err) } return } c.JSON(http.StatusOK, gin.H{ "status": "success", "checksum": chunk.Sum, }) } func (s *Service) FileChunkExists(c *gin.Context) { checksum := c.Param("checksum") if len(checksum) != 40 { s.Error(c, http.StatusBadRequest, photoserrors.ErrStoreBadChecksum) return } if s.Store.Chunk(checksum).FileExists() { c.Status(http.StatusOK) } else { c.Status(http.StatusNotFound) } } func (s *Service) FileExists(c *gin.Context) { checksum := c.Param("checksum") if len(checksum) != 40 { s.Error(c, http.StatusBadRequest, photoserrors.ErrStoreBadChecksum) return } var fileExists int64 if err := s.DB.Model(&models.File{}).Where("checksum = ?", checksum).Count(&fileExists).Error; err != nil { s.Error(c, http.StatusInternalServerError, err) return } if fileExists > 0 { c.Status(http.StatusOK) } else { c.Status(http.StatusNotFound) } } func (s *Service) FileGet(c *gin.Context) { checksum := c.Param("checksum") if len(checksum) != 40 { s.Error(c, http.StatusBadRequest, photoserrors.ErrStoreBadChecksum) return } if checksum == c.GetHeader("If-None-Match") { c.Status(http.StatusNotModified) return } f := &models.File{} if err := s.DB.Debug().Preload("Chunks").Where("checksum = ?", checksum).First(&f).Error; err != nil { s.Error(c, http.StatusBadRequest, err) return } chunks := make([]string, len(f.Chunks)) for _, fc := range f.Chunks { chunks[fc.Part-1] = fc.Checksum } reader, err := s.Store.NewStoreReader(chunks) if err != nil { s.Error(c, http.StatusInternalServerError, err) return } defer reader.Close() c.DataFromReader( http.StatusOK, reader.Size, mime.TypeByExtension(filepath.Ext(f.Name)), reader, map[string]string{ "Content-Disposition": fmt.Sprintf("inline; filename=\"%s\"", f.Name), "ETag": f.Checksum, }, ) }