package photosapi import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "net/http" "os" "time" "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") ErrFileAlreadExists = errors.New("file already exists") ) // Model type File struct { ID uint32 `gorm:"primary_key" json:"id"` Name string `gorm:"not null" json:"name"` Checksum string `gorm:"unique;size:44;not null"` Size uint64 `gorm:"not null"` Author *Account `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE" json:"author"` AuthorId *uint32 `json:"-"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (s *Service) UploadCreate(c *gin.Context) { sha, err := uuid.NewRandom() if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } upload := &Upload{sha.String()} if err := s.StorageTmp.Create(upload.Id); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.JSON(http.StatusCreated, upload) } // Service type Upload struct { Id string `json:"upload_id" uri:"upload_id" binding:"required,uuid"` } type UploadPartQuery struct { Part uint `form:"part" binding:"required"` PartSha256 string `form:"sha256" binding:"required,sha256"` } 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) UploadPart(c *gin.Context) { var ( upload Upload 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) _, 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 } err = os.Rename(tmp_file, file) if err != nil { os.Remove(tmp_file) c.AbortWithError(http.StatusInternalServerError, err) return } c.Status(http.StatusNoContent) } func (s *Service) UploadCancel(c *gin.Context) { var upload Upload 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) } func (s *Service) UploadComplete(c *gin.Context) { var ( upload Upload uploadCompleteRequest UploadCompleteRequest ) if c.BindUri(&upload) != nil || c.BindJSON(&uploadCompleteRequest) != nil { return } if !s.StorageTmp.Exists(upload.Id) { c.AbortWithError(http.StatusNotFound, ErrUploadNotExists) return } f, err := s.StorageUpload.Stat(uploadCompleteRequest.Sha256[0:1], uploadCompleteRequest.Sha256[1:2], uploadCompleteRequest.Sha256) fmt.Println(err) if err == nil && f.Mode().IsRegular() { c.AbortWithError(http.StatusConflict, ErrFileAlreadExists) return } c.Status(http.StatusNoContent) } func (s *Service) UploadInit() { upload := s.Gin.Group("/upload") upload.Use(s.RequireSession) // 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) }