diff --git a/internal/photos/api/main.go b/internal/photos/api/main.go index 4b88f94..05da661 100644 --- a/internal/photos/api/main.go +++ b/internal/photos/api/main.go @@ -15,12 +15,13 @@ import ( ) type Service struct { - Gin *gin.Engine - DB *gorm.DB - Config *ServiceConfig - Store *store.Store - LogOk *Logger - LogErr *Logger + Gin *gin.Engine + DB *gorm.DB + Config *ServiceConfig + Store *store.Store + Storage *Storage + LogOk *Logger + LogErr *Logger } type ServiceConfig struct { @@ -31,11 +32,12 @@ type ServiceConfig struct { func New(config *ServiceConfig) *Service { return &Service{ - Gin: gin.New(), - Config: config, - Store: &store.Store{Path: config.StorePath}, - LogOk: &Logger{os.Stdout, "Photos"}, - LogErr: &Logger{os.Stderr, "Photos"}, + Gin: gin.New(), + Config: config, + Store: &store.Store{Path: config.StorePath}, + Storage: &Storage{Path: config.StorePath}, + LogOk: &Logger{os.Stdout, "Photos"}, + LogErr: &Logger{os.Stderr, "Photos"}, } } @@ -49,6 +51,7 @@ func (s *Service) SetupRoutes() { s.AccountInit() s.MeInit() s.FileInit() + s.UploadInit() s.Gin.NoRoute(func(c *gin.Context) { s.Error(c, http.StatusNotFound, photoserrors.ErrReqNotFound) diff --git a/internal/photos/api/storage.go b/internal/photos/api/storage.go new file mode 100644 index 0000000..1ec2197 --- /dev/null +++ b/internal/photos/api/storage.go @@ -0,0 +1,40 @@ +package api + +import ( + "fmt" + "os" + "path/filepath" +) + +type Storage struct { + Path string +} + +const ( + StorageTmp = "tmp" + StorageUpload = "upload" +) + +func (s *Storage) Join(paths ...string) string { + return filepath.Join(s.Path, filepath.Join(paths...)) +} + +func (s *Storage) Create(paths ...string) error { + return os.MkdirAll(s.Join(paths...), 0755) +} + +func (s *Storage) Exists(paths ...string) bool { + f, err := os.Stat(s.Join(paths...)) + if err != nil { + return false + } + return f.IsDir() +} + +func (s *Storage) Delete(paths ...string) error { + if s.Exists(paths...) { + return os.RemoveAll(s.Join(paths...)) + } else { + return fmt.Errorf("%s doesn't exists", s.Join(paths...)) + } +} diff --git a/internal/photos/api/upload.go b/internal/photos/api/upload.go new file mode 100644 index 0000000..26592c1 --- /dev/null +++ b/internal/photos/api/upload.go @@ -0,0 +1,117 @@ +package api + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func (s *Service) UploadCreate(c *gin.Context) { + sha, err := uuid.NewRandom() + if err != nil { + s.Error(c, http.StatusInternalServerError, err) + return + } + + if err := s.Storage.Create(StorageTmp, sha.String()); err != nil { + s.Error(c, http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "status": "success", + "upload_id": sha.String(), + }) +} + +type UploadPartRequest struct { + UploadId string `uri:"upload_id" binding:"required,uuid"` + Part uint `uri:"part" binding:"required"` +} + +func (s *Service) UploadPart(c *gin.Context) { + var uploadPart UploadPartRequest + if err := c.ShouldBindUri(&uploadPart); err != nil { + s.Error(c, http.StatusBadRequest, err) + return + } + + if !s.Storage.Exists(StorageTmp, uploadPart.UploadId) { + s.Error(c, http.StatusNotFound, errors.New("upload id doesn't exists")) + return + } + + f, err := os.Create( + s.Storage.Join(StorageTmp, uploadPart.UploadId, fmt.Sprint(uploadPart.Part)), + ) + if err != nil { + s.Error(c, http.StatusInternalServerError, err) + return + } + + defer f.Close() + defer c.Request.Body.Close() + + sha := sha256.New() + t := io.TeeReader(c.Request.Body, sha) + w, err := io.Copy(f, t) + if err != nil { + s.Error(c, http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "upload_id": uploadPart.UploadId, + "part": uploadPart.Part, + "size": w, + "sha256": hex.EncodeToString(sha.Sum(nil)), + "status": "success", + }) +} + +func (s *Service) UploadCancel(c *gin.Context) { + upload_id := c.Param("upload_id") + + if err := s.Storage.Delete(StorageTmp, upload_id); err != nil { + s.Error(c, http.StatusNotFound, errors.New("upload id doesn't exists")) + return + } + + c.JSON(http.StatusOK, gin.H{ + "status": "success", + }) +} + +type UploadCompleteRequest struct { + SHA256 string `json:"sha256" binding:"required,lowercase,alphanum,len=64"` + Name string `json:"name" binding:"required"` + Parts uint `json:"parts" binding:"required"` +} + +func (s *Service) UploadComplete(c *gin.Context) { + var uploadCompleteRequest UploadCompleteRequest + if err := c.ShouldBindJSON(&uploadCompleteRequest); err != nil { + s.Error(c, http.StatusBadRequest, err) + 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") + upload.GET("/create", s.UploadCreate) + upload.POST("/part/:upload_id/:part", s.UploadPart) + upload.GET("/cancel/:upload_id", s.UploadCancel) + upload.POST("/complete/:upload_id", s.UploadComplete) +} diff --git a/start-db.sh b/start-db.sh index e15e9bf..c4fe4b3 100755 --- a/start-db.sh +++ b/start-db.sh @@ -13,4 +13,3 @@ then createdb photos createuser photos fi -tail -f data/db.log