Skip to content

Commit

Permalink
feat: allow file uploads to s3 (if enabled on project)
Browse files Browse the repository at this point in the history
  • Loading branch information
darmiel committed Nov 23, 2023
1 parent 91acb05 commit 98b5613
Show file tree
Hide file tree
Showing 14 changed files with 530 additions and 20 deletions.
14 changes: 14 additions & 0 deletions backend/api/handlers/midddleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,17 @@ func (a MiddlewareHandler) MeetingLocalsMiddleware(ctx *fiber.Ctx) error {
ctx.Locals("meeting", *meeting)
return ctx.Next()
}

func (a MiddlewareHandler) FileLocalsMiddleware(ctx *fiber.Ctx) error {
p := ctx.Locals("project").(model.Project)
fileID, err := ctx.ParamsInt("file_id")
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(presenter.ErrorResponse(err))
}
file, err := a.projectSrv.FindFile(p.ID, uint(fileID))
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
ctx.Locals("file", *file)
return ctx.Next()
}
144 changes: 143 additions & 1 deletion backend/api/handlers/project_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
gofiberfirebaseauth "github.com/ralf-life/gofiber-firebaseauth"
"go.uber.org/zap"
"sort"
"time"
)

var ErrNoAccess = errors.New("no access")
Expand All @@ -24,17 +25,19 @@ var ErrOnlyUser = errors.New("only users can perform this action")
type ProjectHandler struct {
srv services.ProjectService
userSrv services.UserService
s3Srv services.S3Service
logger *zap.SugaredLogger
validator *validator.Validate
}

func NewProjectHandler(
srv services.ProjectService,
userSrv services.UserService,
s3Srv services.S3Service,
logger *zap.SugaredLogger,
validator *validator.Validate,
) *ProjectHandler {
return &ProjectHandler{srv, userSrv, logger, validator}
return &ProjectHandler{srv, userSrv, s3Srv, logger, validator}
}

type projectDto struct {
Expand Down Expand Up @@ -263,3 +266,142 @@ func (h *ProjectHandler) RemoveUser(ctx *fiber.Ctx) error {

return ctx.Status(fiber.StatusOK).JSON(presenter.SuccessResponse("user added", nil))
}

// Files

/*
files.Post("/", handler.UploadFile)
files.Get("/", handler.ListFiles)
files.Get("/:file_id", handler.GetFile)
files.Delete("/:file_id", handler.DeleteFile)
files.Get("/:file_id/download", handler.DownloadFile)
*/

var (
ErrNoFileUploaded = errors.New("no files")
ErrFileTooBig = errors.New("file too big")
ErrNoFileQuotaLeft = errors.New("no file quota left")
)

func (h *ProjectHandler) UploadFile(ctx *fiber.Ctx) error {
u := ctx.Locals("user").(gofiberfirebaseauth.User)
p := ctx.Locals("project").(model.Project)

form, err := ctx.MultipartForm()
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(presenter.ErrorResponse(err))
}
files := form.File["file"]
if len(files) == 0 {
return ctx.Status(fiber.StatusBadRequest).JSON(presenter.ErrorResponse(ErrNoFileUploaded))
}

// if the project has a file size quota, check if the user is allowed to upload
var totalSize *uint64
if p.ProjectFileSizeQuota >= 0 {
ts, err := h.srv.GetTotalProjectFileSize(p.ID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
totalSize = &ts
}

var uploaded uint
for _, file := range files {
if p.MaxProjectFileSize >= 0 && file.Size > p.MaxProjectFileSize {
return ctx.Status(fiber.StatusForbidden).JSON(presenter.ErrorResponse(ErrFileTooBig))
}
if totalSize != nil {
*totalSize += uint64(file.Size)
if *totalSize > uint64(p.ProjectFileSizeQuota) {
return ctx.Status(fiber.StatusForbidden).JSON(presenter.ErrorResponse(ErrNoFileQuotaLeft))
}
}
key, err := h.s3Srv.UploadFile(u.UserID, p.ID, file)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
// save file to database
if err := h.srv.CreateFile(p.ID, model.ProjectFile{
Name: file.Filename,
ObjectKey: key,
Size: file.Size,
ProjectID: p.ID,
CreatorID: u.UserID,
LastAccessedAt: time.Now(),
AccessCount: 0,
}); err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
uploaded++
}

return ctx.Status(fiber.StatusOK).JSON(presenter.SuccessResponse(
fmt.Sprintf("%d files uploaded", uploaded),
nil,
))
}

func (h *ProjectHandler) ListFiles(ctx *fiber.Ctx) error {
p := ctx.Locals("project").(model.Project)
files, err := h.srv.FindFiles(p.ID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
return ctx.Status(fiber.StatusOK).JSON(presenter.SuccessResponse("project files", files))
}

func (h *ProjectHandler) GetFile(ctx *fiber.Ctx) error {
f := ctx.Locals("file").(model.ProjectFile)
return ctx.Status(fiber.StatusOK).JSON(presenter.SuccessResponse("project file", f))
}

func (h *ProjectHandler) DeleteFile(ctx *fiber.Ctx) error {
f := ctx.Locals("file").(model.ProjectFile)
// try to delete file from s3
if err := h.s3Srv.DeleteFile(f.ObjectKey); err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
// delete file from database
if err := h.srv.DeleteFile(f.ProjectID, f.ID); err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
return ctx.Status(fiber.StatusOK).JSON(presenter.SuccessResponse("file deleted", nil))
}

func (h *ProjectHandler) DownloadFile(ctx *fiber.Ctx) error {
// get file from s3
f := ctx.Locals("file").(model.ProjectFile)
req, _ := h.s3Srv.GetObjectRequest(f.ObjectKey)
if req.Error != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(req.Error))
}
// create presigned url
url, err := req.Presign(60 * time.Minute)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
return ctx.Redirect(url, fiber.StatusTemporaryRedirect)
}

