Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ bin/

# Go workspace file
go.work

# Ignore custom logo
web/static/img
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ The application can be configured with the following environment variables:
- `SSE_TYPE`: Specified server side encryption (defaults blank) Valid values can be `SSE`, `KMS`, `SSE-C` all others values don't enable the SSE
- `SSE_KEY`: The key needed for SSE method (only for `KMS` and `SSE-C`)
- `TIMEOUT`: The read and write timeout in seconds (default to `600` - 10 minutes)
- `NAVBAR_COLOR`: The background color for the navigation bar (defaults to `#ee6e73` - Materialize teal color)
- `LOGO_PATH`: Custom path to logo image (defaults to `/static/img/logo.png` if the file exists, empty otherwise). When a logo is provided, both the logo and "S3 Manager" text are displayed together in the navbar.
- `BUTTON_COLOR`: The background color for primary buttons (defaults to `#f44336` - Materialize red color)

#### UI Customization Examples

Customize the navbar color to match your brand:
```bash
export NAVBAR_COLOR="#2196F3" # Blue navbar
export NAVBAR_COLOR="#4CAF50" # Green navbar
export NAVBAR_COLOR="#FF9800" # Orange navbar
```

Customize button colors:
```bash
export BUTTON_COLOR="#2196F3" # Blue buttons
export BUTTON_COLOR="#4CAF50" # Green buttons
export BUTTON_COLOR="#FF9800" # Orange buttons
```

Use a custom logo (must be accessible as a static file):
```bash
export LOGO_PATH="/static/img/company-logo.png"
```

The application automatically detects if `web/static/img/logo.png` exists and uses it as the default logo.

#### Custom Styling

The application uses CSS custom properties (CSS variables) for dynamic theming. All styling is properly separated into CSS files rather than inline styles for better maintainability. The main custom styles are located in `/static/css/s3manager.css`.

### Build and Run Locally

Expand Down
21 changes: 10 additions & 11 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module github.com/cloudlena/s3manager

go 1.24.5
go 1.24.6

require (
github.com/cloudlena/adapters v0.0.0-20250728121319-727f6c720317
github.com/cloudlena/adapters v0.0.0-20250828143942-ace429f59c2f
github.com/gorilla/mux v1.8.1
github.com/matryer/is v1.4.1
github.com/minio/minio-go/v7 v7.0.95
Expand All @@ -19,22 +19,21 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/sagikazarmark/locafero v0.10.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
github.com/tinylib/msgp v1.4.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
38 changes: 18 additions & 20 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/cloudlena/adapters v0.0.0-20250728121319-727f6c720317 h1:a4lRsiWfF4iVMvMHEH5gDTFk4oz8g48PlvfonuSHdOA=
github.com/cloudlena/adapters v0.0.0-20250728121319-727f6c720317/go.mod h1:U84ZSc+LapVZkHCa4FRNeu9xD+MTXA/9MY2YzfcgChE=
github.com/cloudlena/adapters v0.0.0-20250828143942-ace429f59c2f h1:KerRA0WTG7174GQz2eXFm18OiKt9pEqQlVrKI7U4hIs=
github.com/cloudlena/adapters v0.0.0-20250828143942-ace429f59c2f/go.mod h1:lusi5rp1QyxyCUyZnXB4BXxn9Y8iW5heDPlaNDIAJZc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
Expand Down Expand Up @@ -31,8 +31,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
Expand All @@ -47,10 +47,10 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
Expand All @@ -63,18 +63,16 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
17 changes: 15 additions & 2 deletions internal/app/s3manager/bucket_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

// HandleBucketView shows the details page of a bucket.
func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bool) http.HandlerFunc {
func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bool, navbarColor, logoPath, buttonColor string) http.HandlerFunc {
type objectWithIcon struct {
Key string
Size int64
Expand All @@ -31,6 +31,9 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo
AllowDelete bool
Paths []string
CurrentPath string
NavbarColor string
LogoPath string
ButtonColor string
}

return func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -53,14 +56,21 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo
return
}

displayName := strings.TrimSuffix(strings.TrimPrefix(object.Key, path), "/")

// Skip the current folder itself (empty display name)
if displayName == "" {
continue
}

obj := objectWithIcon{
Key: object.Key,
Size: object.Size,
LastModified: object.LastModified,
Owner: object.Owner.DisplayName,
Icon: icon(object.Key),
IsFolder: strings.HasSuffix(object.Key, "/"),
DisplayName: strings.TrimSuffix(strings.TrimPrefix(object.Key, path), "/"),
DisplayName: displayName,
}
objs = append(objs, obj)
}
Expand All @@ -70,6 +80,9 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo
AllowDelete: allowDelete,
Paths: removeEmptyStrings(strings.Split(path, "/")),
CurrentPath: path,
NavbarColor: navbarColor,
LogoPath: logoPath,
ButtonColor: buttonColor,
}

