fleet/server/datastore/filesystem/org_logo.go
Nico b4a207fb5a
Add ability to upload custom org logos (#44390)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #44330, Resolves #44331

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests. (I'd defer integration tests to a
separate PR since this one is pretty large already.)

- [x] QA'd all new/changed functionality manually. I've tested this on
both the setup flow and the organization settings page. I haven't had
the time to test this on other places where we render the logo (macOS
setup experience / MDM migration dialog).


https://github.com/user-attachments/assets/95d4eae5-3da6-40f4-98a1-8575b97d96b3

## New Fleet configuration settings

- [x] Setting(s) is/are explicitly excluded from GitOps.

Will handle GitOps in a separate PR.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Organizations can upload custom logos for light and dark modes.
* Registration and Org Settings support logo file upload, preview,
per-mode replace/delete, and validation (size & image formats).
* Activity feed records logo changes/deletions; site nav displays
uploaded logos per theme.
* File uploader/preview adds a Fleet logo graphic option and improved
logo validation.
  * Config/GitOps outputs now include separate dark/light logo fields.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-05 14:42:52 +02:00

88 lines
2.2 KiB
Go

package filesystem
import (
"context"
"io"
"os"
"path/filepath"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
)
const orgLogosPrefix = "org-logos"
type orgLogoNotFoundError struct{}
var _ fleet.NotFoundError = (*orgLogoNotFoundError)(nil)
func (orgLogoNotFoundError) Error() string { return "org logo not found" }
func (orgLogoNotFoundError) IsNotFound() bool { return true }
type OrgLogoStore struct {
rootDir string
}
func NewOrgLogoStore(rootDir string) (*OrgLogoStore, error) {
dir := filepath.Join(rootDir, orgLogosPrefix)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
return &OrgLogoStore{rootDir: rootDir}, nil
}
func (s *OrgLogoStore) pathFor(mode fleet.OrgLogoMode) string {
return filepath.Join(s.rootDir, orgLogosPrefix, string(mode))
}
func (s *OrgLogoStore) Get(ctx context.Context, mode fleet.OrgLogoMode) (io.ReadCloser, int64, error) {
p := s.pathFor(mode)
f, err := os.Open(p)
if err != nil {
if os.IsNotExist(err) {
return nil, 0, orgLogoNotFoundError{}
}
return nil, 0, ctxerr.Wrap(ctx, err, "open org logo")
}
st, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, 0, ctxerr.Wrap(ctx, err, "stat org logo")
}
return f, st.Size(), nil
}
func (s *OrgLogoStore) Put(ctx context.Context, mode fleet.OrgLogoMode, content io.ReadSeeker) error {
p := s.pathFor(mode)
f, err := os.Create(p)
if err != nil {
return ctxerr.Wrap(ctx, err, "create org logo file")
}
defer f.Close()
if _, err := io.Copy(f, content); err != nil {
return ctxerr.Wrap(ctx, err, "write org logo file")
}
if err := f.Close(); err != nil {
return ctxerr.Wrap(ctx, err, "close org logo file")
}
return nil
}
func (s *OrgLogoStore) Delete(ctx context.Context, mode fleet.OrgLogoMode) error {
p := s.pathFor(mode)
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
return ctxerr.Wrap(ctx, err, "remove org logo file")
}
return nil
}
func (s *OrgLogoStore) Exists(ctx context.Context, mode fleet.OrgLogoMode) (bool, error) {
p := s.pathFor(mode)
if _, err := os.Stat(p); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, ctxerr.Wrap(ctx, err, "stat org logo")
}
return true, nil
}