Spaces:
Running
Running
Commit ·
7d3a71d
1
Parent(s): f4187e3
Deploy files from GitHub repository
Browse files- controllers/academy_exam_controller.go +6 -6
- controllers/upload_controller.go +9 -6
- models/dto/academy_dto.go +2 -0
- models/entity/entity.go +1 -0
- models/error/error.go +3 -0
- provider/controller_provider.go +3 -3
- services/academy_service.go +39 -30
- services/academy_service_helpers.go +0 -353
- services/upload_service.go +59 -10
- utils/utils.go +8 -0
controllers/academy_exam_controller.go
CHANGED
|
@@ -14,15 +14,15 @@ type AcademyExamController interface {
|
|
| 14 |
List(ctx *gin.Context)
|
| 15 |
}
|
| 16 |
|
| 17 |
-
type academyExamController struct {
|
| 18 |
|
| 19 |
-
func NewAcademyExamController(
|
| 20 |
|
| 21 |
func (c *academyExamController) Attempt(ctx *gin.Context) {
|
| 22 |
academySlug := ctx.Param("academy_slug")
|
| 23 |
examSlug := ctx.Param("exam_slug")
|
| 24 |
accountId := ParseAccountId(ctx)
|
| 25 |
-
res, err := c.
|
| 26 |
ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "exam_slug": examSlug}, res, err)
|
| 27 |
}
|
| 28 |
|
|
@@ -30,19 +30,19 @@ func (c *academyExamController) Answer(ctx *gin.Context) {
|
|
| 30 |
academySlug := ctx.Param("academy_slug")
|
| 31 |
attemptId, _ := utils.ToUUID(ctx.Param("attempt_id"))
|
| 32 |
req := RequestJSON[dto.AnswerExamEventRequest](ctx)
|
| 33 |
-
res, err := c.
|
| 34 |
ResponseJSON(ctx, gin.H{"cp_grader_result": res}, req, err)
|
| 35 |
}
|
| 36 |
|
| 37 |
func (c *academyExamController) Submit(ctx *gin.Context) {
|
| 38 |
attemptId, _ := utils.ToUUID(ctx.Param("attempt_id"))
|
| 39 |
-
res, err := c.
|
| 40 |
ResponseJSON(ctx, gin.H{}, res, err)
|
| 41 |
}
|
| 42 |
|
| 43 |
func (c *academyExamController) List(ctx *gin.Context) {
|
| 44 |
academySlug := ctx.Param("academy_slug")
|
| 45 |
accountId := ParseAccountId(ctx)
|
| 46 |
-
res, err := c.
|
| 47 |
ResponseJSON(ctx, gin.H{}, res, err)
|
| 48 |
}
|
|
|
|
| 14 |
List(ctx *gin.Context)
|
| 15 |
}
|
| 16 |
|
| 17 |
+
type academyExamController struct { academyExamService services.AcademyExamService }
|
| 18 |
|
| 19 |
+
func NewAcademyExamController(academyExamService services.AcademyExamService) AcademyExamController { return &academyExamController{ academyExamService: academyExamService } }
|
| 20 |
|
| 21 |
func (c *academyExamController) Attempt(ctx *gin.Context) {
|
| 22 |
academySlug := ctx.Param("academy_slug")
|
| 23 |
examSlug := ctx.Param("exam_slug")
|
| 24 |
accountId := ParseAccountId(ctx)
|
| 25 |
+
res, err := c.academyExamService.AttemptExamAcademy(ctx.Request.Context(), academySlug, examSlug, accountId)
|
| 26 |
ResponseJSON(ctx, gin.H{"academy_slug": academySlug, "exam_slug": examSlug}, res, err)
|
| 27 |
}
|
| 28 |
|
|
|
|
| 30 |
academySlug := ctx.Param("academy_slug")
|
| 31 |
attemptId, _ := utils.ToUUID(ctx.Param("attempt_id"))
|
| 32 |
req := RequestJSON[dto.AnswerExamEventRequest](ctx)
|
| 33 |
+
res, err := c.academyExamService.AnswerExamAcademy(ctx.Request.Context(), academySlug, attemptId, req.QuestionId, req.Answer)
|
| 34 |
ResponseJSON(ctx, gin.H{"cp_grader_result": res}, req, err)
|
| 35 |
}
|
| 36 |
|
| 37 |
func (c *academyExamController) Submit(ctx *gin.Context) {
|
| 38 |
attemptId, _ := utils.ToUUID(ctx.Param("attempt_id"))
|
| 39 |
+
res, err := c.academyExamService.SubmitExamAcademy(ctx.Request.Context(), attemptId)
|
| 40 |
ResponseJSON(ctx, gin.H{}, res, err)
|
| 41 |
}
|
| 42 |
|
| 43 |
func (c *academyExamController) List(ctx *gin.Context) {
|
| 44 |
academySlug := ctx.Param("academy_slug")
|
| 45 |
accountId := ParseAccountId(ctx)
|
| 46 |
+
res, err := c.academyExamService.ListExamByAcademy(ctx.Request.Context(), academySlug, accountId)
|
| 47 |
ResponseJSON(ctx, gin.H{}, res, err)
|
| 48 |
}
|
controllers/upload_controller.go
CHANGED
|
@@ -17,13 +17,16 @@ import (
|
|
| 17 |
"abdanhafidz.com/go-boilerplate/services"
|
| 18 |
)
|
| 19 |
|
| 20 |
-
type UploadController
|
| 21 |
-
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
-
func (
|
|
|
|
|
|
|
| 27 |
fmt.Println("👉 Content-Type:", ctx.GetHeader("Content-Type"))
|
| 28 |
|
| 29 |
if !strings.Contains(ctx.GetHeader("Content-Type"), "multipart/form-data") {
|
|
@@ -162,7 +165,7 @@ func (c *UploadController) Upload(ctx *gin.Context) {
|
|
| 162 |
})
|
| 163 |
}
|
| 164 |
|
| 165 |
-
func (c *
|
| 166 |
fileIDStr := ctx.Param("id")
|
| 167 |
fileID, err := uuid.Parse(fileIDStr)
|
| 168 |
if err != nil {
|
|
@@ -224,7 +227,7 @@ func (c *UploadController) GetFileByID(ctx *gin.Context) {
|
|
| 224 |
})
|
| 225 |
}
|
| 226 |
|
| 227 |
-
func (c *
|
| 228 |
images := map[string]bool{
|
| 229 |
".jpg": true, ".jpeg": true, ".png": true, ".webp": true,
|
| 230 |
}
|
|
|
|
| 17 |
"abdanhafidz.com/go-boilerplate/services"
|
| 18 |
)
|
| 19 |
|
| 20 |
+
type UploadController interface{
|
| 21 |
+
Upload(ctx *gin.Context)
|
| 22 |
+
GetFileByID(ctx *gin.Context)
|
| 23 |
}
|
| 24 |
|
| 25 |
+
type uploadController struct{uploadService services.UploadService}
|
| 26 |
|
| 27 |
+
func NewUploadController(uploadService services.UploadService) UploadController { return &uploadController{ uploadService: uploadService } }
|
| 28 |
+
|
| 29 |
+
func (c *uploadController) Upload(ctx *gin.Context) {
|
| 30 |
fmt.Println("👉 Content-Type:", ctx.GetHeader("Content-Type"))
|
| 31 |
|
| 32 |
if !strings.Contains(ctx.GetHeader("Content-Type"), "multipart/form-data") {
|
|
|
|
| 165 |
})
|
| 166 |
}
|
| 167 |
|
| 168 |
+
func (c *uploadController) GetFileByID(ctx *gin.Context) {
|
| 169 |
fileIDStr := ctx.Param("id")
|
| 170 |
fileID, err := uuid.Parse(fileIDStr)
|
| 171 |
if err != nil {
|
|
|
|
| 227 |
})
|
| 228 |
}
|
| 229 |
|
| 230 |
+
func (c *uploadController) inferContextFromExt(ext string) string {
|
| 231 |
images := map[string]bool{
|
| 232 |
".jpg": true, ".jpeg": true, ".png": true, ".webp": true,
|
| 233 |
}
|
models/dto/academy_dto.go
CHANGED
|
@@ -5,6 +5,7 @@ import "github.com/google/uuid"
|
|
| 5 |
type CreateAcademyRequest struct {
|
| 6 |
Title string `json:"title" binding:"required"`
|
| 7 |
Slug string `json:"slug"`
|
|
|
|
| 8 |
Description string `json:"description"`
|
| 9 |
ImageUrl string `json:"image_url"`
|
| 10 |
}
|
|
@@ -65,6 +66,7 @@ type AcademyDetailResponse struct {
|
|
| 65 |
Id uuid.UUID `json:"id"`
|
| 66 |
Title string `json:"title"`
|
| 67 |
Slug string `json:"slug"`
|
|
|
|
| 68 |
Description string `json:"description"`
|
| 69 |
ImageUrl string `json:"image_url"`
|
| 70 |
MaterialsCount int64 `json:"materials_count"`
|
|
|
|
| 5 |
type CreateAcademyRequest struct {
|
| 6 |
Title string `json:"title" binding:"required"`
|
| 7 |
Slug string `json:"slug"`
|
| 8 |
+
Code string `json:"code"`
|
| 9 |
Description string `json:"description"`
|
| 10 |
ImageUrl string `json:"image_url"`
|
| 11 |
}
|
|
|
|
| 66 |
Id uuid.UUID `json:"id"`
|
| 67 |
Title string `json:"title"`
|
| 68 |
Slug string `json:"slug"`
|
| 69 |
+
Code string `json:"code"`
|
| 70 |
Description string `json:"description"`
|
| 71 |
ImageUrl string `json:"image_url"`
|
| 72 |
MaterialsCount int64 `json:"materials_count"`
|
models/entity/entity.go
CHANGED
|
@@ -263,6 +263,7 @@ type Academy struct {
|
|
| 263 |
Id uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
| 264 |
Title string `json:"title,omitempty"`
|
| 265 |
Slug string `gorm:"unique" json:"slug,omitempty"`
|
|
|
|
| 266 |
Description string `json:"description,omitempty"`
|
| 267 |
ImageUrl string `json:"image_url,omitempty"`
|
| 268 |
MaterialsCount int64 `json:"materials_count,omitempty"`
|
|
|
|
| 263 |
Id uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
| 264 |
Title string `json:"title,omitempty"`
|
| 265 |
Slug string `gorm:"unique" json:"slug,omitempty"`
|
| 266 |
+
Code string `gorm:"unique" json:"code,omitempty"`
|
| 267 |
Description string `json:"description,omitempty"`
|
| 268 |
ImageUrl string `json:"image_url,omitempty"`
|
| 269 |
MaterialsCount int64 `json:"materials_count,omitempty"`
|
models/error/error.go
CHANGED
|
@@ -30,6 +30,9 @@ var (
|
|
| 30 |
EVENT_FINISHED = errors.New("The event has ended, you are disallowed to take the exam")
|
| 31 |
EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't started yet! You cannot take the exam")
|
| 32 |
EXAMS_SUBMITTED = errors.New("You have submitted the exam, you are disallowed to answer the question")
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
// ================= FILE UPLOAD =================
|
| 35 |
FILE_TOO_LARGE = errors.New("File size exceeds the maximum limit")
|
|
|
|
| 30 |
EVENT_FINISHED = errors.New("The event has ended, you are disallowed to take the exam")
|
| 31 |
EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't started yet! You cannot take the exam")
|
| 32 |
EXAMS_SUBMITTED = errors.New("You have submitted the exam, you are disallowed to answer the question")
|
| 33 |
+
IMAGE_REQUIRED = errors.New("Image is required")
|
| 34 |
+
DESCRIPTION_REQUIRED = errors.New("Description is required")
|
| 35 |
+
CODE_REQUIRED = errors.New("Code is required")
|
| 36 |
|
| 37 |
// ================= FILE UPLOAD =================
|
| 38 |
FILE_TOO_LARGE = errors.New("File size exceeds the maximum limit")
|
provider/controller_provider.go
CHANGED
|
@@ -12,7 +12,7 @@ type ControllerProvider interface {
|
|
| 12 |
ProvideForgotPasswordController() controllers.ForgotPasswordController
|
| 13 |
ProvideOptionController() controllers.OptionController
|
| 14 |
ProvideRegionController() controllers.RegionController
|
| 15 |
-
ProvideUploadController()
|
| 16 |
ProvideAcademyExamController() controllers.AcademyExamController
|
| 17 |
}
|
| 18 |
|
|
@@ -26,7 +26,7 @@ type controllerProvider struct {
|
|
| 26 |
forgotPasswordController controllers.ForgotPasswordController
|
| 27 |
optionController controllers.OptionController
|
| 28 |
regionController controllers.RegionController
|
| 29 |
-
uploadController
|
| 30 |
academyExamController controllers.AcademyExamController
|
| 31 |
}
|
| 32 |
|
|
@@ -96,7 +96,7 @@ func (c *controllerProvider) ProvideRegionController() controllers.RegionControl
|
|
| 96 |
return c.regionController
|
| 97 |
}
|
| 98 |
|
| 99 |
-
func (c *controllerProvider) ProvideUploadController()
|
| 100 |
return c.uploadController
|
| 101 |
}
|
| 102 |
|
|
|
|
| 12 |
ProvideForgotPasswordController() controllers.ForgotPasswordController
|
| 13 |
ProvideOptionController() controllers.OptionController
|
| 14 |
ProvideRegionController() controllers.RegionController
|
| 15 |
+
ProvideUploadController() controllers.UploadController
|
| 16 |
ProvideAcademyExamController() controllers.AcademyExamController
|
| 17 |
}
|
| 18 |
|
|
|
|
| 26 |
forgotPasswordController controllers.ForgotPasswordController
|
| 27 |
optionController controllers.OptionController
|
| 28 |
regionController controllers.RegionController
|
| 29 |
+
uploadController controllers.UploadController
|
| 30 |
academyExamController controllers.AcademyExamController
|
| 31 |
}
|
| 32 |
|
|
|
|
| 96 |
return c.regionController
|
| 97 |
}
|
| 98 |
|
| 99 |
+
func (c *controllerProvider) ProvideUploadController() controllers.UploadController {
|
| 100 |
return c.uploadController
|
| 101 |
}
|
| 102 |
|
services/academy_service.go
CHANGED
|
@@ -37,7 +37,6 @@ type AcademyService interface {
|
|
| 37 |
GetAcademyResponse(ctx context.Context, accountId uuid.UUID, slug string) (*dto.AcademyDetailResponse, error)
|
| 38 |
GetMaterialResponse(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string) (*dto.MaterialDetailResponse, error)
|
| 39 |
}
|
| 40 |
-
|
| 41 |
type academyService struct {
|
| 42 |
academyRepo repositories.AcademyRepository
|
| 43 |
}
|
|
@@ -46,14 +45,6 @@ func NewAcademyService(academyRepo repositories.AcademyRepository) AcademyServic
|
|
| 46 |
return &academyService{academyRepo: academyRepo}
|
| 47 |
}
|
| 48 |
|
| 49 |
-
func timePtrToString(t *time.Time) *string {
|
| 50 |
-
if t == nil {
|
| 51 |
-
return nil
|
| 52 |
-
}
|
| 53 |
-
s := t.Format(time.RFC3339)
|
| 54 |
-
return &s
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
func (s *academyService) GetAcademy(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
|
| 58 |
return s.academyRepo.GetAcademyWithProgress(ctx, accountId, slug)
|
| 59 |
}
|
|
@@ -64,25 +55,42 @@ func (s *academyService) GetAcademyDetail(ctx context.Context, id uuid.UUID) (en
|
|
| 64 |
}
|
| 65 |
|
| 66 |
func (s *academyService) CreateAcademy(ctx context.Context, req dto.CreateAcademyRequest) (entity.Academy, error) {
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
func (s *academyService) UpdateAcademy(ctx context.Context, id uuid.UUID, req dto.UpdateAcademyRequest) (entity.Academy, error) {
|
|
@@ -490,6 +498,7 @@ func (s *academyService) GetAcademyResponse(ctx context.Context, accountId uuid.
|
|
| 490 |
Id: academy.Id,
|
| 491 |
Title: academy.Title,
|
| 492 |
Slug: academy.Slug,
|
|
|
|
| 493 |
Description: academy.Description,
|
| 494 |
ImageUrl: academy.ImageUrl,
|
| 495 |
MaterialsCount: academy.MaterialsCount,
|
|
@@ -500,7 +509,7 @@ func (s *academyService) GetAcademyResponse(ctx context.Context, accountId uuid.
|
|
| 500 |
Status: academyProgress.Status,
|
| 501 |
Progress: academyProgress.Progress,
|
| 502 |
TotalCompletedMaterials: academyProgress.TotalCompletedMaterials,
|
| 503 |
-
CompletedAt:
|
| 504 |
},
|
| 505 |
}
|
| 506 |
|
|
@@ -594,7 +603,7 @@ func (s *academyService) GetMaterialResponse(ctx context.Context, accountId uuid
|
|
| 594 |
Progress: materialProgress.Progress,
|
| 595 |
TotalCompletedContents: materialProgress.TotalCompletedContents,
|
| 596 |
Status: materialProgress.Status,
|
| 597 |
-
CompletedAt:
|
| 598 |
},
|
| 599 |
}
|
| 600 |
|
|
|
|
| 37 |
GetAcademyResponse(ctx context.Context, accountId uuid.UUID, slug string) (*dto.AcademyDetailResponse, error)
|
| 38 |
GetMaterialResponse(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string) (*dto.MaterialDetailResponse, error)
|
| 39 |
}
|
|
|
|
| 40 |
type academyService struct {
|
| 41 |
academyRepo repositories.AcademyRepository
|
| 42 |
}
|
|
|
|
| 45 |
return &academyService{academyRepo: academyRepo}
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
func (s *academyService) GetAcademy(ctx context.Context, accountId uuid.UUID, slug string) (entity.Academy, error) {
|
| 49 |
return s.academyRepo.GetAcademyWithProgress(ctx, accountId, slug)
|
| 50 |
}
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
func (s *academyService) CreateAcademy(ctx context.Context, req dto.CreateAcademyRequest) (entity.Academy, error) {
|
| 58 |
+
if strings.TrimSpace(req.Title) == "" {
|
| 59 |
+
return entity.Academy{}, http_error.TITLE_REQUIRED
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
if strings.TrimSpace(req.Code) == "" {
|
| 63 |
+
return entity.Academy{}, http_error.CODE_REQUIRED
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if strings.TrimSpace(req.Description) == "" {
|
| 67 |
+
return entity.Academy{}, http_error.DESCRIPTION_REQUIRED
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if strings.TrimSpace(req.ImageUrl) == "" {
|
| 71 |
+
return entity.Academy{}, http_error.IMAGE_REQUIRED
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
slugVal := req.Slug
|
| 75 |
+
if slugVal == "" {
|
| 76 |
+
slugVal = slug.Make(req.Title)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
if _, err := s.academyRepo.GetAcademyBySlug(ctx, slugVal); err == nil {
|
| 80 |
+
return entity.Academy{}, http_error.DUPLICATE_DATA
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
a := entity.Academy{
|
| 84 |
+
Id: uuid.New(),
|
| 85 |
+
Title: req.Title,
|
| 86 |
+
Slug: slugVal,
|
| 87 |
+
Code: req.Code,
|
| 88 |
+
Description: req.Description,
|
| 89 |
+
ImageUrl: req.ImageUrl,
|
| 90 |
+
MaterialsCount: 0,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
return s.academyRepo.CreateAcademy(ctx, a)
|
| 94 |
}
|
| 95 |
|
| 96 |
func (s *academyService) UpdateAcademy(ctx context.Context, id uuid.UUID, req dto.UpdateAcademyRequest) (entity.Academy, error) {
|
|
|
|
| 498 |
Id: academy.Id,
|
| 499 |
Title: academy.Title,
|
| 500 |
Slug: academy.Slug,
|
| 501 |
+
Code: academy.Code,
|
| 502 |
Description: academy.Description,
|
| 503 |
ImageUrl: academy.ImageUrl,
|
| 504 |
MaterialsCount: academy.MaterialsCount,
|
|
|
|
| 509 |
Status: academyProgress.Status,
|
| 510 |
Progress: academyProgress.Progress,
|
| 511 |
TotalCompletedMaterials: academyProgress.TotalCompletedMaterials,
|
| 512 |
+
CompletedAt: utils.TimePtrToString(academyProgress.CompletedAt),
|
| 513 |
},
|
| 514 |
}
|
| 515 |
|
|
|
|
| 603 |
Progress: materialProgress.Progress,
|
| 604 |
TotalCompletedContents: materialProgress.TotalCompletedContents,
|
| 605 |
Status: materialProgress.Status,
|
| 606 |
+
CompletedAt: utils.TimePtrToString(materialProgress.CompletedAt),
|
| 607 |
},
|
| 608 |
}
|
| 609 |
|
services/academy_service_helpers.go
DELETED
|
@@ -1,353 +0,0 @@
|
|
| 1 |
-
package services
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
"math"
|
| 6 |
-
"time"
|
| 7 |
-
|
| 8 |
-
"github.com/google/uuid"
|
| 9 |
-
|
| 10 |
-
"abdanhafidz.com/go-boilerplate/models/dto"
|
| 11 |
-
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 12 |
-
"abdanhafidz.com/go-boilerplate/repositories"
|
| 13 |
-
"abdanhafidz.com/go-boilerplate/utils"
|
| 14 |
-
)
|
| 15 |
-
|
| 16 |
-
func (s *academyService) getOrCreateID(id uuid.UUID) uuid.UUID {
|
| 17 |
-
if id == uuid.Nil {
|
| 18 |
-
return uuid.New()
|
| 19 |
-
}
|
| 20 |
-
return id
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
func (s *academyService) calculateProgress(completed, total int64) float64 {
|
| 24 |
-
if total <= 0 {
|
| 25 |
-
return 0
|
| 26 |
-
}
|
| 27 |
-
progress := (float64(completed) / float64(total)) * 100
|
| 28 |
-
return math.Round(progress*100) / 100
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
func (s *academyService) getProgressStatus(progress float64, completed, total int64) string {
|
| 32 |
-
if progress >= 100 || (total > 0 && completed >= total) {
|
| 33 |
-
return entity.StatusCompleted
|
| 34 |
-
}
|
| 35 |
-
if progress > 0 {
|
| 36 |
-
return entity.StatusInProgress
|
| 37 |
-
}
|
| 38 |
-
return entity.StatusNotStarted
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
func (s *academyService) upsertContentProgressSimplified(
|
| 42 |
-
ctx context.Context,
|
| 43 |
-
txRepo repositories.AcademyRepository,
|
| 44 |
-
accountId, academyId, materialId, contentId uuid.UUID,
|
| 45 |
-
) (entity.AcademyContentProgress, error) {
|
| 46 |
-
existing, err := txRepo.GetContentProgress(ctx, accountId, academyId, materialId, contentId)
|
| 47 |
-
if err != nil {
|
| 48 |
-
// propagate non-not-found errors
|
| 49 |
-
return entity.AcademyContentProgress{}, err
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
acp := entity.AcademyContentProgress{
|
| 53 |
-
Id: s.getOrCreateID(existing.Id),
|
| 54 |
-
AccountId: accountId,
|
| 55 |
-
AcademyId: academyId,
|
| 56 |
-
MaterialId: materialId,
|
| 57 |
-
ContentId: contentId,
|
| 58 |
-
Status: entity.StatusCompleted,
|
| 59 |
-
CompletedAt: utils.Ptr(time.Now()),
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
if _, err := txRepo.UpsertContentProgress(ctx, acp); err != nil {
|
| 63 |
-
return entity.AcademyContentProgress{}, err
|
| 64 |
-
}
|
| 65 |
-
return acp, nil
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
func (s *academyService) calculateMaterialProgress(
|
| 69 |
-
ctx context.Context,
|
| 70 |
-
txRepo repositories.AcademyRepository,
|
| 71 |
-
accountId, academyId, materialId uuid.UUID,
|
| 72 |
-
totalCompleted, totalContents int64,
|
| 73 |
-
) entity.AcademyMaterialProgress {
|
| 74 |
-
existing, _ := txRepo.GetMaterialProgress(ctx, accountId, academyId, materialId)
|
| 75 |
-
|
| 76 |
-
progress := s.calculateProgress(totalCompleted, totalContents)
|
| 77 |
-
status := s.getProgressStatus(progress, totalCompleted, totalContents)
|
| 78 |
-
|
| 79 |
-
var completedAt *time.Time
|
| 80 |
-
if status == entity.StatusCompleted {
|
| 81 |
-
completedAt = utils.Ptr(time.Now())
|
| 82 |
-
progress = 100
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
return entity.AcademyMaterialProgress{
|
| 86 |
-
Id: s.getOrCreateID(existing.Id),
|
| 87 |
-
AccountId: accountId,
|
| 88 |
-
AcademyId: academyId,
|
| 89 |
-
MaterialId: materialId,
|
| 90 |
-
Progress: progress,
|
| 91 |
-
TotalCompletedContents: uint(totalCompleted),
|
| 92 |
-
Status: status,
|
| 93 |
-
CompletedAt: completedAt,
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
func (s *academyService) calculateAcademyProgress(
|
| 99 |
-
ctx context.Context,
|
| 100 |
-
txRepo repositories.AcademyRepository,
|
| 101 |
-
accountId, academyId uuid.UUID,
|
| 102 |
-
accumulatedProgress float64,
|
| 103 |
-
totalMaterials int64,
|
| 104 |
-
) entity.AcademyProgress {
|
| 105 |
-
existing, _ := txRepo.GetAcademyProgress(ctx, accountId, academyId)
|
| 106 |
-
|
| 107 |
-
var progress float64
|
| 108 |
-
if totalMaterials > 0 {
|
| 109 |
-
progress = math.Round((accumulatedProgress/float64(totalMaterials))*100) / 100
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
status := s.getProgressStatus(progress, 0, totalMaterials)
|
| 113 |
-
|
| 114 |
-
var completedAt *time.Time
|
| 115 |
-
if status == entity.StatusCompleted {
|
| 116 |
-
completedAt = utils.Ptr(time.Now())
|
| 117 |
-
progress = 100
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
totalCompleted, _ := txRepo.CountCompletedMaterialsByAcademyAndAccount(ctx, accountId, academyId)
|
| 121 |
-
|
| 122 |
-
return entity.AcademyProgress{
|
| 123 |
-
Id: s.getOrCreateID(existing.Id),
|
| 124 |
-
AccountId: accountId,
|
| 125 |
-
AcademyId: academyId,
|
| 126 |
-
Progress: progress,
|
| 127 |
-
TotalCompletedMaterials: uint(totalCompleted),
|
| 128 |
-
Status: status,
|
| 129 |
-
CompletedAt: completedAt,
|
| 130 |
-
}
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
func (s *academyService) updateAcademyMaterialCount(ctx context.Context, txRepo repositories.AcademyRepository, academyId uuid.UUID) error {
|
| 134 |
-
count, err := txRepo.CountMaterialsByAcademyID(ctx, academyId)
|
| 135 |
-
if err != nil { return err }
|
| 136 |
-
academy, err := txRepo.GetAcademyByID(ctx, academyId)
|
| 137 |
-
if err != nil { return err }
|
| 138 |
-
academy.MaterialsCount = count
|
| 139 |
-
_, err = txRepo.UpdateAcademy(ctx, academy)
|
| 140 |
-
return err
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
func (s *academyService) updateMaterialContentCount(ctx context.Context, txRepo repositories.AcademyRepository, materialId uuid.UUID) error {
|
| 145 |
-
count, err := txRepo.CountContentsByMaterialID(ctx, materialId)
|
| 146 |
-
if err != nil { return err }
|
| 147 |
-
material, err := txRepo.GetMaterialByID(ctx, materialId)
|
| 148 |
-
if err != nil { return err }
|
| 149 |
-
material.ContentsCount = count
|
| 150 |
-
_, err = txRepo.UpdateMaterial(ctx, material)
|
| 151 |
-
return err
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
func formatTime(t *time.Time) *string {
|
| 155 |
-
if t == nil {
|
| 156 |
-
return nil
|
| 157 |
-
}
|
| 158 |
-
formatted := t.Format("2006-01-02T15:04:05Z07:00")
|
| 159 |
-
return &formatted
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
func buildAcademyProgressResponse(ap entity.AcademyProgress) *dto.AcademyProgressResponse {
|
| 164 |
-
if ap.Id == uuid.Nil {
|
| 165 |
-
return nil
|
| 166 |
-
}
|
| 167 |
-
return &dto.AcademyProgressResponse{
|
| 168 |
-
Id: ap.Id,
|
| 169 |
-
AccountId: ap.AccountId,
|
| 170 |
-
AcademyId: ap.AcademyId,
|
| 171 |
-
Status: ap.Status,
|
| 172 |
-
Progress: ap.Progress,
|
| 173 |
-
TotalCompletedMaterials: ap.TotalCompletedMaterials,
|
| 174 |
-
CompletedAt: formatTime(ap.CompletedAt),
|
| 175 |
-
}
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
func buildAcademyContentResponse(content entity.AcademyContent, progress entity.AcademyContentProgress) dto.AcademyContentResponse {
|
| 179 |
-
status := entity.StatusNotStarted
|
| 180 |
-
if progress.Id != uuid.Nil {
|
| 181 |
-
status = progress.Status
|
| 182 |
-
}
|
| 183 |
-
return dto.AcademyContentResponse{
|
| 184 |
-
Id: content.Id,
|
| 185 |
-
Order: content.Order,
|
| 186 |
-
Title: content.Title,
|
| 187 |
-
Status: status,
|
| 188 |
-
}
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
func buildAcademyMaterialResponse(
|
| 193 |
-
material entity.AcademyMaterial,
|
| 194 |
-
contents []entity.AcademyContent,
|
| 195 |
-
progressMap map[uuid.UUID]entity.AcademyContentProgress,
|
| 196 |
-
progress entity.AcademyMaterialProgress,
|
| 197 |
-
) dto.AcademyMaterialResponse {
|
| 198 |
-
status := entity.StatusNotStarted
|
| 199 |
-
if progress.Id != uuid.Nil {
|
| 200 |
-
status = progress.Status
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
contentDTOs := make([]dto.AcademyContentResponse, 0)
|
| 204 |
-
for _, content := range contents {
|
| 205 |
-
contentProgress := progressMap[content.Id]
|
| 206 |
-
contentDTOs = append(contentDTOs, buildAcademyContentResponse(content, contentProgress))
|
| 207 |
-
}
|
| 208 |
-
|
| 209 |
-
return dto.AcademyMaterialResponse{
|
| 210 |
-
Id: material.Id,
|
| 211 |
-
Order: material.Order,
|
| 212 |
-
Title: material.Title,
|
| 213 |
-
Slug: material.Slug,
|
| 214 |
-
Status: status,
|
| 215 |
-
Progress: progress.Progress,
|
| 216 |
-
TotalCompletedContents: progress.TotalCompletedContents,
|
| 217 |
-
ContentsCount: material.ContentsCount,
|
| 218 |
-
Contents: contentDTOs,
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
func buildAcademyDetailResponse(
|
| 223 |
-
academy entity.Academy,
|
| 224 |
-
materials []entity.AcademyMaterial,
|
| 225 |
-
academyProgress entity.AcademyProgress,
|
| 226 |
-
ctx context.Context,
|
| 227 |
-
repo repositories.AcademyRepository,
|
| 228 |
-
) *dto.AcademyDetailResponse {
|
| 229 |
-
materialDTOs := make([]dto.AcademyMaterialResponse, 0)
|
| 230 |
-
|
| 231 |
-
materialIds := make([]uuid.UUID, 0)
|
| 232 |
-
contentIds := make([]uuid.UUID, 0)
|
| 233 |
-
|
| 234 |
-
for _, material := range materials {
|
| 235 |
-
materialIds = append(materialIds, material.Id)
|
| 236 |
-
for _, content := range material.Contents {
|
| 237 |
-
contentIds = append(contentIds, content.Id)
|
| 238 |
-
}
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
materialProgressMap, _ := repo.GetMaterialProgressBatch(ctx, academyProgress.AccountId, academyProgress.AcademyId, materialIds)
|
| 242 |
-
contentProgressMap, _ := repo.GetContentProgressBatch(ctx, academyProgress.AccountId, academyProgress.AcademyId, contentIds)
|
| 243 |
-
|
| 244 |
-
for _, material := range materials {
|
| 245 |
-
materialProgress := materialProgressMap[material.Id]
|
| 246 |
-
contentDTOs := make([]dto.AcademyContentResponse, 0)
|
| 247 |
-
|
| 248 |
-
for _, content := range material.Contents {
|
| 249 |
-
contentProgress := contentProgressMap[content.Id]
|
| 250 |
-
contentDTOs = append(contentDTOs, buildAcademyContentResponse(content, contentProgress))
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
materialDTOs = append(materialDTOs, dto.AcademyMaterialResponse{
|
| 254 |
-
Id: material.Id,
|
| 255 |
-
Order: material.Order,
|
| 256 |
-
Title: material.Title,
|
| 257 |
-
Slug: material.Slug,
|
| 258 |
-
Status: getContentStatus(materialProgress),
|
| 259 |
-
Progress: materialProgress.Progress,
|
| 260 |
-
TotalCompletedContents: materialProgress.TotalCompletedContents,
|
| 261 |
-
ContentsCount: material.ContentsCount,
|
| 262 |
-
Contents: contentDTOs,
|
| 263 |
-
})
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
return &dto.AcademyDetailResponse{
|
| 267 |
-
Id: academy.Id,
|
| 268 |
-
Title: academy.Title,
|
| 269 |
-
Slug: academy.Slug,
|
| 270 |
-
Description: academy.Description,
|
| 271 |
-
ImageUrl: academy.ImageUrl,
|
| 272 |
-
MaterialsCount: academy.MaterialsCount,
|
| 273 |
-
UserProgress: buildAcademyProgressResponse(academyProgress),
|
| 274 |
-
Materials: materialDTOs,
|
| 275 |
-
}
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
func getContentStatus(progress entity.AcademyMaterialProgress) string {
|
| 279 |
-
if progress.Id != uuid.Nil {
|
| 280 |
-
return progress.Status
|
| 281 |
-
}
|
| 282 |
-
return entity.StatusNotStarted
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
func buildMaterialProgressResponse(mp entity.AcademyMaterialProgress) *dto.MaterialProgressResponse {
|
| 286 |
-
if mp.Id == uuid.Nil {
|
| 287 |
-
return nil
|
| 288 |
-
}
|
| 289 |
-
return &dto.MaterialProgressResponse{
|
| 290 |
-
Id: mp.Id,
|
| 291 |
-
AccountId: mp.AccountId,
|
| 292 |
-
AcademyId: mp.AcademyId,
|
| 293 |
-
MaterialId: mp.MaterialId,
|
| 294 |
-
Progress: mp.Progress,
|
| 295 |
-
TotalCompletedContents: mp.TotalCompletedContents,
|
| 296 |
-
Status: mp.Status,
|
| 297 |
-
CompletedAt: formatTime(mp.CompletedAt),
|
| 298 |
-
}
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
func buildContentDetailResponse(content entity.AcademyContent, progress entity.AcademyContentProgress) dto.ContentDetailResponse {
|
| 302 |
-
status := entity.StatusNotStarted
|
| 303 |
-
if progress.Id != uuid.Nil {
|
| 304 |
-
status = progress.Status
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
return dto.ContentDetailResponse{
|
| 308 |
-
Id: content.Id,
|
| 309 |
-
Order: content.Order,
|
| 310 |
-
Title: content.Title,
|
| 311 |
-
Status: status,
|
| 312 |
-
}
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
func buildMaterialDetailResponse(
|
| 317 |
-
material entity.AcademyMaterial,
|
| 318 |
-
contents []entity.AcademyContent,
|
| 319 |
-
materialProgress entity.AcademyMaterialProgress,
|
| 320 |
-
accountId, academyId uuid.UUID,
|
| 321 |
-
ctx context.Context,
|
| 322 |
-
repo repositories.AcademyRepository,
|
| 323 |
-
academySlug, materialSlug string,
|
| 324 |
-
) *dto.MaterialDetailResponse {
|
| 325 |
-
contentDTOs := make([]dto.ContentDetailResponse, 0)
|
| 326 |
-
contentIds := make([]uuid.UUID, len(contents))
|
| 327 |
-
for i, c := range contents {
|
| 328 |
-
contentIds[i] = c.Id
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
contentProgressMap, _ := repo.GetContentProgressBatch(ctx, accountId, academyId, contentIds)
|
| 332 |
-
|
| 333 |
-
for _, content := range contents {
|
| 334 |
-
contentProgress := contentProgressMap[content.Id]
|
| 335 |
-
contentDTOs = append(contentDTOs, buildContentDetailResponse(content, contentProgress))
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
return &dto.MaterialDetailResponse{
|
| 339 |
-
Id: material.Id,
|
| 340 |
-
AcademyId: material.AcademyId,
|
| 341 |
-
Title: material.Title,
|
| 342 |
-
Slug: material.Slug,
|
| 343 |
-
Description: material.Description,
|
| 344 |
-
Order: material.Order,
|
| 345 |
-
ContentsCount: material.ContentsCount,
|
| 346 |
-
Progress: buildMaterialProgressResponse(materialProgress),
|
| 347 |
-
Contents: contentDTOs,
|
| 348 |
-
Meta: map[string]string{
|
| 349 |
-
"academy_slug": academySlug,
|
| 350 |
-
"material_slug": materialSlug,
|
| 351 |
-
},
|
| 352 |
-
}
|
| 353 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/upload_service.go
CHANGED
|
@@ -6,6 +6,7 @@ import (
|
|
| 6 |
"fmt"
|
| 7 |
"io"
|
| 8 |
"mime/multipart"
|
|
|
|
| 9 |
"path/filepath"
|
| 10 |
"regexp"
|
| 11 |
"strings"
|
|
@@ -89,7 +90,8 @@ func (s *uploadService) GetFileByID(ctx context.Context, fileID uuid.UUID, accou
|
|
| 89 |
}
|
| 90 |
|
| 91 |
func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multipart.FileHeader, config config.UploadRule, uploadContext string, accountID uuid.UUID) (*entity.File, error) {
|
| 92 |
-
|
|
|
|
| 93 |
return nil, err
|
| 94 |
}
|
| 95 |
|
|
@@ -103,8 +105,7 @@ func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multi
|
|
| 103 |
}
|
| 104 |
defer src.Close()
|
| 105 |
|
| 106 |
-
|
| 107 |
-
publicURL, err := s.storageProvider.UploadFile(ctx, src, storagePath, contentType)
|
| 108 |
if err != nil {
|
| 109 |
return nil, http_error.UPLOAD_FAILED
|
| 110 |
}
|
|
@@ -113,7 +114,7 @@ func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multi
|
|
| 113 |
Id: uuid.New(),
|
| 114 |
OriginalName: fileHeader.Filename,
|
| 115 |
StoredName: storedFilename,
|
| 116 |
-
MimeType:
|
| 117 |
Size: fileHeader.Size,
|
| 118 |
Path: publicURL,
|
| 119 |
Context: uploadContext,
|
|
@@ -128,22 +129,70 @@ func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multi
|
|
| 128 |
return fileEntity, nil
|
| 129 |
}
|
| 130 |
|
| 131 |
-
func (s *uploadService) validateFile(file *multipart.FileHeader, config config.UploadRule) error {
|
| 132 |
if file.Size == 0 || file.Size > config.MaxBytes {
|
| 133 |
-
return http_error.FILE_TOO_LARGE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
|
|
|
|
|
|
|
| 136 |
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(file.Filename)))
|
| 137 |
if !config.AllowedExts[ext] {
|
| 138 |
-
return http_error.INVALID_FILE_TYPE
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
blockedExts := map[string]bool{".exe": true, ".sh": true, ".bat": true, ".php": true}
|
| 142 |
if blockedExts[ext] {
|
| 143 |
-
return http_error.INVALID_FILE_TYPE
|
| 144 |
}
|
| 145 |
|
| 146 |
-
return nil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
}
|
| 148 |
|
| 149 |
func (s *uploadService) generateStoredFilename(originalName string, ext string) string {
|
|
@@ -212,4 +261,4 @@ func (s *uploadService) UploadRawFile(ctx context.Context, reader io.Reader, ori
|
|
| 212 |
}
|
| 213 |
|
| 214 |
return fileEntity, nil
|
| 215 |
-
}
|
|
|
|
| 6 |
"fmt"
|
| 7 |
"io"
|
| 8 |
"mime/multipart"
|
| 9 |
+
"net/http"
|
| 10 |
"path/filepath"
|
| 11 |
"regexp"
|
| 12 |
"strings"
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
func (s *uploadService) processSingleFile(ctx context.Context, fileHeader *multipart.FileHeader, config config.UploadRule, uploadContext string, accountID uuid.UUID) (*entity.File, error) {
|
| 93 |
+
detectedMimeType, err := s.validateFile(fileHeader, config)
|
| 94 |
+
if err != nil {
|
| 95 |
return nil, err
|
| 96 |
}
|
| 97 |
|
|
|
|
| 105 |
}
|
| 106 |
defer src.Close()
|
| 107 |
|
| 108 |
+
publicURL, err := s.storageProvider.UploadFile(ctx, src, storagePath, detectedMimeType)
|
|
|
|
| 109 |
if err != nil {
|
| 110 |
return nil, http_error.UPLOAD_FAILED
|
| 111 |
}
|
|
|
|
| 114 |
Id: uuid.New(),
|
| 115 |
OriginalName: fileHeader.Filename,
|
| 116 |
StoredName: storedFilename,
|
| 117 |
+
MimeType: detectedMimeType,
|
| 118 |
Size: fileHeader.Size,
|
| 119 |
Path: publicURL,
|
| 120 |
Context: uploadContext,
|
|
|
|
| 129 |
return fileEntity, nil
|
| 130 |
}
|
| 131 |
|
| 132 |
+
func (s *uploadService) validateFile(file *multipart.FileHeader, config config.UploadRule) (string, error) {
|
| 133 |
if file.Size == 0 || file.Size > config.MaxBytes {
|
| 134 |
+
return "", http_error.FILE_TOO_LARGE
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
src, err := file.Open()
|
| 138 |
+
if err != nil {
|
| 139 |
+
return "", http_error.INTERNAL_SERVER_ERROR
|
| 140 |
+
}
|
| 141 |
+
defer src.Close()
|
| 142 |
+
|
| 143 |
+
buffer := make([]byte, 512)
|
| 144 |
+
_, err = src.Read(buffer)
|
| 145 |
+
if err != nil && err != io.EOF {
|
| 146 |
+
return "", http_error.INTERNAL_SERVER_ERROR
|
| 147 |
}
|
| 148 |
|
| 149 |
+
detectedMimeType := http.DetectContentType(buffer)
|
| 150 |
+
|
| 151 |
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(file.Filename)))
|
| 152 |
if !config.AllowedExts[ext] {
|
| 153 |
+
return "", http_error.INVALID_FILE_TYPE
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
if !isValidMimeForExt(ext, detectedMimeType) {
|
| 157 |
+
return "", http_error.INVALID_FILE_TYPE
|
| 158 |
}
|
| 159 |
|
| 160 |
blockedExts := map[string]bool{".exe": true, ".sh": true, ".bat": true, ".php": true}
|
| 161 |
if blockedExts[ext] {
|
| 162 |
+
return "", http_error.INVALID_FILE_TYPE
|
| 163 |
}
|
| 164 |
|
| 165 |
+
return detectedMimeType, nil
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
func isValidMimeForExt(ext string, mimeType string) bool {
|
| 169 |
+
baseMime := strings.Split(mimeType, ";")[0]
|
| 170 |
+
baseMime = strings.TrimSpace(baseMime)
|
| 171 |
+
|
| 172 |
+
switch ext {
|
| 173 |
+
case ".jpg", ".jpeg":
|
| 174 |
+
return baseMime == "image/jpeg"
|
| 175 |
+
case ".png":
|
| 176 |
+
return baseMime == "image/png"
|
| 177 |
+
case ".gif":
|
| 178 |
+
return baseMime == "image/gif"
|
| 179 |
+
case ".pdf":
|
| 180 |
+
return baseMime == "application/pdf"
|
| 181 |
+
case ".txt":
|
| 182 |
+
return baseMime == "text/plain"
|
| 183 |
+
case ".c", ".cpp":
|
| 184 |
+
return baseMime == "submissions/x-c++src"
|
| 185 |
+
case ".py":
|
| 186 |
+
return baseMime == "submissions/x-python3src"
|
| 187 |
+
case ".java":
|
| 188 |
+
return baseMime == "submissions/x-java8src"
|
| 189 |
+
case ".cs":
|
| 190 |
+
return baseMime == "submissions/x-csharp8src"
|
| 191 |
+
case ".js":
|
| 192 |
+
return baseMime == "submissions/x-javascriptsrc"
|
| 193 |
+
default:
|
| 194 |
+
return false
|
| 195 |
+
}
|
| 196 |
}
|
| 197 |
|
| 198 |
func (s *uploadService) generateStoredFilename(originalName string, ext string) string {
|
|
|
|
| 261 |
}
|
| 262 |
|
| 263 |
return fileEntity, nil
|
| 264 |
+
}
|
utils/utils.go
CHANGED
|
@@ -35,3 +35,11 @@ func CalculateRemainingTime(startTime, dueTime time.Time) int {
|
|
| 35 |
func Ptr[T any](v T) *T {
|
| 36 |
return &v
|
| 37 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
func Ptr[T any](v T) *T {
|
| 36 |
return &v
|
| 37 |
}
|
| 38 |
+
|
| 39 |
+
func TimePtrToString(t *time.Time) *string {
|
| 40 |
+
if t == nil {
|
| 41 |
+
return nil
|
| 42 |
+
}
|
| 43 |
+
s := t.Format(time.RFC3339)
|
| 44 |
+
return &s
|
| 45 |
+
}
|