Spaces:
Running
Running
Commit ·
36c99b0
1
Parent(s): a6ebab6
Deploy files from GitHub repository
Browse files- .gitignore +2 -1
- config/supabase.go +0 -19
- config/supabase_config.go +25 -0
- config/upload_config.go +49 -0
- controllers/academy_controller.go +0 -2
- controllers/upload_controller.go +3 -5
- models/entity/contant.go +3 -1
- models/entity/entity.go +13 -13
- provider/provider.go +6 -5
- provider/services_provider.go +8 -6
- provider/storage_provider.go +10 -0
- provider/supabase_storage.go +0 -47
- repositories/academy_repository.go +6 -99
- services/academy_service.go +22 -25
- services/academy_service_helpers.go +27 -23
- services/account_service.go +4 -2
- services/supabase_storage_service.go +27 -0
- services/upload_service.go +73 -132
- space/.gitignore +2 -1
.gitignore
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
-
/.env
|
|
|
|
|
|
| 1 |
+
/.env
|
| 2 |
+
go-boilerplate.exe
|
config/supabase.go
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
package config
|
| 2 |
-
|
| 3 |
-
type Supabase interface{
|
| 4 |
-
NewSupabaseConfig(url string, key string, bucket string)
|
| 5 |
-
}
|
| 6 |
-
|
| 7 |
-
type SupabaseConfig struct {
|
| 8 |
-
URL string
|
| 9 |
-
ServiceKey string
|
| 10 |
-
BucketName string
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
func NewSupabaseConfig(url string, key string, bucket string) SupabaseConfig {
|
| 14 |
-
return SupabaseConfig{
|
| 15 |
-
URL: url,
|
| 16 |
-
ServiceKey: key,
|
| 17 |
-
BucketName: bucket,
|
| 18 |
-
}
|
| 19 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config/supabase_config.go
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package config
|
| 2 |
+
|
| 3 |
+
type SupabaseConfig interface {
|
| 4 |
+
GetURL() string
|
| 5 |
+
GetServiceKey() string
|
| 6 |
+
GetBucketName() string
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
type supabaseConfig struct {
|
| 10 |
+
url string
|
| 11 |
+
serviceKey string
|
| 12 |
+
bucketName string
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
func NewSupabaseConfig(url string, key string, bucket string) SupabaseConfig {
|
| 16 |
+
return &supabaseConfig{
|
| 17 |
+
url: url,
|
| 18 |
+
serviceKey: key,
|
| 19 |
+
bucketName: bucket,
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func (c *supabaseConfig) GetURL() string { return c.url }
|
| 24 |
+
func (c *supabaseConfig) GetServiceKey() string { return c.serviceKey }
|
| 25 |
+
func (c *supabaseConfig) GetBucketName() string { return c.bucketName }
|
config/upload_config.go
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package config
|
| 2 |
+
|
| 3 |
+
import models "abdanhafidz.com/go-boilerplate/models/entity"
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
type UploadRule struct {
|
| 7 |
+
MaxBytes int64
|
| 8 |
+
AllowedExts map[string]bool
|
| 9 |
+
PathPrefix string
|
| 10 |
+
MaxCount int
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
type UploadConfig interface {
|
| 14 |
+
Get(contextType string) (UploadRule, error)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
type uploadConfig struct{}
|
| 18 |
+
|
| 19 |
+
func NewUploadConfig() UploadConfig { return &uploadConfig{} }
|
| 20 |
+
|
| 21 |
+
func (c *uploadConfig) Get(contextType string) (UploadRule, error) {
|
| 22 |
+
codeExts := map[string]bool{".cpp": true, ".c": true, ".py": true, ".java": true, ".go": true, ".js": true, ".txt": true}
|
| 23 |
+
imgExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".webp": true, ".gif": true}
|
| 24 |
+
docExts := map[string]bool{".pdf": true, ".doc": true, ".docx": true}
|
| 25 |
+
|
| 26 |
+
allExts := make(map[string]bool)
|
| 27 |
+
for k, v := range codeExts { allExts[k] = v }
|
| 28 |
+
for k, v := range imgExts { allExts[k] = v }
|
| 29 |
+
for k, v := range docExts { allExts[k] = v }
|
| 30 |
+
|
| 31 |
+
switch contextType {
|
| 32 |
+
case "image":
|
| 33 |
+
return UploadRule{ MaxBytes: 10 * models.MB, AllowedExts: imgExts, PathPrefix: "images", MaxCount: 5 }, nil
|
| 34 |
+
case "material":
|
| 35 |
+
return UploadRule{ MaxBytes: 10 * models.MB, AllowedExts: docExts, PathPrefix: "materials", MaxCount: 1 }, nil
|
| 36 |
+
case "submission":
|
| 37 |
+
return UploadRule{ MaxBytes: 1 * models.MB, AllowedExts: codeExts, PathPrefix: "submissions", MaxCount: 1 }, nil
|
| 38 |
+
case "general":
|
| 39 |
+
return UploadRule{ MaxBytes: 5 * models.MB, AllowedExts: allExts, PathPrefix: "temp", MaxCount: 5 }, nil
|
| 40 |
+
default:
|
| 41 |
+
return UploadRule{}, ErrInvalidUploadContext
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
var ErrInvalidUploadContext = errInvalidUploadContext{}
|
| 46 |
+
|
| 47 |
+
type errInvalidUploadContext struct{}
|
| 48 |
+
|
| 49 |
+
func (e errInvalidUploadContext) Error() string { return "invalid upload context" }
|
controllers/academy_controller.go
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
package controllers
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
"fmt"
|
| 5 |
"net/http"
|
| 6 |
"strconv"
|
| 7 |
|
|
@@ -77,7 +76,6 @@ func (c *academyController) ListAcademies(ctx *gin.Context) {
|
|
| 77 |
return
|
| 78 |
}
|
| 79 |
|
| 80 |
-
fmt.Println("Account ID in ListAcademies:", accountId)
|
| 81 |
res, err := c.academyService.ListAcademies(ctx.Request.Context(), accountId)
|
| 82 |
ResponseJSON(ctx, gin.H{}, res, err)
|
| 83 |
}
|
|
|
|
| 1 |
package controllers
|
| 2 |
|
| 3 |
import (
|
|
|
|
| 4 |
"net/http"
|
| 5 |
"strconv"
|
| 6 |
|
|
|
|
| 76 |
return
|
| 77 |
}
|
| 78 |
|
|
|
|
| 79 |
res, err := c.academyService.ListAcademies(ctx.Request.Context(), accountId)
|
| 80 |
ResponseJSON(ctx, gin.H{}, res, err)
|
| 81 |
}
|
controllers/upload_controller.go
CHANGED
|
@@ -18,12 +18,10 @@ import (
|
|
| 18 |
)
|
| 19 |
|
| 20 |
type UploadController struct {
|
| 21 |
-
|
| 22 |
}
|
| 23 |
|
| 24 |
-
func NewUploadController(s
|
| 25 |
-
return &UploadController{uploadService: s}
|
| 26 |
-
}
|
| 27 |
|
| 28 |
func (c *UploadController) Upload(ctx *gin.Context) {
|
| 29 |
fmt.Println("👉 Content-Type:", ctx.GetHeader("Content-Type"))
|
|
@@ -248,4 +246,4 @@ func (c *UploadController) inferContextFromExt(ext string) string {
|
|
| 248 |
return "material"
|
| 249 |
}
|
| 250 |
return ""
|
| 251 |
-
}
|
|
|
|
| 18 |
)
|
| 19 |
|
| 20 |
type UploadController struct {
|
| 21 |
+
uploadService services.UploadService
|
| 22 |
}
|
| 23 |
|
| 24 |
+
func NewUploadController(s services.UploadService) *UploadController { return &UploadController{ uploadService: s } }
|
|
|
|
|
|
|
| 25 |
|
| 26 |
func (c *UploadController) Upload(ctx *gin.Context) {
|
| 27 |
fmt.Println("👉 Content-Type:", ctx.GetHeader("Content-Type"))
|
|
|
|
| 246 |
return "material"
|
| 247 |
}
|
| 248 |
return ""
|
| 249 |
+
}
|
models/entity/contant.go
CHANGED
|
@@ -4,4 +4,6 @@ const (
|
|
| 4 |
StatusNotStarted = "NOT_STARTED"
|
| 5 |
StatusInProgress = "IN_PROGRESS"
|
| 6 |
StatusCompleted = "COMPLETED"
|
| 7 |
-
)
|
|
|
|
|
|
|
|
|
| 4 |
StatusNotStarted = "NOT_STARTED"
|
| 5 |
StatusInProgress = "IN_PROGRESS"
|
| 6 |
StatusCompleted = "COMPLETED"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
const MB = 1024 * 1024
|
models/entity/entity.go
CHANGED
|
@@ -267,7 +267,7 @@ type Academy struct {
|
|
| 267 |
ImageUrl string `json:"image_url,omitempty"`
|
| 268 |
MaterialsCount int64 `json:"materials_count,omitempty"`
|
| 269 |
Materials []AcademyMaterial `gorm:"foreignKey:AcademyId;references:Id" json:"materials,omitempty"`
|
| 270 |
-
|
| 271 |
}
|
| 272 |
|
| 273 |
func (Academy) TableName() string { return "academy" }
|
|
@@ -300,9 +300,9 @@ func (AcademyContent) TableName() string { return "academy_contents" }
|
|
| 300 |
// Progress
|
| 301 |
|
| 302 |
type AcademyProgress struct {
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
Status string `gorm:"type:varchar(50);default:'not attempted'" json:"status,omitempty"`
|
| 307 |
Progress float64 `gorm:"default:0" json:"progress"`
|
| 308 |
TotalCompletedMaterials uint `gorm:"default:0" json:"total_completed_materials"`
|
|
@@ -312,10 +312,10 @@ type AcademyProgress struct {
|
|
| 312 |
func (AcademyProgress) TableName() string { return "academy_progress" }
|
| 313 |
|
| 314 |
type AcademyMaterialProgress struct {
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
Progress float64 `gorm:"default:0" json:"progress,omitempty"`
|
| 320 |
TotalCompletedContents uint `gorm:"default:0" json:"total_completed_contents,omitempty"`
|
| 321 |
Status string `gorm:"type:varchar(50);default:'not attempted'" json:"status,omitempty"`
|
|
@@ -325,11 +325,11 @@ type AcademyMaterialProgress struct {
|
|
| 325 |
func (AcademyMaterialProgress) TableName() string { return "academy_material_progress" }
|
| 326 |
|
| 327 |
type AcademyContentProgress struct {
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
Status string `gorm:"type:varchar(50);default:'not attempted'" json:"status,omitempty"`
|
| 334 |
CompletedAt *time.Time `json:"completed_at"`
|
| 335 |
}
|
|
|
|
| 267 |
ImageUrl string `json:"image_url,omitempty"`
|
| 268 |
MaterialsCount int64 `json:"materials_count,omitempty"`
|
| 269 |
Materials []AcademyMaterial `gorm:"foreignKey:AcademyId;references:Id" json:"materials,omitempty"`
|
| 270 |
+
AcademyProgress AcademyProgress `gorm:"foreignKey:AcademyId;references:Id" json:"academy_progresses,omitempty"`
|
| 271 |
}
|
| 272 |
|
| 273 |
func (Academy) TableName() string { return "academy" }
|
|
|
|
| 300 |
// Progress
|
| 301 |
|
| 302 |
type AcademyProgress struct {
|
| 303 |
+
Id uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
| 304 |
+
AccountId uuid.UUID `gorm:"type:uuid;uniqueIndex:idx_account_academy" json:"account_id,omitempty"`
|
| 305 |
+
AcademyId uuid.UUID `gorm:"type:uuid;uniqueIndex:idx_account_academy" json:"academy_id,omitempty"`
|
| 306 |
Status string `gorm:"type:varchar(50);default:'not attempted'" json:"status,omitempty"`
|
| 307 |
Progress float64 `gorm:"default:0" json:"progress"`
|
| 308 |
TotalCompletedMaterials uint `gorm:"default:0" json:"total_completed_materials"`
|
|
|
|
| 312 |
func (AcademyProgress) TableName() string { return "academy_progress" }
|
| 313 |
|
| 314 |
type AcademyMaterialProgress struct {
|
| 315 |
+
Id uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
| 316 |
+
AccountId uuid.UUID `gorm:"type:uuid;uniqueIndex:idx_account_material" json:"account_id,omitempty"`
|
| 317 |
+
AcademyId uuid.UUID `gorm:"type:uuid;index" json:"academy_id,omitempty"`
|
| 318 |
+
MaterialId uuid.UUID `gorm:"type:uuid;uniqueIndex:idx_account_material" json:"material_id,omitempty"`
|
| 319 |
Progress float64 `gorm:"default:0" json:"progress,omitempty"`
|
| 320 |
TotalCompletedContents uint `gorm:"default:0" json:"total_completed_contents,omitempty"`
|
| 321 |
Status string `gorm:"type:varchar(50);default:'not attempted'" json:"status,omitempty"`
|
|
|
|
| 325 |
func (AcademyMaterialProgress) TableName() string { return "academy_material_progress" }
|
| 326 |
|
| 327 |
type AcademyContentProgress struct {
|
| 328 |
+
Id uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
| 329 |
+
AccountId uuid.UUID `gorm:"type:uuid;uniqueIndex:idx_account_content" json:"account_id,omitempty"`
|
| 330 |
+
AcademyId uuid.UUID `gorm:"type:uuid;index" json:"academy_id,omitempty"`
|
| 331 |
+
MaterialId uuid.UUID `gorm:"type:uuid;index" json:"material_id,omitempty"`
|
| 332 |
+
ContentId uuid.UUID `gorm:"type:uuid;uniqueIndex:idx_account_content" json:"content_id,omitempty"`
|
| 333 |
Status string `gorm:"type:varchar(50);default:'not attempted'" json:"status,omitempty"`
|
| 334 |
CompletedAt *time.Time `json:"completed_at"`
|
| 335 |
}
|
provider/provider.go
CHANGED
|
@@ -5,6 +5,7 @@ import (
|
|
| 5 |
"strings"
|
| 6 |
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
"github.com/gin-gonic/gin"
|
|
|
|
| 8 |
)
|
| 9 |
|
| 10 |
type AppProvider interface {
|
|
@@ -30,16 +31,16 @@ func NewAppProvider() AppProvider {
|
|
| 30 |
configProvider := NewConfigProvider()
|
| 31 |
repositoriesProvider := NewRepositoriesProvider(configProvider)
|
| 32 |
supabaseCfg := configProvider.ProvideSupabaseConfig()
|
| 33 |
-
if supabaseCfg.
|
| 34 |
log.Fatalf("Supabase configuration is invalid: URL, ServiceKey, or BucketName is empty")
|
| 35 |
}
|
| 36 |
-
if !strings.HasPrefix(supabaseCfg.
|
| 37 |
log.Fatalf("Supabase URL is invalid")
|
| 38 |
}
|
| 39 |
-
if strings.Count(supabaseCfg.
|
| 40 |
log.Fatalf("Supabase service key is not a valid compact JWS")
|
| 41 |
}
|
| 42 |
-
storageDriver :=
|
| 43 |
servicesProvider := NewServicesProvider(repositoriesProvider, configProvider, storageDriver)
|
| 44 |
controllerProvider := NewControllerProvider(servicesProvider)
|
| 45 |
middlewareProvider := NewMiddlewareProvider(servicesProvider)
|
|
@@ -129,4 +130,4 @@ func (a *appProvider) ProvideControllers() ControllerProvider {
|
|
| 129 |
|
| 130 |
func (a *appProvider) ProvideMiddlewares() MiddlewareProvider {
|
| 131 |
return a.middlewareProvider
|
| 132 |
-
}
|
|
|
|
| 5 |
"strings"
|
| 6 |
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 7 |
"github.com/gin-gonic/gin"
|
| 8 |
+
"abdanhafidz.com/go-boilerplate/services"
|
| 9 |
)
|
| 10 |
|
| 11 |
type AppProvider interface {
|
|
|
|
| 31 |
configProvider := NewConfigProvider()
|
| 32 |
repositoriesProvider := NewRepositoriesProvider(configProvider)
|
| 33 |
supabaseCfg := configProvider.ProvideSupabaseConfig()
|
| 34 |
+
if supabaseCfg.GetURL() == "" || supabaseCfg.GetServiceKey() == "" || supabaseCfg.GetBucketName() == "" {
|
| 35 |
log.Fatalf("Supabase configuration is invalid: URL, ServiceKey, or BucketName is empty")
|
| 36 |
}
|
| 37 |
+
if !strings.HasPrefix(supabaseCfg.GetURL(), "https://") || !strings.Contains(supabaseCfg.GetURL(), ".supabase.co") {
|
| 38 |
log.Fatalf("Supabase URL is invalid")
|
| 39 |
}
|
| 40 |
+
if strings.Count(supabaseCfg.GetServiceKey(), ".") != 2 {
|
| 41 |
log.Fatalf("Supabase service key is not a valid compact JWS")
|
| 42 |
}
|
| 43 |
+
storageDriver := services.NewSupabaseStorageService(supabaseCfg.GetURL(), supabaseCfg.GetServiceKey(), supabaseCfg.GetBucketName())
|
| 44 |
servicesProvider := NewServicesProvider(repositoriesProvider, configProvider, storageDriver)
|
| 45 |
controllerProvider := NewControllerProvider(servicesProvider)
|
| 46 |
middlewareProvider := NewMiddlewareProvider(servicesProvider)
|
|
|
|
| 130 |
|
| 131 |
func (a *appProvider) ProvideMiddlewares() MiddlewareProvider {
|
| 132 |
return a.middlewareProvider
|
| 133 |
+
}
|
provider/services_provider.go
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
"abdanhafidz.com/go-boilerplate/
|
|
|
|
| 5 |
)
|
| 6 |
|
| 7 |
type ServicesProvider interface {
|
|
@@ -16,7 +17,7 @@ type ServicesProvider interface {
|
|
| 16 |
ProvideForgotPasswordService() services.ForgotPasswordService
|
| 17 |
ProvideEmailVerificationService() services.EmailVerificationService
|
| 18 |
ProvideExternalAuthService() services.ExternalAuthService
|
| 19 |
-
ProvideUploadService()
|
| 20 |
ProvideAcademyExamService() services.AcademyExamService
|
| 21 |
}
|
| 22 |
|
|
@@ -32,14 +33,14 @@ type servicesProvider struct {
|
|
| 32 |
forgotPasswordService services.ForgotPasswordService
|
| 33 |
emailVerificationService services.EmailVerificationService
|
| 34 |
externalAuthService services.ExternalAuthService
|
| 35 |
-
uploadService
|
| 36 |
academyExamService services.AcademyExamService
|
| 37 |
}
|
| 38 |
|
| 39 |
func NewServicesProvider(
|
| 40 |
repoProvider RepositoriesProvider,
|
| 41 |
configProvider ConfigProvider,
|
| 42 |
-
storageProvider
|
| 43 |
) ServicesProvider {
|
| 44 |
|
| 45 |
eventService := services.NewEventService(repoProvider.ProvideEventsRepository(), repoProvider.ProvideEventAssignRepository())
|
|
@@ -66,6 +67,7 @@ func NewServicesProvider(
|
|
| 66 |
uploadService := services.NewUploadService(
|
| 67 |
storageProvider,
|
| 68 |
repoProvider.ProvideFileRepository(),
|
|
|
|
| 69 |
)
|
| 70 |
|
| 71 |
return &servicesProvider{
|
|
@@ -119,8 +121,8 @@ func (s *servicesProvider) ProvideExternalAuthService() services.ExternalAuthSer
|
|
| 119 |
return s.externalAuthService
|
| 120 |
}
|
| 121 |
|
| 122 |
-
func (s *servicesProvider) ProvideUploadService()
|
| 123 |
return s.uploadService
|
| 124 |
}
|
| 125 |
|
| 126 |
-
func (s *servicesProvider) ProvideAcademyExamService() services.AcademyExamService { return s.academyExamService }
|
|
|
|
| 1 |
package provider
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"abdanhafidz.com/go-boilerplate/config"
|
| 5 |
+
"abdanhafidz.com/go-boilerplate/services"
|
| 6 |
)
|
| 7 |
|
| 8 |
type ServicesProvider interface {
|
|
|
|
| 17 |
ProvideForgotPasswordService() services.ForgotPasswordService
|
| 18 |
ProvideEmailVerificationService() services.EmailVerificationService
|
| 19 |
ProvideExternalAuthService() services.ExternalAuthService
|
| 20 |
+
ProvideUploadService() services.UploadService
|
| 21 |
ProvideAcademyExamService() services.AcademyExamService
|
| 22 |
}
|
| 23 |
|
|
|
|
| 33 |
forgotPasswordService services.ForgotPasswordService
|
| 34 |
emailVerificationService services.EmailVerificationService
|
| 35 |
externalAuthService services.ExternalAuthService
|
| 36 |
+
uploadService services.UploadService
|
| 37 |
academyExamService services.AcademyExamService
|
| 38 |
}
|
| 39 |
|
| 40 |
func NewServicesProvider(
|
| 41 |
repoProvider RepositoriesProvider,
|
| 42 |
configProvider ConfigProvider,
|
| 43 |
+
storageProvider StorageProvider,
|
| 44 |
) ServicesProvider {
|
| 45 |
|
| 46 |
eventService := services.NewEventService(repoProvider.ProvideEventsRepository(), repoProvider.ProvideEventAssignRepository())
|
|
|
|
| 67 |
uploadService := services.NewUploadService(
|
| 68 |
storageProvider,
|
| 69 |
repoProvider.ProvideFileRepository(),
|
| 70 |
+
config.NewUploadConfig(),
|
| 71 |
)
|
| 72 |
|
| 73 |
return &servicesProvider{
|
|
|
|
| 121 |
return s.externalAuthService
|
| 122 |
}
|
| 123 |
|
| 124 |
+
func (s *servicesProvider) ProvideUploadService() services.UploadService {
|
| 125 |
return s.uploadService
|
| 126 |
}
|
| 127 |
|
| 128 |
+
func (s *servicesProvider) ProvideAcademyExamService() services.AcademyExamService { return s.academyExamService }
|
provider/storage_provider.go
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package provider
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"io"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
type StorageProvider interface {
|
| 9 |
+
UploadFile(ctx context.Context, file io.Reader, destinationPath string, contentType string) (string, error)
|
| 10 |
+
}
|
provider/supabase_storage.go
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
package provider
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
"fmt"
|
| 6 |
-
"io"
|
| 7 |
-
"mime/multipart"
|
| 8 |
-
storage_go "github.com/supabase-community/storage-go"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type StorageProvider interface {
|
| 12 |
-
UploadFile(ctx context.Context, file multipart.File, header *multipart.FileHeader) (string, error)
|
| 13 |
-
GetFileURL(path string) (string, error)
|
| 14 |
-
DeleteFile(path string) error
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
type SupabaseStorage struct {
|
| 18 |
-
client *storage_go.Client
|
| 19 |
-
bucketName string
|
| 20 |
-
url string
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
func NewSupabaseStorage(url string, key string, bucketName string) *SupabaseStorage {
|
| 24 |
-
client := storage_go.NewClient(url+"/storage/v1", key, nil)
|
| 25 |
-
return &SupabaseStorage{
|
| 26 |
-
client: client,
|
| 27 |
-
bucketName: bucketName,
|
| 28 |
-
url: url,
|
| 29 |
-
}
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
func (s *SupabaseStorage) UploadFile(ctx context.Context, file io.Reader, destinationPath string, contentType string) (string, error) {
|
| 33 |
-
_, err := s.client.UploadFile(s.bucketName, destinationPath, file, storage_go.FileOptions{
|
| 34 |
-
ContentType: &contentType,
|
| 35 |
-
Upsert: new(bool),
|
| 36 |
-
})
|
| 37 |
-
|
| 38 |
-
if err != nil {
|
| 39 |
-
return "", err
|
| 40 |
-
}
|
| 41 |
-
publicURL := s.client.GetPublicUrl(s.bucketName, destinationPath).SignedURL
|
| 42 |
-
if publicURL == "" {
|
| 43 |
-
publicURL = fmt.Sprintf("%s/storage/v1/object/public/%s/%s", s.url, s.bucketName, destinationPath)
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
return publicURL, nil
|
| 47 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
repositories/academy_repository.go
CHANGED
|
@@ -58,9 +58,6 @@ type AcademyRepository interface {
|
|
| 58 |
DecrementContentOrdersGreaterThan(ctx context.Context, materialId uuid.UUID, order uint) error
|
| 59 |
GetAccumulatedMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (float64, error)
|
| 60 |
|
| 61 |
-
BatchRecalculateMaterialProgress(ctx context.Context, materialId uuid.UUID) error
|
| 62 |
-
BatchRecalculateAcademyProgress(ctx context.Context, academyId uuid.UUID) error
|
| 63 |
-
|
| 64 |
GetMaterialProgressBatch(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialIds []uuid.UUID) (map[uuid.UUID]entity.AcademyMaterialProgress, error)
|
| 65 |
GetContentProgressBatch(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, contentIds []uuid.UUID) (map[uuid.UUID]entity.AcademyContentProgress, error)
|
| 66 |
}
|
|
@@ -107,7 +104,7 @@ func (r *academyRepository) GetAcademyWithProgress(ctx context.Context, accountI
|
|
| 107 |
if err != nil {
|
| 108 |
return a, err
|
| 109 |
}
|
| 110 |
-
a.
|
| 111 |
return a, nil
|
| 112 |
}
|
| 113 |
|
|
@@ -152,9 +149,9 @@ func (r *academyRepository) ListAcademy(ctx context.Context, accountId uuid.UUID
|
|
| 152 |
|
| 153 |
for i := range list {
|
| 154 |
if p, exists := progressMap[list[i].Id]; exists {
|
| 155 |
-
list[i].
|
| 156 |
} else {
|
| 157 |
-
list[i].
|
| 158 |
AccountId: accountId,
|
| 159 |
AcademyId: list[i].Id,
|
| 160 |
Status: entity.StatusNotStarted,
|
|
@@ -289,7 +286,7 @@ func (r *academyRepository) GetAcademyProgress(ctx context.Context, accountId uu
|
|
| 289 |
|
| 290 |
func (r *academyRepository) UpsertAcademyProgress(ctx context.Context, p entity.AcademyProgress) (entity.AcademyProgress, error) {
|
| 291 |
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 292 |
-
Columns: []clause.Column{{Name: "
|
| 293 |
UpdateAll: true,
|
| 294 |
}).Save(&p).Error
|
| 295 |
}
|
|
@@ -311,7 +308,7 @@ func (r *academyRepository) GetMaterialProgress(ctx context.Context, accountId u
|
|
| 311 |
|
| 312 |
func (r *academyRepository) UpsertMaterialProgress(ctx context.Context, p entity.AcademyMaterialProgress) (entity.AcademyMaterialProgress, error) {
|
| 313 |
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 314 |
-
Columns: []clause.Column{{Name: "
|
| 315 |
UpdateAll: true,
|
| 316 |
}).Save(&p).Error
|
| 317 |
}
|
|
@@ -334,7 +331,7 @@ func (r *academyRepository) GetContentProgress(ctx context.Context, accountId uu
|
|
| 334 |
|
| 335 |
func (r *academyRepository) UpsertContentProgress(ctx context.Context, p entity.AcademyContentProgress) (entity.AcademyContentProgress, error) {
|
| 336 |
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 337 |
-
Columns: []clause.Column{{Name: "
|
| 338 |
UpdateAll: true,
|
| 339 |
}).Save(&p).Error
|
| 340 |
}
|
|
@@ -396,96 +393,6 @@ func (r *academyRepository) GetAccumulatedMaterialProgress(ctx context.Context,
|
|
| 396 |
return total, err
|
| 397 |
}
|
| 398 |
|
| 399 |
-
func (r *academyRepository) BatchRecalculateMaterialProgress(ctx context.Context, materialId uuid.UUID) error {
|
| 400 |
-
totalContents, err := r.CountContentsByMaterialID(ctx, materialId)
|
| 401 |
-
if err != nil {
|
| 402 |
-
return err
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
if totalContents == 0 {
|
| 406 |
-
return r.db.WithContext(ctx).Model(&entity.AcademyMaterialProgress{}).
|
| 407 |
-
Where("material_id = ?", materialId).
|
| 408 |
-
Updates(map[string]interface{}{
|
| 409 |
-
"progress": 100,
|
| 410 |
-
"status": entity.StatusCompleted,
|
| 411 |
-
"total_completed_contents": 0,
|
| 412 |
-
}).Error
|
| 413 |
-
}
|
| 414 |
-
|
| 415 |
-
return r.db.WithContext(ctx).Exec(`
|
| 416 |
-
UPDATE academy_material_progresses amp
|
| 417 |
-
SET
|
| 418 |
-
total_completed_contents = (
|
| 419 |
-
SELECT COUNT(id) FROM academy_content_progresses acp
|
| 420 |
-
WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
|
| 421 |
-
),
|
| 422 |
-
progress = (
|
| 423 |
-
SELECT COUNT(id) FROM academy_content_progresses acp
|
| 424 |
-
WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
|
| 425 |
-
)::float / ? * 100,
|
| 426 |
-
status = CASE
|
| 427 |
-
WHEN (
|
| 428 |
-
SELECT COUNT(id) FROM academy_content_progresses acp
|
| 429 |
-
WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
|
| 430 |
-
) >= ? THEN 'COMPLETED'
|
| 431 |
-
ELSE 'IN_PROGRESS'
|
| 432 |
-
END,
|
| 433 |
-
completed_at = CASE
|
| 434 |
-
WHEN (
|
| 435 |
-
SELECT COUNT(id) FROM academy_content_progresses acp
|
| 436 |
-
WHERE acp.material_id = amp.material_id AND acp.account_id = amp.account_id AND acp.status = 'COMPLETED'
|
| 437 |
-
) >= ? THEN NOW()
|
| 438 |
-
ELSE NULL
|
| 439 |
-
END
|
| 440 |
-
WHERE amp.material_id = ?
|
| 441 |
-
`, totalContents, totalContents, totalContents, materialId).Error
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
-
func (r *academyRepository) BatchRecalculateAcademyProgress(ctx context.Context, academyId uuid.UUID) error {
|
| 445 |
-
totalMaterials, err := r.CountMaterialsByAcademyID(ctx, academyId)
|
| 446 |
-
if err != nil {
|
| 447 |
-
return err
|
| 448 |
-
}
|
| 449 |
-
|
| 450 |
-
if totalMaterials == 0 {
|
| 451 |
-
return r.db.WithContext(ctx).Model(&entity.AcademyProgress{}).
|
| 452 |
-
Where("academy_id = ?", academyId).
|
| 453 |
-
Updates(map[string]interface{}{
|
| 454 |
-
"progress": 100,
|
| 455 |
-
"status": entity.StatusCompleted,
|
| 456 |
-
"total_completed_materials": 0,
|
| 457 |
-
}).Error
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
return r.db.WithContext(ctx).Exec(`
|
| 461 |
-
UPDATE academy_progress ap
|
| 462 |
-
SET
|
| 463 |
-
progress = (
|
| 464 |
-
SELECT COALESCE(SUM(amp.progress), 0) FROM academy_material_progresses amp
|
| 465 |
-
WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id
|
| 466 |
-
)::float / ?,
|
| 467 |
-
total_completed_materials = (
|
| 468 |
-
SELECT COUNT(id) FROM academy_material_progresses amp
|
| 469 |
-
WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
|
| 470 |
-
),
|
| 471 |
-
status = CASE
|
| 472 |
-
WHEN (
|
| 473 |
-
SELECT COUNT(id) FROM academy_material_progresses amp
|
| 474 |
-
WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
|
| 475 |
-
) >= ? THEN 'COMPLETED'
|
| 476 |
-
ELSE 'IN_PROGRESS'
|
| 477 |
-
END,
|
| 478 |
-
completed_at = CASE
|
| 479 |
-
WHEN (
|
| 480 |
-
SELECT COUNT(id) FROM academy_material_progresses amp
|
| 481 |
-
WHERE amp.academy_id = ap.academy_id AND amp.account_id = ap.account_id AND amp.status = 'COMPLETED'
|
| 482 |
-
) >= ? THEN NOW()
|
| 483 |
-
ELSE NULL
|
| 484 |
-
END
|
| 485 |
-
WHERE ap.academy_id = ?
|
| 486 |
-
`, totalMaterials, totalMaterials, totalMaterials, academyId).Error
|
| 487 |
-
}
|
| 488 |
-
|
| 489 |
func (r *academyRepository) GetMaterialProgressBatch(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialIds []uuid.UUID) (map[uuid.UUID]entity.AcademyMaterialProgress, error) {
|
| 490 |
var progresses []entity.AcademyMaterialProgress
|
| 491 |
result := r.db.WithContext(ctx).Where("account_id = ? AND academy_id = ? AND material_id IN ?", accountId, academyId, materialIds).Find(&progresses)
|
|
|
|
| 58 |
DecrementContentOrdersGreaterThan(ctx context.Context, materialId uuid.UUID, order uint) error
|
| 59 |
GetAccumulatedMaterialProgress(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID) (float64, error)
|
| 60 |
|
|
|
|
|
|
|
|
|
|
| 61 |
GetMaterialProgressBatch(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialIds []uuid.UUID) (map[uuid.UUID]entity.AcademyMaterialProgress, error)
|
| 62 |
GetContentProgressBatch(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, contentIds []uuid.UUID) (map[uuid.UUID]entity.AcademyContentProgress, error)
|
| 63 |
}
|
|
|
|
| 104 |
if err != nil {
|
| 105 |
return a, err
|
| 106 |
}
|
| 107 |
+
a.AcademyProgress = ap
|
| 108 |
return a, nil
|
| 109 |
}
|
| 110 |
|
|
|
|
| 149 |
|
| 150 |
for i := range list {
|
| 151 |
if p, exists := progressMap[list[i].Id]; exists {
|
| 152 |
+
list[i].AcademyProgress = p
|
| 153 |
} else {
|
| 154 |
+
list[i].AcademyProgress = entity.AcademyProgress{
|
| 155 |
AccountId: accountId,
|
| 156 |
AcademyId: list[i].Id,
|
| 157 |
Status: entity.StatusNotStarted,
|
|
|
|
| 286 |
|
| 287 |
func (r *academyRepository) UpsertAcademyProgress(ctx context.Context, p entity.AcademyProgress) (entity.AcademyProgress, error) {
|
| 288 |
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 289 |
+
Columns: []clause.Column{{Name: "account_id"}, {Name: "academy_id"}},
|
| 290 |
UpdateAll: true,
|
| 291 |
}).Save(&p).Error
|
| 292 |
}
|
|
|
|
| 308 |
|
| 309 |
func (r *academyRepository) UpsertMaterialProgress(ctx context.Context, p entity.AcademyMaterialProgress) (entity.AcademyMaterialProgress, error) {
|
| 310 |
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 311 |
+
Columns: []clause.Column{{Name: "account_id"}, {Name: "material_id"}},
|
| 312 |
UpdateAll: true,
|
| 313 |
}).Save(&p).Error
|
| 314 |
}
|
|
|
|
| 331 |
|
| 332 |
func (r *academyRepository) UpsertContentProgress(ctx context.Context, p entity.AcademyContentProgress) (entity.AcademyContentProgress, error) {
|
| 333 |
return p, r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
| 334 |
+
Columns: []clause.Column{{Name: "account_id"}, {Name: "content_id"}},
|
| 335 |
UpdateAll: true,
|
| 336 |
}).Save(&p).Error
|
| 337 |
}
|
|
|
|
| 393 |
return total, err
|
| 394 |
}
|
| 395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
func (r *academyRepository) GetMaterialProgressBatch(ctx context.Context, accountId uuid.UUID, academyId uuid.UUID, materialIds []uuid.UUID) (map[uuid.UUID]entity.AcademyMaterialProgress, error) {
|
| 397 |
var progresses []entity.AcademyMaterialProgress
|
| 398 |
result := r.db.WithContext(ctx).Where("account_id = ? AND academy_id = ? AND material_id IN ?", accountId, academyId, materialIds).Find(&progresses)
|
services/academy_service.go
CHANGED
|
@@ -172,9 +172,7 @@ func (s *academyService) CreateMaterial(ctx context.Context, req dto.CreateMater
|
|
| 172 |
return err
|
| 173 |
}
|
| 174 |
|
| 175 |
-
|
| 176 |
-
return err
|
| 177 |
-
}
|
| 178 |
|
| 179 |
return nil
|
| 180 |
})
|
|
@@ -223,9 +221,7 @@ func (s *academyService) DeleteMaterial(ctx context.Context, id uuid.UUID) error
|
|
| 223 |
return err
|
| 224 |
}
|
| 225 |
|
| 226 |
-
|
| 227 |
-
return err
|
| 228 |
-
}
|
| 229 |
|
| 230 |
return nil
|
| 231 |
})
|
|
@@ -268,12 +264,7 @@ func (s *academyService) CreateContent(ctx context.Context, req dto.CreateConten
|
|
| 268 |
return err
|
| 269 |
}
|
| 270 |
|
| 271 |
-
|
| 272 |
-
return err
|
| 273 |
-
}
|
| 274 |
-
if err := txRepo.BatchRecalculateAcademyProgress(ctx, m.AcademyId); err != nil {
|
| 275 |
-
return err
|
| 276 |
-
}
|
| 277 |
|
| 278 |
return nil
|
| 279 |
})
|
|
@@ -315,20 +306,13 @@ func (s *academyService) DeleteContent(ctx context.Context, id uuid.UUID) error
|
|
| 315 |
if _, err := txRepo.UpdateMaterial(ctx, material); err != nil {
|
| 316 |
return err
|
| 317 |
}
|
| 318 |
-
|
| 319 |
-
return err
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
// Update Academy juga
|
| 323 |
-
if err := txRepo.BatchRecalculateAcademyProgress(ctx, material.AcademyId); err != nil {
|
| 324 |
-
return err
|
| 325 |
-
}
|
| 326 |
|
| 327 |
return nil
|
| 328 |
})
|
| 329 |
}
|
| 330 |
|
| 331 |
-
// ================= UPDATE PROGRESS =================
|
| 332 |
|
| 333 |
func (s *academyService) UpdateContentProgress(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContentProgress, entity.AcademyMaterialProgress, entity.AcademyProgress, error) {
|
| 334 |
academy, err := s.academyRepo.GetAcademyBySlug(ctx, academySlug)
|
|
@@ -383,7 +367,7 @@ func (s *academyService) UpdateContentProgress(ctx context.Context, accountId uu
|
|
| 383 |
if totalContentsCompleted >= m.ContentsCount {
|
| 384 |
matStatus = entity.StatusCompleted
|
| 385 |
matCompletedAt = utils.Ptr(time.Now())
|
| 386 |
-
progressPct = 100
|
| 387 |
}
|
| 388 |
} else {
|
| 389 |
matStatus = entity.StatusCompleted
|
|
@@ -411,7 +395,7 @@ func (s *academyService) UpdateContentProgress(ctx context.Context, accountId uu
|
|
| 411 |
}
|
| 412 |
|
| 413 |
accumulatedProgress, _ := txRepo.GetAccumulatedMaterialProgress(ctx, accountId, academy.Id)
|
| 414 |
-
a, _ := txRepo.GetAcademyByID(ctx, academy.Id)
|
| 415 |
|
| 416 |
acadStatus := entity.StatusNotStarted
|
| 417 |
var acadCompletedAt *time.Time
|
|
@@ -467,7 +451,11 @@ func (s *academyService) GetAcademyResponse(ctx context.Context, accountId uuid.
|
|
| 467 |
if err != nil {
|
| 468 |
return nil, http_error.ACADEMY_NOT_FOUND
|
| 469 |
}
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
materials, err := s.academyRepo.GetMaterialsWithContents(ctx, academy.Id)
|
| 472 |
if err != nil {
|
| 473 |
materials = []entity.AcademyMaterial{}
|
|
@@ -489,7 +477,16 @@ func (s *academyService) GetMaterialResponse(ctx context.Context, accountId uuid
|
|
| 489 |
if err != nil {
|
| 490 |
return nil, http_error.MATERIAL_NOT_FOUND
|
| 491 |
}
|
| 492 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
_, contents, err := s.academyRepo.GetMaterialWithContents(ctx, material.Id)
|
| 494 |
if err != nil {
|
| 495 |
contents = []entity.AcademyContent{}
|
|
|
|
| 172 |
return err
|
| 173 |
}
|
| 174 |
|
| 175 |
+
// progress recalculation handled on get
|
|
|
|
|
|
|
| 176 |
|
| 177 |
return nil
|
| 178 |
})
|
|
|
|
| 221 |
return err
|
| 222 |
}
|
| 223 |
|
| 224 |
+
// progress recalculation handled on get
|
|
|
|
|
|
|
| 225 |
|
| 226 |
return nil
|
| 227 |
})
|
|
|
|
| 264 |
return err
|
| 265 |
}
|
| 266 |
|
| 267 |
+
// progress recalculation handled on get
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
return nil
|
| 270 |
})
|
|
|
|
| 306 |
if _, err := txRepo.UpdateMaterial(ctx, material); err != nil {
|
| 307 |
return err
|
| 308 |
}
|
| 309 |
+
// progress recalculation handled on get
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
|
| 311 |
return nil
|
| 312 |
})
|
| 313 |
}
|
| 314 |
|
| 315 |
+
// ================= UPDATE PROGRESS =================
|
| 316 |
|
| 317 |
func (s *academyService) UpdateContentProgress(ctx context.Context, accountId uuid.UUID, academySlug string, materialSlug string, order uint) (entity.AcademyContentProgress, entity.AcademyMaterialProgress, entity.AcademyProgress, error) {
|
| 318 |
academy, err := s.academyRepo.GetAcademyBySlug(ctx, academySlug)
|
|
|
|
| 367 |
if totalContentsCompleted >= m.ContentsCount {
|
| 368 |
matStatus = entity.StatusCompleted
|
| 369 |
matCompletedAt = utils.Ptr(time.Now())
|
| 370 |
+
progressPct = 100
|
| 371 |
}
|
| 372 |
} else {
|
| 373 |
matStatus = entity.StatusCompleted
|
|
|
|
| 395 |
}
|
| 396 |
|
| 397 |
accumulatedProgress, _ := txRepo.GetAccumulatedMaterialProgress(ctx, accountId, academy.Id)
|
| 398 |
+
a, _ := txRepo.GetAcademyByID(ctx, academy.Id)
|
| 399 |
|
| 400 |
acadStatus := entity.StatusNotStarted
|
| 401 |
var acadCompletedAt *time.Time
|
|
|
|
| 451 |
if err != nil {
|
| 452 |
return nil, http_error.ACADEMY_NOT_FOUND
|
| 453 |
}
|
| 454 |
+
accumulatedProgress, _ := s.academyRepo.GetAccumulatedMaterialProgress(ctx, accountId, academy.Id)
|
| 455 |
+
academyProgress := s.calculateAcademyProgress(ctx, s.academyRepo, accountId, academy.Id, accumulatedProgress, academy.MaterialsCount)
|
| 456 |
+
if _, err := s.academyRepo.UpsertAcademyProgress(ctx, academyProgress); err != nil {
|
| 457 |
+
return nil, err
|
| 458 |
+
}
|
| 459 |
materials, err := s.academyRepo.GetMaterialsWithContents(ctx, academy.Id)
|
| 460 |
if err != nil {
|
| 461 |
materials = []entity.AcademyMaterial{}
|
|
|
|
| 477 |
if err != nil {
|
| 478 |
return nil, http_error.MATERIAL_NOT_FOUND
|
| 479 |
}
|
| 480 |
+
totalCompleted, _ := s.academyRepo.CountCompletedContentsByMaterialAndAccount(ctx, accountId, material.Id)
|
| 481 |
+
materialProgress := s.calculateMaterialProgress(ctx, s.academyRepo, accountId, academy.Id, material.Id, totalCompleted, material.ContentsCount)
|
| 482 |
+
if _, err := s.academyRepo.UpsertMaterialProgress(ctx, materialProgress); err != nil {
|
| 483 |
+
return nil, err
|
| 484 |
+
}
|
| 485 |
+
accumulatedProgress, _ := s.academyRepo.GetAccumulatedMaterialProgress(ctx, accountId, academy.Id)
|
| 486 |
+
academyProgress := s.calculateAcademyProgress(ctx, s.academyRepo, accountId, academy.Id, accumulatedProgress, academy.MaterialsCount)
|
| 487 |
+
if _, err := s.academyRepo.UpsertAcademyProgress(ctx, academyProgress); err != nil {
|
| 488 |
+
return nil, err
|
| 489 |
+
}
|
| 490 |
_, contents, err := s.academyRepo.GetMaterialWithContents(ctx, material.Id)
|
| 491 |
if err != nil {
|
| 492 |
contents = []entity.AcademyContent{}
|
services/academy_service_helpers.go
CHANGED
|
@@ -39,11 +39,15 @@ func (s *academyService) getProgressStatus(progress float64, completed, total in
|
|
| 39 |
}
|
| 40 |
|
| 41 |
func (s *academyService) upsertContentProgressSimplified(
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
) entity.AcademyContentProgress {
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
acp := entity.AcademyContentProgress{
|
| 49 |
Id: s.getOrCreateID(existing.Id),
|
|
@@ -55,8 +59,10 @@ func (s *academyService) upsertContentProgressSimplified(
|
|
| 55 |
CompletedAt: utils.Ptr(time.Now()),
|
| 56 |
}
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
func (s *academyService) calculateMaterialProgress(
|
|
@@ -125,26 +131,24 @@ func (s *academyService) calculateAcademyProgress(
|
|
| 125 |
}
|
| 126 |
|
| 127 |
func (s *academyService) updateAcademyMaterialCount(ctx context.Context, txRepo repositories.AcademyRepository, academyId uuid.UUID) error {
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
return err
|
| 136 |
}
|
| 137 |
|
| 138 |
|
| 139 |
func (s *academyService) updateMaterialContentCount(ctx context.Context, txRepo repositories.AcademyRepository, materialId uuid.UUID) error {
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
return err
|
| 148 |
}
|
| 149 |
|
| 150 |
func formatTime(t *time.Time) *string {
|
|
|
|
| 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),
|
|
|
|
| 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(
|
|
|
|
| 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 {
|
services/account_service.go
CHANGED
|
@@ -3,6 +3,7 @@ package services
|
|
| 3 |
import (
|
| 4 |
"context"
|
| 5 |
"errors"
|
|
|
|
| 6 |
"regexp"
|
| 7 |
"strings"
|
| 8 |
|
|
@@ -37,7 +38,6 @@ func NewAccountService(jwtService JWTService, accountRepo repositories.AccountRe
|
|
| 37 |
jwtService: jwtService,
|
| 38 |
accountRepo: accountRepo,
|
| 39 |
accountDetailRepo: accountDetailRepo,
|
| 40 |
-
|
| 41 |
}
|
| 42 |
}
|
| 43 |
|
|
@@ -68,7 +68,9 @@ func (s *accountService) Create(ctx context.Context, name string, email string,
|
|
| 68 |
return entity.Account{}, err
|
| 69 |
}
|
| 70 |
|
| 71 |
-
_,
|
|
|
|
|
|
|
| 72 |
|
| 73 |
return created, nil
|
| 74 |
|
|
|
|
| 3 |
import (
|
| 4 |
"context"
|
| 5 |
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
"regexp"
|
| 8 |
"strings"
|
| 9 |
|
|
|
|
| 38 |
jwtService: jwtService,
|
| 39 |
accountRepo: accountRepo,
|
| 40 |
accountDetailRepo: accountDetailRepo,
|
|
|
|
| 41 |
}
|
| 42 |
}
|
| 43 |
|
|
|
|
| 68 |
return entity.Account{}, err
|
| 69 |
}
|
| 70 |
|
| 71 |
+
if _, err := s.CreateEmptyDetail(ctx, created.Id); err != nil {
|
| 72 |
+
return entity.Account{}, fmt.Errorf("create empty detail: %w", err)
|
| 73 |
+
}
|
| 74 |
|
| 75 |
return created, nil
|
| 76 |
|
services/supabase_storage_service.go
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"io"
|
| 7 |
+
storage_go "github.com/supabase-community/storage-go"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type SupabaseStorageService struct {
|
| 11 |
+
client *storage_go.Client
|
| 12 |
+
bucketName string
|
| 13 |
+
url string
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
func NewSupabaseStorageService(url string, key string, bucketName string) *SupabaseStorageService {
|
| 17 |
+
client := storage_go.NewClient(url+"/storage/v1", key, nil)
|
| 18 |
+
return &SupabaseStorageService{ client: client, bucketName: bucketName, url: url }
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func (s *SupabaseStorageService) UploadFile(ctx context.Context, file io.Reader, destinationPath string, contentType string) (string, error) {
|
| 22 |
+
_, err := s.client.UploadFile(s.bucketName, destinationPath, file, storage_go.FileOptions{ ContentType: &contentType, Upsert: new(bool) })
|
| 23 |
+
if err != nil { return "", err }
|
| 24 |
+
publicURL := s.client.GetPublicUrl(s.bucketName, destinationPath).SignedURL
|
| 25 |
+
if publicURL == "" { publicURL = fmt.Sprintf("%s/storage/v1/object/public/%s/%s", s.url, s.bucketName, destinationPath) }
|
| 26 |
+
return publicURL, nil
|
| 27 |
+
}
|
services/upload_service.go
CHANGED
|
@@ -1,54 +1,45 @@
|
|
| 1 |
package services
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
| 18 |
)
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
AllowedExts map[string]bool
|
| 25 |
-
PathPrefix string
|
| 26 |
-
MaxCount int
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
type StorageProvider interface {
|
| 30 |
-
UploadFile(ctx context.Context, file io.Reader, destinationPath string, contentType string) (string, error)
|
| 31 |
}
|
| 32 |
|
| 33 |
-
type
|
| 34 |
-
|
| 35 |
-
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
-
|
| 39 |
-
storageProvider
|
| 40 |
-
fileRepo FileRepository
|
| 41 |
}
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
storageProvider: storage,
|
| 46 |
-
fileRepo: repo,
|
| 47 |
-
}
|
| 48 |
}
|
| 49 |
|
| 50 |
-
func (s *
|
| 51 |
-
config, err := s.
|
| 52 |
if err != nil {
|
| 53 |
return nil, err
|
| 54 |
}
|
|
@@ -81,7 +72,7 @@ func (s *UploadService) UploadFiles(ctx context.Context, files []*multipart.File
|
|
| 81 |
return uploadedFiles, nil
|
| 82 |
}
|
| 83 |
|
| 84 |
-
func (s *
|
| 85 |
file, err := s.fileRepo.FindByID(ctx, fileID)
|
| 86 |
if err != nil {
|
| 87 |
if errors.Is(err, http_error.NOT_FOUND_ERROR) {
|
|
@@ -97,7 +88,7 @@ func (s *UploadService) GetFileByID(ctx context.Context, fileID uuid.UUID, accou
|
|
| 97 |
return file, nil
|
| 98 |
}
|
| 99 |
|
| 100 |
-
func (s *
|
| 101 |
if err := s.validateFile(fileHeader, config); err != nil {
|
| 102 |
return nil, err
|
| 103 |
}
|
|
@@ -137,7 +128,7 @@ func (s *UploadService) processSingleFile(ctx context.Context, fileHeader *multi
|
|
| 137 |
return fileEntity, nil
|
| 138 |
}
|
| 139 |
|
| 140 |
-
func (s *
|
| 141 |
if file.Size == 0 || file.Size > config.MaxBytes {
|
| 142 |
return http_error.FILE_TOO_LARGE
|
| 143 |
}
|
|
@@ -155,7 +146,7 @@ func (s *UploadService) validateFile(file *multipart.FileHeader, config uploadCo
|
|
| 155 |
return nil
|
| 156 |
}
|
| 157 |
|
| 158 |
-
func (s *
|
| 159 |
originalNameWithoutExt := strings.TrimSuffix(originalName, ext)
|
| 160 |
reg := regexp.MustCompile("[^a-zA-Z0-9._-]+")
|
| 161 |
sanitizedRawName := reg.ReplaceAllString(originalNameWithoutExt, "_")
|
|
@@ -165,7 +156,7 @@ func (s *UploadService) generateStoredFilename(originalName string, ext string)
|
|
| 165 |
return fmt.Sprintf("%s-%d-%s%s", uniqueID.String(), timestamp, sanitizedRawName, ext)
|
| 166 |
}
|
| 167 |
|
| 168 |
-
func (s *
|
| 169 |
switch contextType {
|
| 170 |
case "submission":
|
| 171 |
now := time.Now()
|
|
@@ -177,98 +168,48 @@ func (s *UploadService) generateStoragePath(prefix, contextType, filename string
|
|
| 177 |
}
|
| 178 |
}
|
| 179 |
|
| 180 |
-
func (s *
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
}
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
return uploadConfig{
|
| 199 |
-
MaxBytes: 10 * MB,
|
| 200 |
-
AllowedExts: imgExts,
|
| 201 |
-
PathPrefix: "images",
|
| 202 |
-
MaxCount: 5,
|
| 203 |
-
}, nil
|
| 204 |
-
case "material":
|
| 205 |
-
return uploadConfig{
|
| 206 |
-
MaxBytes: 10 * MB,
|
| 207 |
-
AllowedExts: docExts,
|
| 208 |
-
PathPrefix: "materials",
|
| 209 |
-
MaxCount: 1,
|
| 210 |
-
}, nil
|
| 211 |
-
case "submission":
|
| 212 |
-
return uploadConfig{
|
| 213 |
-
MaxBytes: 1 * MB,
|
| 214 |
-
AllowedExts: codeExts,
|
| 215 |
-
PathPrefix: "submissions",
|
| 216 |
-
MaxCount: 1,
|
| 217 |
-
}, nil
|
| 218 |
-
case "general":
|
| 219 |
-
return uploadConfig{
|
| 220 |
-
MaxBytes: 5 * MB,
|
| 221 |
-
AllowedExts: allExts,
|
| 222 |
-
PathPrefix: "temp",
|
| 223 |
-
MaxCount: 5,
|
| 224 |
-
}, nil
|
| 225 |
-
default:
|
| 226 |
-
return uploadConfig{}, http_error.INVALID_DATA_PAYLOAD
|
| 227 |
}
|
| 228 |
-
}
|
| 229 |
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
if err != nil {
|
| 233 |
-
return nil, http_error.BAD_REQUEST_ERROR
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(originalName)))
|
| 237 |
-
if ext == "" {
|
| 238 |
-
if strings.Contains(contentType, "pdf") {
|
| 239 |
-
ext = ".pdf"
|
| 240 |
-
} else if strings.Contains(contentType, "png") {
|
| 241 |
-
ext = ".png"
|
| 242 |
-
} else if strings.Contains(contentType, "jpeg") || strings.Contains(contentType, "jpg") {
|
| 243 |
-
ext = ".jpg"
|
| 244 |
-
} else {
|
| 245 |
-
ext = ".bin"
|
| 246 |
-
}
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
storedFilename := s.generateStoredFilename(originalName, ext)
|
| 250 |
-
storagePath := s.generateStoragePath(config.PathPrefix, uploadContext, storedFilename, accountID)
|
| 251 |
-
|
| 252 |
-
publicURL, err := s.storageProvider.UploadFile(ctx, reader, storagePath, contentType)
|
| 253 |
-
if err != nil {
|
| 254 |
-
return nil, err
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
fileEntity := &entity.File{
|
| 258 |
-
Id: uuid.New(),
|
| 259 |
-
OriginalName: originalName,
|
| 260 |
-
StoredName: storedFilename,
|
| 261 |
-
MimeType: contentType,
|
| 262 |
-
Size: 0,
|
| 263 |
-
Path: publicURL,
|
| 264 |
-
Context: uploadContext,
|
| 265 |
-
AccountId: accountID,
|
| 266 |
-
CreatedAt: time.Now(),
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
if err := s.fileRepo.Create(ctx, fileEntity); err != nil {
|
| 270 |
-
return nil, err
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
return fileEntity, nil
|
| 274 |
-
}
|
|
|
|
| 1 |
package services
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"mime/multipart"
|
| 9 |
+
"path/filepath"
|
| 10 |
+
"regexp"
|
| 11 |
+
"strings"
|
| 12 |
+
"time"
|
| 13 |
+
|
| 14 |
+
"abdanhafidz.com/go-boilerplate/config"
|
| 15 |
+
entity "abdanhafidz.com/go-boilerplate/models/entity"
|
| 16 |
+
http_error "abdanhafidz.com/go-boilerplate/models/error"
|
| 17 |
+
"abdanhafidz.com/go-boilerplate/repositories"
|
| 18 |
+
"github.com/google/uuid"
|
| 19 |
)
|
| 20 |
|
| 21 |
+
type UploadService interface {
|
| 22 |
+
UploadFiles(ctx context.Context, files []*multipart.FileHeader, uploadContext string, accountID uuid.UUID) ([]entity.File, error)
|
| 23 |
+
GetFileByID(ctx context.Context, fileID uuid.UUID, accountID uuid.UUID) (*entity.File, error)
|
| 24 |
+
UploadRawFile(ctx context.Context, reader io.Reader, originalName string, contentType string, uploadContext string, accountID uuid.UUID) (*entity.File, error)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
+
type uploadService struct {
|
| 28 |
+
storageProvider storageUploader
|
| 29 |
+
fileRepo repositories.FileRepository
|
| 30 |
+
cfg config.UploadConfig
|
| 31 |
}
|
| 32 |
|
| 33 |
+
func NewUploadService(storage storageUploader, repo repositories.FileRepository, cfg config.UploadConfig) UploadService {
|
| 34 |
+
return &uploadService{storageProvider: storage, fileRepo: repo, cfg: cfg}
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
+
type storageUploader interface {
|
| 38 |
+
UploadFile(ctx context.Context, file io.Reader, destinationPath string, contentType string) (string, error)
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
+
func (s *uploadService) UploadFiles(ctx context.Context, files []*multipart.FileHeader, uploadContext string, accountID uuid.UUID) ([]entity.File, error) {
|
| 42 |
+
config, err := s.cfg.Get(uploadContext)
|
| 43 |
if err != nil {
|
| 44 |
return nil, err
|
| 45 |
}
|
|
|
|
| 72 |
return uploadedFiles, nil
|
| 73 |
}
|
| 74 |
|
| 75 |
+
func (s *uploadService) GetFileByID(ctx context.Context, fileID uuid.UUID, accountID uuid.UUID) (*entity.File, error) {
|
| 76 |
file, err := s.fileRepo.FindByID(ctx, fileID)
|
| 77 |
if err != nil {
|
| 78 |
if errors.Is(err, http_error.NOT_FOUND_ERROR) {
|
|
|
|
| 88 |
return file, nil
|
| 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 |
if err := s.validateFile(fileHeader, config); err != nil {
|
| 93 |
return nil, err
|
| 94 |
}
|
|
|
|
| 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 |
}
|
|
|
|
| 146 |
return nil
|
| 147 |
}
|
| 148 |
|
| 149 |
+
func (s *uploadService) generateStoredFilename(originalName string, ext string) string {
|
| 150 |
originalNameWithoutExt := strings.TrimSuffix(originalName, ext)
|
| 151 |
reg := regexp.MustCompile("[^a-zA-Z0-9._-]+")
|
| 152 |
sanitizedRawName := reg.ReplaceAllString(originalNameWithoutExt, "_")
|
|
|
|
| 156 |
return fmt.Sprintf("%s-%d-%s%s", uniqueID.String(), timestamp, sanitizedRawName, ext)
|
| 157 |
}
|
| 158 |
|
| 159 |
+
func (s *uploadService) generateStoragePath(prefix, contextType, filename string, accountID uuid.UUID) string {
|
| 160 |
switch contextType {
|
| 161 |
case "submission":
|
| 162 |
now := time.Now()
|
|
|
|
| 168 |
}
|
| 169 |
}
|
| 170 |
|
| 171 |
+
func (s *uploadService) UploadRawFile(ctx context.Context, reader io.Reader, originalName string, contentType string, uploadContext string, accountID uuid.UUID) (*entity.File, error) {
|
| 172 |
+
rule, err := s.cfg.Get(uploadContext)
|
| 173 |
+
if err != nil {
|
| 174 |
+
return nil, http_error.BAD_REQUEST_ERROR
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(originalName)))
|
| 178 |
+
if ext == "" {
|
| 179 |
+
if strings.Contains(contentType, "pdf") {
|
| 180 |
+
ext = ".pdf"
|
| 181 |
+
} else if strings.Contains(contentType, "png") {
|
| 182 |
+
ext = ".png"
|
| 183 |
+
} else if strings.Contains(contentType, "jpeg") || strings.Contains(contentType, "jpg") {
|
| 184 |
+
ext = ".jpg"
|
| 185 |
+
} else {
|
| 186 |
+
ext = ".bin"
|
| 187 |
+
}
|
| 188 |
}
|
| 189 |
+
|
| 190 |
+
storedFilename := s.generateStoredFilename(originalName, ext)
|
| 191 |
+
storagePath := s.generateStoragePath(rule.PathPrefix, uploadContext, storedFilename, accountID)
|
| 192 |
+
|
| 193 |
+
publicURL, err := s.storageProvider.UploadFile(ctx, reader, storagePath, contentType)
|
| 194 |
+
if err != nil {
|
| 195 |
+
return nil, err
|
| 196 |
}
|
| 197 |
+
|
| 198 |
+
fileEntity := &entity.File{
|
| 199 |
+
Id: uuid.New(),
|
| 200 |
+
OriginalName: originalName,
|
| 201 |
+
StoredName: storedFilename,
|
| 202 |
+
MimeType: contentType,
|
| 203 |
+
Size: 0,
|
| 204 |
+
Path: publicURL,
|
| 205 |
+
Context: uploadContext,
|
| 206 |
+
AccountId: accountID,
|
| 207 |
+
CreatedAt: time.Now(),
|
| 208 |
}
|
| 209 |
|
| 210 |
+
if err := s.fileRepo.Create(ctx, fileEntity); err != nil {
|
| 211 |
+
return nil, err
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
}
|
|
|
|
| 213 |
|
| 214 |
+
return fileEntity, nil
|
| 215 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
space/.gitignore
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
-
/.env
|
|
|
|
|
|
| 1 |
+
/.env
|
| 2 |
+
go-boilerplate.exe
|