From c999d5559a6495e1de0a5c1aa5e4a860cb77078a Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 7 Nov 2019 16:49:02 +0100 Subject: [PATCH 1/6] Add support for finding app icons on Linux (MVP) --- profile/api.go | 5 +- profile/get.go | 5 +- profile/icons/find_default.go | 10 +++ profile/icons/find_linux.go | 112 +++++++++++++++++++++++++++++++ profile/icons/find_linux_test.go | 32 +++++++++ profile/{ => icons}/icon.go | 5 +- profile/{ => icons}/icons.go | 15 +++-- profile/icons/locations_linux.go | 68 +++++++++++++++++++ profile/merge.go | 5 +- profile/module.go | 3 +- profile/profile.go | 33 ++++++++- 11 files changed, 276 insertions(+), 17 deletions(-) create mode 100644 profile/icons/find_default.go create mode 100644 profile/icons/find_linux.go create mode 100644 profile/icons/find_linux_test.go rename profile/{ => icons}/icon.go (96%) rename profile/{ => icons}/icons.go (78%) create mode 100644 profile/icons/locations_linux.go diff --git a/profile/api.go b/profile/api.go index 1a69a4e4..0855d3bb 100644 --- a/profile/api.go +++ b/profile/api.go @@ -10,6 +10,7 @@ import ( "github.com/safing/portbase/api" "github.com/safing/portbase/formats/dsd" "github.com/safing/portbase/utils" + "github.com/safing/portmaster/profile/icons" ) func registerAPIEndpoints() error { @@ -98,7 +99,7 @@ func handleGetProfileIcon(ar *api.Request) (data []byte, err error) { ext := filepath.Ext(name) // Get profile icon. - data, err = GetProfileIcon(name) + data, err = icons.GetProfileIcon(name) if err != nil { return nil, err } @@ -152,7 +153,7 @@ func handleUpdateProfileIcon(ar *api.Request) (any, error) { } // Update profile icon. - filename, err := UpdateProfileIcon(ar.InputData, ext) + filename, err := icons.UpdateProfileIcon(ar.InputData, ext) if err != nil { return nil, err } diff --git a/profile/get.go b/profile/get.go index 3fcf78e5..56501180 100644 --- a/profile/get.go +++ b/profile/get.go @@ -1,6 +1,7 @@ package profile import ( + "context" "errors" "fmt" "path" @@ -146,7 +147,9 @@ func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *P // Trigger further metadata fetching from system if profile was created. if created && profile.UsePresentationPath && !special { - module.StartWorker("get profile metadata", profile.updateMetadataFromSystem) + module.StartWorker("get profile metadata", func(ctx context.Context) error { + return profile.updateMetadataFromSystem(ctx, md) + }) } // Prepare profile for first use. diff --git a/profile/icons/find_default.go b/profile/icons/find_default.go new file mode 100644 index 00000000..782824e1 --- /dev/null +++ b/profile/icons/find_default.go @@ -0,0 +1,10 @@ +//go:build !linux + +package icons + +import "github.com/safing/portmaster/profile" + +// FindIcon returns nil, nil for unsupported platforms. +func FindIcon(binName string, homeDir string) (*profile.Icon, error) { + return nil, nil +} diff --git a/profile/icons/find_linux.go b/profile/icons/find_linux.go new file mode 100644 index 00000000..86a79534 --- /dev/null +++ b/profile/icons/find_linux.go @@ -0,0 +1,112 @@ +package icons + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +// FindIcon finds an icon for the given binary name. +// Providing the home directory of the user running the process of that binary can help find an icon. +func FindIcon(ctx context.Context, binName string, homeDir string) (*Icon, error) { + // Search for icon. + iconPath, err := search(binName, homeDir) + if iconPath == "" { + if err != nil { + return nil, fmt.Errorf("failed to find icon for %s: %w", binName, err) + } + return nil, nil + } + + // Load icon and save it. + data, err := os.ReadFile(iconPath) + if err != nil { + return nil, fmt.Errorf("failed to read icon %s: %w", iconPath, err) + } + filename, err := UpdateProfileIcon(data, filepath.Ext(iconPath)) + if err != nil { + return nil, fmt.Errorf("failed to import icon %s: %w", iconPath, err) + } + return &Icon{ + Type: IconTypeAPI, + Value: filename, + }, nil +} + +func search(binName string, homeDir string) (iconPath string, err error) { + binName = strings.ToLower(binName) + + // Search for icon path. + for _, iconLoc := range iconLocations { + basePath := iconLoc.GetPath(binName, homeDir) + if basePath == "" { + continue + } + + switch iconLoc.Type { + case FlatDir: + iconPath, err = searchDirectory(basePath, binName) + case XDGIcons: + iconPath, err = searchXDGIconStructure(basePath, binName) + } + + if iconPath != "" { + return + } + } + return +} + +func searchXDGIconStructure(baseDirectory string, binName string) (iconPath string, err error) { + for _, xdgIconDir := range xdgIconPaths { + directory := filepath.Join(baseDirectory, xdgIconDir) + iconPath, err = searchDirectory(directory, binName) + if iconPath != "" { + return + } + } + return +} + +func searchDirectory(directory string, binName string) (iconPath string, err error) { + entries, err := os.ReadDir(directory) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return "", nil + } + return "", fmt.Errorf("failed to read directory %s: %w", directory, err) + } + fmt.Println(directory) + + var ( + bestMatch string + bestMatchExcessChars int + ) + for _, entry := range entries { + // Skip dirs. + if entry.IsDir() { + continue + } + + iconName := strings.ToLower(entry.Name()) + iconName = strings.TrimSuffix(iconName, filepath.Ext(iconName)) + switch { + case len(iconName) < len(binName): + // Continue to next. + case iconName == binName: + // Exact match, return immediately. + return filepath.Join(directory, entry.Name()), nil + case strings.HasPrefix(iconName, binName): + excessChars := len(iconName) - len(binName) + if bestMatch == "" || excessChars < bestMatchExcessChars { + bestMatch = entry.Name() + bestMatchExcessChars = excessChars + } + } + } + + return bestMatch, nil +} diff --git a/profile/icons/find_linux_test.go b/profile/icons/find_linux_test.go new file mode 100644 index 00000000..f9f18852 --- /dev/null +++ b/profile/icons/find_linux_test.go @@ -0,0 +1,32 @@ +package icons + +import ( + "os" + "testing" +) + +func TestFindIcon(t *testing.T) { + if testing.Short() { + t.Skip("test depends on linux desktop environment") + } + t.Parallel() + + home := os.Getenv("HOME") + testFindIcon(t, "evolution", home) + testFindIcon(t, "nextcloud", home) +} + +func testFindIcon(t *testing.T, binName string, homeDir string) { + t.Helper() + + iconPath, err := search(binName, homeDir) + if err != nil { + t.Error(err) + return + } + if iconPath == "" { + t.Errorf("no icon found for %s", binName) + return + } + t.Logf("icon for %s found: %s", binName, iconPath) +} diff --git a/profile/icon.go b/profile/icons/icon.go similarity index 96% rename from profile/icon.go rename to profile/icons/icon.go index fe5f5c72..a989e139 100644 --- a/profile/icon.go +++ b/profile/icons/icon.go @@ -1,4 +1,4 @@ -package profile +package icons import ( "errors" @@ -42,7 +42,8 @@ func (t IconType) sortOrder() int { } } -func sortAndCompactIcons(icons []Icon) []Icon { +// SortAndCompact sorts and compacts a list of icons. +func SortAndCompact(icons []Icon) []Icon { // Sort. slices.SortFunc[[]Icon, Icon](icons, func(a, b Icon) int { aOrder := a.Type.sortOrder() diff --git a/profile/icons.go b/profile/icons/icons.go similarity index 78% rename from profile/icons.go rename to profile/icons/icons.go index 15ee316e..9e701f5f 100644 --- a/profile/icons.go +++ b/profile/icons/icons.go @@ -1,4 +1,4 @@ -package profile +package icons import ( "crypto" @@ -13,18 +13,21 @@ import ( "github.com/safing/portbase/api" ) -var profileIconStoragePath = "" +// ProfileIconStoragePath defines the location where profile icons are stored. +// Must be set before anything else from this package is called. +// Must not be changed once set. +var ProfileIconStoragePath = "" // GetProfileIcon returns the profile icon with the given ID and extension. func GetProfileIcon(name string) (data []byte, err error) { // Check if enabled. - if profileIconStoragePath == "" { + if ProfileIconStoragePath == "" { return nil, errors.New("api icon storage not configured") } // Build storage path. iconPath := filepath.Clean( - filepath.Join(profileIconStoragePath, name), + filepath.Join(ProfileIconStoragePath, name), ) iconPath, err = filepath.Abs(iconPath) @@ -34,7 +37,7 @@ func GetProfileIcon(name string) (data []byte, err error) { // Do a quick check if we are still within the right directory. // This check is not entirely correct, but is sufficient for this use case. - if filepath.Dir(iconPath) != profileIconStoragePath { + if filepath.Dir(iconPath) != ProfileIconStoragePath { return nil, api.ErrorWithStatus(errors.New("invalid icon"), http.StatusBadRequest) } @@ -72,7 +75,7 @@ func UpdateProfileIcon(data []byte, ext string) (filename string, err error) { // Save to disk. filename = sum + "." + ext - return filename, os.WriteFile(filepath.Join(profileIconStoragePath, filename), data, 0o0644) //nolint:gosec + return filename, os.WriteFile(filepath.Join(ProfileIconStoragePath, filename), data, 0o0644) //nolint:gosec } // TODO: Clean up icons regularly. diff --git a/profile/icons/locations_linux.go b/profile/icons/locations_linux.go new file mode 100644 index 00000000..de4e21b0 --- /dev/null +++ b/profile/icons/locations_linux.go @@ -0,0 +1,68 @@ +package icons + +import ( + "fmt" +) + +// IconLocation describes an icon location. +type IconLocation struct { + Directory string + Type IconLocationType + PathArg PathArg +} + +// IconLocationType describes an icon location type. +type IconLocationType uint8 + +// Icon Location Types. +const ( + FlatDir IconLocationType = iota + XDGIcons +) + +// PathArg describes an icon location path argument. +type PathArg uint8 + +// Path Args. +const ( + NoPathArg PathArg = iota + Home + BinName +) + +var ( + iconLocations = []IconLocation{ + {Directory: "/usr/share/pixmaps", Type: FlatDir}, + {Directory: "/usr/share", Type: XDGIcons}, + {Directory: "%s/.local/share", Type: XDGIcons, PathArg: Home}, + {Directory: "%s/.local/share/flatpak/exports/share", Type: XDGIcons, PathArg: Home}, + {Directory: "/usr/share/%s", Type: XDGIcons, PathArg: BinName}, + } + + xdgIconPaths = []string{ + // UI currently uses 48x48, so 256x256 should suffice for the future, even at 2x. (12.2023) + "icons/hicolor/256x256/apps", + "icons/hicolor/192x192/apps", + "icons/hicolor/128x128/apps", + "icons/hicolor/96x96/apps", + "icons/hicolor/72x72/apps", + "icons/hicolor/64x64/apps", + "icons/hicolor/48x48/apps", + "icons/hicolor/512x512/apps", + } +) + +// GetPath returns the path of an icon. +func (il IconLocation) GetPath(binName string, homeDir string) string { + switch il.PathArg { + case NoPathArg: + return il.Directory + case Home: + if homeDir != "" { + return fmt.Sprintf(il.Directory, homeDir) + } + case BinName: + return fmt.Sprintf(il.Directory, binName) + } + return "" +} diff --git a/profile/merge.go b/profile/merge.go index 29e274dc..358f9bf7 100644 --- a/profile/merge.go +++ b/profile/merge.go @@ -7,6 +7,7 @@ import ( "time" "github.com/safing/portbase/database/record" + "github.com/safing/portmaster/profile/icons" ) // MergeProfiles merges multiple profiles into a new one. @@ -52,12 +53,12 @@ func MergeProfiles(name string, primary *Profile, secondaries ...*Profile) (newP } // Collect all icons. - newProfile.Icons = make([]Icon, 0, len(secondaries)+1) // Guess the needed space. + newProfile.Icons = make([]icons.Icon, 0, len(secondaries)+1) // Guess the needed space. newProfile.Icons = append(newProfile.Icons, primary.Icons...) for _, sp := range secondaries { newProfile.Icons = append(newProfile.Icons, sp.Icons...) } - newProfile.Icons = sortAndCompactIcons(newProfile.Icons) + newProfile.Icons = icons.SortAndCompact(newProfile.Icons) // Collect all fingerprints. newProfile.Fingerprints = make([]Fingerprint, 0, len(primary.Fingerprints)+len(secondaries)) // Guess the needed space. diff --git a/profile/module.go b/profile/module.go index 6036d79a..577d4843 100644 --- a/profile/module.go +++ b/profile/module.go @@ -11,6 +11,7 @@ import ( "github.com/safing/portbase/log" "github.com/safing/portbase/modules" _ "github.com/safing/portmaster/core/base" + "github.com/safing/portmaster/profile/icons" "github.com/safing/portmaster/updates" ) @@ -52,7 +53,7 @@ func prep() error { if err := iconsDir.Ensure(); err != nil { return fmt.Errorf("failed to create/check icons directory: %w", err) } - profileIconStoragePath = iconsDir.Path + icons.ProfileIconStoragePath = iconsDir.Path return nil } diff --git a/profile/profile.go b/profile/profile.go index 8cdb2f58..43101fd8 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -18,6 +18,7 @@ import ( "github.com/safing/portbase/utils/osdetail" "github.com/safing/portmaster/intel/filterlists" "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/profile/icons" ) // ProfileSource is the source of the profile. @@ -68,9 +69,9 @@ type Profile struct { //nolint:maligned // not worth the effort // See IconType for more information. Icon string // Deprecated: IconType describes the type of the Icon property. - IconType IconType + IconType icons.IconType // Icons holds a list of icons to represent the application. - Icons []Icon + Icons []icons.Icon // Deprecated: LinkedPath used to point to the executableis this // profile was created for. @@ -505,7 +506,7 @@ func (profile *Profile) updateMetadata(binaryPath string) (changed bool) { // updateMetadataFromSystem updates the profile metadata with data from the // operating system and saves it afterwards. -func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error { +func (profile *Profile) updateMetadataFromSystem(ctx context.Context, md MatchingData) error { var changed bool // This function is only valid for local profiles. @@ -531,6 +532,22 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error { return nil } + // Get icon if path matches presentation path. + var newIcon *icons.Icon + if profile.PresentationPath == md.Path() { + // Get home from ENV. + var home string + if env := md.Env(); env != nil { + home = env["HOME"] + } + var err error + newIcon, err = icons.FindIcon(ctx, profile.PresentationPath, home) + if err != nil { + log.Warningf("profile: failed to find icon for %s: %s", profile.PresentationPath, err) + newIcon = nil + } + } + // Apply new data to profile. func() { // Lock profile for applying metadata. @@ -542,6 +559,16 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context) error { profile.Name = newName changed = true } + + // Apply new icon if found. + if newIcon != nil { + if len(profile.Icons) == 0 { + profile.Icons = []icons.Icon{*newIcon} + } else { + profile.Icons = append(profile.Icons, *newIcon) + profile.Icons = icons.SortAndCompact(profile.Icons) + } + } }() // If anything changed, save the profile. From 1e2491c3b3233af4e81fd89f0e2152299dfd2f7d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 Dec 2023 15:53:37 +0100 Subject: [PATCH 2/6] Improve profile ID migration errors and run migration again --- profile/migrations.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/profile/migrations.go b/profile/migrations.go index eca9d8df..cbc16527 100644 --- a/profile/migrations.go +++ b/profile/migrations.go @@ -11,6 +11,7 @@ import ( "github.com/safing/portbase/database/migration" "github.com/safing/portbase/database/query" "github.com/safing/portbase/log" + "github.com/safing/portmaster/profile/icons" ) func registerMigrations() error { @@ -27,7 +28,7 @@ func registerMigrations() error { }, migration.Migration{ Description: "Migrate from random profile IDs to fingerprint-derived IDs", - Version: "v1.6.0", + Version: "v1.6.3", // Re-run after mixed results in v1.6.0 MigrateFunc: migrateToDerivedIDs, }, ) @@ -102,7 +103,7 @@ func migrateIcons(ctx context.Context, _, to *version.Version, db *database.Inte } // Migrate to icon list. - profile.Icons = []Icon{{ + profile.Icons = []icons.Icon{{ Type: profile.IconType, Value: profile.Icon, }} @@ -161,6 +162,8 @@ func migrateToDerivedIDs(ctx context.Context, _, to *version.Version, db *databa // Parse profile. profile, err := EnsureProfile(r) if err != nil { + failed++ + lastErr = err log.Tracer(ctx).Debugf("profiles: failed to parse profile %s for migration: %s", r.Key(), err) continue } From 307fb5a7675d71d8fe0b263f36a583297e28c6f1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 Dec 2023 15:54:25 +0100 Subject: [PATCH 3/6] Improve profile import and export regarding IDs, fingerprints and defaults --- sync/profile.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/sync/profile.go b/sync/profile.go index c6e936d5..20aeb7dd 100644 --- a/sync/profile.go +++ b/sync/profile.go @@ -254,6 +254,9 @@ func ExportProfile(scopedID string) (*ProfileExport, error) { export.Created = &created } + // Derive ID to ensure the ID is always correct. + export.ID = profile.DeriveProfileID(p.Fingerprints) + // Add first exportable icon to export. if len(p.Icons) > 0 { var err error @@ -270,6 +273,14 @@ func ExportProfile(scopedID string) (*ProfileExport, error) { } } + // Remove presentation path if both Name and Icon are set. + if export.Name != "" && export.IconData != "" { + p.UsePresentationPath = false + } + if !p.UsePresentationPath { + p.PresentationPath = "" + } + return export, nil } @@ -284,11 +295,15 @@ func ImportProfile(r *ProfileImportRequest, requiredProfileSource profile.Profil if r.Export.Source != "" && r.Export.Source != requiredProfileSource { return nil, ErrMismatch } - // Check ID. + // Convert fingerprints to internal representation. fingerprints := convertFingerprintsToInternal(r.Export.Fingerprints) + if len(fingerprints) == 0 { + return nil, fmt.Errorf("%w: the export contains no fingerprints", ErrInvalidProfileData) + } + // Derive ID from fingerprints. profileID := profile.DeriveProfileID(fingerprints) if r.Export.ID != "" && r.Export.ID != profileID { - return nil, ErrMismatch + return nil, fmt.Errorf("%w: the export profile ID does not match the fingerprints, remove to ignore", ErrInvalidProfileData) } r.Export.ID = profileID // Check Fingerprints. @@ -385,6 +400,14 @@ func ImportProfile(r *ProfileImportRequest, requiredProfileSource profile.Profil p.Created = in.Created.Unix() } + // Fill in required values. + if p.Config == nil { + p.Config = make(map[string]any) + } + if p.Created == 0 { + p.Created = time.Now().Unix() + } + // Add icon to profile, if set. if in.IconData != "" { du, err := dataurl.DecodeString(in.IconData) From cd34d64ca6ef83882bf447c3cff3480980f86dc6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 Dec 2023 15:59:19 +0100 Subject: [PATCH 4/6] Fix profile icon package refs --- sync/profile.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sync/profile.go b/sync/profile.go index 20aeb7dd..2ddafeaa 100644 --- a/sync/profile.go +++ b/sync/profile.go @@ -14,6 +14,7 @@ import ( "github.com/safing/portbase/config" "github.com/safing/portbase/log" "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/profile/icons" ) // ProfileExport holds an export of a profile. @@ -414,12 +415,12 @@ func ImportProfile(r *ProfileImportRequest, requiredProfileSource profile.Profil if err != nil { return nil, fmt.Errorf("%w: icon data is invalid: %w", ErrImportFailed, err) } - filename, err := profile.UpdateProfileIcon(du.Data, du.MediaType.Subtype) + filename, err := icons.UpdateProfileIcon(du.Data, du.MediaType.Subtype) if err != nil { return nil, fmt.Errorf("%w: icon is invalid: %w", ErrImportFailed, err) } - p.Icons = []profile.Icon{{ - Type: profile.IconTypeAPI, + p.Icons = []icons.Icon{{ + Type: icons.IconTypeAPI, Value: filename, }} } From cf3d4e030fe5102107b8730b29aa38f5e738e74b Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 Dec 2023 16:01:30 +0100 Subject: [PATCH 5/6] Fix unimplemented find icon fallback function --- profile/icons/find_default.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/profile/icons/find_default.go b/profile/icons/find_default.go index 782824e1..489b70a5 100644 --- a/profile/icons/find_default.go +++ b/profile/icons/find_default.go @@ -2,9 +2,9 @@ package icons -import "github.com/safing/portmaster/profile" +import "context" // FindIcon returns nil, nil for unsupported platforms. -func FindIcon(binName string, homeDir string) (*profile.Icon, error) { +func FindIcon(ctx context.Context, binName string, homeDir string) (*Icon, error) { return nil, nil } From 9c969f9465a4bda46c6a3335cf6056af70779e90 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 Dec 2023 16:25:48 +0100 Subject: [PATCH 6/6] Load Windows Svc icon with new icon system --- process/tags/svchost_windows.go | 15 ++++++++++++--- profile/icons/find_linux.go | 14 +------------- profile/icons/icons.go | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/process/tags/svchost_windows.go b/process/tags/svchost_windows.go index 417604f2..ca3f2067 100644 --- a/process/tags/svchost_windows.go +++ b/process/tags/svchost_windows.go @@ -1,6 +1,7 @@ package tags import ( + "context" "fmt" "strings" @@ -8,6 +9,7 @@ import ( "github.com/safing/portbase/utils/osdetail" "github.com/safing/portmaster/process" "github.com/safing/portmaster/profile" + "github.com/safing/portmaster/profile/icons" ) func init() { @@ -81,11 +83,10 @@ func (h *SVCHostTagHandler) AddTags(p *process.Process) { // Returns nil to skip. func (h *SVCHostTagHandler) CreateProfile(p *process.Process) *profile.Profile { if tag, ok := p.GetTag(svchostTagKey); ok { - return profile.New(&profile.Profile{ + // Create new profile based on tag. + newProfile := profile.New(&profile.Profile{ Source: profile.SourceLocal, Name: "Windows Service: " + osdetail.GenerateBinaryNameFromPath(tag.Value), - Icon: `C:\Windows\System32\@WLOGO_48x48.png`, - IconType: profile.IconTypeFile, UsePresentationPath: false, Fingerprints: []profile.Fingerprint{ profile.Fingerprint{ @@ -96,6 +97,14 @@ func (h *SVCHostTagHandler) CreateProfile(p *process.Process) *profile.Profile { }, }, }) + + // Load default icon for windows service. + icon, err := icons.LoadAndSaveIcon(context.TODO(), `C:\Windows\System32\@WLOGO_48x48.png`) + if err == nil { + newProfile.Icons = []icons.Icon{*icon} + } + + return newProfile } return nil diff --git a/profile/icons/find_linux.go b/profile/icons/find_linux.go index 86a79534..5162eaad 100644 --- a/profile/icons/find_linux.go +++ b/profile/icons/find_linux.go @@ -21,19 +21,7 @@ func FindIcon(ctx context.Context, binName string, homeDir string) (*Icon, error return nil, nil } - // Load icon and save it. - data, err := os.ReadFile(iconPath) - if err != nil { - return nil, fmt.Errorf("failed to read icon %s: %w", iconPath, err) - } - filename, err := UpdateProfileIcon(data, filepath.Ext(iconPath)) - if err != nil { - return nil, fmt.Errorf("failed to import icon %s: %w", iconPath, err) - } - return &Icon{ - Type: IconTypeAPI, - Value: filename, - }, nil + return LoadAndSaveIcon(ctx, iconPath) } func search(binName string, homeDir string) (iconPath string, err error) { diff --git a/profile/icons/icons.go b/profile/icons/icons.go index 9e701f5f..e5833504 100644 --- a/profile/icons/icons.go +++ b/profile/icons/icons.go @@ -1,6 +1,7 @@ package icons import ( + "context" "crypto" "encoding/hex" "errors" @@ -78,4 +79,22 @@ func UpdateProfileIcon(data []byte, ext string) (filename string, err error) { return filename, os.WriteFile(filepath.Join(ProfileIconStoragePath, filename), data, 0o0644) //nolint:gosec } +// LoadAndSaveIcon loads an icon from disk, updates it in the icon database +// and returns the icon object. +func LoadAndSaveIcon(ctx context.Context, iconPath string) (*Icon, error) { + // Load icon and save it. + data, err := os.ReadFile(iconPath) + if err != nil { + return nil, fmt.Errorf("failed to read icon %s: %w", iconPath, err) + } + filename, err := UpdateProfileIcon(data, filepath.Ext(iconPath)) + if err != nil { + return nil, fmt.Errorf("failed to import icon %s: %w", iconPath, err) + } + return &Icon{ + Type: IconTypeAPI, + Value: filename, + }, nil +} + // TODO: Clean up icons regularly.