| -rw-r--r-- | CLAUDE.md | 62 | ||||
| -rw-r--r-- | config/config.go | 79 | ||||
| -rw-r--r-- | go.mod | 12 | ||||
| -rw-r--r-- | go.sum | 24 | ||||
| -rw-r--r-- | internal/b2/client.go (renamed from internal/blog/b2.go) | 29 | ||||
| -rw-r--r-- | internal/blog/client.go | 27 | ||||
| -rw-r--r-- | internal/blog/s3.go | 140 | ||||
| -rw-r--r-- | internal/blogtrigger/blogtrigger.go | 22 | ||||
| -rw-r--r-- | internal/factgiver/factgiver.go | 10 | ||||
| -rw-r--r-- | internal/mailer/mailer.go | 4 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-blog-search.go | 6 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-like-get.go | 2 | ||||
| -rw-r--r-- | internal/router/handlers/api-v1-like-put.go | 2 | ||||
| -rw-r--r-- | internal/router/handlers/lang-blog-title.go | 16 | ||||
| -rw-r--r-- | internal/router/handlers/lang-blog.go | 8 | ||||
| -rw-r--r-- | internal/router/handlers/lang-map.go | 8 | ||||
| -rw-r--r-- | internal/router/handlers/lang-user.go | 2 | ||||
| -rw-r--r-- | internal/router/router.go | 10 |
18 files changed, 63 insertions, 400 deletions
diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 34a12ce..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,62 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build & Run - -```bash -# Build (CGO required for SQLite) -CGO_ENABLED=1 go build -o sayana-web . - -# Run (select config via ENVIRONMENT) -ENVIRONMENT=local ./sayana-web - -# Generate Tailwind CSS -npx @tailwindcss/cli -i ./static/input.css -o ./static/output.css - -# Docker build -docker build -t sayana-web . -``` - -Server listens on `:3000`. No Makefile. No test suite currently. - -## Environment - -Config selected by `ENVIRONMENT` var (`local`/`stage`/`prod`) → `config/config.{env}.yaml`. - -Key env vars: `B2_KEY_ID`, `B2_APPLICATION_KEY`, `MAIL_HOST`, `MAIL_ADDRESS`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_SALT`. See `.envrc` for local setup. - -## Architecture - -**Go + Fiber v2** web app serving a multi-language blog/travel site with SSR. - -### Request Flow -1. Fiber router with middleware (CORS, compression, ETags, trailing slash) -2. Routes auto-register via `init()` in `internal/router/handlers/` — each handler file adds to `router.Routes` global slice -3. Handlers implement `Route` interface: `Filter()`, `IsTemplated()`, `Render()`, `RenderBody()` -4. Templated routes render via `TemplateManager` (Go `html/template`) with layout composition -5. Pages cached in Ristretto (512MB in-memory) with TTL; segments loadable via `/api/v1/general-page/:part` - -### Storage -- **Blog content:** Markdown files with YAML frontmatter on B2 or S3 (configurable via `blog.Client` interface) -- **Database:** SQLite3 via `database/sql` (no ORM). Migrations in `migrations/` via golang-migrate -- **Tables:** `blog_likes`, `blog_views`, `user_email_table`, subscription tables -- **Session cache:** SQLite-backed `ClientCache` for client IDs/preferences - -### Key Subsystems -- **Markdown rendering:** Goldmark with custom extensions — `TailwindExtension` (applies Tailwind classes) and `GLightboxExtension` (image galleries) -- **Mailer:** SMTP email with verification codes, subscription management -- **BlogTrigger:** gocron scheduler that detects new posts and sends notification emails -- **i18n:** Language in URL path (`/en/`, `/ru/`), locale YAML files in `locale/` - -### Handler Pattern -Handlers in `internal/router/handlers/` follow naming convention matching routes: -- `lang-blog.go` → `/:lang/blog/` pages -- `api-v1-like-put.go` → `/api/v1/like` PUT endpoint -- Each handler self-registers in `init()`, no central route table to maintain - -## Deployment - -CI/CD via GitHub Actions → Docker image to ghcr.io → Ansible playbook (`deploy/`). -- `stage` branch → stage environment -- Version tags → prod environment diff --git a/config/config.go b/config/config.go index e48a28b..eb4a91d 100644 --- a/config/config.go +++ b/config/config.go @@ -1,8 +1,6 @@ package config import ( - "encoding/json" - "fmt" "log/slog" "os" @@ -27,72 +25,8 @@ type BlogPagesConfig struct { } type StorageConfig struct { - Type string `json:"type" yaml:"type,oneof=b2 s3"` - Config any `json:"Config" yaml:"config" validate:"required"` -} - -func (sc *StorageConfig) UnmarshalJSON(data []byte) error { - var tmp struct { - Type string `json:"Type"` - Config json.RawMessage `json:"Config"` - } - - if err := json.Unmarshal(data, &tmp); err != nil { - return err - } - - sc.Type = tmp.Type - - switch tmp.Type { - case "b2": - var b2Config B2Config - if err := json.Unmarshal(tmp.Config, &b2Config); err != nil { - return fmt.Errorf("unmarshal B2Config: %w", err) - } - sc.Config = &b2Config - case "s3": - var s3Config S3Config - if err := json.Unmarshal(tmp.Config, &s3Config); err != nil { - return fmt.Errorf("unmarshal S3Config: %w", err) - } - sc.Config = &s3Config - default: - return fmt.Errorf("unsupported storage type: %s", tmp.Type) - } - - return nil -} - -func (sc *StorageConfig) UnmarshalYAML(value *yaml.Node) error { - var tmp struct { - Type string `yaml:"type"` - Config yaml.Node `yaml:"config"` - } - - if err := value.Decode(&tmp); err != nil { - return err - } - - sc.Type = tmp.Type - - switch tmp.Type { - case "b2": - var b2Config B2Config - if err := tmp.Config.Decode(&b2Config); err != nil { - return fmt.Errorf("unmarshal B2Config: %w", err) - } - sc.Config = &b2Config - case "s3": - var s3Config S3Config - if err := tmp.Config.Decode(&s3Config); err != nil { - return fmt.Errorf("unmarshal S3Config: %w", err) - } - sc.Config = &s3Config - default: - return fmt.Errorf("unsupported storage type: %s", tmp.Type) - } - - return nil + Type string `json:"type" yaml:"type"` + Config B2Config `json:"Config" yaml:"config" validate:"required"` } type B2Config struct { @@ -103,15 +37,6 @@ type B2Config struct { ApplicationKey string `json:"ApplicationKey" yaml:"applicationKey"` } -type S3Config struct { - BucketName string `json:"BucketName" yaml:"bucketName" validate:"required,min=1"` - Region string `json:"Region" yaml:"region" validate:"required,min=1"` - Prefix string `json:"Prefix" yaml:"prefix"` - Endpoint string `json:"Endpoint" yaml:"endpoint" validate:"required,url"` - AccessKeyID string `json:"AccessKeyID" yaml:"accessKeyID"` - SecretAccessKey string `json:"SecretAccessKey" yaml:"secretAccessKey"` -} - type FactGiverConfig struct { Storage StorageConfig `json:"Storage" yaml:"storage" validate:"required"` FactsFileName string `json:"FactsFileName" yaml:"factsFileName" validate:"required"` @@ -19,18 +19,6 @@ require ( require ( github.com/andybalholm/brotli v1.2.1 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect - github.com/aws/smithy-go v1.24.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -2,30 +2,6 @@ github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQ github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= -github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk= -github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= diff --git a/internal/blog/b2.go b/internal/b2/client.go index 0caa0ba..fda79e0 100644 --- a/internal/blog/b2.go +++ b/internal/b2/client.go @@ -1,4 +1,4 @@ -package blog +package b2 import ( "context" @@ -18,27 +18,30 @@ type B2Client struct { b2cl *b2.Client } -func NewB2Client(cfg *config.StorageConfig) (Client, error) { - if cfg.Type != "b2" { - return nil, fmt.Errorf("invalid storage type for B2InputClient") - } - b2cfg := cfg.Config.(*config.B2Config) - - b2cl, err := b2.NewClient(context.Background(), b2cfg.KeyID, b2cfg.ApplicationKey) +func NewB2Client(cfg *config.B2Config) (*B2Client, error) { + b2cl, err := b2.NewClient(context.Background(), cfg.KeyID, cfg.ApplicationKey) if err != nil { return nil, err } - bucket, err := b2cl.Bucket(context.Background(), b2cfg.BucketName) + bucket, err := b2cl.Bucket(context.Background(), cfg.BucketName) if err != nil { return nil, err } - return &B2Client{b2cl: b2cl, bucket: bucket, prefix: b2cfg.Prefix}, nil + return &B2Client{b2cl: b2cl, bucket: bucket, prefix: cfg.Prefix}, nil +} + +type BlogPage struct { + Link string + FileName string + Lang string + ModifiedTime time.Time + Metadata *frontmatter.Metadata } -func (c *B2Client) Scan(prefix string) ([]*Page, error) { - filePaths := []*Page{} +func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) { + filePaths := []*BlogPage{} iter := c.bucket.List(context.Background(), b2.ListPrefix(c.prefix+prefix)) @@ -79,7 +82,7 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) { lang, _ := strings.CutPrefix(linkParts[0], c.prefix) - filePaths = append(filePaths, &Page{ + filePaths = append(filePaths, &BlogPage{ Link: obj.Name(), FileName: fileName, Lang: lang, diff --git a/internal/blog/client.go b/internal/blog/client.go deleted file mode 100644 index 56bbee4..0000000 --- a/internal/blog/client.go +++ /dev/null @@ -1,27 +0,0 @@ -package blog - -import ( - "time" - - "github.com/SayaAndy/saya-today-web/config" - "github.com/SayaAndy/saya-today-web/internal/frontmatter" -) - -type Page struct { - Link string - FileName string - Lang string - ModifiedTime time.Time - Metadata *frontmatter.Metadata -} - -type Client interface { - Scan(prefix string) ([]*Page, error) - ReadAll(path string) ([]byte, error) - ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) -} - -var NewClientMap = map[string]func(*config.StorageConfig) (Client, error){ - "b2": NewB2Client, - "s3": NewS3Client, -} diff --git a/internal/blog/s3.go b/internal/blog/s3.go deleted file mode 100644 index de4eb3a..0000000 --- a/internal/blog/s3.go +++ /dev/null @@ -1,140 +0,0 @@ -package blog - -import ( - "context" - "fmt" - "io" - "slices" - "strings" - "time" - - "github.com/SayaAndy/saya-today-web/config" - "github.com/SayaAndy/saya-today-web/internal/frontmatter" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/s3" -) - -type S3Client struct { - prefix string - bucketName string - s3cl *s3.Client -} - -func NewS3Client(cfg *config.StorageConfig) (Client, error) { - if cfg.Type != "s3" { - return nil, fmt.Errorf("invalid storage type for S3Client") - } - s3cfg := cfg.Config.(*config.S3Config) - - s3cl := s3.New(s3.Options{ - Region: s3cfg.Region, - BaseEndpoint: aws.String(s3cfg.Endpoint), - Credentials: credentials.NewStaticCredentialsProvider(s3cfg.AccessKeyID, s3cfg.SecretAccessKey, ""), - }) - - return &S3Client{s3cfg.Prefix, s3cfg.BucketName, s3cl}, nil -} - -func (c *S3Client) Scan(prefix string) ([]*Page, error) { - pages := []*Page{} - - fullPrefix := c.prefix + prefix - input := &s3.ListObjectsV2Input{ - Bucket: aws.String(c.bucketName), - Prefix: aws.String(fullPrefix), - } - - paginator := s3.NewListObjectsV2Paginator(c.s3cl, input) - - for paginator.HasMorePages() { - output, err := paginator.NextPage(context.Background()) - if err != nil { - return nil, fmt.Errorf("list S3 objects: %w", err) - } - - for _, obj := range output.Contents { - key := aws.ToString(obj.Key) - - if !strings.HasSuffix(key, ".md") { - continue - } - - head, err := c.s3cl.HeadObject(context.Background(), &s3.HeadObjectInput{ - Bucket: aws.String(c.bucketName), - Key: aws.String(key), - }) - if err != nil { - return nil, fmt.Errorf("head S3 object %s: %w", key, err) - } - - if head.ContentType == nil || !strings.Contains(*head.ContentType, "text/markdown") { - continue - } - - meta := head.Metadata - if meta["title"] == "" { - continue - } - - publishedTime, err := time.Parse(time.RFC3339, meta["published-time"]) - if err != nil { - return nil, fmt.Errorf("failed to parse published time metadata field: %w", err) - } - - linkParts := strings.Split(key, "/") - nameParts := strings.Split(linkParts[len(linkParts)-1], ".") - fileName := strings.Join(nameParts[:len(nameParts)-1], ".") - - tags := strings.Split(meta["tags"], ",") - slices.Sort(tags) - - lang, _ := strings.CutPrefix(linkParts[0], c.prefix) - - pages = append(pages, &Page{ - Link: key, - FileName: fileName, - Lang: lang, - ModifiedTime: aws.ToTime(obj.LastModified), - Metadata: &frontmatter.Metadata{ - Title: meta["title"], - ShortDescription: meta["short-description"], - ActionDate: meta["action-date"], - PublishedTime: publishedTime, - Thumbnail: meta["thumbnail"], - Tags: tags, - Geolocation: meta["geolocation"], - }, - }) - } - } - - return pages, nil -} - -func (c *S3Client) ReadAll(path string) ([]byte, error) { - output, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{ - Bucket: aws.String(c.bucketName), - Key: aws.String(c.prefix + path), - }) - if err != nil { - return nil, fmt.Errorf("get S3 object: %w", err) - } - defer output.Body.Close() - - content, err := io.ReadAll(output.Body) - if err != nil { - return nil, fmt.Errorf("read S3 object body: %w", err) - } - - return content, nil -} - -func (c *S3Client) ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) { - contentBytes, err := c.ReadAll(path) - if err != nil { - return nil, nil, fmt.Errorf("failed to read file for frontmatter parsing: %w", err) - } - - return frontmatter.ParseFrontmatter(contentBytes) -} diff --git a/internal/blogtrigger/blogtrigger.go b/internal/blogtrigger/blogtrigger.go index 81efafe..c5b3097 100644 --- a/internal/blogtrigger/blogtrigger.go +++ b/internal/blogtrigger/blogtrigger.go @@ -5,29 +5,29 @@ import ( "log/slog" "github.com/SayaAndy/saya-today-web/config" - "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/go-co-op/gocron/v2" ) type BlogTriggerScheduler struct { s gocron.Scheduler - knownBlogPages map[string]map[string]*blog.Page - blogClient blog.Client - onTrigger func([]*blog.Page) error + knownBlogPages map[string]map[string]*b2.BlogPage + b2Client *b2.B2Client + onTrigger func([]*b2.BlogPage) error } -func NewBlogTriggerScheduler(blogClient blog.Client, availableLanguages []config.AvailableLanguageConfig, cron string, onTrigger func([]*blog.Page) error) (*BlogTriggerScheduler, error) { +func NewBlogTriggerScheduler(b2Client *b2.B2Client, availableLanguages []config.AvailableLanguageConfig, cron string, onTrigger func([]*b2.BlogPage) error) (*BlogTriggerScheduler, error) { s, err := gocron.NewScheduler() if err != nil { return nil, fmt.Errorf("failed to create new scheduler: %w", err) } - knownBlogPages := make(map[string]map[string]*blog.Page, len(availableLanguages)) + knownBlogPages := make(map[string]map[string]*b2.BlogPage, len(availableLanguages)) for _, lang := range availableLanguages { - knownBlogPages[lang.Name] = make(map[string]*blog.Page) + knownBlogPages[lang.Name] = make(map[string]*b2.BlogPage) } - bts := &BlogTriggerScheduler{s, knownBlogPages, blogClient, onTrigger} + bts := &BlogTriggerScheduler{s, knownBlogPages, b2Client, onTrigger} defer bts.s.Start() bts.s.NewJob(gocron.CronJob(cron, false), gocron.NewTask(func(bts *BlogTriggerScheduler) { @@ -53,10 +53,10 @@ func (bts *BlogTriggerScheduler) Close() error { return bts.s.Shutdown() } -func (bts *BlogTriggerScheduler) scan() (newPages []*blog.Page, err error) { - newPages = make([]*blog.Page, 0) +func (bts *BlogTriggerScheduler) scan() (newPages []*b2.BlogPage, err error) { + newPages = make([]*b2.BlogPage, 0) for lang := range bts.knownBlogPages { - posts, err := bts.blogClient.Scan(lang + "/") + posts, err := bts.b2Client.Scan(lang + "/") if err != nil { return nil, fmt.Errorf("failed to scan blog pages in b2 on '%s': %w", lang, err) } diff --git a/internal/factgiver/factgiver.go b/internal/factgiver/factgiver.go index c5b8bc4..5b418ee 100644 --- a/internal/factgiver/factgiver.go +++ b/internal/factgiver/factgiver.go @@ -8,11 +8,11 @@ import ( "time" "github.com/SayaAndy/saya-today-web/config" - "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/b2" ) type FactGiver struct { - blogClient blog.Client + b2Client *b2.B2Client cache map[string][]string langs []config.AvailableLanguageConfig factsFileName string @@ -21,13 +21,13 @@ type FactGiver struct { } func NewFactGiver(cfg *config.FactGiverConfig, langs []config.AvailableLanguageConfig) (*FactGiver, error) { - blogClient, err := blog.NewClientMap[cfg.Storage.Type](&cfg.Storage) + b2Client, err := b2.NewB2Client(&cfg.Storage.Config) if err != nil { return nil, fmt.Errorf("fail to init b2 client for a new fact giver: %s", err.Error()) } factGiver := &FactGiver{ - blogClient: blogClient, + b2Client: b2Client, cache: make(map[string][]string, len(langs)), langs: langs, factsFileName: cfg.FactsFileName, @@ -53,7 +53,7 @@ func (g *FactGiver) Give(lang string) [3]string { func (g *FactGiver) initCache() error { for _, lang := range g.langs { localFacts := strings.Replace(g.factsFileName, "*", lang.Name, 1) - factsContentBytes, err := g.blogClient.ReadAll(localFacts) + factsContentBytes, err := g.b2Client.ReadAll(localFacts) if err != nil { return fmt.Errorf("fail to read '%s' facts file: %s", lang.Name, err.Error()) } diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index ffa7b30..d6dbd17 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -14,7 +14,7 @@ import ( "sync" "time" - "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/internal/templatemanager" "github.com/SayaAndy/saya-today-web/locale" "github.com/dgraph-io/ristretto/v2" @@ -418,7 +418,7 @@ func (m *Mailer) Subscribe(userIdHash []byte, subscriptionType SubscriptionType, return nil } -func (m *Mailer) NewPost(post *blog.Page) error { +func (m *Mailer) NewPost(post *b2.BlogPage) error { tx, err := m.db.Begin() if err != nil { return fmt.Errorf("failed to initialize transaction with db: %s", err) diff --git a/internal/router/handlers/api-v1-blog-search.go b/internal/router/handlers/api-v1-blog-search.go index c9a91ad..1e8a480 100644 --- a/internal/router/handlers/api-v1-blog-search.go +++ b/internal/router/handlers/api-v1-blog-search.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/internal/router" "github.com/gofiber/fiber/v2" ) @@ -56,11 +56,11 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements cacheKey := "blog-search." + lang + ".pages-list" - var pages []*blog.Page + var pages []*b2.BlogPage if pagesBytes, ok := supplements.PageCache.Get(cacheKey); pagesBytes != nil || ok { json.Unmarshal(pagesBytes, &pages) } else { - pages, err = supplements.BlogClient.Scan(lang + "/") + pages, err = supplements.B2Client.Scan(lang + "/") if err != nil { return fiber.StatusInternalServerError, fmt.Errorf("failed to scan pages for '%s' lang: %w", lang, err) } diff --git a/internal/router/handlers/api-v1-like-get.go b/internal/router/handlers/api-v1-like-get.go index 0eaf5f6..e9c176e 100644 --- a/internal/router/handlers/api-v1-like-get.go +++ b/internal/router/handlers/api-v1-like-get.go @@ -48,7 +48,7 @@ func (r *GetLikeHandler) Render(c *fiber.Ctx, supplements *router.Supplements, l page := pathParts[2] pageLink := lang + "/" + page + ".md" - if pages, _ := supplements.BlogClient.Scan(pageLink); len(pages) == 0 { + if pages, _ := supplements.B2Client.Scan(pageLink); len(pages) == 0 { return fiber.StatusNotFound, fmt.Errorf("server did not find '%s' article", pageLink) } diff --git a/internal/router/handlers/api-v1-like-put.go b/internal/router/handlers/api-v1-like-put.go index 434f15a..ea4ef4b 100644 --- a/internal/router/handlers/api-v1-like-put.go +++ b/internal/router/handlers/api-v1-like-put.go @@ -49,7 +49,7 @@ func (r *PutLikeHandler) Render(c *fiber.Ctx, supplements *router.Supplements, l page := pathParts[2] pageLink := lang + "/" + page + ".md" - if pages, _ := supplements.BlogClient.Scan(pageLink); len(pages) == 0 { + if pages, _ := supplements.B2Client.Scan(pageLink); len(pages) == 0 { return fiber.StatusNotFound, fmt.Errorf("server did not find '%s' article", pageLink) } diff --git a/internal/router/handlers/lang-blog-title.go b/internal/router/handlers/lang-blog-title.go index f1be0a5..5bfc6e7 100644 --- a/internal/router/handlers/lang-blog-title.go +++ b/internal/router/handlers/lang-blog-title.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/internal/frontmatter" "github.com/SayaAndy/saya-today-web/internal/router" "github.com/gofiber/fiber/v2" @@ -49,7 +49,7 @@ func (r *BlogPageHandler) ToValidateLang() router.LangSetting { func (r *BlogPageHandler) SitemapInfo(supplements *router.Supplements) []router.SitemapInfo { sitemapInfo := []router.SitemapInfo{} - pages, err := supplements.BlogClient.Scan("") + pages, err := supplements.B2Client.Scan("") if err != nil { return sitemapInfo } @@ -66,7 +66,7 @@ func (r *BlogPageHandler) SitemapInfo(supplements *router.Supplements) []router. } func (r *BlogPageHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) { - metadata, _, err := supplements.BlogClient.ReadFrontmatter(lang + "/" + c.Params("title") + ".md") + metadata, _, err := supplements.B2Client.ReadFrontmatter(lang + "/" + c.Params("title") + ".md") if err != nil { return nil, fmt.Errorf("failed to read frontmatter of the desired blog post: %w", err) } @@ -82,7 +82,7 @@ func (r *BlogPageHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, } func (r *BlogPageHandler) AddLinkedData(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (ld map[string]any, err error) { - metadata, _, err := supplements.BlogClient.ReadFrontmatter(lang + "/" + c.Params("title") + ".md") + metadata, _, err := supplements.B2Client.ReadFrontmatter(lang + "/" + c.Params("title") + ".md") if err != nil { return nil, fmt.Errorf("failed to read frontmatter of the desired blog post: %w", err) } @@ -103,7 +103,7 @@ func (r *BlogPageHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplemen return fiber.StatusBadRequest, fmt.Errorf("failed to get path from referer: %w", err) } - metadata, parsedMarkdown, err := readBlogPost(supplements.MarkdownRenderer, supplements.BlogClient, lang+"/"+pathParts[2]) + metadata, parsedMarkdown, err := readBlogPost(supplements.MarkdownRenderer, supplements.B2Client, lang+"/"+pathParts[2]) if err != nil { return fiber.StatusNotFound, fmt.Errorf("failed to find '%s' post: %w", pathParts[2], err) } @@ -138,7 +138,7 @@ func (r *BlogPageHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplem return fiber.StatusBadRequest, fmt.Errorf("failed to get path from referer: %w", err) } - metadata, _, err := supplements.BlogClient.ReadFrontmatter(lang + "/" + pathParts[2] + ".md") + metadata, _, err := supplements.B2Client.ReadFrontmatter(lang + "/" + pathParts[2] + ".md") if err != nil { return fiber.StatusNotFound, fmt.Errorf("could not read '%s' for metadata: %w", path, err) } @@ -148,8 +148,8 @@ func (r *BlogPageHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplem return fiber.StatusOK, nil } -func readBlogPost(md goldmark.Markdown, blogClient blog.Client, sourceName string) (metadata *frontmatter.Metadata, html string, err error) { - metadata, markdown, err := blogClient.ReadFrontmatter(sourceName + ".md") +func readBlogPost(md goldmark.Markdown, b2Client *b2.B2Client, sourceName string) (metadata *frontmatter.Metadata, html string, err error) { + metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md") if err != nil { return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err) } diff --git a/internal/router/handlers/lang-blog.go b/internal/router/handlers/lang-blog.go index 79ec9f3..709d3dc 100644 --- a/internal/router/handlers/lang-blog.go +++ b/internal/router/handlers/lang-blog.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/internal/router" "github.com/gofiber/fiber/v2" ) @@ -79,7 +79,7 @@ func (r *CatalogueHandler) RenderBody(c *fiber.Ctx, supplements *router.Suppleme queryTags = append(queryTags, string(match[1])) } - tagsArray, err := getTags(supplements.BlogClient, lang) + tagsArray, err := getTags(supplements.B2Client, lang) if err != nil { return fiber.StatusInternalServerError, fmt.Errorf("failed to get the available tags") } @@ -102,8 +102,8 @@ type Tag struct { Count int `json:"Count" yaml:"count"` } -func getTags(blogClient blog.Client, lang string) (tags []Tag, err error) { - pages, err := blogClient.Scan(lang + "/") +func getTags(b2Client *b2.B2Client, lang string) (tags []Tag, err error) { + pages, err := b2Client.Scan(lang + "/") if err != nil { slog.Warn("failed to scan pages via b2", slog.String("error", err.Error())) return nil, fmt.Errorf("failed to scan pages via b2: %w", err) diff --git a/internal/router/handlers/lang-map.go b/internal/router/handlers/lang-map.go index d452bc7..551b485 100644 --- a/internal/router/handlers/lang-map.go +++ b/internal/router/handlers/lang-map.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/internal/router" "github.com/gofiber/fiber/v2" ) @@ -49,8 +49,8 @@ func (r *MapHandler) SitemapInfo(supplements *router.Supplements) []router.Sitem } func (r *MapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - pages, err := supplements.BlogClient.Scan(lang + "/") - slices.SortFunc(pages, func(a *blog.Page, b *blog.Page) int { + pages, err := supplements.B2Client.Scan(lang + "/") + slices.SortFunc(pages, func(a *b2.BlogPage, b *b2.BlogPage) int { return a.Metadata.PublishedTime.Compare(b.Metadata.PublishedTime) }) status := fiber.StatusOK @@ -60,7 +60,7 @@ func (r *MapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang slog.String("lang", lang), ) status = fiber.StatusPartialContent - pages = []*blog.Page{} + pages = []*b2.BlogPage{} } type MapMarker struct { diff --git a/internal/router/handlers/lang-user.go b/internal/router/handlers/lang-user.go index 0dda529..eae1685 100644 --- a/internal/router/handlers/lang-user.go +++ b/internal/router/handlers/lang-user.go @@ -55,7 +55,7 @@ func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, slog.Error("get info from mailer about a client", slog.String("error", err.Error())) } - tagsArray, err := getTags(supplements.BlogClient, lang) + tagsArray, err := getTags(supplements.B2Client, lang) if err != nil { return fiber.ErrInternalServerError.Code, fmt.Errorf("failed to get the available tags") } diff --git a/internal/router/router.go b/internal/router/router.go index e8fb47a..3242759 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -13,7 +13,7 @@ import ( "time" "github.com/SayaAndy/saya-today-web/config" - "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/internal/blogtrigger" "github.com/SayaAndy/saya-today-web/internal/factgiver" "github.com/SayaAndy/saya-today-web/internal/glightbox" @@ -90,7 +90,7 @@ type Route interface { type Supplements struct { DB *sql.DB - BlogClient blog.Client + B2Client *b2.B2Client Localization map[string]*locale.LocaleConfig AvailableLanguages []config.AvailableLanguageConfig ClientCache *ClientCache @@ -140,7 +140,7 @@ func NewRouter(cfg *config.Config) (*Router, error) { } slog.Debug("successfully applied migrations") - supplements.BlogClient, err = blog.NewClientMap[cfg.BlogPages.Storage.Type](&cfg.BlogPages.Storage) + supplements.B2Client, err = b2.NewB2Client(&cfg.BlogPages.Storage.Config) if err != nil { return nil, fmt.Errorf("fail to initialize b2 client: %w", err) } @@ -198,8 +198,8 @@ func NewRouter(cfg *config.Config) (*Router, error) { return nil, fmt.Errorf("fail to initialize mailer: %w", err) } - supplements.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(supplements.BlogClient, cfg.AvailableLanguages, cfg.Mail.Trigger.OnNewPost, - func(bp []*blog.Page) error { + supplements.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(supplements.B2Client, cfg.AvailableLanguages, cfg.Mail.Trigger.OnNewPost, + func(bp []*b2.BlogPage) error { for _, post := range bp { if err := supplements.Mailer.NewPost(post); err != nil { return err |