summaryrefslogtreecommitdiff
path: root/main.go
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go261
1 files changed, 199 insertions, 62 deletions
diff --git a/main.go b/main.go
index 16523fd..f3c1196 100644
--- a/main.go
+++ b/main.go
@@ -1,26 +1,33 @@
package main
import (
+ "bytes"
+ "encoding/csv"
+ "encoding/json"
"flag"
+ "hash/crc32"
+ "io"
"log/slog"
"os"
"os/signal"
+ "strconv"
+ "strings"
"sync"
"syscall"
"github.com/SayaAndy/saya-today-thumbnail-generator/config"
"github.com/SayaAndy/saya-today-thumbnail-generator/internal/client/input"
- "github.com/SayaAndy/saya-today-thumbnail-generator/internal/client/output"
"github.com/SayaAndy/saya-today-thumbnail-generator/internal/converter"
)
var (
- configPath = flag.String("c", "config.json", "Path to the configuration file")
- sigTermChan = make(chan os.Signal, 1)
+ configPath = flag.String("c", "config.json", "Path to the configuration file")
+ sigTermChan = make(chan os.Signal, 1)
+ cacheMapMutex = &sync.RWMutex{}
+ cacheMap = make(map[string]map[uint32]struct{})
)
func main() {
- var err error
signal.Notify(sigTermChan, os.Interrupt, syscall.SIGTERM)
flag.Parse()
@@ -41,33 +48,29 @@ 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)
- if err != nil {
- slog.Error("fail to initialize output client", slog.String("error", err.Error()))
- os.Exit(1)
+ converters := make([]converter.Converter, 0, len(cfg.Converters))
+ converterTypes := make([]string, 0, len(cfg.Converters))
+ converterHashes := make([]uint32, 0, len(cfg.Converters))
+ for _, converterCfg := range cfg.Converters {
+ converterBytes, _ := json.Marshal(converterCfg)
+ conv, err := converter.NewConverterMap[converterCfg.Type](&converterCfg)
+ if err != nil {
+ slog.Error("fail to initialize converter", slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+ converters = append(converters, conv)
+ converterTypes = append(converterTypes, converterCfg.Type)
+ converterHashes = append(converterHashes, crc32.ChecksumIEEE(converterBytes))
}
- var conv converter.Converter
- conv, err = converter.NewWebpConverter(&cfg.Converter)
- if err != nil {
- slog.Error("fail to initialize converter", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- generalLogger := slog.With(
- slog.String("input_storage", cfg.Input.Storage.Type),
- slog.String("converter_type", cfg.Converter.Type),
- slog.String("output_storage", cfg.Output.Storage.Type),
- )
- generalLogger.Info("initialized clients and converter")
+ generalLogger := slog.With(slog.String("input_storage", cfg.Input.Storage.Type))
+ generalLogger.Info("initialized input client and converters", slog.String("converter_types", strings.Join(converterTypes, " ")))
select {
case <-sigTermChan:
@@ -81,7 +84,49 @@ func main() {
generalLogger.Error("fail to scan input files", slog.String("error", err.Error()))
os.Exit(1)
}
- generalLogger.Info("scanned files", slog.Int("file_count", len(files)))
+ fileCount := len(files)
+ generalLogger.Info("scanned files", slog.Int("file_count", fileCount))
+
+ select {
+ case <-sigTermChan:
+ generalLogger.Info("exiting due to termination signal")
+ os.Exit(130)
+ default:
+ }
+
+ if cfg.Input.CacheProcessed {
+ cacheFile, err := os.OpenFile(cfg.Input.CacheProcessedCsvPath, os.O_CREATE|os.O_RDONLY, 0644)
+ if err != nil {
+ generalLogger.Error("fail to initialize cache file", slog.String("cache_path", cfg.Input.CacheProcessedCsvPath), slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+ defer cacheFile.Close()
+
+ csvReader := csv.NewReader(cacheFile)
+ for {
+ rec, err := csvReader.Read()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ generalLogger.Error("fail to initialize cache while reading file", slog.String("cache_path", cfg.Input.CacheProcessedCsvPath), slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+ if len(rec) != 2 {
+ generalLogger.Error("fail to initialize cache while reading file", slog.Int("record_length", len(rec)), slog.String("error", "incorrect format: expected '{image-name},{semicolon-separated-processor-hashes}'"))
+ os.Exit(1)
+ }
+ hashes := strings.Split(rec[1], ";")
+ cacheMap[rec[0]] = make(map[uint32]struct{})
+ for _, hash := range hashes {
+ hashUint, err := strconv.ParseUint(hash, 10, 32)
+ if err != nil {
+ generalLogger.Error("fail to initialize cache while reading file", slog.Int("record_length", len(rec)), slog.String("error", err.Error()))
+ }
+ cacheMap[rec[0]][uint32(hashUint)] = struct{}{}
+ }
+ }
+ }
select {
case <-sigTermChan:
@@ -90,66 +135,137 @@ func main() {
default:
}
- semaphore := make(chan struct{}, cfg.MaxConcurrentJobs)
+ processSemaphore := make(chan struct{}, cfg.MaxProcessThreads)
+ queueSemaphore := make(chan struct{}, cfg.MaxPreProcessThreads)
var wg sync.WaitGroup
- wg.Add(len(files))
+ wg.Add(fileCount)
+
+ processTerminating := false
for i, file := range files {
- go func(index int, inputName string) {
- outputName := conv.DeductOutputPath(inputName)
- fileLogger := generalLogger.With(slog.String("input_path", inputName), slog.String("output_path", outputName), slog.Int("file_index", index))
+ queueSemaphore <- struct{}{}
+
+ select {
+ case <-sigTermChan:
+ generalLogger.Info("exiting due to termination signal")
+ processTerminating = true
+ default:
+ }
+
+ go func(index int, inputName string, earlyTerminate bool) {
+ defer func() { <-queueSemaphore; wg.Done() }()
- threadSigTermChannel := make(chan os.Signal, 1)
- signal.Notify(threadSigTermChannel, os.Interrupt, syscall.SIGTERM)
+ fileLogger := generalLogger.With(slog.String("input_path", inputName), slog.Int("file_index", index))
- defer func() { <-semaphore }()
- defer wg.Done()
- semaphore <- struct{}{}
- select {
- case <-threadSigTermChannel:
- fileLogger.Info("exiting due to termination signal")
+ if earlyTerminate {
+ fileLogger.Info("skip processing file (process is terminating)")
return
- default:
}
- inputMetadata, err := inputClient.ReadMetadata(inputName)
- if err != nil {
- fileLogger.Warn("fail to read metadata of (supposedly existing) input file", slog.String("error", err.Error()))
- return
+ var inputMetadata *input.MetadataStruct
+
+ id := inputClient.ID(file)
+ cacheMapMutex.Lock()
+ if _, ok := cacheMap[id]; !ok {
+ cacheMap[id] = make(map[uint32]struct{})
}
+ cacheMapMutex.Unlock()
- 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
+ convertersToLaunch := []int{}
+ for j, conv := range converters {
+ if cfg.Input.CacheProcessed {
+ cacheMapMutex.RLock()
+ if _, ok := cacheMap[id][converterHashes[j]]; ok {
+ cacheMapMutex.RUnlock()
+ fileLogger.Info("skip already processed file (based on cache file containing it and processor)",
+ slog.String("file_id", id),
+ slog.Uint64("conv_hash", uint64(converterHashes[j])),
+ slog.Int("conv_index", j))
+ continue
+ }
+ cacheMapMutex.RUnlock()
}
- if inputMetadata.Hash == outputMetadata.HashOriginal {
- fileLogger.Info("skip already processed file (based on equal hash)", slog.String("input_hash", inputMetadata.Hash))
- return
+ outputName := conv.DeductOutputPath(inputName)
+ originalInputHash := ""
+ convLogger := fileLogger.With(slog.String("output_path", outputName), slog.Int("conv_index", j))
+
+ if inputMetadata == nil {
+ inputMetadata, err = inputClient.ReadMetadata(inputName)
+ if err != nil {
+ fileLogger.Warn("fail to read metadata of (supposedly existing) input file", slog.String("error", err.Error()))
+ return
+ }
}
+
+ switch cfg.Converters[j].Output.RewriteOn {
+ case "Never":
+ if !conv.IsMissing(outputName) {
+ convLogger.Info("skip already existing file")
+ continue
+ }
+ case "UnequalHashInCache":
+ if !conv.IsMissing(outputName) {
+ outputMetadata, err := conv.ReadMetadata(outputName)
+ if err != nil {
+ convLogger.Warn("fail to read metadata of (supposedly existing) output file", slog.String("error", err.Error()))
+ continue
+ }
+ originalInputHash = outputMetadata.HashOriginal
+ if inputMetadata.Hash == originalInputHash {
+ convLogger.Info("skip already processed file (based on equal hash)", slog.String("input_hash", inputMetadata.Hash))
+ cacheMapMutex.Lock()
+ cacheMap[id][converterHashes[j]] = struct{}{}
+ cacheMapMutex.Unlock()
+ continue
+ }
+ }
+ case "Always":
+ }
+
+ convertersToLaunch = append(convertersToLaunch, j)
+ }
+
+ if len(convertersToLaunch) == 0 {
+ return
}
- fileLogger.Info("start to process file")
+ processSemaphore <- struct{}{}
+ defer func() { <-processSemaphore }()
+
+ fileLogger.Info("start to process file", slog.String("input_hash", inputMetadata.Hash))
reader, err := inputClient.GetReader(inputName)
if err != nil {
fileLogger.Warn("fail to get reader for input file", slog.String("error", err.Error()))
return
}
- writer, err := outputClient.GetWriter(outputName, inputMetadata)
+ fileContent, err := io.ReadAll(reader)
if err != nil {
- fileLogger.Warn("fail to get writer for output file", slog.String("error", err.Error()))
+ fileLogger.Warn("fail to read content of input file", slog.String("error", err.Error()))
return
+ } else if int64(len(fileContent)) != inputMetadata.Size {
+ fileLogger.Warn("the downloaded file seems to be missing some of its content",
+ slog.Int("actual_size_bytes", len(fileContent)),
+ slog.Int64("expected_size_bytes", inputMetadata.Size))
}
+ reader.Close()
- if err := conv.Process(inputMetadata.ContentType, reader, writer); err != nil {
- fileLogger.Warn("fail to convert file", slog.String("error", err.Error()))
- return
- }
+ for _, convIndex := range convertersToLaunch {
+ conv := converters[convIndex]
+ outputName := conv.DeductOutputPath(inputName)
+ convLogger := fileLogger.With(slog.String("output_path", outputName), slog.Int("conv_index", convIndex))
- fileLogger.Info("successfully processed file", slog.String("input_hash", inputMetadata.Hash))
- }(i, file)
+ if err := conv.Process(inputMetadata, bytes.NewReader(fileContent), outputName); err != nil {
+ convLogger.Warn("fail to convert file", slog.String("error", err.Error()))
+ return
+ }
+
+ convLogger.Info("successfully processed file", slog.String("input_hash", inputMetadata.Hash))
+ cacheMapMutex.Lock()
+ cacheMap[id][converterHashes[convIndex]] = struct{}{}
+ cacheMapMutex.Unlock()
+ }
+ }(i, file, processTerminating)
}
wg.Wait()
@@ -159,7 +275,28 @@ func main() {
generalLogger.Info("exiting due to termination signal")
os.Exit(130)
default:
- generalLogger.Info("all files processed successfully, exiting")
- os.Exit(0)
}
+
+ generalLogger.Info("all files processed successfully")
+
+ if cfg.Input.CacheProcessed {
+ generalLogger.Info("writing cache file")
+ cacheFile, err := os.OpenFile(cfg.Input.CacheProcessedCsvPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ generalLogger.Error("error writing cache into file")
+ } else {
+ for id, hashes := range cacheMap {
+ var hashStrings = make([]string, 0, len(hashes))
+ for hash := range hashes {
+ hashStrings = append(hashStrings, strconv.FormatUint(uint64(hash), 10))
+ }
+ cacheFile.Write([]byte(id))
+ cacheFile.Write([]byte{','})
+ cacheFile.WriteString(strings.Join(hashStrings, ";"))
+ cacheFile.Write([]byte{'\n'})
+ }
+ }
+ }
+
+ os.Exit(0)
}