summaryrefslogtreecommitdiff
diff options
from:
to:
context:
space:
mode:
authorGravatar SayaAndy <montferrat@tuta.io> 2025-07-16 17:49:03 +0700
committerGravatar SayaAndy <montferrat@tuta.io> 2025-07-16 17:49:03 +0700
commitf968d443dc5c6e1f5140922313b0d6e8019fc269 (patch)
treeefe1507ec20feccbe408ca2b05303400546431b1
parent4fa22c837cb00b87cd43c0aceec26729b8c3fa48 (diff)
downloadthumbnail-generator-f968d443dc5c6e1f5140922313b0d6e8019fc269.tar.gz
thumbnail-generator-f968d443dc5c6e1f5140922313b0d6e8019fc269.zip
feat: implement local-unix clients
-rw-r--r--config/config-b2.sample.json (renamed from config/config.json)2
-rw-r--r--config/config-local-unix.sample.json40
-rw-r--r--config/config.go77
-rw-r--r--go.mod14
-rw-r--r--go.sum13
-rw-r--r--internal/client/input/b2.go2
-rw-r--r--internal/client/input/input_client_interface.go7
-rw-r--r--internal/client/input/local_unix.go107
-rw-r--r--internal/client/output/b2.go2
-rw-r--r--internal/client/output/local_unix.go130
-rw-r--r--internal/client/output/output_client_interface.go6
-rw-r--r--internal/converter/converter_interface.go16
-rw-r--r--internal/converter/processor_interface.go8
-rw-r--r--internal/converter/webp.go2
-rw-r--r--main.go16
15 files changed, 395 insertions, 47 deletions
diff --git a/config/config.json b/config/config-b2.sample.json
index db4ddd4..d3d772e 100644
--- a/config/config.json
+++ b/config/config-b2.sample.json
@@ -41,4 +41,4 @@
}
}
}
-}
+} \ No newline at end of file
diff --git a/config/config-local-unix.sample.json b/config/config-local-unix.sample.json
new file mode 100644
index 0000000..637a05a
--- /dev/null
+++ b/config/config-local-unix.sample.json
@@ -0,0 +1,40 @@
+{
+ "ForceRewrite": false,
+ "MaxConcurrentJobs": 4,
+ "LogLevel": "debug",
+ "Input": {
+ "Storage": {
+ "Type": "local-unix",
+ "Config": {
+ "MaxDepth": 3,
+ "Path": "/tmp/thumbnailing/full/"
+ }
+ },
+ "KnownExtensions": [
+ "jpg",
+ "jpeg",
+ "png"
+ ]
+ },
+ "Converter": {
+ "Type": "webp",
+ "Config": {
+ "Quality": 80,
+ "Size": {
+ "MaxWidth": 800,
+ "MaxHeight": 0
+ }
+ }
+ },
+ "Output": {
+ "Storage": {
+ "Type": "local-unix",
+ "Config": {
+ "Path": "/tmp/thumbnailing/thumbnails/",
+ "DirPermissionMode": "0755",
+ "FilePermissionMode": "0644",
+ "AttributesImplementation": "xattr"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/config/config.go b/config/config.go
index 3ca8f84..d2b7072 100644
--- a/config/config.go
+++ b/config/config.go
@@ -19,16 +19,16 @@ type Config struct {
}
type InputConfig struct {
- Storage StorageConfig `json:"Storage" validate:"required"`
- KnownExtensions []string `json:"KnownExtensions" validate:"required,min=0,dive,min=1"`
+ Storage InputStorageConfig `json:"Storage" validate:"required"`
+ KnownExtensions []string `json:"KnownExtensions" validate:"required,min=0,dive,min=1"`
}
-type StorageConfig struct {
- Type string `json:"Type" validate:"required,oneof=b2 local"`
+type InputStorageConfig struct {
+ Type string `json:"Type" validate:"required,oneof=b2 local-unix"`
Config any `json:"Config" validate:"required"`
}
-func (sc *StorageConfig) UnmarshalJSON(data []byte) error {
+func (sc *InputStorageConfig) UnmarshalJSON(data []byte) error {
var tmp struct {
Type string `json:"Type"`
Config json.RawMessage `json:"Config"`
@@ -47,12 +47,49 @@ func (sc *StorageConfig) UnmarshalJSON(data []byte) error {
return fmt.Errorf("unmarshal B2Config: %w", err)
}
sc.Config = &b2Config
- case "local":
- var localConfig LocalConfig
- if err := json.Unmarshal(tmp.Config, &localConfig); err != nil {
- return fmt.Errorf("unmarshal LocalConfig: %w", err)
+ case "local-unix":
+ var localUnixConfig InputLocalUnixConfig
+ if err := json.Unmarshal(tmp.Config, &localUnixConfig); err != nil {
+ return fmt.Errorf("unmarshal LocalUnixConfig: %w", err)
}
- sc.Config = &localConfig
+ sc.Config = &localUnixConfig
+ default:
+ return fmt.Errorf("unsupported storage type: %s", tmp.Type)
+ }
+
+ return nil
+}
+
+type OutputStorageConfig struct {
+ Type string `json:"Type" validate:"required,oneof=b2 local-unix"`
+ Config any `json:"Config" validate:"required"`
+}
+
+func (sc *OutputStorageConfig) 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 "local-unix":
+ var localUnixConfig OutputLocalUnixConfig
+ if err := json.Unmarshal(tmp.Config, &localUnixConfig); err != nil {
+ return fmt.Errorf("unmarshal LocalUnixConfig: %w", err)
+ }
+ sc.Config = &localUnixConfig
default:
return fmt.Errorf("unsupported storage type: %s", tmp.Type)
}
@@ -68,8 +105,20 @@ type B2Config struct {
ApplicationKey string `json:"ApplicationKey"`
}
-type LocalConfig struct {
- Path string `json:"Path" validate:"required,min=1"`
+type InputLocalUnixConfig struct {
+ MaxDepth int `json:"MaxDepth" validate:"required,min=0"`
+ Path string `json:"Path" validate:"required,min=1"`
+}
+
+type OutputConfig struct {
+ Storage OutputStorageConfig `json:"Storage" validate:"required"`
+}
+
+type OutputLocalUnixConfig struct {
+ Path string `json:"Path" validate:"required,min=1"`
+ DirPermissionMode string `json:"DirPermissionMode" validate:"required,min=3"`
+ FilePermissionMode string `json:"FilePermissionMode" validate:"required,min=3"`
+ AttributesImplementation string `json:"AttributesImplementation" validate:"required,oneof=xattr none"`
}
type ConverterConfig struct {
@@ -113,10 +162,6 @@ type SizeConfig struct {
MaxHeight int `json:"MaxHeight" validate:"required,min=0"`
}
-type OutputConfig struct {
- Storage StorageConfig `json:"Storage" validate:"required"`
-}
-
func LoadConfig(path string, config *Config) error {
fileBytes, err := os.ReadFile(path)
if err != nil {
diff --git a/go.mod b/go.mod
index fcfa935..82b25c0 100644
--- a/go.mod
+++ b/go.mod
@@ -3,17 +3,19 @@ module github.com/SayaAndy/saya-today-thumbnail-generator
go 1.24.4
require (
- github.com/Backblaze/blazer v0.7.2 // indirect
- github.com/benbusby/b2 v1.4.0 // indirect
+ github.com/Backblaze/blazer v0.7.2
+ github.com/go-playground/validator/v10 v10.27.0
+ github.com/kolesa-team/go-webp v1.0.5
+ golang.org/x/image v0.29.0
+ golang.org/x/sys v0.30.0
+)
+
+require (
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
- github.com/go-playground/validator/v10 v10.27.0 // indirect
- github.com/kolesa-team/go-webp v1.0.5 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
- golang.org/x/image v0.29.0 // indirect
golang.org/x/net v0.34.0 // indirect
- golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.27.0 // indirect
)
diff --git a/go.sum b/go.sum
index 2f0c3d2..bbb6c11 100644
--- a/go.sum
+++ b/go.sum
@@ -1,10 +1,12 @@
github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM=
github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10=
-github.com/benbusby/b2 v1.4.0 h1:TbbSskomOrJhJUzOSo3EnU/FCveGwBOk5GrefxdINS0=
-github.com/benbusby/b2 v1.4.0/go.mod h1:33DCcJUrJLjGlFT1wLIxzg+/oVv4TlYBnbdlfazKnTg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -15,9 +17,12 @@ github.com/kolesa-team/go-webp v1.0.5 h1:GZQHJBaE8dsNKZltfwqsL0qVJ7vqHXsfA+4AHrQ
github.com/kolesa-team/go-webp v1.0.5/go.mod h1:QmJu0YHXT3ex+4SgUvs+a+1SFCDcCqyZg+LbIuNNTnE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
@@ -26,9 +31,9 @@ golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/client/input/b2.go b/internal/client/input/b2.go
index 54a6b09..0092313 100644
--- a/internal/client/input/b2.go
+++ b/internal/client/input/b2.go
@@ -21,7 +21,7 @@ type B2InputClient struct {
knownExtensions []string
}
-func NewB2InputClient(cfg *config.InputConfig) (*B2InputClient, error) {
+func NewB2InputClient(cfg *config.InputConfig) (InputClient, error) {
if cfg.Storage.Type != "b2" {
return nil, fmt.Errorf("invalid storage type for B2InputClient")
}
diff --git a/internal/client/input/input_client_interface.go b/internal/client/input/input_client_interface.go
index c896ca4..5d12fa8 100644
--- a/internal/client/input/input_client_interface.go
+++ b/internal/client/input/input_client_interface.go
@@ -3,6 +3,8 @@ package input
import (
"io"
"time"
+
+ "github.com/SayaAndy/saya-today-thumbnail-generator/config"
)
type InputClient interface {
@@ -20,3 +22,8 @@ type MetadataStruct struct {
LastModified time.Time
Misc map[string]string
}
+
+var NewInputClientMap = map[string]func(cfg *config.InputConfig) (InputClient, error){
+ "b2": NewB2InputClient,
+ "local-unix": NewLocalUnixInputClient,
+}
diff --git a/internal/client/input/local_unix.go b/internal/client/input/local_unix.go
new file mode 100644
index 0000000..2df3a41
--- /dev/null
+++ b/internal/client/input/local_unix.go
@@ -0,0 +1,107 @@
+package input
+
+import (
+ "fmt"
+ "io"
+ "log/slog"
+ "mime"
+ "os"
+ "slices"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/SayaAndy/saya-today-thumbnail-generator/config"
+)
+
+var _ InputClient = (*LocalUnixInputClient)(nil)
+
+type LocalUnixInputClient struct {
+ path string
+ maxDepth int
+ knownExtensions []string
+}
+
+func NewLocalUnixInputClient(cfg *config.InputConfig) (InputClient, error) {
+ if cfg.Storage.Type != "local-unix" {
+ return nil, fmt.Errorf("invalid storage type for LocalUnixInputClient")
+ }
+ localCfg := cfg.Storage.Config.(*config.InputLocalUnixConfig)
+
+ return &LocalUnixInputClient{
+ path: localCfg.Path,
+ maxDepth: localCfg.MaxDepth,
+ knownExtensions: cfg.KnownExtensions,
+ }, nil
+}
+
+func (c *LocalUnixInputClient) Scan() ([]string, error) {
+ return c.recursiveScan(c.path, c.maxDepth)
+}
+
+func (c *LocalUnixInputClient) recursiveScan(dir string, depth int) ([]string, error) {
+ filePaths := make([]string, 0)
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, fmt.Errorf("fail to read directory: %w", err)
+ }
+
+ for _, entry := range entries {
+ if entry.IsDir() && depth > 0 {
+ subFilePaths, err := c.recursiveScan(dir+entry.Name()+"/", depth-1)
+ if err != nil {
+ return nil, fmt.Errorf("fail to scan subdirectory '%s': %w", entry.Name(), err)
+ }
+ filePaths = append(filePaths, subFilePaths...)
+ } else if !entry.IsDir() {
+ nameParts := strings.Split(entry.Name(), ".")
+ if len(nameParts) < 2 {
+ continue
+ }
+ if slices.Contains(c.knownExtensions, strings.ToLower(nameParts[len(nameParts)-1])) {
+ filePaths = append(filePaths, strings.TrimPrefix(dir+entry.Name(), c.path))
+ }
+ fmt.Println(entry.Name())
+ }
+ }
+
+ return filePaths, nil
+}
+
+func (c *LocalUnixInputClient) ReadMetadata(path string) (*MetadataStruct, error) {
+ nodePathParts := strings.Split(path, "/")
+ nodeName := nodePathParts[len(nodePathParts)-1]
+ nodeNameParts := strings.Split(nodeName, ".")
+ nodeExt := ""
+ if len(nodeNameParts) >= 2 {
+ nodeExt = nodeNameParts[len(nodeNameParts)-1]
+ }
+ slog.Debug("got a file extension", slog.String("extension", nodeExt), slog.String("path", path), slog.String("filename", nodeName))
+
+ fileInfo, err := os.Stat(c.path + path)
+ if err != nil {
+ return nil, fmt.Errorf("fail to read file info: %w", err)
+ }
+
+ stat_t := fileInfo.Sys().(*syscall.Stat_t)
+ creationTime := time.Unix(stat_t.Ctim.Sec, stat_t.Ctim.Nsec)
+
+ misc := map[string]string{
+ "Size": strconv.FormatInt(fileInfo.Size(), 10),
+ }
+
+ return &MetadataStruct{
+ Name: fileInfo.Name(),
+ StorageType: "local-unix",
+ Hash: strconv.FormatInt(fileInfo.ModTime().Unix(), 16),
+ ContentType: mime.TypeByExtension("." + nodeExt),
+ FirstCreated: creationTime,
+ LastModified: fileInfo.ModTime(),
+ Misc: misc,
+ }, nil
+}
+
+func (c *LocalUnixInputClient) GetReader(path string) (io.ReadCloser, error) {
+ return os.Open(c.path + path)
+}
diff --git a/internal/client/output/b2.go b/internal/client/output/b2.go
index 6979e5b..e741e25 100644
--- a/internal/client/output/b2.go
+++ b/internal/client/output/b2.go
@@ -19,7 +19,7 @@ type B2OutputClient struct {
b2cl *b2.Client
}
-func NewB2OutputClient(cfg *config.OutputConfig) (*B2OutputClient, error) {
+func NewB2OutputClient(cfg *config.OutputConfig) (OutputClient, error) {
if cfg.Storage.Type != "b2" {
return nil, fmt.Errorf("invalid storage type for B2OutputClient")
}
diff --git a/internal/client/output/local_unix.go b/internal/client/output/local_unix.go
new file mode 100644
index 0000000..798be3b
--- /dev/null
+++ b/internal/client/output/local_unix.go
@@ -0,0 +1,130 @@
+package output
+
+import (
+ "fmt"
+ "io"
+ "mime"
+ "os"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/SayaAndy/saya-today-thumbnail-generator/config"
+ "github.com/SayaAndy/saya-today-thumbnail-generator/internal/client/input"
+ "golang.org/x/sys/unix"
+)
+
+var _ OutputClient = (*LocalUnixOutputClient)(nil)
+
+type LocalUnixOutputClient struct {
+ path string
+ fileMode uint32
+ dirMode uint32
+ attrMode string
+}
+
+func NewLocalUnixOutputClient(cfg *config.OutputConfig) (OutputClient, error) {
+ if cfg.Storage.Type != "local-unix" {
+ return nil, fmt.Errorf("invalid storage type for LocalUnixOutputClient")
+ }
+ localCfg := cfg.Storage.Config.(*config.OutputLocalUnixConfig)
+
+ fpm, err := strconv.ParseInt(localCfg.FilePermissionMode, 8, 32)
+ if err != nil {
+ return nil, fmt.Errorf("fail to parse file permission mode as an octal number: %w", err)
+ }
+
+ dpm, err := strconv.ParseInt(localCfg.DirPermissionMode, 8, 32)
+ if err != nil {
+ return nil, fmt.Errorf("fail to parse directory permission mode as an octal number: %w", err)
+ }
+
+ return &LocalUnixOutputClient{localCfg.Path, uint32(fpm), uint32(dpm), localCfg.AttributesImplementation}, nil
+}
+
+func (c *LocalUnixOutputClient) GetWriter(path string, inputMetadata *input.MetadataStruct) (io.WriteCloser, error) {
+ pathSegments := strings.Split(path, "/")
+ dirpath := strings.Join(pathSegments[0:len(pathSegments)-1], "/")
+ if err := os.MkdirAll(c.path+dirpath, os.FileMode(c.dirMode)); err != nil {
+ return nil, fmt.Errorf("fail to mkdir parent directories for a path: %w", err)
+ }
+
+ var err error
+ if _, err = os.Stat(c.path + path); err != nil && !os.IsNotExist(err) {
+ return nil, fmt.Errorf("fail to stat a file, although file exists: %w", err)
+ }
+ if os.IsNotExist(err) {
+ f, err := os.OpenFile(c.path+path, os.O_WRONLY|os.O_CREATE, os.FileMode(c.fileMode))
+ if err != nil {
+ return nil, fmt.Errorf("fail to create a file: %w", err)
+ }
+ f.Close()
+ }
+
+ switch c.attrMode {
+ case "xattr":
+ if err := unix.Setxattr(c.path+path, "user.originalfile.mddate", []byte(strconv.FormatInt(inputMetadata.LastModified.Unix(), 16)), 0); err != nil {
+ return nil, fmt.Errorf("fail to write user.originalfile.mddate xattribute: %w", err)
+ }
+ case "none":
+ default:
+ return nil, fmt.Errorf("unknown attributes implementation: %s", c.attrMode)
+ }
+
+ return os.OpenFile(c.path+path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(c.fileMode))
+}
+
+func (c *LocalUnixOutputClient) ReadMetadata(path string) (*MetadataStruct, error) {
+ nodePathParts := strings.Split(path, "/")
+ nodeName := nodePathParts[len(nodePathParts)-1]
+ nodeNameParts := strings.Split(nodeName, ".")
+ nodeExt := ""
+ if len(nodeNameParts) >= 2 {
+ nodeExt = nodeNameParts[len(nodeNameParts)-1]
+ }
+
+ fileInfo, err := os.Stat(c.path + path)
+ if err != nil {
+ return nil, fmt.Errorf("fail to read file info: %w", err)
+ }
+
+ stat_t := fileInfo.Sys().(*syscall.Stat_t)
+ creationTime := time.Unix(stat_t.Ctim.Sec, stat_t.Ctim.Nsec)
+
+ misc := map[string]string{
+ "Size": strconv.FormatInt(fileInfo.Size(), 10),
+ }
+
+ mddateOriginal := make([]byte, 0)
+ switch c.attrMode {
+ case "xattr":
+ sz, err := unix.Getxattr(c.path+path, "user.originalfile.mddate", nil)
+ if err != nil {
+ return nil, fmt.Errorf("fail to get size of user.originalfile.mddate attribute: %w", err)
+ }
+ mddateOriginal = make([]byte, sz)
+ if _, err = unix.Getxattr(c.path+path, "user.originalfile.mddate", mddateOriginal); err != nil {
+ return nil, fmt.Errorf("fail to get user.originalfile.mddate attribute: %w", err)
+ }
+ case "none":
+ default:
+ return nil, fmt.Errorf("unknown attributes implementation: %s", c.attrMode)
+ }
+
+ return &MetadataStruct{
+ Name: fileInfo.Name(),
+ StorageType: "local-unix",
+ Hash: strconv.FormatInt(fileInfo.ModTime().Unix(), 16),
+ HashOriginal: string(mddateOriginal),
+ ContentType: mime.TypeByExtension("." + nodeExt),
+ FirstCreated: creationTime,
+ LastModified: fileInfo.ModTime(),
+ Misc: misc,
+ }, nil
+}
+
+func (c *LocalUnixOutputClient) IsMissing(path string) bool {
+ _, err := os.Stat(c.path + path)
+ return err != nil
+}
diff --git a/internal/client/output/output_client_interface.go b/internal/client/output/output_client_interface.go
index 4ec4728..437895c 100644
--- a/internal/client/output/output_client_interface.go
+++ b/internal/client/output/output_client_interface.go
@@ -4,6 +4,7 @@ import (
"io"
"time"
+ "github.com/SayaAndy/saya-today-thumbnail-generator/config"
"github.com/SayaAndy/saya-today-thumbnail-generator/internal/client/input"
)
@@ -23,3 +24,8 @@ type MetadataStruct struct {
LastModified time.Time
Misc map[string]string
}
+
+var NewOutputClientMap = map[string]func(cfg *config.OutputConfig) (OutputClient, error){
+ "b2": NewB2OutputClient,
+ "local-unix": NewLocalUnixOutputClient,
+}
diff --git a/internal/converter/converter_interface.go b/internal/converter/converter_interface.go
new file mode 100644
index 0000000..342ca1e
--- /dev/null
+++ b/internal/converter/converter_interface.go
@@ -0,0 +1,16 @@
+package converter
+
+import (
+ "io"
+
+ "github.com/SayaAndy/saya-today-thumbnail-generator/config"
+)
+
+type Converter interface {
+ DeductOutputPath(inputPath string) string
+ Process(ext string, reader io.ReadCloser, writer io.WriteCloser) error
+}
+
+var NewConverterMap = map[string]func(cfg *config.ConverterConfig) (Converter, error){
+ "webp": NewWebpConverter,
+}
diff --git a/internal/converter/processor_interface.go b/internal/converter/processor_interface.go
deleted file mode 100644
index 88b5ac3..0000000
--- a/internal/converter/processor_interface.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package converter
-
-import "io"
-
-type Converter interface {
- DeductOutputPath(inputPath string) string
- Process(ext string, reader io.ReadCloser, writer io.WriteCloser) error
-}
diff --git a/internal/converter/webp.go b/internal/converter/webp.go
index b7f1216..8a9c09a 100644
--- a/internal/converter/webp.go
+++ b/internal/converter/webp.go
@@ -24,7 +24,7 @@ type WebpConverter struct {
quality int
}
-func NewWebpConverter(cfg *config.ConverterConfig) (*WebpConverter, error) {
+func NewWebpConverter(cfg *config.ConverterConfig) (Converter, error) {
if cfg.Type != "webp" {
return nil, fmt.Errorf("invalid storage type for WebpConverter")
}
diff --git a/main.go b/main.go
index 16523fd..d41baa9 100644
--- a/main.go
+++ b/main.go
@@ -20,7 +20,6 @@ var (
)
func main() {
- var err error
signal.Notify(sigTermChan, os.Interrupt, syscall.SIGTERM)
flag.Parse()
@@ -41,22 +40,19 @@ func main() {
default:
}
- var inputClient input.InputClient
- inputClient, err = input.NewB2InputClient(&cfg.Input)
+ inputClient, err := input.NewInputClientMap[cfg.Input.Storage.Type](&cfg.Input)
if err != nil {
slog.Error("fail to initialize input client", slog.String("error", err.Error()))
os.Exit(1)
}
- var outputClient output.OutputClient
- outputClient, err = output.NewB2OutputClient(&cfg.Output)
+ outputClient, err := output.NewOutputClientMap[cfg.Output.Storage.Type](&cfg.Output)
if err != nil {
slog.Error("fail to initialize output client", slog.String("error", err.Error()))
os.Exit(1)
}
- var conv converter.Converter
- conv, err = converter.NewWebpConverter(&cfg.Converter)
+ conv, err := converter.NewConverterMap[cfg.Converter.Type](&cfg.Converter)
if err != nil {
slog.Error("fail to initialize converter", slog.String("error", err.Error()))
os.Exit(1)
@@ -118,19 +114,21 @@ func main() {
return
}
+ originalInputHash := ""
if !cfg.ForceRewrite && !outputClient.IsMissing(outputName) {
outputMetadata, err := outputClient.ReadMetadata(outputName)
if err != nil {
fileLogger.Warn("fail to read metadata of (supposedly existing) output file", slog.String("error", err.Error()))
return
}
- if inputMetadata.Hash == outputMetadata.HashOriginal {
+ originalInputHash = outputMetadata.HashOriginal
+ if inputMetadata.Hash == originalInputHash {
fileLogger.Info("skip already processed file (based on equal hash)", slog.String("input_hash", inputMetadata.Hash))
return
}
}
- fileLogger.Info("start to process file")
+ fileLogger.Info("start to process file", slog.String("input_hash", inputMetadata.Hash), slog.String("original_input_hash", originalInputHash))
reader, err := inputClient.GetReader(inputName)
if err != nil {
fileLogger.Warn("fail to get reader for input file", slog.String("error", err.Error()))