package photosapi import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "net/http" "os" "github.com/gin-gonic/gin" "github.com/google/uuid" ) const ( MaxUploadPartSize = 4 << 20 // 4MB ) var ( ErrUploadNotExists = errors.New("upload id doesn't exists") ErrUploadPartTooLarge = fmt.Errorf("upload part too large (> %d B)", MaxUploadPartSize) ErrUploadPartWrongSha256 = errors.New("upload part wrong sha256") ) func (s *Service) UploadCreate(c *gin.Context) { sha, err := uuid.NewRandom() if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } if err := s.StorageTmp.Create(sha.String()); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.JSON(http.StatusCreated, gin.H{ "upload_id": sha.String(), }) } type UploadUri struct { Id string `uri:"upload_id" binding:"required,uuid"` } type UploadPartQuery struct { Part uint `form:"part" binding:"required"` PartSha256 string `form:"sha256" binding:"required,sha256"` } func (s *Service) UploadPart(c *gin.Context) { var ( upload UploadUri uploadPart UploadPartQuery ) if c.BindUri(&upload) != nil || c.BindQuery(&uploadPart) != nil { return } if !s.StorageTmp.Exists(upload.Id) { c.AbortWithError(http.StatusNotFound, ErrUploadNotExists) return } if c.Request.ContentLength > MaxUploadPartSize { c.AbortWithError(http.StatusRequestEntityTooLarge, ErrUploadPartTooLarge) return } tmp_file := s.StorageTmp.Join(upload.Id, fmt.Sprintf("._tmp_%d", uploadPart.Part)) file := s.StorageTmp.Join(upload.Id, fmt.Sprint(uploadPart.Part)) f, err := os.Create(tmp_file) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } sha := sha256.New() t := io.TeeReader(c.Request.Body, sha) w, err := io.Copy(f, t) if err != nil { f.Close() os.Remove(tmp_file) c.AbortWithError(http.StatusInternalServerError, err) return } f.Close() shastr := hex.EncodeToString(sha.Sum(nil)) if shastr != uploadPart.PartSha256 { os.Remove(tmp_file) c.AbortWithError(http.StatusBadRequest, ErrUploadPartWrongSha256) return } if err = os.Rename(tmp_file, file); err != nil { os.Remove(tmp_file) c.AbortWithError(http.StatusInternalServerError, err) return } c.JSON(http.StatusCreated, gin.H{ "upload_id": upload.Id, "part": uploadPart.Part, "size": w, "sha256": uploadPart.PartSha256, }) } func (s *Service) UploadCancel(c *gin.Context) { var upload UploadUri if c.BindUri(&upload) != nil { return } if err := s.StorageTmp.Delete(upload.Id); err != nil { c.AbortWithError(http.StatusNotFound, ErrUploadNotExists) return } c.Status(http.StatusNoContent) } type UploadCompleteRequest struct { Sha256 string `json:"sha256" binding:"required,sha256"` Name string `json:"name" binding:"required"` Parts uint `json:"parts" binding:"required"` } func (s *Service) UploadComplete(c *gin.Context) { var uploadCompleteRequest UploadCompleteRequest if c.BindJSON(&uploadCompleteRequest) != nil { return } c.JSON(http.StatusOK, gin.H{ "sha256": uploadCompleteRequest.Sha256, "parts": uploadCompleteRequest.Parts, "name": uploadCompleteRequest.Name, }) } func (s *Service) UploadInit() { upload := s.Gin.Group("/upload") // start upload.POST("", s.UploadCreate) // Cancel upload.DELETE("/:upload_id", s.UploadCancel) // Add part upload.PUT("/:upload_id", s.UploadPart) // Complete upload.POST("/:upload_id", s.UploadComplete) }