package blog import ( "context" "encoding/json" "fmt" "slices" "strings" "time" "github.com/Backblaze/blazer/b2" "github.com/SayaAndy/saya-today-web/config" "github.com/SayaAndy/saya-today-web/internal/frontmatter" ) type B2Client struct { prefix string bucket *b2.Bucket 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) if err != nil { return nil, err } bucket, err := b2cl.Bucket(context.Background(), b2cfg.BucketName) if err != nil { return nil, err } return &B2Client{b2cl: b2cl, bucket: bucket, prefix: b2cfg.Prefix}, nil } func (c *B2Client) GetMedleys() ([]MedleyEntry, error) { idxRaw, err := c.readAll(MedleysIndexFileName) if err != nil { return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err) } var idx []MedleyEntry if err := json.Unmarshal(idxRaw, &idx); err != nil { return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err) } return idx, nil } func (c *B2Client) Scan(prefix string) ([]*Page, error) { filePaths := []*Page{} iter := c.bucket.List(context.Background(), b2.ListPrefix(c.prefix+prefix)) for iter.Next() { obj := iter.Object() if obj == nil { return nil, fmt.Errorf("failed to reference object in B2 bucket") } attrs, err := obj.Attrs(context.Background()) if err != nil { return nil, fmt.Errorf("get attributes for object: %w", err) } if attrs.Status != b2.Uploaded { continue } if !strings.Contains(attrs.ContentType, "text/markdown") { continue } if _, ok := attrs.Info["title"]; !ok { continue } publishedTime, err := time.Parse(time.RFC3339, attrs.Info["published-time"]) if err != nil { return nil, fmt.Errorf("failed to parse published time metadata field: %w", err) } link := obj.Name() fileName := link[strings.LastIndex(link, "/")+1 : strings.LastIndex(link, ".")] tags := strings.Split(attrs.Info["tags"], ",") slices.Sort(tags) lang, _ := strings.CutPrefix(link[0:strings.Index(link, "/")], c.prefix) filePaths = append(filePaths, &Page{ Link: link, FileName: fileName, Lang: lang, ModifiedTime: attrs.LastModified, Metadata: &frontmatter.Metadata{ Title: attrs.Info["title"], ShortDescription: attrs.Info["short-description"], ActionDate: attrs.Info["action-date"], PublishedTime: publishedTime, Thumbnail: attrs.Info["thumbnail"], Tags: tags, Geolocation: attrs.Info["geolocation"], }, }) } if err := iter.Err(); err != nil { return nil, fmt.Errorf("iterate over B2 objects: %w", err) } return filePaths, nil } func (c *B2Client) ReadAll(path string) ([]byte, error) { return c.readAll(c.prefix + path) } func (c *B2Client) readAll(path string) ([]byte, error) { obj := c.bucket.Object(path) if obj == nil { return nil, fmt.Errorf("failed to reference object in B2 bucket") } attrs, err := obj.Attrs(context.Background()) if err != nil { return nil, fmt.Errorf("error getting attributes of an object: %w", err) } content := make([]byte, attrs.Size) reader := obj.NewReader(context.Background()) if _, err = reader.Read(content); err != nil { return nil, fmt.Errorf("failed to read file content: %w", err) } return content, nil } func (c *B2Client) 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) }