Compare commits

..

No commits in common. "5dbd59fe8f60bd1add7e067fa103f96c5ef02856" and "fff782569fb1c35533ca906bbe820f2dcf17cf15" have entirely different histories.

13 changed files with 717 additions and 307 deletions

View File

@ -1,10 +1,10 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
photosapi "gitlab.celogeek.com/photos/api/internal/photos/api"
) )
type LoginCommand struct { type LoginCommand struct {
@ -13,6 +13,19 @@ type LoginCommand struct {
Password string `short:"p" long:"password" description:"Password" required:"true"` Password string `short:"p" long:"password" description:"Password" required:"true"`
} }
type LoginRequest struct {
Login string `json:"login"`
Password string `json:"password"`
}
type LoginError struct {
Error string
}
type LoginResponse struct {
Token string
}
func (c *LoginCommand) Execute(args []string) error { func (c *LoginCommand) Execute(args []string) error {
logger.Printf("Login on %s...\n", c.Url) logger.Printf("Login on %s...\n", c.Url)
@ -20,25 +33,22 @@ func (c *LoginCommand) Execute(args []string) error {
resp, err := cli. resp, err := cli.
R(). R().
SetBody(&photosapi.LoginRequest{ SetBody(&LoginRequest{c.Login, c.Password}).
Login: c.Login, SetResult(&LoginResponse{}).
Password: c.Password, SetError(&LoginError{}).
}).
SetResult(&photosapi.LoginResponse{}).
SetError(&photosapi.ErrorWithDetails{}).
Post("/account/login") Post("/account/login")
if err != nil { if err != nil {
return err return err
} }
if resp.IsError() { if err, ok := resp.Error().(*LoginError); ok {
logger.Printf("Login failed!") logger.Printf("Login failed!")
return resp.Error().(*photosapi.ErrorWithDetails) return errors.New(err.Error)
} }
logger.Println("Login succeed!") logger.Println("Login succeed!")
if result, ok := resp.Result().(*photosapi.LoginResponse); ok { if result, ok := resp.Result().(*LoginResponse); ok {
fmt.Println(result.Token) fmt.Println(result.Token)
} }

View File

@ -1,8 +1,9 @@
package main package main
import ( import (
"errors"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
photosapi "gitlab.celogeek.com/photos/api/internal/photos/api"
) )
type RegisterCommand struct { type RegisterCommand struct {
@ -11,6 +12,18 @@ type RegisterCommand struct {
Password string `short:"p" long:"password" description:"Password" required:"true"` Password string `short:"p" long:"password" description:"Password" required:"true"`
} }
type RegisterRequest struct {
Login string `json:"login"`
Password string `json:"password"`
}
type RegisterResponse struct {
}
type RegisterError struct {
Error string
}
func (c *RegisterCommand) Execute(args []string) error { func (c *RegisterCommand) Execute(args []string) error {
logger.Printf("Registering on %s...\n", c.Url) logger.Printf("Registering on %s...\n", c.Url)
@ -18,20 +31,18 @@ func (c *RegisterCommand) Execute(args []string) error {
resp, err := cli. resp, err := cli.
R(). R().
SetError(&photosapi.ErrorWithDetails{}). SetBody(&RegisterRequest{c.Login, c.Password}).
SetBody(&photosapi.SignupRequest{ SetResult(&RegisterResponse{}).
Login: c.Login, SetError(&RegisterError{}).
Password: c.Password,
}).
Post("/account/signup") Post("/account/signup")
if err != nil { if err != nil {
return err return err
} }
if resp.IsError() { if err, ok := resp.Error().(*RegisterError); ok {
logger.Println("Registering failed!") logger.Println("Registering failed!")
return resp.Error().(*photosapi.ErrorWithDetails) return errors.New(err.Error)
} }
logger.Println("Registering succeed!") logger.Println("Registering succeed!")

View File

@ -1,12 +1,16 @@
package main package main
import ( import (
"crypto/sha1"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/schollz/progressbar/v3" "github.com/schollz/progressbar/v3"
@ -20,22 +24,197 @@ type UploadCommand struct {
Workers uint32 `short:"w" long:"workers" description:"Number of workers for uploading chunks" default:"4"` Workers uint32 `short:"w" long:"workers" description:"Number of workers for uploading chunks" default:"4"`
} }
type UploadError struct {
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 {
UploadId string `json:"upload_id"`
}
type UploadPartResult struct {
UploadId string `json:"upload_id"`
Part uint `json:"part"`
Size uint `json:"size"`
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
Chunks []string
}
type UploadFileResponse struct {
Sum string
NbChunks uint32
Size uint64
}
func (c *UploadCommand) Cli() *resty.Client { func (c *UploadCommand) Cli() *resty.Client {
return resty.New().SetBaseURL(c.Url).SetAuthScheme("Private").SetAuthToken(c.Token) return resty.New().SetBaseURL(c.Url).SetAuthScheme("Private").SetAuthToken(c.Token)
} }
func (c *UploadCommand) Execute(args []string) error { func (c *UploadCommand) FileExists() (string, error) {
f, err := os.Open(c.File)
if err != nil {
return "", err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return "", err
}
progress := progressbar.DefaultBytes(st.Size(), fmt.Sprintf("Checking %s", filepath.Base(c.File)))
defer progress.Close()
tee := io.TeeReader(f, progress)
checksum := sha1.New()
io.Copy(checksum, tee)
sum := hex.EncodeToString(checksum.Sum(nil))
resp, err := c.Cli().R().Head(fmt.Sprintf("/file/%s", sum))
if err != nil {
return "", err
}
if resp.IsSuccess() {
return "", errors.New("file already exists")
}
return sum, nil
}
func (c *UploadCommand) FileUpload(sum string) error {
f, err := os.Open(c.File)
if err != nil {
return err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return err
}
nbChunks := st.Size() / photosapi.CHUNK_SIZE
if st.Size()%photosapi.CHUNK_SIZE > 0 {
nbChunks++
}
uploadFile := &UploadFileRequest{
Name: filepath.Base(c.File),
Chunks: make([]string, nbChunks),
Checksum: sum,
}
cli := c.Cli() cli := c.Cli()
resp, err := cli.R().SetError(&photosapi.ErrorWithDetails{}).SetResult(&photosapi.Upload{}).Post("/upload") progress := progressbar.DefaultBytes(st.Size(), fmt.Sprintf("Uploading %s", uploadFile.Name))
defer progress.Close()
wg := sync.WaitGroup{}
mu := sync.Mutex{}
wg.Add(4)
wgErrors := make([]error, c.Workers)
i := int64(0)
for w := uint32(0); w < c.Workers; w++ {
go func(w uint32) {
defer wg.Done()
b := make([]byte, photosapi.CHUNK_SIZE)
for {
mu.Lock()
part := i
i++
n, err := f.Read(b)
mu.Unlock()
if n == 0 {
if err == io.EOF {
break
}
wgErrors[w] = err
return
}
checksum := sha1.New()
checksum.Write(b[0:n])
sum := hex.EncodeToString(checksum.Sum(nil))
resp, err := cli.R().Head(fmt.Sprintf("/file/chunk/%s", sum))
if err != nil {
wgErrors[w] = err
return
}
if resp.IsSuccess() {
uploadFile.Chunks[part] = sum
progress.Add(n)
continue
}
resp, err = cli.R().SetError(&UploadError{}).SetBody(b[0:n]).Post("/file/chunk")
if err != nil {
wgErrors[w] = err
return
}
if err, ok := resp.Error().(*UploadError); ok {
wgErrors[w] = err
return
}
uploadFile.Chunks[part] = sum
progress.Add(n)
}
}(w)
}
wg.Wait()
for _, err := range wgErrors {
if err != nil {
return err
}
}
resp, err := cli.R().SetBody(uploadFile).SetError(&UploadError{}).SetResult(&UploadFileResponse{}).Post("/file")
if err != nil { if err != nil {
return err return err
} }
if resp.IsError() { if err, ok := resp.Error().(*UploadError); ok {
return resp.Error().(*photosapi.ErrorWithDetails) logger.Println("Upload failed")
return err
} }
uploadId := resp.Result().(*photosapi.Upload).Id if result, ok := resp.Result().(*UploadFileResponse); ok {
fmt.Printf("Upload succeed\nSum: %s\nNbChunks: %d\nSize: %d\n", result.Sum, result.NbChunks, result.Size)
}
return nil
}
func (c *UploadCommand) Execute(args []string) error {
cli := c.Cli()
resp, err := cli.R().SetError(&UploadError{}).SetResult(&UploadCreate{}).Post("/upload")
if err != nil {
return err
}
if err, ok := resp.Error().(*UploadError); ok {
return err
}
uploadId := resp.Result().(*UploadCreate).UploadId
f, err := os.Open(c.File) f, err := os.Open(c.File)
if err != nil { if err != nil {
@ -70,7 +249,8 @@ func (c *UploadCommand) Execute(args []string) error {
resp, err := cli. resp, err := cli.
R(). R().
SetError(&photosapi.ErrorWithDetails{}). SetError(&UploadError{}).
SetResult(&UploadPartResult{}).
SetQueryParam("part", fmt.Sprint(parts)). SetQueryParam("part", fmt.Sprint(parts)).
SetQueryParam("sha256", hex.EncodeToString(partsha256.Sum(nil))). SetQueryParam("sha256", hex.EncodeToString(partsha256.Sum(nil))).
SetBody(b[:n]). SetBody(b[:n]).
@ -81,22 +261,22 @@ func (c *UploadCommand) Execute(args []string) error {
return err return err
} }
if resp.IsError() { if err, ok := resp.Error().(*UploadError); ok {
return resp.Error().(*photosapi.ErrorWithDetails) return err
} }
} }
fmt.Printf( fmt.Printf(
"Result:\n - Upload ID: %s\n - Parts: %d\n", "Upload: %s\nParts: %d\n",
uploadId, uploadId,
parts, parts,
) )
resp, err = cli. resp, err = cli.
R(). R().
SetError(&photosapi.ErrorWithDetails{}). SetError(&UploadError{}).
SetPathParam("id", uploadId). SetPathParam("id", uploadId).
SetBody(&photosapi.UploadCompleteRequest{ SetBody(&UploadCompleteRequest{
Sha256: hex.EncodeToString(completesha256.Sum(nil)), Sha256: hex.EncodeToString(completesha256.Sum(nil)),
Parts: uint(parts), Parts: uint(parts),
Name: filepath.Base(c.File), Name: filepath.Base(c.File),
@ -107,10 +287,12 @@ func (c *UploadCommand) Execute(args []string) error {
return err return err
} }
if resp.IsError() { if err, ok := resp.Error().(*UploadError); ok {
return resp.Error().(*photosapi.ErrorWithDetails) return err
} }
fmt.Printf("Response: %s\n", resp.Body())
cli.R().SetPathParam("id", uploadId).Delete("/upload/{id}") cli.R().SetPathParam("id", uploadId).Delete("/upload/{id}")
return nil return nil

View File

@ -50,22 +50,13 @@ func NewAccount(login string, password string) *Account {
} }
// Service // Service
type SignupRequest struct { type SignupOrLoginRequest struct {
Login string `json:"login" binding:"required,min=3,max=40,alphanum"` Login string `binding:"min=3,max=40,alphanum"`
Password string `json:"password" binding:"required,min=8,max=40"` Password string `binding:"min=8,max=40"`
}
type LoginRequest struct {
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
} }
func (s *Service) Signup(c *gin.Context) { func (s *Service) Signup(c *gin.Context) {
var account *SignupRequest var account *SignupOrLoginRequest
if c.BindJSON(&account) != nil { if c.BindJSON(&account) != nil {
return return
@ -89,7 +80,7 @@ func (s *Service) Signup(c *gin.Context) {
} }
func (s *Service) Login(c *gin.Context) { func (s *Service) Login(c *gin.Context) {
var account *LoginRequest var account *SignupOrLoginRequest
if c.BindJSON(&account) != nil { if c.BindJSON(&account) != nil {
return return
@ -105,7 +96,9 @@ func (s *Service) Login(c *gin.Context) {
return return
} }
c.JSON(http.StatusOK, LoginResponse{session.Token}) c.JSON(http.StatusOK, gin.H{
"token": session.Token,
})
} }
func (s *Service) Logout(c *gin.Context) { func (s *Service) Logout(c *gin.Context) {

View File

@ -35,6 +35,7 @@ func (s *Service) Migrate() {
tx.AutoMigrate(&Account{}) tx.AutoMigrate(&Account{})
tx.AutoMigrate(&Session{}) tx.AutoMigrate(&Session{})
tx.AutoMigrate(&File{}) tx.AutoMigrate(&File{})
tx.AutoMigrate(&FileChunk{})
} }
func (s *Service) DBConnect() { func (s *Service) DBConnect() {

View File

@ -2,7 +2,6 @@ package photosapi
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -12,18 +11,6 @@ var (
ErrReqMissingBody = errors.New("missing body") ErrReqMissingBody = errors.New("missing body")
) )
type ErrorWithDetails struct {
Err string `json:"error"`
Details []string `json:"details"`
}
func (u *ErrorWithDetails) Error() string {
if len(u.Details) == 0 {
return u.Err
}
return fmt.Sprintf("%s: \n - %s", u.Err, strings.Join(u.Details, "\n - "))
}
func (s *Service) HandleError(c *gin.Context) { func (s *Service) HandleError(c *gin.Context) {
c.Next() c.Next()
err := c.Errors.Last() err := c.Errors.Last()
@ -31,15 +18,20 @@ func (s *Service) HandleError(c *gin.Context) {
return return
} }
errWithDetails := &ErrorWithDetails{err.Error(), nil} details := err.Error()
if details == "EOF" {
if errWithDetails.Err == "EOF" { details = "missing body"
errWithDetails.Err = "missing body"
} }
if err.Type == gin.ErrorTypeBind { switch err.Type {
errWithDetails.Err, errWithDetails.Details = "binding error", strings.Split(errWithDetails.Err, "\n") case gin.ErrorTypeBind:
c.JSON(-1, gin.H{
"error": "binding error",
"details": strings.Split(details, "\n"),
})
default:
c.JSON(-1, gin.H{
"error": details,
})
} }
c.JSON(-1, errWithDetails)
} }

View File

@ -1,7 +1,19 @@
package photosapi package photosapi
import ( import (
"bytes"
"errors" "errors"
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"github.com/dsoprea/go-exif/v3"
"github.com/gin-gonic/gin"
"github.com/jackc/pgconn"
"gorm.io/gorm"
) )
// Error // Error
@ -17,252 +29,252 @@ var (
ErrStoreMissingName = errors.New("name required") ErrStoreMissingName = errors.New("name required")
) )
// // Service // Service
// var CHUNK_SIZE int64 = 4 << 20 var CHUNK_SIZE int64 = 4 << 20
// type FileRequest struct { type FileRequest struct {
// Name string `json:"name"` Name string `json:"name"`
// Checksum string `json:"checksum"` Checksum string `json:"checksum"`
// Chunks []string `json:"chunks"` Chunks []string `json:"chunks"`
// } }
// func (s *Service) FileCreate(c *gin.Context) { func (s *Service) FileCreate(c *gin.Context) {
// file := &FileRequest{} file := &FileRequest{}
// if c.BindJSON(file) != nil { if c.BindJSON(file) != nil {
// return return
// } }
// if len(file.Name) < 1 { if len(file.Name) < 1 {
// c.AbortWithError(http.StatusBadRequest, ErrStoreMissingName) c.AbortWithError(http.StatusBadRequest, ErrStoreMissingName)
// return return
// } }
// if len(file.Checksum) != 40 { if len(file.Checksum) != 40 {
// c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum) c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum)
// return return
// } }
// if len(file.Chunks) == 0 { if len(file.Chunks) == 0 {
// c.AbortWithError(http.StatusBadRequest, ErrStoreMissingChunks) c.AbortWithError(http.StatusBadRequest, ErrStoreMissingChunks)
// return return
// } }
// for _, chunk := range file.Chunks { for _, chunk := range file.Chunks {
// if len(chunk) != 40 { if len(chunk) != 40 {
// c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum) c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum)
// return return
// } }
// } }
// r, rs, err := s.Store.Combine(file.Chunks) r, rs, err := s.Store.Combine(file.Chunks)
// if err != nil { if err != nil {
// if strings.HasPrefix(err.Error(), "chunk") && strings.HasSuffix(err.Error(), "doesn't exists") { if strings.HasPrefix(err.Error(), "chunk") && strings.HasSuffix(err.Error(), "doesn't exists") {
// c.AbortWithError(http.StatusBadRequest, err) c.AbortWithError(http.StatusBadRequest, err)
// } else { } else {
// c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
// } }
// return return
// } }
// if r != file.Checksum { if r != file.Checksum {
// c.AbortWithError(http.StatusExpectationFailed, ErrStoreMismatchChecksum) c.AbortWithError(http.StatusExpectationFailed, ErrStoreMismatchChecksum)
// return return
// } }
// sess := s.CurrentSession(c) sess := s.CurrentSession(c)
// f := &File{ f := &File{
// Name: file.Name, Name: file.Name,
// Checksum: file.Checksum, Checksum: file.Checksum,
// Size: rs, Size: rs,
// AuthorId: &sess.AccountId, AuthorId: &sess.AccountId,
// } }
// err = s.DB.Transaction(func(tx *gorm.DB) error { err = s.DB.Transaction(func(tx *gorm.DB) error {
// if err := tx.Create(f).Error; err != nil { if err := tx.Create(f).Error; err != nil {
// return err return err
// } }
// for i, chunk := range file.Chunks { for i, chunk := range file.Chunks {
// fc := &FileChunk{ fc := &FileChunk{
// FileId: f.ID, FileId: f.ID,
// Part: uint32(i + 1), Part: uint32(i + 1),
// Checksum: chunk, Checksum: chunk,
// } }
// if err := tx.Create(fc).Error; err != nil { if err := tx.Create(fc).Error; err != nil {
// return err return err
// } }
// } }
// return nil return nil
// }) })
// if nerr, ok := err.(*pgconn.PgError); ok { if nerr, ok := err.(*pgconn.PgError); ok {
// if nerr.Code == "23505" && nerr.Detail == fmt.Sprintf("Key (checksum)=(%s) already exists.", file.Checksum) { if nerr.Code == "23505" && nerr.Detail == fmt.Sprintf("Key (checksum)=(%s) already exists.", file.Checksum) {
// err = nil err = nil
// } }
// } }
// if err != nil { if err != nil {
// c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
// return return
// } }
// c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
// "sum": file.Checksum, "sum": file.Checksum,
// "nbChunks": len(file.Chunks), "nbChunks": len(file.Chunks),
// "size": rs, "size": rs,
// }) })
// } }
// func (s *Service) FileCreateChunk(c *gin.Context) { func (s *Service) FileCreateChunk(c *gin.Context) {
// if c.Request.ContentLength > CHUNK_SIZE { if c.Request.ContentLength > CHUNK_SIZE {
// c.AbortWithError(http.StatusBadRequest, ErrStoreBadChunkSize) c.AbortWithError(http.StatusBadRequest, ErrStoreBadChunkSize)
// return return
// } }
// b := bytes.NewBuffer([]byte{}) b := bytes.NewBuffer([]byte{})
// io.Copy(b, c.Request.Body) io.Copy(b, c.Request.Body)
// sess := s.CurrentSession(c) sess := s.CurrentSession(c)
// chunk := s.Store.NewChunk(b.Bytes()) chunk := s.Store.NewChunk(b.Bytes())
// if err := chunk.Save(sess.Account.Login); err != nil { if err := chunk.Save(sess.Account.Login); err != nil {
// if errors.Is(err, ErrStoreChunkAlreadyExists) { if errors.Is(err, ErrStoreChunkAlreadyExists) {
// c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
// "checksum": chunk.Sum, "checksum": chunk.Sum,
// }) })
// } else { } else {
// c.AbortWithError(http.StatusBadRequest, err) c.AbortWithError(http.StatusBadRequest, err)
// } }
// return return
// } }
// c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
// "checksum": chunk.Sum, "checksum": chunk.Sum,
// }) })
// } }
// func (s *Service) FileChunkExists(c *gin.Context) { func (s *Service) FileChunkExists(c *gin.Context) {
// checksum := c.Param("checksum") checksum := c.Param("checksum")
// if len(checksum) != 40 { if len(checksum) != 40 {
// c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum) c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum)
// return return
// } }
// if s.Store.Chunk(checksum).FileExists() { if s.Store.Chunk(checksum).FileExists() {
// c.Status(http.StatusOK) c.Status(http.StatusOK)
// } else { } else {
// c.Status(http.StatusNotFound) c.Status(http.StatusNotFound)
// } }
// } }
// func (s *Service) FileExists(c *gin.Context) { func (s *Service) FileExists(c *gin.Context) {
// checksum := c.Param("checksum") checksum := c.Param("checksum")
// if len(checksum) != 40 { if len(checksum) != 40 {
// c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum) c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum)
// return return
// } }
// var fileExists int64 var fileExists int64
// if err := s.DB.Model(&File{}).Where("checksum = ?", checksum).Count(&fileExists).Error; err != nil { if err := s.DB.Model(&File{}).Where("checksum = ?", checksum).Count(&fileExists).Error; err != nil {
// c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
// return return
// } }
// if fileExists > 0 { if fileExists > 0 {
// c.Status(http.StatusOK) c.Status(http.StatusOK)
// } else { } else {
// c.Status(http.StatusNotFound) c.Status(http.StatusNotFound)
// } }
// } }
// func (s *Service) FileGet(c *gin.Context) { func (s *Service) FileGet(c *gin.Context) {
// checksum := c.Param("checksum") checksum := c.Param("checksum")
// if len(checksum) != 40 { if len(checksum) != 40 {
// c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum) c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum)
// return return
// } }
// if checksum == c.GetHeader("If-None-Match") { if checksum == c.GetHeader("If-None-Match") {
// c.Status(http.StatusNotModified) c.Status(http.StatusNotModified)
// return return
// } }
// f := &File{} f := &File{}
// if err := s.DB.Debug().Preload("Chunks").Where("checksum = ?", checksum).First(&f).Error; err != nil { if err := s.DB.Debug().Preload("Chunks").Where("checksum = ?", checksum).First(&f).Error; err != nil {
// c.AbortWithError(http.StatusBadRequest, err) c.AbortWithError(http.StatusBadRequest, err)
// return return
// } }
// chunks := make([]string, len(f.Chunks)) chunks := make([]string, len(f.Chunks))
// for _, fc := range f.Chunks { for _, fc := range f.Chunks {
// chunks[fc.Part-1] = fc.Checksum chunks[fc.Part-1] = fc.Checksum
// } }
// reader, err := s.Store.NewStoreReader(chunks) reader, err := s.Store.NewStoreReader(chunks)
// if err != nil { if err != nil {
// c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
// return return
// } }
// defer reader.Close() defer reader.Close()
// c.DataFromReader( c.DataFromReader(
// http.StatusOK, http.StatusOK,
// reader.Size, reader.Size,
// mime.TypeByExtension(filepath.Ext(f.Name)), mime.TypeByExtension(filepath.Ext(f.Name)),
// reader, reader,
// map[string]string{ map[string]string{
// "Content-Disposition": fmt.Sprintf("inline; filename=\"%s\"", f.Name), "Content-Disposition": fmt.Sprintf("inline; filename=\"%s\"", f.Name),
// "ETag": f.Checksum, "ETag": f.Checksum,
// }, },
// ) )
// } }
// func (s *Service) FileAnalyze(c *gin.Context) { func (s *Service) FileAnalyze(c *gin.Context) {
// checksum := c.Param("checksum") checksum := c.Param("checksum")
// if len(checksum) != 40 { if len(checksum) != 40 {
// c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum) c.AbortWithError(http.StatusBadRequest, ErrStoreBadChecksum)
// return return
// } }
// f := &File{} f := &File{}
// if err := s.DB.Debug().Preload("Chunks").Where("checksum = ?", checksum).First(&f).Error; err != nil { if err := s.DB.Debug().Preload("Chunks").Where("checksum = ?", checksum).First(&f).Error; err != nil {
// c.AbortWithError(http.StatusBadRequest, err) c.AbortWithError(http.StatusBadRequest, err)
// return return
// } }
// chunks := make([]string, len(f.Chunks)) chunks := make([]string, len(f.Chunks))
// for _, fc := range f.Chunks { for _, fc := range f.Chunks {
// chunks[fc.Part-1] = fc.Checksum chunks[fc.Part-1] = fc.Checksum
// } }
// reader, err := s.Store.NewStoreReader(chunks) reader, err := s.Store.NewStoreReader(chunks)
// if err != nil { if err != nil {
// c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
// return return
// } }
// defer reader.Close() defer reader.Close()
// rawExif, err := exif.SearchAndExtractExifWithReader(reader) rawExif, err := exif.SearchAndExtractExifWithReader(reader)
// if err != nil { if err != nil {
// c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
// return return
// } }
// entries, _, err := exif.GetFlatExifDataUniversalSearch(rawExif, nil, true) entries, _, err := exif.GetFlatExifDataUniversalSearch(rawExif, nil, true)
// if err != nil { if err != nil {
// c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
// return return
// } }
// c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
// "exif": entries, "exif": entries,
// }) })
// } }
func (s *Service) FileInit() { func (s *Service) FileInit() {
file := s.Gin.Group("/file") file := s.Gin.Group("/file")
file.Use(s.RequireSession) file.Use(s.RequireSession)
// file.POST("", s.FileCreate) file.POST("", s.FileCreate)
// file.HEAD("/:checksum", s.FileExists) file.HEAD("/:checksum", s.FileExists)
// file.GET("/:checksum", s.FileGet) file.GET("/:checksum", s.FileGet)
// file.POST("/chunk", s.FileCreateChunk) file.POST("/chunk", s.FileCreateChunk)
// file.HEAD("/chunk/:checksum", s.FileChunkExists) file.HEAD("/chunk/:checksum", s.FileChunkExists)
// file.GET("/analyze/:checksum", s.FileAnalyze) file.GET("/analyze/:checksum", s.FileAnalyze)
} }

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
photosstore "gitlab.celogeek.com/photos/api/internal/photos/store"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -22,6 +23,7 @@ type Service struct {
Gin *gin.Engine Gin *gin.Engine
DB *gorm.DB DB *gorm.DB
Config *ServiceConfig Config *ServiceConfig
Store *photosstore.Store
StorageTmp *Storage StorageTmp *Storage
StorageUpload *Storage StorageUpload *Storage
LogOk *Logger LogOk *Logger
@ -38,6 +40,7 @@ func New(config *ServiceConfig) *Service {
return &Service{ return &Service{
Gin: gin.New(), Gin: gin.New(),
Config: config, Config: config,
Store: &photosstore.Store{Path: config.StorePath},
StorageTmp: NewStorage(config.StorePath, "tmp"), StorageTmp: NewStorage(config.StorePath, "tmp"),
StorageUpload: NewStorage(config.StorePath, "upload"), StorageUpload: NewStorage(config.StorePath, "upload"),
LogOk: &Logger{os.Stdout, "Photos"}, LogOk: &Logger{os.Stdout, "Photos"},
@ -64,7 +67,7 @@ func (s *Service) SetupRoutes() {
} }
func (s *Service) PrepareStore() { func (s *Service) PrepareStore() {
d, err := os.Stat(s.Config.StorePath) d, err := os.Stat(s.Store.Path)
if err != nil { if err != nil {
s.LogErr.Fatal("Store", err) s.LogErr.Fatal("Store", err)
} }

View File

@ -6,12 +6,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type Me struct {
User string `json:"user"`
}
func (s *Service) Me(c *gin.Context) { func (s *Service) Me(c *gin.Context) {
c.JSON(http.StatusOK, Me{s.CurrentSession(c).Account.Login}) c.JSON(http.StatusOK, gin.H{
"user": s.CurrentSession(c).Account.Login,
})
} }
func (s *Service) MeInit() { func (s *Service) MeInit() {

View File

@ -2,7 +2,6 @@ package photosapi
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -18,9 +17,9 @@ func (s *Service) Recovery(c *gin.Context) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
s.LogErr.Print("PANIC", err) s.LogErr.Print("PANIC", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, &ErrorWithDetails{ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
ErrUnexpected.Error(), "error": ErrUnexpected.Error(),
[]string{fmt.Sprint(err)}, "details": err,
}) })
} }
}() }()

View File

@ -32,6 +32,16 @@ type File struct {
Size uint64 `gorm:"not null"` Size uint64 `gorm:"not null"`
Author *Account `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE" json:"author"` Author *Account `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE" json:"author"`
AuthorId *uint32 `json:"-"` 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"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
@ -43,18 +53,18 @@ func (s *Service) UploadCreate(c *gin.Context) {
return return
} }
upload := &Upload{sha.String()} if err := s.StorageTmp.Create(sha.String()); err != nil {
if err := s.StorageTmp.Create(upload.Id); err != nil {
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.JSON(http.StatusCreated, upload) c.JSON(http.StatusCreated, gin.H{
"upload_id": sha.String(),
})
} }
type Upload struct { type UploadUri struct {
Id string `json:"upload_id" uri:"upload_id" binding:"required,uuid"` Id string `uri:"upload_id" binding:"required,uuid"`
} }
type UploadPartQuery struct { type UploadPartQuery struct {
@ -64,7 +74,7 @@ type UploadPartQuery struct {
func (s *Service) UploadPart(c *gin.Context) { func (s *Service) UploadPart(c *gin.Context) {
var ( var (
upload Upload upload UploadUri
uploadPart UploadPartQuery uploadPart UploadPartQuery
) )
@ -93,7 +103,7 @@ func (s *Service) UploadPart(c *gin.Context) {
sha := sha256.New() sha := sha256.New()
t := io.TeeReader(c.Request.Body, sha) t := io.TeeReader(c.Request.Body, sha)
_, err = io.Copy(f, t) w, err := io.Copy(f, t)
if err != nil { if err != nil {
f.Close() f.Close()
os.Remove(tmp_file) os.Remove(tmp_file)
@ -109,18 +119,22 @@ func (s *Service) UploadPart(c *gin.Context) {
return return
} }
err = os.Rename(tmp_file, file) if err = os.Rename(tmp_file, file); err != nil {
if err != nil {
os.Remove(tmp_file) os.Remove(tmp_file)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.Status(http.StatusNoContent) 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) { func (s *Service) UploadCancel(c *gin.Context) {
var upload Upload var upload UploadUri
if c.BindUri(&upload) != nil { if c.BindUri(&upload) != nil {
return return
} }
@ -141,7 +155,7 @@ type UploadCompleteRequest struct {
func (s *Service) UploadComplete(c *gin.Context) { func (s *Service) UploadComplete(c *gin.Context) {
var ( var (
upload Upload upload UploadUri
uploadCompleteRequest UploadCompleteRequest uploadCompleteRequest UploadCompleteRequest
) )
if c.BindUri(&upload) != nil || c.BindJSON(&uploadCompleteRequest) != nil { if c.BindUri(&upload) != nil || c.BindJSON(&uploadCompleteRequest) != nil {
@ -153,12 +167,15 @@ func (s *Service) UploadComplete(c *gin.Context) {
return return
} }
c.Status(http.StatusNoContent) c.JSON(http.StatusOK, gin.H{
"sha256": uploadCompleteRequest.Sha256,
"parts": uploadCompleteRequest.Parts,
"name": uploadCompleteRequest.Name,
})
} }
func (s *Service) UploadInit() { func (s *Service) UploadInit() {
upload := s.Gin.Group("/upload") upload := s.Gin.Group("/upload")
upload.Use(s.RequireSession)
// start // start
upload.POST("", s.UploadCreate) upload.POST("", s.UploadCreate)

View File

@ -0,0 +1,122 @@
package photosstore
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
)
var (
ErrStoreChunkAlreadyExists = errors.New("chunk file already exists")
)
type Store struct {
Path string
}
type Chunk struct {
*Store
Sum string
Bytes []byte
}
func (s *Store) NewChunk(b []byte) *Chunk {
sum := sha1.New()
sum.Write(b)
sumString := hex.EncodeToString(sum.Sum(nil))
return &Chunk{s, sumString, b}
}
func (s *Store) LoadChunk(sum string) (*Chunk, error) {
c := s.Chunk(sum)
if !c.FileExists() {
return nil, fmt.Errorf("chunk %s doesn't exists", sum)
}
b, err := os.ReadFile(c.Filename())
if err != nil {
return nil, err
}
c.Bytes = b
return c, nil
}
func (s *Store) Chunk(sum string) *Chunk {
return &Chunk{s, sum, nil}
}
func (c *Chunk) Dir() string {
return filepath.Join(c.Path, "storage", c.Sum[0:1], c.Sum[1:2])
}
func (c *Chunk) Filename() string {
return filepath.Join(c.Dir(), c.Sum)
}
func (c *Chunk) Size() int64 {
st, err := os.Stat(c.Filename())
if err != nil {
return -1
}
return st.Size()
}
func (c *Chunk) FileExists() bool {
fs, err := os.Stat(c.Filename())
if errors.Is(err, os.ErrNotExist) {
return false
}
return !fs.IsDir()
}
func (c *Chunk) Mkdir() error {
return os.MkdirAll(c.Dir(), 0755)
}
func (c *Chunk) Save(login string) error {
if c.FileExists() {
return ErrStoreChunkAlreadyExists
}
if err := c.Mkdir(); err != nil {
return err
}
if err := os.WriteFile(c.Filename(), c.Bytes, 0666); err != nil {
return err
}
meta, err := os.Create(c.Filename() + ".json")
if err != nil {
return err
}
enc := json.NewEncoder(meta)
enc.SetIndent("", " ")
return enc.Encode(gin.H{
"author": login,
"date": time.Now().UTC().Format("2006-01-02 15:04:05"),
"checksum": c.Sum,
"size": len(c.Bytes),
})
}
func (s *Store) Combine(sumb []string) (string, uint64, error) {
sum := sha1.New()
size := uint64(0)
for _, sb := range sumb {
c, err := s.LoadChunk(sb)
if err != nil {
return "", 0, err
}
sum.Write(c.Bytes)
size += uint64(len(c.Bytes))
}
return hex.EncodeToString(sum.Sum(nil)), size, nil
}

View File

@ -0,0 +1,70 @@
package photosstore
import (
"errors"
"io"
"os"
)
var (
ErrStoreMissingChunks = errors.New("part checksum missing")
)
type StoreReaderChunk struct {
Filename string
Size int64
}
type StoreReader struct {
current *os.File
chunk int
chunks []StoreReaderChunk
Size int64
}
func (s *Store) NewStoreReader(chunks []string) (*StoreReader, error) {
sr := &StoreReader{nil, 0, make([]StoreReaderChunk, len(chunks)), 0}
for i, chunk := range chunks {
c := s.Chunk(chunk)
name := c.Filename()
size := c.Size()
if size < 0 {
return nil, ErrStoreMissingChunks
}
sr.chunks[i] = StoreReaderChunk{name, size}
sr.Size += size
}
return sr, nil
}
func (s *StoreReader) Read(p []byte) (n int, err error) {
if s.current == nil {
f, err := os.Open(s.chunks[s.chunk].Filename)
if err != nil {
return -1, err
}
s.current = f
}
n, err = s.current.Read(p)
if err == io.EOF {
s.chunk++
if s.chunk > len(s.chunks)-1 {
return
}
s.Close()
return s.Read(p)
}
return
}
func (s *StoreReader) Close() {
if s.current != nil {
s.current.Close()
s.current = nil
}
}
// func (s *StoreReader) Seek(offset int64, whence int) (int64, error) {
// }