Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,118 @@
package storage
import (
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/thumbnails/pkg/config"
)
const (
filesDir = "files"
)
// NewFileSystemStorage creates a new instance of FileSystem
func NewFileSystemStorage(cfg config.FileSystemStorage, logger log.Logger) FileSystem {
return FileSystem{
root: cfg.RootDirectory,
logger: logger,
}
}
// FileSystem represents a storage for the thumbnails using the local file system.
type FileSystem struct {
root string
logger log.Logger
}
// Stat returns if a file for the given key exists on the filesystem
func (s FileSystem) Stat(key string) bool {
img := filepath.Join(s.root, filesDir, key)
if _, err := os.Stat(img); err != nil {
return false
}
return true
}
// Get returns the file content for the given key
func (s FileSystem) Get(key string) ([]byte, error) {
img := filepath.Join(s.root, filesDir, key)
content, err := os.ReadFile(img)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
s.logger.Debug().Str("err", err.Error()).Str("key", key).Msg("could not load thumbnail from store")
}
return nil, err
}
return content, nil
}
// Put stores image data in the file system for the given key
func (s FileSystem) Put(key string, img []byte) error {
imgPath := filepath.Join(s.root, filesDir, key)
dir := filepath.Dir(imgPath)
if err := os.MkdirAll(dir, 0700); err != nil {
return errors.Wrapf(err, "error while creating directory %s", dir)
}
if _, err := os.Stat(imgPath); os.IsNotExist(err) {
f, err := os.CreateTemp(dir, "tmpthumb")
if err != nil {
return errors.Wrapf(err, "could not create temporary file for \"%s\"", key)
}
defer f.Close()
// if there was a problem writing, remove the temporary file
if _, writeErr := f.Write(img); writeErr != nil {
if remErr := os.Remove(f.Name()); remErr != nil {
return errors.Wrapf(remErr, "could not cleanup temporary file for \"%s\"", key)
}
return errors.Wrapf(writeErr, "could not write to temporary file for \"%s\"", key)
}
// if there wasn't a problem, ensure the data is written into disk
if synErr := f.Sync(); synErr != nil {
return errors.Wrapf(synErr, "could not sync temporary file data into the disk for \"%s\"", key)
}
// rename the temporary file to the final file
if renErr := os.Rename(f.Name(), imgPath); renErr != nil {
// if we couldn't rename, remove the temporary file
if remErr := os.Remove(f.Name()); remErr != nil {
return errors.Wrapf(remErr, "rename failed and could not cleanup temporary file for \"%s\"", key)
}
return errors.Wrapf(renErr, "could not rename temporary file to \"%s\"", key)
}
}
return nil
}
// BuildKey generate the unique key for a thumbnail.
// The key is structure as follows:
//
// <first two letters of checksum>/<next two letters of checksum>/<rest of checksum>/<width>x<height>.<filetype>
//
// e.g. 97/9f/4c8db98f7b82e768ef478d3c8612/500x300.png
//
// The key also represents the path to the thumbnail in the filesystem under the configured root directory.
func (s FileSystem) BuildKey(r Request) string {
checksum := r.Checksum
filetype := r.Types[0]
parts := []string{strconv.Itoa(r.Resolution.Dx()), "x", strconv.Itoa(r.Resolution.Dy())}
if r.Characteristic != "" {
parts = append(parts, "-", r.Characteristic)
}
parts = append(parts, ".", filetype)
return filepath.Join(checksum[:2], checksum[2:4], checksum[4:], strings.Join(parts, ""))
}
@@ -0,0 +1,64 @@
package storage_test
import (
"image"
"testing"
tAssert "github.com/stretchr/testify/assert"
"github.com/qsfera/server/services/thumbnails/pkg/thumbnail/storage"
)
func TestFileSystem_BuildKey(t *testing.T) {
tests := []struct {
r storage.Request
want string
}{
{
r: storage.Request{
Checksum: "120EA8A25E5D487BF68B5F7096440019",
Types: []string{"png", "jpg"},
Resolution: image.Rectangle{
Min: image.Point{
X: 1,
Y: 2,
},
Max: image.Point{
X: 3,
Y: 4,
},
},
Characteristic: "",
},
want: "12/0E/A8A25E5D487BF68B5F7096440019/2x2.png",
},
{
r: storage.Request{
Checksum: "120EA8A25E5D487BF68B5F7096440019",
Types: []string{"png", "jpg"},
Resolution: image.Rectangle{
Min: image.Point{
X: 1,
Y: 2,
},
Max: image.Point{
X: 3,
Y: 4,
},
},
Characteristic: "fill",
},
want: "12/0E/A8A25E5D487BF68B5F7096440019/2x2-fill.png",
},
}
s := storage.FileSystem{}
assert := tAssert.New(t)
for _, tt := range tests {
t.Run("", func(t *testing.T) {
assert.Equal(s.BuildKey(tt.r), tt.want)
})
}
}
@@ -0,0 +1,32 @@
package storage
import (
"image"
)
// Request combines different attributes needed for storage operations.
type Request struct {
// The checksum of the source file
// Will be used to determine if a thumbnail exists
Checksum string
// Types provided by the encoder.
// Contains the mimetypes of the thumbnail.
// In case of jpg/jpeg it will contain both.
Types []string
// The resolution of the thumbnail
Resolution image.Rectangle
// Characteristic defines the different image characteristics,
// for example, if it's scaled up to fit in the bounding box or not,
// is it a chroma version of the image, and so on...
// the main propose for this is to be able to differentiate between images which have
// the same resolution but different characteristics.
Characteristic string
}
// Storage defines the interface for a thumbnail store.
type Storage interface {
Stat(key string) bool
Get(key string) ([]byte, error)
Put(key string, img []byte) error
BuildKey(r Request) string
}