summaryrefslogtreecommitdiffci
path: root/internal/blog/s3.go
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
authorGravatar SayaAndy <saya.andy@posteo.com> 2026-04-15 16:11:51 +0700
committerGravatar SayaAndy <saya.andy@posteo.com> 2026-04-15 16:11:51 +0700
commit66b2a96fcca113105043be3cb9c49f8ee7fe240c (patch)
tree04a9d8b816f6507076bf778769f7990bf153fce4 /internal/blog/s3.go
parenta3aa1dd07abfbbb27a7dfeeb536a6fcc51709256 (diff)
downloadweb-66b2a96fcca113105043be3cb9c49f8ee7fe240c.tar.gz
web-66b2a96fcca113105043be3cb9c49f8ee7fe240c.zip
feat: support s3 for blog pages
refactor: make blog client an interface feat: claude.md
Diffstat (limited to 'internal/blog/s3.go')
-rw-r--r--internal/blog/s3.go140
1 files changed, 140 insertions, 0 deletions
diff --git a/internal/blog/s3.go b/internal/blog/s3.go
new file mode 100644
index 0000000..de4eb3a
--- /dev/null
+++ b/internal/blog/s3.go
@@ -0,0 +1,140 @@
+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)
+}