From 4fbe92cdcf0617f14f71d6ffde249006c0ac2a7f Mon Sep 17 00:00:00 2001
From: celogeek <me@celogeek.com>
Date: Mon, 2 May 2022 09:00:46 +0200
Subject: [PATCH] upload part

---
 internal/photos/api/main.go    |  25 +++----
 internal/photos/api/storage.go |  40 +++++++++++
 internal/photos/api/upload.go  | 117 +++++++++++++++++++++++++++++++++
 start-db.sh                    |   1 -
 4 files changed, 171 insertions(+), 12 deletions(-)
 create mode 100644 internal/photos/api/storage.go
 create mode 100644 internal/photos/api/upload.go

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