t, err := template.ParseFS(templates, "layout.html.tmpl", "bucket.html.tmpl")
Expand Down
2 changes: 1 addition & 1 deletion internal/app/s3manager/bucket_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func TestHandleBucketView(t *testing.T) {

templates := os.DirFS(filepath.Join("..", "..", "..", "web", "template"))
r := mux.NewRouter()
r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, true, true)).Methods(http.MethodGet)
r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, true, true, "#ee6e73", "/static/img/logo.png", "#f44336")).Methods(http.MethodGet)

ts := httptest.NewServer(r)
defer ts.Close()
Expand Down
8 changes: 7 additions & 1 deletion internal/app/s3manager/buckets_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import (
)

// HandleBucketsView renders all buckets on an HTML page.
func HandleBucketsView(s3 S3, templates fs.FS, allowDelete bool) http.HandlerFunc {
func HandleBucketsView(s3 S3, templates fs.FS, allowDelete bool, navbarColor, logoPath, buttonColor string) http.HandlerFunc {
type pageData struct {
Buckets []minio.BucketInfo
AllowDelete bool
NavbarColor string
LogoPath string
ButtonColor string
}

return func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -26,6 +29,9 @@ func HandleBucketsView(s3 S3, templates fs.FS, allowDelete bool) http.HandlerFun
data := pageData{
Buckets: buckets,
AllowDelete: allowDelete,
NavbarColor: navbarColor,
LogoPath: logoPath,
ButtonColor: buttonColor,
}

t, err := template.ParseFS(templates, "layout.html.tmpl", "buckets.html.tmpl")
Expand Down
2 changes: 1 addition & 1 deletion internal/app/s3manager/buckets_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestHandleBucketsView(t *testing.T) {
is.NoErr(err)

rr := httptest.NewRecorder()
handler := s3manager.HandleBucketsView(s3, templates, true)
handler := s3manager.HandleBucketsView(s3, templates, true, "#ee6e73", "/static/img/logo.png", "#f44336")

handler.ServeHTTP(rr, req)
resp := rr.Result()
Expand Down
28 changes: 26 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type configuration struct {
Timeout int32
SseType string
SseKey string
NavbarColor string
LogoPath string
ButtonColor string
}

func parseConfiguration() configuration {
Expand Down Expand Up @@ -98,6 +101,24 @@ func parseConfiguration() configuration {
viper.SetDefault("SSE_KEY", "")
sseKey := viper.GetString("SSE_KEY")

// UI Customization options
viper.SetDefault("NAVBAR_COLOR", "#ee6e73") // Default materialize teal color
navbarColor := viper.GetString("NAVBAR_COLOR")

viper.SetDefault("BUTTON_COLOR", "#f44336") // Default red color (materialize red)
buttonColor := viper.GetString("BUTTON_COLOR")

// Logo path - check if logo exists, if not leave empty
logoPath := ""
// Since we're using embedded filesystem, check if the logo file would be available
// In production, the file is embedded, so we can assume it exists if it's in the repo
logoPath = "/static/img/logo.png"

// Allow override via environment variable
if envLogoPath := viper.GetString("LOGO_PATH"); envLogoPath != "" {
logoPath = envLogoPath
}

return configuration{
Endpoint: endpoint,
UseIam: useIam,
Expand All @@ -115,6 +136,9 @@ func parseConfiguration() configuration {
Timeout: timeout,
SseType: sseType,
SseKey: sseKey,
NavbarColor: navbarColor,
LogoPath: logoPath,
ButtonColor: buttonColor,
}
}

Expand Down Expand Up @@ -175,8 +199,8 @@ func main() {
r := mux.NewRouter()
r.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet)
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(statics)))).Methods(http.MethodGet)
r.Handle("/buckets", s3manager.HandleBucketsView(s3, templates, configuration.AllowDelete)).Methods(http.MethodGet)
r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, configuration.AllowDelete, configuration.ListRecursive)).Methods(http.MethodGet)
r.Handle("/buckets", s3manager.HandleBucketsView(s3, templates, configuration.AllowDelete, configuration.NavbarColor, configuration.LogoPath, configuration.ButtonColor)).Methods(http.MethodGet)
r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, configuration.AllowDelete, configuration.ListRecursive, configuration.NavbarColor, configuration.LogoPath, configuration.ButtonColor)).Methods(http.MethodGet)
r.Handle("/api/buckets", s3manager.HandleCreateBucket(s3)).Methods(http.MethodPost)
if configuration.AllowDelete {
r.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucket(s3)).Methods(http.MethodDelete)
Expand Down
Loading