diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..cbaefa8 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: ./bin/web +worker: ./bin/worker diff --git a/README.md b/README.md index 1f5baf9..d312c94 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,54 @@ # Codex Backend -Backend for Codex - novel reader app. +Backend for Codex - novel reading platform. -# Details +## Details -Codex-Backend is built in `GoLang`, using `Gin` for server and `AWS-dynamoDB` for database. +Codex-Backend is built in `GoLang`, using `Gin` for server and ~AWS-dynamoDB~ firestore (moving to Heroku Postgres) for database. It is deployed on `Heroku` (thats why the code is in api directory). -### Endpoints +air config is outdated and not recommended. use [Run](Run guide instead) -This will be updated later (the version before was already outdated) +## Run +run server: -### Run +```bash +go run api/cmd/web/main.go +``` -- run server: - ```bash - go run api/cmd/codex/main.go - ``` +```bash +go run api/cmd/worker/main.go +``` + +Both are needed + +## Endpoints + +3 Groups of endpoints: Client, Manage and User. + +- Client is responsible for basic GET requests. +- Manage is responsible for Upload/Modification operations. +- User is responsible for user authentication, authorization and Registration (Delete is not yet implemented). + +### Client: base path followed by request path +- `/all` - Get all novels +- `/:novel` - Get a novel by id +- `/:novel/:chapter` - Get chapter from novel using both ids +- `/:novel/all` - Get all chapters from novel using id +- `/:novel/chapter` - Get cursor paginated chapters from novel using id + + Options: limit (max 100), cursor (chapter index (integer)) and sort ("asc" || "desc"). + + Defaults: limit=100, cursor=0, sort="desc" + +### Manage: `/manage` followed by request path +- `/upload` - Upload novel +- `/:novel` - Update novel +- `/:novel/:chapter` - Update chapter + +### User: `/user` followed by request path +- `/validate` - Validate user token +- `/login` - Login user +- `/register` - Register user +- `/logout` - Logout user diff --git a/api/cmd/codex/main.go b/api/cmd/codex/main.go deleted file mode 100644 index f9fb079..0000000 --- a/api/cmd/codex/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "Codex-Backend/api/internal/common" - firestore_server "Codex-Backend/api/internal/interfaces/rest" - "log" - - "github.com/gin-gonic/gin" - _ "github.com/heroku/x/hmetrics/onload" -) - -func init() { - if gin.Mode() == gin.DebugMode { - err := common.LoadEnvVariables() - if err != nil { - log.Fatalf("Error loading env file: %s", err.Error()) - } - } -} - -func main() { - firestore_server.Server() -} diff --git a/api/cmd/web/main.go b/api/cmd/web/main.go new file mode 100644 index 0000000..f64186a --- /dev/null +++ b/api/cmd/web/main.go @@ -0,0 +1,19 @@ +package main + +import ( + cmn "Codex-Backend/api/internal/common" + firestore_server "Codex-Backend/api/internal/interfaces/rest" + "os" + + _ "github.com/heroku/x/hmetrics/onload" +) + +func init() { + if mode := os.Getenv("GIN_MODE"); mode == "debug" { + cmn.LoadEnvVariables() + } +} + +func main() { + firestore_server.Server() +} diff --git a/api/cmd/worker/main.go b/api/cmd/worker/main.go new file mode 100644 index 0000000..47cdf16 --- /dev/null +++ b/api/cmd/worker/main.go @@ -0,0 +1,47 @@ +package main + +import ( + cmn "Codex-Backend/api/internal/common" + queue "Codex-Backend/api/internal/common/river" + "context" + "log" + "os" + "os/signal" + "syscall" + "time" +) + +func init() { + if mode := os.Getenv("GIN_MODE"); mode == "debug" { + cmn.LoadEnvVariables() + } +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + riverClient := queue.GetRiverClient(ctx) + if err := riverClient.Start(ctx); err != nil { + log.Fatal("Failed to start River client:", err) + } + + log.Println("River worker started successfully, waiting for jobs...") + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + <-sigChan + log.Println("Shutdown signal received, starting graceful shutdown...") + + cancel() + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer shutdownCancel() + + if err := riverClient.Stop(shutdownCtx); err != nil { + log.Printf("Error during River client shutdown: %v", err) + } else { + log.Println("River worker stopped gracefully") + } +} diff --git a/api/internal/common/env.go b/api/internal/common/env.go index fa521a2..773e746 100644 --- a/api/internal/common/env.go +++ b/api/internal/common/env.go @@ -2,21 +2,25 @@ package common import ( "errors" + "log" "net/http" "os" "github.com/joho/godotenv" ) -func LoadEnvVariables() error { - return godotenv.Load(".env") +func LoadEnvVariables() { + err := godotenv.Load(".env") + if err != nil { + log.Fatal(&Error{Err: errors.New("Failed to load environment variables"), Status: http.StatusInternalServerError}) + } } -func GetEnvVariable(v string) (string, error) { +func GetEnvVariable(v string) string { env_variable := os.Getenv(v) if env_variable == "" { - return "", &Error{Err: errors.New("Environmental Variable " + v + " Not Found"), Status: http.StatusNotFound} + log.Fatal(&Error{Err: errors.New("Environmental Variable " + v + " Not Found"), Status: http.StatusNotFound}) } - return env_variable, nil + return env_variable } diff --git a/api/internal/common/river/client.go b/api/internal/common/river/client.go new file mode 100644 index 0000000..58f2b42 --- /dev/null +++ b/api/internal/common/river/client.go @@ -0,0 +1,60 @@ +package queue + +import ( + "Codex-Backend/api/internal/usecases/worker" + "context" + "log" + "log/slog" + "os" + "sync" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/riverqueue/river" + "github.com/riverqueue/river/riverdriver/riverpgxv5" +) + +var ( + riverClient *river.Client[pgx.Tx] + riverOnce sync.Once +) + +func InitializeRiverClient(ctx context.Context, workers *river.Workers) *river.Client[pgx.Tx] { + dbPool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) + if err != nil { + panic(err) + } + + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + + riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{ + Logger: logger, + Queues: map[string]river.QueueConfig{ + river.QueueDefault: {MaxWorkers: 10}, + }, + MaxAttempts: 3, + Workers: workers, + }) + if err != nil { + panic(err) + } + + return riverClient +} + +func GetRiverClient(ctx context.Context) *river.Client[pgx.Tx] { + riverOnce.Do(func() { + log.Println("Initializing River client...") + + workers := river.NewWorkers() + river.AddWorker(workers, &worker.EPUBWorker{}) + + riverClient = InitializeRiverClient(ctx, workers) + + log.Println("River client initialized successfully") + }) + + return riverClient +} diff --git a/api/internal/common/token.go b/api/internal/common/token.go index 42ab15c..65675a3 100644 --- a/api/internal/common/token.go +++ b/api/internal/common/token.go @@ -10,10 +10,7 @@ import ( func GenerateToken(email string) (string, error) { - key, err := GetEnvVariable("JWT_SIGN_KEY") - if err != nil { - return "", err - } + key := GetEnvVariable("JWT_SIGN_KEY") signKey := []byte(key) expirationTime := time.Now().Add(time.Hour * 24) diff --git a/api/internal/infrastructure/client/client.go b/api/internal/infrastructure/client/client.go index de8f586..f15b10a 100644 --- a/api/internal/infrastructure/client/client.go +++ b/api/internal/infrastructure/client/client.go @@ -18,10 +18,7 @@ type Client struct { func FirestoreClient() (*firestore.Client, error) { ctx := context.Background() - credentials_json, err := cmn.GetEnvVariable("GOOGLE_CREDENTIALS") - if err != nil { - return nil, &cmn.Error{Err: errors.New("Firestore Client Error - GOOGLE_CREDENTIALS: " + err.Error()), Status: http.StatusInternalServerError} - } + credentials_json := cmn.GetEnvVariable("GOOGLE_CREDENTIALS") sa := option.WithCredentialsJSON([]byte(credentials_json)) diff --git a/api/internal/infrastructure/collections/chapters.go b/api/internal/infrastructure/collections/chapters.go index 84af87f..c9c2f30 100644 --- a/api/internal/infrastructure/collections/chapters.go +++ b/api/internal/infrastructure/collections/chapters.go @@ -21,30 +21,16 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont limit := min(max(options.Limit, 1), 100) snapshots := []*firestore.DocumentSnapshot{} + var err error if options.Cursor == 0 { - snaps, err := query.Limit(1).Documents(ctx).GetAll() - if err != nil { - return nil, err - } - - if len(snaps) == 0 { - return nil, &cmn.Error{ - Err: fmt.Errorf("Firestore Client Error - Get Paginated Chapters - No Chapters Found for Novel: %s", options.NovelID), - Status: http.StatusNotFound, - } - } - - snapshots, err = query.StartAt(snaps[0]).Limit(limit + 1).Documents(ctx).GetAll() - if err != nil { - return nil, err - } + snapshots, err = query.Limit(limit + 1).Documents(ctx).GetAll() } else { - var err error snapshots, err = query.StartAt(options.Cursor).Limit(limit + 1).Documents(ctx).GetAll() - if err != nil { - return nil, err - } + } + + if err != nil { + return nil, err } if len(snapshots) == 0 { @@ -54,22 +40,10 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont } } - nextCursor := 0 - if len(snapshots) > limit { - var chapter domain.Chapter - if err := snapshots[len(snapshots)-1].DataTo(&chapter); err != nil { - return nil, err - } - nextCursor = chapter.Index - } - - snapLen := len(snapshots) - 1 - if snapLen <= 0 { - snapLen++ - } + actualLimit := min(len(snapshots), limit) + chapters := make([]domain.FrontendChapter, 0, actualLimit) - chapters := []domain.FrontendChapter{} - for _, snapshot := range snapshots[:snapLen] { + for _, snapshot := range snapshots[:actualLimit] { var chapter domain.Chapter if err := snapshot.DataTo(&chapter); err != nil { return nil, err @@ -82,6 +56,15 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont }) } + nextCursor := 0 + if len(snapshots) > limit { + var lastChapter domain.Chapter + if err := snapshots[limit].DataTo(&lastChapter); err != nil { + return nil, err + } + nextCursor = lastChapter.Index + } + return &domain.CursorResponse{ Chapters: chapters, NextCursor: nextCursor, @@ -95,7 +78,7 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, for i := 0; i < len(chapters); i += chunkSize { subset := chapters[i:min(i+chunkSize, len(chapters))] - batchCtx, cancel := context.WithTimeout(ctx, 300*time.Second) + batchCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() bw := c.Client.BulkWriter(batchCtx) @@ -104,6 +87,7 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, for _, chap := range subset { job, err := bw.Set(coll.Doc(chap.ID), chap) if err != nil { + cancel() return &cmn.Error{ Err: fmt.Errorf("Firestore Client Error - Batch Upload Chapters - Enqueue failed for chapter %s: %w", chap.ID, err), Status: http.StatusInternalServerError, @@ -119,6 +103,7 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, for j, job := range jobs { if _, err := job.Results(); err != nil { chap := subset[j] + cancel() return &cmn.Error{ Err: fmt.Errorf("Firestore Client Error - Batch Upload Chapters - Write failed for chapter %s: %w", chap.ID, err), Status: http.StatusInternalServerError, @@ -126,7 +111,10 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, } } - time.Sleep(100 * time.Millisecond) + cancel() + if i+chunkSize < len(chapters) { + time.Sleep(200 * time.Millisecond) + } } return nil diff --git a/api/internal/interfaces/rest/handlers/chapters.go b/api/internal/interfaces/rest/handlers/chapters.go index 23b7902..70f7021 100644 --- a/api/internal/interfaces/rest/handlers/chapters.go +++ b/api/internal/interfaces/rest/handlers/chapters.go @@ -137,52 +137,6 @@ func FindAllChapters(c *gin.Context) { }) } -func BatchUploadChapters(c *gin.Context) { - ctx := c.Request.Context() - defer ctx.Done() - - novelId := c.Param("novel") - - if novelId == "" { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ - "error": "Novel ID not found", - }) - return - } - - var chapters []domain.Chapter - if err := c.ShouldBindJSON(&chapters); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ - "error": "Failed to get chapters data: " + err.Error(), - }) - return - } - - if len(chapters) == 0 { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ - "error": "Nothing to upload", - }) - return - } - - err := firestore_services.BatchUploadChapters(novelId, chapters, ctx) - if e, ok := err.(*cmn.Error); ok { - c.AbortWithStatusJSON(e.StatusCode(), gin.H{ - "error": "Failed to upload chapters: " + e.Error(), - }) - return - } else if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to upload chapters: " + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Chapters uploaded successfully", - }) -} - func CreateChapter(c *gin.Context) { ctx := c.Request.Context() defer ctx.Done() diff --git a/api/internal/interfaces/rest/handlers/novels.go b/api/internal/interfaces/rest/handlers/novels.go index 0983969..48bc890 100644 --- a/api/internal/interfaces/rest/handlers/novels.go +++ b/api/internal/interfaces/rest/handlers/novels.go @@ -2,8 +2,11 @@ package firestore_handlers import ( cmn "Codex-Backend/api/internal/common" + queue "Codex-Backend/api/internal/common/river" "Codex-Backend/api/internal/domain" firestore_services "Codex-Backend/api/internal/usecases/collections" + "Codex-Backend/api/internal/usecases/worker" + "io" "net/http" "strings" @@ -28,18 +31,33 @@ func EPUBNovel(c *gin.Context) { return } - err = firestore_services.CreateNovelFromEPUB(epubFile, ctx) - if e, ok := err.(*cmn.Error); ok { - c.AbortWithStatusJSON(e.StatusCode(), gin.H{ - "error": "Failed to upload EPUB file: " + e.Error(), - }) + maxSize := int64(32 * 1024 * 1024) // 32MB limit + if epubFile.Size > maxSize { + c.JSON(http.StatusBadRequest, gin.H{"error": "File too large (max 32MB)"}) return - } else if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to upload EPUB file: " + err.Error(), + } + + file, err := epubFile.Open() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": "Failed to open EPUB file: " + err.Error(), }) return } + defer file.Close() + + fileData, err := io.ReadAll(file) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file"}) + return + } + + riverClient := queue.GetRiverClient(ctx) + _, err = riverClient.Insert(ctx, worker.ProcessEPUBArgs{File: fileData}, nil) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } c.JSON(http.StatusOK, gin.H{ "message": "EPUB file uploaded successfully", diff --git a/api/internal/interfaces/rest/handlers/users.go b/api/internal/interfaces/rest/handlers/users.go index 8ccdce4..c036b76 100644 --- a/api/internal/interfaces/rest/handlers/users.go +++ b/api/internal/interfaces/rest/handlers/users.go @@ -80,7 +80,7 @@ func LoginUser(c *gin.Context) { return } - c.SetCookie("Authorization", token, 3600*24, "", "", true, true) + c.SetCookie("Authorization", token, 3600*24, "/", cmn.GetEnvVariable("DOMAIN"), true, true) c.JSON(http.StatusOK, gin.H{ "user": gin.H{ diff --git a/api/internal/interfaces/rest/middleware/token.go b/api/internal/interfaces/rest/middleware/token.go index dd6241d..62a516f 100644 --- a/api/internal/interfaces/rest/middleware/token.go +++ b/api/internal/interfaces/rest/middleware/token.go @@ -2,6 +2,7 @@ package firestore_middleware import ( "Codex-Backend/api/internal/common" + cmn "Codex-Backend/api/internal/common" "Codex-Backend/api/internal/domain" firestore_client "Codex-Backend/api/internal/infrastructure/client" firestore_collections "Codex-Backend/api/internal/infrastructure/collections" @@ -29,10 +30,7 @@ func ValidateToken() gin.HandlerFunc { return nil, jwt.ErrSignatureInvalid } - key, err := common.GetEnvVariable("JWT_SIGN_KEY") - if err != nil { - return nil, err - } + key := cmn.GetEnvVariable("JWT_SIGN_KEY") return []byte(key), nil }) diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index e9fa68f..782d6c9 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -1,6 +1,7 @@ package firestore_server import ( + cmn "Codex-Backend/api/internal/common" firestore_handlers "Codex-Backend/api/internal/interfaces/rest/handlers" firestore_middleware "Codex-Backend/api/internal/interfaces/rest/middleware" @@ -9,11 +10,14 @@ import ( ) func RegisteredRoutes(r *gin.Engine) { + domain := cmn.GetEnvVariable("DOMAIN") + if gin.Mode() == gin.DebugMode && domain == "" { + domain = "*" + } r.Use(cors.New(cors.Config{ AllowOrigins: []string{ - "http://localhost:3000", // Local - "https://codex-reader.vercel.app", // Remote TODO: change this to include url from env later. + domain, }, AllowMethods: []string{ "GET", @@ -30,10 +34,16 @@ func RegisteredRoutes(r *gin.Engine) { "X-Requested-With", "Authorization", "Accept", - "Acces-Control-Allow-Origin", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Credentials", + "Set-Cookie", }, ExposeHeaders: []string{ "Content-Length", + "Content-Type", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Credentials", + "Set-Cookie", }, AllowCredentials: true, })) @@ -52,11 +62,16 @@ func RegisteredRoutes(r *gin.Engine) { manage := r.Group("/manage") { + // Create manage.POST("/novel", firestore_middleware.ValidateToken(), firestore_handlers.CreateNovel) manage.POST("/:novel/chapter", firestore_middleware.ValidateToken(), firestore_handlers.CreateChapter) manage.POST("/epub", firestore_middleware.ValidateToken(), firestore_handlers.EPUBNovel) + + // Update manage.PUT("/:novel", firestore_middleware.ValidateToken(), firestore_handlers.UpdateNovel) manage.PUT("/:novel/:chapter", firestore_middleware.ValidateToken(), firestore_handlers.UpdateChapter) + + // Delete manage.DELETE("/:novel", firestore_middleware.ValidateToken(), firestore_handlers.DeleteNovel) manage.DELETE("/:novel/:chapter", firestore_middleware.ValidateToken(), firestore_handlers.DeleteChapter) } diff --git a/api/internal/interfaces/rest/server.go b/api/internal/interfaces/rest/server.go index 391e9ec..1740f26 100644 --- a/api/internal/interfaces/rest/server.go +++ b/api/internal/interfaces/rest/server.go @@ -2,21 +2,59 @@ package firestore_server import ( cmn "Codex-Backend/api/internal/common" + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" "github.com/gin-gonic/gin" ) func Server() { - mode, err := cmn.GetEnvVariable("GIN_MODE") - if err != nil { - panic(err) - } - + mode := cmn.GetEnvVariable("GIN_MODE") gin.SetMode(mode) r := gin.Default() - RegisteredRoutes(r) - r.Run() + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + srv := &http.Server{ + Addr: ":" + port, + Handler: r, + + ReadTimeout: 15 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + } + + go func() { + log.Printf("Starting server on port %s", port) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start server: %v", err) + } + }() + + log.Println("Server started successfully") + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + <-sigChan + log.Println("Shutdown signal received, starting graceful shutdown...") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Printf("Server forced to shutdown: %v", err) + } else { + log.Println("Server stopped gracefully") + } + } diff --git a/api/internal/usecases/collections/chapters.go b/api/internal/usecases/collections/chapters.go index 1eb7f27..57bf043 100644 --- a/api/internal/usecases/collections/chapters.go +++ b/api/internal/usecases/collections/chapters.go @@ -41,30 +41,14 @@ func BatchUploadChapters(novelId string, chapters []domain.Chapter, ctx context. c := firestore_collections.Client{Client: client} - final := []domain.Chapter{} - - for _, chapter := range chapters { - id, err := cmn.GenerateID("chapter") - if err != nil { - return err - } - - chapter.ID = id - chapter.CreatedAt = time.Now().Format("2006-01-02 15:04:05") - chapter.UpdatedAt = time.Now().Format("2006-01-02 15:04:05") - chapter.Deleted = false - - final = append(final, chapter) - } - - if len(final) == 0 { + if len(chapters) == 0 { return &cmn.Error{ Err: errors.New("Nothing to upload"), Status: http.StatusInternalServerError, } } - err = c.BatchUploadChapters(novelId, final, ctx) + err = c.BatchUploadChapters(novelId, chapters, ctx) if err != nil { return err } diff --git a/api/internal/usecases/collections/novels.go b/api/internal/usecases/collections/novels.go index bf08930..014e191 100644 --- a/api/internal/usecases/collections/novels.go +++ b/api/internal/usecases/collections/novels.go @@ -7,8 +7,6 @@ import ( firestore_collections "Codex-Backend/api/internal/infrastructure/collections" "context" "errors" - "io" - "mime/multipart" "net/http" "strings" "time" @@ -25,7 +23,7 @@ func cleanHtml(input string) (string, error) { return "", err } - doc.Find("head, script, style, nav").Remove() + doc.Find("head, script, style, nav, a, img, code, pre").Remove() html, err := doc.Find("body").Html() if err != nil { @@ -35,18 +33,7 @@ func cleanHtml(input string) (string, error) { return htmltomarkdown.ConvertString(html) } -func CreateNovelFromEPUB(epubFile *multipart.FileHeader, ctx context.Context) error { - multipartFIle, err := epubFile.Open() - if err != nil { - return err - } - defer multipartFIle.Close() - - data, err := io.ReadAll(multipartFIle) - if err != nil { - return err - } - +func CreateNovelFromEPUB(data []byte, ctx context.Context) error { parser, err := pamphlet.OpenBytes(data) if err != nil { return err diff --git a/api/internal/usecases/worker/args.go b/api/internal/usecases/worker/args.go new file mode 100644 index 0000000..4b23265 --- /dev/null +++ b/api/internal/usecases/worker/args.go @@ -0,0 +1,22 @@ +package worker + +import ( + firestore_services "Codex-Backend/api/internal/usecases/collections" + "context" + + "github.com/riverqueue/river" +) + +type EPUBWorker struct { + river.WorkerDefaults[ProcessEPUBArgs] +} + +func (w *EPUBWorker) Work(ctx context.Context, job *river.Job[ProcessEPUBArgs]) error { + return firestore_services.CreateNovelFromEPUB(job.Args.File, ctx) +} + +type ProcessEPUBArgs struct { + File []byte `json:"file"` +} + +func (ProcessEPUBArgs) Kind() string { return "process_epub" } diff --git a/go.mod b/go.mod index 4166496..fef7985 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,11 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/heroku/x v0.5.2 + github.com/jackc/pgx/v5 v5.7.5 github.com/joho/godotenv v1.5.1 github.com/oklog/ulid/v2 v2.1.1 + github.com/riverqueue/river v0.24.0 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0 github.com/timsims/pamphlet v0.1.6 golang.org/x/crypto v0.41.0 golang.org/x/time v0.10.0 @@ -35,6 +38,7 @@ require ( github.com/bytedance/sonic v1.12.9 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/cloudwego/base64x v0.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.0.0 // indirect @@ -49,6 +53,9 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -56,6 +63,15 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/riverqueue/river/riverdriver v0.24.0 // indirect + github.com/riverqueue/river/rivershared v0.24.0 // indirect + github.com/riverqueue/river/rivertype v0.24.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -64,6 +80,7 @@ require ( go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.uber.org/goleak v1.3.0 // indirect golang.org/x/arch v0.14.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect diff --git a/go.sum b/go.sum index 3002dfc..9397196 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,16 @@ github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPq github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= github.com/heroku/x v0.5.2 h1:B3g+m78yQk70Mhe1MsrHICgDJwJMZNVDkaQtLhsSA/U= github.com/heroku/x v0.5.2/go.mod h1:B025iaZU9I0gPJSsBYmipizkS5TKgX9NWEx8IQQYTuI= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -113,6 +123,18 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/riverqueue/river v0.24.0 h1:CesL6vymWgz0d+zNwtnSGRWaB+E8Dax+o9cxD7sUmKc= +github.com/riverqueue/river v0.24.0/go.mod h1:UZ3AxU5t6WtyqNssaea/AkRS8h/kJ+E9ImSB3xyb3ns= +github.com/riverqueue/river/riverdriver v0.24.0 h1:HqGgGkls11u+YKDA7cKOdYKlQwRNJyHuGa3UtOvpdT0= +github.com/riverqueue/river/riverdriver v0.24.0/go.mod h1:dEew9DDIKenNvzpm8Edw8+PkqP3c0zl1fKjiQTq2n/w= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0 h1:yV37OIbRrhRwIiGeRT7P4D3szhAemu87BgCf8gTCoU4= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0/go.mod h1:QfznySVKC4ljx53syd/bA/LRSsydAyuD3Q9/EbSniKA= +github.com/riverqueue/river/rivershared v0.24.0 h1:KysokksW75pug2a5RTOc6WESOupWmsylVc6VWvAx+4Y= +github.com/riverqueue/river/rivershared v0.24.0/go.mod h1:UIBfSdai0oWFlwFcoqG4DZX83iA/fLWTEBGrj7Oe1ho= +github.com/riverqueue/river/rivertype v0.24.0 h1:xrQZm/h6U8TBPyTsQPYD5leOapuoBAcdz30bdBwTqOg= +github.com/riverqueue/river/rivertype v0.24.0/go.mod h1:lmdl3vLNDfchDWbYdW2uAocIuwIN+ZaXqAukdSCFqWs= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= @@ -131,6 +153,16 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/timsims/pamphlet v0.1.6 h1:6EX2hJeU4GbAu3AclA1apxVVbPug6KuzXsIP1NqNVb8= github.com/timsims/pamphlet v0.1.6/go.mod h1:i3lq9uyxZeAn2HT1c2+5IJ50oc9tH1K31gHT3WDYi8M= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -158,6 +190,8 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=