mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-22 01:11:41 +03:00
chore: minify background image (#933)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
committed by
GitHub
parent
fb92906c3a
commit
a897b31166
@@ -1,9 +1,13 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
"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
|
// initApplicationImages copies the images from the images directory to the application-images directory
|
||||||
func initApplicationImages() error {
|
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"
|
dirPath := common.EnvConfig.UploadPath + "/application-images"
|
||||||
|
|
||||||
sourceFiles, err := resources.FS.ReadDir("images")
|
sourceFiles, err := resources.FS.ReadDir("images")
|
||||||
@@ -24,15 +37,48 @@ func initApplicationImages() error {
|
|||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("failed to read directory: %w", 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
|
// Copy images from the images directory to the application-images directory if they don't already exist
|
||||||
for _, sourceFile := range sourceFiles {
|
for _, sourceFile := range sourceFiles {
|
||||||
if sourceFile.IsDir() || imageAlreadyExists(sourceFile.Name(), destinationFiles) {
|
// Skip if it's a directory
|
||||||
|
if sourceFile.IsDir() {
|
||||||
continue
|
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)
|
err := utils.CopyEmbeddedFileToDisk(srcFilePath, destFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to copy file: %w", err)
|
return fmt.Errorf("failed to copy file: %w", err)
|
||||||
@@ -42,25 +88,49 @@ func initApplicationImages() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool {
|
func getBuiltInImageHashes() imageHashMap {
|
||||||
for _, destinationFile := range destinationFiles {
|
return imageHashMap{
|
||||||
sourceFileWithoutExtension := getImageNameWithoutExtension(fileName)
|
"background.webp": mustDecodeHex("3fc436a66d6b872b01d96a4e75046c46b5c3e2daccd51e98ecdf98fd445599ab"),
|
||||||
destinationFileWithoutExtension := getImageNameWithoutExtension(destinationFile.Name())
|
"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 true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
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 {
|
func getImageNameWithoutExtension(fileName string) string {
|
||||||
idx := strings.LastIndexByte(fileName, '.')
|
idx := strings.LastIndexByte(fileName, '.')
|
||||||
if idx < 1 {
|
if idx < 1 {
|
||||||
// No dot found, or fileName starts with a dot
|
// No dot found, or fileName starts with a dot
|
||||||
return fileName
|
return fileName
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileName[:idx]
|
return fileName[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustDecodeHex(str string) []byte {
|
||||||
|
b, err := hex.DecodeString(str)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -70,7 +70,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
|||||||
SignupDefaultCustomClaims: model.AppConfigVariable{Value: "[]"},
|
SignupDefaultCustomClaims: model.AppConfigVariable{Value: "[]"},
|
||||||
AccentColor: model.AppConfigVariable{Value: "default"},
|
AccentColor: model.AppConfigVariable{Value: "default"},
|
||||||
// Internal
|
// Internal
|
||||||
BackgroundImageType: model.AppConfigVariable{Value: "jpg"},
|
BackgroundImageType: model.AppConfigVariable{Value: "webp"},
|
||||||
LogoLightImageType: model.AppConfigVariable{Value: "svg"},
|
LogoLightImageType: model.AppConfigVariable{Value: "svg"},
|
||||||
LogoDarkImageType: model.AppConfigVariable{Value: "svg"},
|
LogoDarkImageType: model.AppConfigVariable{Value: "svg"},
|
||||||
InstanceID: model.AppConfigVariable{Value: ""},
|
InstanceID: model.AppConfigVariable{Value: ""},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package email
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
htemplate "html/template"
|
htemplate "html/template"
|
||||||
"path"
|
"path/filepath"
|
||||||
ttemplate "text/template"
|
ttemplate "text/template"
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/resources"
|
"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))
|
textTemplates := make(map[string]*ttemplate.Template, len(templates))
|
||||||
for _, tmpl := range templates {
|
for _, tmpl := range templates {
|
||||||
filename := tmpl + "_text.tmpl"
|
filename := tmpl + "_text.tmpl"
|
||||||
templatePath := path.Join("email-templates", filename)
|
templatePath := filepath.Join("email-templates", filename)
|
||||||
|
|
||||||
parsedTemplate, err := ttemplate.ParseFS(resources.FS, templatePath)
|
parsedTemplate, err := ttemplate.ParseFS(resources.FS, templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,7 +47,7 @@ func PrepareHTMLTemplates(templates []string) (map[string]*htemplate.Template, e
|
|||||||
htmlTemplates := make(map[string]*htemplate.Template, len(templates))
|
htmlTemplates := make(map[string]*htemplate.Template, len(templates))
|
||||||
for _, tmpl := range templates {
|
for _, tmpl := range templates {
|
||||||
filename := tmpl + "_html.tmpl"
|
filename := tmpl + "_html.tmpl"
|
||||||
templatePath := path.Join("email-templates", filename)
|
templatePath := filepath.Join("email-templates", filename)
|
||||||
|
|
||||||
parsedTemplate, err := htemplate.ParseFS(resources.FS, templatePath)
|
parsedTemplate, err := htemplate.ParseFS(resources.FS, templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -35,6 +36,12 @@ func GetImageMimeType(ext string) string {
|
|||||||
return "image/x-icon"
|
return "image/x-icon"
|
||||||
case "gif":
|
case "gif":
|
||||||
return "image/gif"
|
return "image/gif"
|
||||||
|
case "webp":
|
||||||
|
return "image/webp"
|
||||||
|
case "avif":
|
||||||
|
return "image/avif"
|
||||||
|
case "heic":
|
||||||
|
return "image/heic"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -43,29 +50,45 @@ func GetImageMimeType(ext string) string {
|
|||||||
func CopyEmbeddedFileToDisk(srcFilePath, destFilePath string) error {
|
func CopyEmbeddedFileToDisk(srcFilePath, destFilePath string) error {
|
||||||
srcFile, err := resources.FS.Open(srcFilePath)
|
srcFile, err := resources.FS.Open(srcFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to open embedded file: %w", err)
|
||||||
}
|
}
|
||||||
defer srcFile.Close()
|
defer srcFile.Close()
|
||||||
|
|
||||||
err = os.MkdirAll(filepath.Dir(destFilePath), os.ModePerm)
|
err = os.MkdirAll(filepath.Dir(destFilePath), os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to create destination directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
destFile, err := os.Create(destFilePath)
|
destFile, err := os.Create(destFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to open destination file: %w", err)
|
||||||
}
|
}
|
||||||
defer destFile.Close()
|
defer destFile.Close()
|
||||||
|
|
||||||
_, err = io.Copy(destFile, srcFile)
|
_, err = io.Copy(destFile, srcFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to write to destination file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 {
|
func SaveFile(file *multipart.FileHeader, dst string) error {
|
||||||
src, err := file.Open()
|
src, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,9 +3,28 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateSha256Hash(input string) string {
|
func CreateSha256Hash(input string) string {
|
||||||
hash := sha256.Sum256([]byte(input))
|
hash := sha256.Sum256([]byte(input))
|
||||||
return hex.EncodeToString(hash[:])
|
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 |
BIN
backend/resources/images/background.webp
Normal file
BIN
backend/resources/images/background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 291 KiB |
@@ -81,7 +81,7 @@
|
|||||||
<FileInput
|
<FileInput
|
||||||
id="profile-picture-input"
|
id="profile-picture-input"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg, image/webp, image/avif, image/heic"
|
||||||
onchange={onImageChange}
|
onchange={onImageChange}
|
||||||
>
|
>
|
||||||
<div class="group relative size-24 rounded-full">
|
<div class="group relative size-24 rounded-full">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
label,
|
label,
|
||||||
image = $bindable(),
|
image = $bindable(),
|
||||||
imageURL,
|
imageURL,
|
||||||
accept = 'image/png, image/jpeg, image/svg+xml, image/gif',
|
accept = 'image/png, image/jpeg, image/svg+xml, image/gif, image/webp, image/avif, image/heic',
|
||||||
forceColorScheme,
|
forceColorScheme,
|
||||||
...restProps
|
...restProps
|
||||||
}: HTMLAttributes<HTMLDivElement> & {
|
}: HTMLAttributes<HTMLDivElement> & {
|
||||||
|
|||||||
@@ -187,7 +187,7 @@
|
|||||||
<FileInput
|
<FileInput
|
||||||
id="logo"
|
id="logo"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
accept="image/png, image/jpeg, image/svg+xml"
|
accept="image/png, image/jpeg, image/svg+xml, image/webp, image/avif, image/heic"
|
||||||
onchange={onLogoChange}
|
onchange={onLogoChange}
|
||||||
>
|
>
|
||||||
<Button variant="secondary">
|
<Button variant="secondary">
|
||||||
|
|||||||
Reference in New Issue
Block a user