chore: minify background image (#933)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Alessandro (Ale) Segala
2025-09-14 08:24:28 -07:00
committed by GitHub
parent fb92906c3a
commit a897b31166
11 changed files with 195 additions and 22 deletions

View File

@@ -1,9 +1,13 @@
package bootstrap
import (
"bytes"
"encoding/hex"
"fmt"
"io/fs"
"log/slog"
"os"
"path"
"path/filepath"
"strings"
"github.com/pocket-id/pocket-id/backend/internal/common"
@@ -13,6 +17,15 @@ import (
// initApplicationImages copies the images from the images directory to the application-images directory
func initApplicationImages() error {
// Images that are built into the Pocket ID binary
builtInImageHashes := getBuiltInImageHashes()
// Previous versions of images
// If these are found, they are deleted
legacyImageHashes := imageHashMap{
"background.jpg": mustDecodeHex("138d510030ed845d1d74de34658acabff562d306476454369a60ab8ade31933f"),
}
dirPath := common.EnvConfig.UploadPath + "/application-images"
sourceFiles, err := resources.FS.ReadDir("images")
@@ -24,15 +37,48 @@ func initApplicationImages() error {
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to read directory: %w", err)
}
destinationFilesMap := make(map[string]bool, len(destinationFiles))
for _, f := range destinationFiles {
name := f.Name()
destFilePath := filepath.Join(dirPath, name)
h, err := utils.CreateSha256FileHash(destFilePath)
if err != nil {
return fmt.Errorf("failed to get hash for file '%s': %w", name, err)
}
// Check if the file is a legacy one - if so, delete it
if legacyImageHashes.Contains(h) {
slog.Info("Found legacy application image that will be removed", slog.String("name", name))
err = os.Remove(destFilePath)
if err != nil {
return fmt.Errorf("failed to remove legacy file '%s': %w", name, err)
}
continue
}
// Check if the file is a built-in one and save it in the map
destinationFilesMap[getImageNameWithoutExtension(name)] = builtInImageHashes.Contains(h)
}
// Copy images from the images directory to the application-images directory if they don't already exist
for _, sourceFile := range sourceFiles {
if sourceFile.IsDir() || imageAlreadyExists(sourceFile.Name(), destinationFiles) {
// Skip if it's a directory
if sourceFile.IsDir() {
continue
}
srcFilePath := path.Join("images", sourceFile.Name())
destFilePath := path.Join(dirPath, sourceFile.Name())
name := sourceFile.Name()
srcFilePath := filepath.Join("images", name)
destFilePath := filepath.Join(dirPath, name)
// Skip if there's already an image at the path
// We do not check the extension because users could have uploaded a different one
if imageAlreadyExists(sourceFile, destinationFilesMap) {
continue
}
slog.Info("Writing new application image", slog.String("name", name))
err := utils.CopyEmbeddedFileToDisk(srcFilePath, destFilePath)
if err != nil {
return fmt.Errorf("failed to copy file: %w", err)
@@ -42,25 +88,49 @@ func initApplicationImages() error {
return nil
}
func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool {
for _, destinationFile := range destinationFiles {
sourceFileWithoutExtension := getImageNameWithoutExtension(fileName)
destinationFileWithoutExtension := getImageNameWithoutExtension(destinationFile.Name())
func getBuiltInImageHashes() imageHashMap {
return imageHashMap{
"background.webp": mustDecodeHex("3fc436a66d6b872b01d96a4e75046c46b5c3e2daccd51e98ecdf98fd445599ab"),
"favicon.ico": mustDecodeHex("70f9c4b6bd4781ade5fc96958b1267511751e91957f83c2354fb880b35ec890a"),
"logo.svg": mustDecodeHex("f1e60707df9784152ce0847e3eb59cb68b9015f918ff160376c27ebff1eda796"),
"logoDark.svg": mustDecodeHex("0421a8d93714bacf54c78430f1db378fd0d29565f6de59b6a89090d44a82eb16"),
"logoLight.svg": mustDecodeHex("6d42c88cf6668f7e57c4f2a505e71ecc8a1e0a27534632aa6adec87b812d0bb0"),
}
}
if sourceFileWithoutExtension == destinationFileWithoutExtension {
type imageHashMap map[string][]byte
func (m imageHashMap) Contains(target []byte) bool {
if len(target) == 0 {
return false
}
for _, h := range m {
if bytes.Equal(h, target) {
return true
}
}
return false
}
func imageAlreadyExists(sourceFile fs.DirEntry, destinationFiles map[string]bool) bool {
sourceFileWithoutExtension := getImageNameWithoutExtension(sourceFile.Name())
_, ok := destinationFiles[sourceFileWithoutExtension]
return ok
}
func getImageNameWithoutExtension(fileName string) string {
idx := strings.LastIndexByte(fileName, '.')
if idx < 1 {
// No dot found, or fileName starts with a dot
return fileName
}
return fileName[:idx]
}
func mustDecodeHex(str string) []byte {
b, err := hex.DecodeString(str)
if err != nil {
panic(err)
}
return b
}

View File

@@ -0,0 +1,61 @@
package bootstrap
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pocket-id/pocket-id/backend/internal/utils"
)
func TestGetBuiltInImageData(t *testing.T) {
// Get the built-in image data map
builtInImages := getBuiltInImageHashes()
// Read the actual images directory from disk
imagesDir := filepath.Join("..", "..", "resources", "images")
actualFiles, err := os.ReadDir(imagesDir)
require.NoError(t, err, "Failed to read images directory")
// Create a map of actual files for comparison
actualFilesMap := make(map[string]struct{})
// Validate each actual file exists in the built-in data with correct hash
for _, file := range actualFiles {
fileName := file.Name()
if file.IsDir() || strings.HasPrefix(fileName, ".") {
continue
}
actualFilesMap[fileName] = struct{}{}
// Check if the file exists in the built-in data
builtInHash, exists := builtInImages[fileName]
assert.True(t, exists, "File %s exists in images directory but not in getBuiltInImageData map", fileName)
if !exists {
continue
}
filePath := filepath.Join(imagesDir, fileName)
// Validate SHA256 hash
actualHash, err := utils.CreateSha256FileHash(filePath)
require.NoError(t, err, "Failed to compute hash for %s", fileName)
assert.Equal(t, actualHash, builtInHash, "SHA256 hash mismatch for file %s", fileName)
}
// Ensure the built-in data doesn't have extra files that don't exist in the directory
for fileName := range builtInImages {
_, exists := actualFilesMap[fileName]
assert.True(t, exists, "File %s exists in getBuiltInImageData map but not in images directory", fileName)
}
// Ensure we have at least some files (sanity check)
assert.NotEmpty(t, actualFilesMap, "Images directory should contain at least one file")
assert.Len(t, actualFilesMap, len(builtInImages), "Number of files in directory should match number in built-in data map")
}

View File

@@ -70,7 +70,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
SignupDefaultCustomClaims: model.AppConfigVariable{Value: "[]"},
AccentColor: model.AppConfigVariable{Value: "default"},
// Internal
BackgroundImageType: model.AppConfigVariable{Value: "jpg"},
BackgroundImageType: model.AppConfigVariable{Value: "webp"},
LogoLightImageType: model.AppConfigVariable{Value: "svg"},
LogoDarkImageType: model.AppConfigVariable{Value: "svg"},
InstanceID: model.AppConfigVariable{Value: ""},

View File

@@ -3,7 +3,7 @@ package email
import (
"fmt"
htemplate "html/template"
"path"
"path/filepath"
ttemplate "text/template"
"github.com/pocket-id/pocket-id/backend/resources"
@@ -30,7 +30,7 @@ func PrepareTextTemplates(templates []string) (map[string]*ttemplate.Template, e
textTemplates := make(map[string]*ttemplate.Template, len(templates))
for _, tmpl := range templates {
filename := tmpl + "_text.tmpl"
templatePath := path.Join("email-templates", filename)
templatePath := filepath.Join("email-templates", filename)
parsedTemplate, err := ttemplate.ParseFS(resources.FS, templatePath)
if err != nil {
@@ -47,7 +47,7 @@ func PrepareHTMLTemplates(templates []string) (map[string]*htemplate.Template, e
htmlTemplates := make(map[string]*htemplate.Template, len(templates))
for _, tmpl := range templates {
filename := tmpl + "_html.tmpl"
templatePath := path.Join("email-templates", filename)
templatePath := filepath.Join("email-templates", filename)
parsedTemplate, err := htemplate.ParseFS(resources.FS, templatePath)
if err != nil {

View File

@@ -2,6 +2,7 @@ package utils
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
@@ -35,6 +36,12 @@ func GetImageMimeType(ext string) string {
return "image/x-icon"
case "gif":
return "image/gif"
case "webp":
return "image/webp"
case "avif":
return "image/avif"
case "heic":
return "image/heic"
default:
return ""
}
@@ -43,29 +50,45 @@ func GetImageMimeType(ext string) string {
func CopyEmbeddedFileToDisk(srcFilePath, destFilePath string) error {
srcFile, err := resources.FS.Open(srcFilePath)
if err != nil {
return err
return fmt.Errorf("failed to open embedded file: %w", err)
}
defer srcFile.Close()
err = os.MkdirAll(filepath.Dir(destFilePath), os.ModePerm)
if err != nil {
return err
return fmt.Errorf("failed to create destination directory: %w", err)
}
destFile, err := os.Create(destFilePath)
if err != nil {
return err
return fmt.Errorf("failed to open destination file: %w", err)
}
defer destFile.Close()
_, err = io.Copy(destFile, srcFile)
if err != nil {
return err
return fmt.Errorf("failed to write to destination file: %w", err)
}
return nil
}
func EmbeddedFileSha256(filePath string) ([]byte, error) {
f, err := resources.FS.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open embedded file: %w", err)
}
defer f.Close()
h := sha256.New()
_, err = io.Copy(h, f)
if err != nil {
return nil, fmt.Errorf("failed to read embedded file: %w", err)
}
return h.Sum(nil), nil
}
func SaveFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open()
if err != nil {

View File

@@ -3,9 +3,28 @@ package utils
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
)
func CreateSha256Hash(input string) string {
hash := sha256.Sum256([]byte(input))
return hex.EncodeToString(hash[:])
}
func CreateSha256FileHash(filePath string) ([]byte, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer f.Close()
h := sha256.New()
_, err = io.Copy(h, f)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
return h.Sum(nil), nil
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB