diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/frontmatter/parser.go | 37 | ||||
| -rw-r--r-- | internal/storage/b2.go | 140 | ||||
| -rw-r--r-- | internal/storage/storage_interface.go | 19 |
3 files changed, 196 insertions, 0 deletions
diff --git a/internal/frontmatter/parser.go b/internal/frontmatter/parser.go new file mode 100644 index 0000000..6cc3ee8 --- /dev/null +++ b/internal/frontmatter/parser.go @@ -0,0 +1,37 @@ +package frontmatter + +import ( + "fmt" + "regexp" + "time" + + "gopkg.in/yaml.v3" +) + +type Metadata struct { + Title string `yaml:"title"` + ShortDescription string `yaml:"shortDescription"` + ActionDate string `yaml:"actionDate"` + PublishedTime time.Time `yaml:"publishedTime"` + Thumbnail string `yaml:"thumbnail"` + Tags []string `yaml:"tags"` +} + +func ParseFrontmatter(content []byte) (metadata *Metadata, markdown []byte, err error) { + frontmatterRegex := regexp.MustCompile(`^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n([\s\S]*)$`) + matches := frontmatterRegex.FindSubmatch(content) + + if len(matches) != 3 { + return nil, content, nil + } + + yamlContent := matches[1] + markdownContent := matches[2] + + metadata = &Metadata{} + if err := yaml.Unmarshal([]byte(yamlContent), &metadata); err != nil { + return nil, nil, fmt.Errorf("failed to parse YAML frontmatter: %w", err) + } + + return metadata, markdownContent, nil +} diff --git a/internal/storage/b2.go b/internal/storage/b2.go new file mode 100644 index 0000000..168690a --- /dev/null +++ b/internal/storage/b2.go @@ -0,0 +1,140 @@ +package storage + +import ( + "context" + "fmt" + "io" + "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) Scan() ([]string, error) { + filePaths := []string{} + + iter := sc.bucket.List(context.Background(), b2.ListPrefix(sc.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 + } + + name := obj.Name() + + nameParts := strings.Split(name, ".") + if len(nameParts) < 2 { + continue + } + + ext := strings.ToLower(nameParts[len(nameParts)-1]) + if ext != "md" { + continue + } + + filePaths = append(filePaths, strings.TrimPrefix(name, sc.prefix)) + } + + if err := iter.Err(); err != nil { + return nil, fmt.Errorf("iterate over B2 objects: %w", err) + } + + return filePaths, nil +} + +func (sc *B2StorageClient) GetReader(path string) (io.ReadCloser, int64, error) { + obj := sc.bucket.Object(sc.prefix + path) + if obj == nil { + return nil, 0, fmt.Errorf("failed to reference object in B2 bucket") + } + attrs, err := obj.Attrs(context.Background()) + if err != nil { + return nil, 0, fmt.Errorf("error getting attributes of an object: %w", err) + } + + return obj.NewReader(context.Background()), attrs.Size, nil +} + +func (sc *B2StorageClient) WriteMetadata(path string, metadata *frontmatter.Metadata) error { + obj := sc.bucket.Object(sc.prefix + path) + if obj == nil { + return fmt.Errorf("failed to reference object in B2 bucket") + } + oldAttrs, err := obj.Attrs(context.Background()) + if err != nil { + return fmt.Errorf("error getting attributes of an object: %w", err) + } + + attrs := &b2.Attrs{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, ","), + "metadata-last-update-sha1": oldAttrs.SHA1, + }} + + writer := obj.NewWriter(context.Background(), b2.WithAttrsOption(attrs)) + writer.Close() + + return nil +} + +func (sc *B2StorageClient) FileHasChanged(path string) bool { + obj := sc.bucket.Object(sc.prefix + path) + if obj == nil { + return true + } + + attrs, err := obj.Attrs(context.Background()) + if err != nil { + return true + } + + lastUpdateSha1, ok := attrs.Info["metadata-last-update-sha1"] + if !ok { + return true + } + + return attrs.SHA1 == lastUpdateSha1 +} diff --git a/internal/storage/storage_interface.go b/internal/storage/storage_interface.go new file mode 100644 index 0000000..91eeedb --- /dev/null +++ b/internal/storage/storage_interface.go @@ -0,0 +1,19 @@ +package storage + +import ( + "io" + + "github.com/SayaAndy/saya-today-article-metadata-add/config" + "github.com/SayaAndy/saya-today-article-metadata-add/internal/frontmatter" +) + +type StorageClient interface { + Scan() (paths []string, err error) + GetReader(path string) (reader io.ReadCloser, sz int64, err error) + WriteMetadata(path string, metadata *frontmatter.Metadata) error + FileHasChanged(path string) bool +} + +var NewStorageClientMap = map[string]func(cfg *config.StorageConfig) (StorageClient, error){ + "b2": NewB2StorageClient, +} |