type quotaInfoResponse struct {
// TotalSize is the total size of all files in the project
TotalSize uint64 `json:"total_size"`
// Quota is the quota (max total size of all files) of the project
Quota int64 `json:"quota"`
// MaxFileSize is the maximum file size of a single file
MaxFileSize int64 `json:"max_file_size"`
}

func (h *ProjectHandler) FileQuotaInfo(ctx *fiber.Ctx) error {
p := ctx.Locals("project").(model.Project)
totalSize, err := h.srv.GetTotalProjectFileSize(p.ID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(presenter.ErrorResponse(err))
}
return ctx.Status(fiber.StatusOK).JSON(presenter.SuccessResponse("", quotaInfoResponse{
TotalSize: totalSize,
Quota: p.ProjectFileSizeQuota,
MaxFileSize: p.MaxProjectFileSize,
}))
}
30 changes: 21 additions & 9 deletions backend/api/routes/project_route.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,29 @@ import (
"github.com/gofiber/fiber/v2"
)

func ProjectRoutes(router fiber.Router, handler *handlers.ProjectHandler) {
func ProjectRoutes(router fiber.Router, handler *handlers.ProjectHandler, middlewares *handlers.MiddlewareHandler) {
router.Post("/", handler.AddProject)
router.Get("/", handler.GetProjects)

router.Use("/:project_id", handler.ProjectAccessMiddleware)
router.Get("/:project_id", handler.GetProject)
router.Get("/:project_id/users", handler.ListUsersForProject)
router.Delete("/:project_id/delete", handler.DeleteProject)
router.Delete("/:project_id/leave", handler.LeaveProject)
router.Put("/:project_id", handler.EditProject)
specific := router.Group("/:project_id")
specific.Use("/", handler.ProjectAccessMiddleware)
specific.Get("/", handler.GetProject)
specific.Get("/users", handler.ListUsersForProject)
specific.Delete("/delete", handler.DeleteProject)
specific.Delete("/leave", handler.LeaveProject)
specific.Put("/", handler.EditProject)

router.Post("/:project_id/user/:user_id", handler.AddUser)
router.Delete("/:project_id/user/:user_id", handler.RemoveUser)
specific.Post("/user/:user_id", handler.AddUser)
specific.Delete("/user/:user_id", handler.RemoveUser)

files := specific.Group("/files")
files.Post("/", handler.UploadFile)
files.Get("/", handler.ListFiles)
files.Get("/quota", handler.FileQuotaInfo)

specificFile := files.Group("/:file_id")
specificFile.Use("/", middlewares.FileLocalsMiddleware)
specificFile.Get("/", handler.GetFile)
specificFile.Delete("/", handler.DeleteFile)
specificFile.Get("/download", handler.DownloadFile)
}
57 changes: 57 additions & 0 deletions backend/api/services/project_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package services

import (
"errors"
"fmt"
"github.com/darmiel/perplex/pkg/model"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -31,6 +32,11 @@ type ProjectService interface {
CreatePriority(title, color string, weight int, projectID uint) (*model.Priority, error)
DeletePriority(priorityID uint) error
EditPriority(priorityID uint, title, color string, weight int) error
CreateFile(projectID uint, file model.ProjectFile) error
FindFile(projectID uint, fileID uint) (*model.ProjectFile, error)
FindFiles(projectID uint) ([]model.ProjectFile, error)
DeleteFile(projectID uint, fileID uint) error
GetTotalProjectFileSize(projectID uint) (uint64, error)
}

type projectService struct {
Expand Down Expand Up @@ -256,3 +262,54 @@ func (p *projectService) EditPriority(priorityID uint, title, color string, weig
Color: color,
}).Error
}

// Files

func (p *projectService) CreateFile(projectID uint, file model.ProjectFile) error {
file.ProjectID = projectID
return p.DB.Create(&file).Error
}

func (p *projectService) FindFile(projectID uint, fileID uint) (*model.ProjectFile, error) {
var file model.ProjectFile
if err := p.DB.Where(&model.ProjectFile{
Model: gorm.Model{
ID: fileID,
},
ProjectID: projectID,
}).First(&file).Error; err != nil {
return nil, err
}
return &file, nil
}

func (p *projectService) FindFiles(projectID uint) ([]model.ProjectFile, error) {
var files []model.ProjectFile
if err := p.DB.Where(&model.ProjectFile{
ProjectID: projectID,
}).Find(&files).Error; err != nil {
return nil, err
}
return files, nil
}

func (p *projectService) DeleteFile(projectID uint, fileID uint) error {
return p.DB.Delete(&model.ProjectFile{
Model: gorm.Model{
ID: fileID,
},
ProjectID: projectID,
}).Error
}

func (p *projectService) GetTotalProjectFileSize(projectID uint) (uint64, error) {
var size uint64
if err := p.DB.Model(&model.ProjectFile{}).
Where("project_id = ?", projectID).
Select("sum(size)").
Scan(&size).Error; err != nil {
fmt.Println("oh oh gorm error!")
return 0, err
}
return size, nil
}
Loading

0 comments on commit 98b5613

Please sign in to comment.