diff --git a/cmd/photos-api-cli/upload.go b/cmd/photos-api-cli/upload.go index e9874f0..c40d913 100644 --- a/cmd/photos-api-cli/upload.go +++ b/cmd/photos-api-cli/upload.go @@ -9,6 +9,7 @@ import ( "io" "os" "path/filepath" + "strings" "sync" "github.com/go-resty/resty/v2" @@ -24,8 +25,15 @@ type UploadCommand struct { } type UploadError struct { - Error string `json:"error"` - Status string `json:"string"` + Err string `json:"error"` + Details []string `json:"details"` +} + +func (u *UploadError) Error() string { + if len(u.Details) == 0 { + return u.Err + } + return fmt.Sprintf("%s: \n - %s", u.Err, strings.Join(u.Details, "\n - ")) } type UploadCreate struct { @@ -39,6 +47,12 @@ type UploadPartResult struct { PartSha256 string `json:"sha256"` } +type UploadCompleteRequest struct { + Sha256 string `json:"sha256" binding:"required,sha256"` + Name string `json:"name" binding:"required"` + Parts uint `json:"parts" binding:"required"` +} + type UploadFileRequest struct { Name string Checksum string @@ -156,7 +170,7 @@ func (c *UploadCommand) FileUpload(sum string) error { } if err, ok := resp.Error().(*UploadError); ok { - wgErrors[w] = errors.New(err.Error) + wgErrors[w] = err return } @@ -179,7 +193,7 @@ func (c *UploadCommand) FileUpload(sum string) error { if err, ok := resp.Error().(*UploadError); ok { logger.Println("Upload failed") - return errors.New(err.Error) + return err } if result, ok := resp.Result().(*UploadFileResponse); ok { @@ -197,7 +211,7 @@ func (c *UploadCommand) Execute(args []string) error { } if err, ok := resp.Error().(*UploadError); ok { - return errors.New(err.Error) + return err } uploadId := resp.Result().(*UploadCreate).UploadId @@ -216,7 +230,8 @@ func (c *UploadCommand) Execute(args []string) error { tee := io.TeeReader(f, progress) b := make([]byte, photosapi.MaxUploadPartSize) - part := 0 + parts := 0 + completesha256 := sha256.New() for { n, err := tee.Read(b) if err != nil { @@ -227,15 +242,16 @@ func (c *UploadCommand) Execute(args []string) error { } } - part++ + parts++ partsha256 := sha256.New() partsha256.Write(b[:n]) + completesha256.Write(b[:n]) resp, err := cli. R(). SetError(&UploadError{}). SetResult(&UploadPartResult{}). - SetQueryParam("part", fmt.Sprint(part)). + SetQueryParam("part", fmt.Sprint(parts)). SetQueryParam("sha256", hex.EncodeToString(partsha256.Sum(nil))). SetBody(b[:n]). SetPathParam("id", uploadId). @@ -246,16 +262,39 @@ func (c *UploadCommand) Execute(args []string) error { } if err, ok := resp.Error().(*UploadError); ok { - return errors.New(err.Error) + return err } } fmt.Printf( "Upload: %s\nParts: %d\n", uploadId, - part, + parts, ) + resp, err = cli. + R(). + SetError(&UploadError{}). + SetPathParam("id", uploadId). + SetBody(&UploadCompleteRequest{ + Sha256: hex.EncodeToString(completesha256.Sum(nil)), + Parts: uint(parts), + Name: filepath.Base(c.File), + }). + Post("/upload/{id}") + + if err != nil { + return err + } + + if err, ok := resp.Error().(*UploadError); ok { + return err + } + + fmt.Printf("Response: %s\n", resp.Body()) + + cli.R().SetPathParam("id", uploadId).Delete("/upload/{id}") + return nil } diff --git a/internal/photos/api/file.go b/internal/photos/api/file.go index 5be71d7..443c1c6 100644 --- a/internal/photos/api/file.go +++ b/internal/photos/api/file.go @@ -9,7 +9,6 @@ import ( "net/http" "path/filepath" "strings" - "time" "github.com/dsoprea/go-exif/v3" "github.com/gin-gonic/gin" @@ -30,28 +29,6 @@ var ( ErrStoreMissingName = errors.New("name required") ) -// 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:"-"` - Chunks []*FileChunk `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type FileChunk struct { - FileId uint32 - File *File `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` - Part uint32 - Checksum string `gorm:"unique;size:44;not null"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - // Service var CHUNK_SIZE int64 = 4 << 20 diff --git a/internal/photos/api/upload.go b/internal/photos/api/upload.go index 5e4cb7d..7b3416f 100644 --- a/internal/photos/api/upload.go +++ b/internal/photos/api/upload.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "os" + "time" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -23,6 +24,28 @@ var ( ErrUploadPartWrongSha256 = errors.New("upload part wrong sha256") ) +// 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:"-"` + Chunks []*FileChunk `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type FileChunk struct { + FileId uint32 + File *File `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + Part uint32 + Checksum string `gorm:"unique;size:44;not null"` + 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 { @@ -131,10 +154,19 @@ type UploadCompleteRequest struct { } func (s *Service) UploadComplete(c *gin.Context) { - var uploadCompleteRequest UploadCompleteRequest - if c.BindJSON(&uploadCompleteRequest) != nil { + var ( + upload UploadUri + uploadCompleteRequest UploadCompleteRequest + ) + if c.BindUri(&upload) != nil || c.BindJSON(&uploadCompleteRequest) != nil { return } + + if !s.StorageTmp.Exists(upload.Id) { + c.AbortWithError(http.StatusNotFound, ErrUploadNotExists) + return + } + c.JSON(http.StatusOK, gin.H{ "sha256": uploadCompleteRequest.Sha256, "parts": uploadCompleteRequest.Parts,