summaryrefslogtreecommitdiff
path: root/internal/storage/b2.go
blob: 669702315ed9a6d3f600ecc52a191a4555eaa84c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package storage

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/Backblaze/blazer/b2"
	"github.com/SayaAndy/saya-today-article-metadata-add/config"
	"github.com/SayaAndy/saya-today-article-metadata-add/internal/frontmatter"
)

var _ StorageClient = &B2StorageClient{}

type B2StorageClient struct {
	prefix string
	bucket *b2.Bucket
	b2cl   *b2.Client
}

func NewB2StorageClient(cfg *config.StorageConfig) (StorageClient, 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 &B2StorageClient{b2cl: b2cl, bucket: bucket, prefix: b2cfg.Prefix}, nil
}

func (sc *B2StorageClient) GetMetadata(key string) (map[string]string, error) {
	obj := sc.bucket.Object(sc.prefix + key)
	if obj == nil {
		return nil, fmt.Errorf("failed to reference object in B2 bucket")
	}
	attrs, err := obj.Attrs(context.Background())
	if err != nil {
		if b2.IsNotExist(err) {
			return map[string]string{}, nil
		}
		return nil, fmt.Errorf("get attributes of B2 object %q: %w", sc.prefix+key, err)
	}
	return attrs.Info, nil
}

func (sc *B2StorageClient) Put(key string, content []byte, metadata *frontmatter.Metadata) error {
	geolocationParts := strings.Split(metadata.Geolocation, " ")
	if (len(geolocationParts) == 1 && geolocationParts[0] != "") || len(geolocationParts) >= 4 {
		return fmt.Errorf("invalid geolocation format, expecting '{x} {y} [areaError]' or an empty string")
	}
	if len(geolocationParts) >= 2 {
		if _, err := strconv.ParseFloat(geolocationParts[0], 64); err != nil {
			return fmt.Errorf("invalid geolocation parameter, expected float for X: %w", err)
		}
		if _, err := strconv.ParseFloat(geolocationParts[1], 64); err != nil {
			return fmt.Errorf("invalid geolocation parameter, expected float for Y: %w", err)
		}
	}
	if len(geolocationParts) == 3 {
		if _, err := strconv.ParseFloat(geolocationParts[2], 64); err != nil {
			return fmt.Errorf("invalid geolocation parameter, expected float for area error: %w", err)
		}
	}

	medley := ""
	if metadata.Medley != "" {
		medley = fmt.Sprintf("%s %d", metadata.Medley, metadata.MedleyPart)
	}

	attrs := &b2.Attrs{
		ContentType: "text/markdown; charset=utf-8",
		Info: map[string]string{
			"title":             metadata.Title,
			"short-description": metadata.ShortDescription,
			"action-date":       metadata.ActionDate,
			"published-time":    metadata.PublishedTime.Format(time.RFC3339),
			"thumbnail":         metadata.Thumbnail,
			"tags":              strings.Join(metadata.Tags, ","),
			"geolocation":       metadata.Geolocation,
			"medley":            medley,
		}}

	prod := sc.bucket.Object(sc.prefix + key)
	if prod == nil {
		return fmt.Errorf("failed to reference prod object in B2 bucket")
	}

	writer := prod.NewWriter(context.Background(), b2.WithAttrsOption(attrs))
	defer writer.Close()
	if _, err := writer.Write(content); err != nil {
		return fmt.Errorf("failed to write an object back after attribute settings: %w", err)
	}

	return nil
}

func (sc *B2StorageClient) BuildIndex() error {
	return fmt.Errorf("BuildIndex not implemented for B2 storage")
}