Skip to content

Commit

Permalink
Switch to Fiber and HTMX
Browse files Browse the repository at this point in the history
  • Loading branch information
cloudlena committed Jan 7, 2025
1 parent ad2d6c5 commit 1b4bcae
Show file tree
Hide file tree
Showing 41 changed files with 1,782 additions and 590 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
uses: actions/checkout@v4
- name: Lint code
uses: golangci/golangci-lint-action@v6
with:
args: --timeout=5m
- name: Run tests
run: make test
- name: Log in to Docker Hub
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build:

.PHONY: run
run:
go run
go run ./...

.PHONY: lint
lint:
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/cloudlena/s3manager)](https://goreportcard.com/report/github.com/cloudlena/s3manager)
[![Build Status](https://github.com/cloudlena/s3manager/actions/workflows/main.yml/badge.svg)](https://github.com/cloudlena/s3manager/actions)

A Web GUI written in Go to manage S3 buckets from any provider.
A Web GUI to manage S3 buckets from any provider.

![Screenshot](https://raw.githubusercontent.com/cloudlena/s3manager/main/screenshot.png)

:rocket: Powered by [Fiber](https://gofiber.io/) and [HTMX](https://htmx.org/)

## Features

- List all buckets in your account
Expand All @@ -29,28 +31,27 @@ The application can be configured with the following environment variables:
- `USE_SSL`: Whether your S3 server uses SSL or not (defaults to `true`)
- `SKIP_SSL_VERIFICATION`: Whether the HTTP client should skip SSL verification (defaults to `false`)
- `SIGNATURE_TYPE`: The signature type to be used (defaults to `V4`; valid values are `V2, V4, V4Streaming, Anonymous`)
- `PORT`: The port the s3manager app should listen on (defaults to `8080`)
- `PORT`: The port the `s3manager` app should listen on (defaults to `8080`)
- `ALLOW_DELETE`: Enable buttons to delete objects (defaults to `true`)
- `FORCE_DOWNLOAD`: Add response headers for object downloading instead of opening in a new tab (defaults to `true`)
- `LIST_RECURSIVE`: List all objects in buckets recursively (defaults to `false`)
- `USE_IAM`: Use IAM role instead of key pair (defaults to `false`)
- `IAM_ENDPOINT`: Endpoint for IAM role retrieving (Can be blank for AWS)
- `SSE_TYPE`: Specified server side encrpytion (defaults blank) Valid values can be `SSE`, `KMS`, `SSE-C` all others values don't enable the SSE
- `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 timout in seconds (default to `600` - 10 minutes)
- `TIMEOUT`: The read and write timeout in seconds (default to `600` - 10 minutes)

### Build and Run Locally
### Run Locally

1. Run `make build`
1. Execute the created binary and visit <http://localhost:8080>
1. Run `make run`

### Run Container image

1. Run `docker run -p 8080:8080 -e 'ACCESS_KEY_ID=XXX' -e 'SECRET_ACCESS_KEY=xxx' cloudlena/s3manager`

### Deploy to Kubernetes

You can deploy s3manager to a Kubernetes cluster using the [Helm chart](https://github.com/sergeyshevch/s3manager-helm).
You can deploy `s3manager` to a Kubernetes cluster using the [Helm chart](https://github.com/sergeyshevch/s3manager-helm).

## Development

Expand Down
13 changes: 12 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,34 @@ module github.com/cloudlena/s3manager
go 1.23.4

require (
github.com/cloudlena/adapters v0.0.0-20250106081220-31472cba684a
github.com/gofiber/fiber/v2 v2.52.6
github.com/gofiber/template/html/v2 v2.1.2
github.com/gorilla/mux v1.8.1
github.com/matryer/is v1.4.1
github.com/minio/minio-go/v7 v7.0.83
github.com/spf13/viper v1.19.0
)

require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
Expand All @@ -31,6 +39,9 @@ require (
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.58.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
Expand Down
32 changes: 30 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/cloudlena/adapters v0.0.0-20250106081220-31472cba684a h1:Cdm8AR/XcToIeNf5feifFtmBPuqVs4DtRizd4wVtQkc=
github.com/cloudlena/adapters v0.0.0-20250106081220-31472cba684a/go.mod h1:DLTKeV3r4hAf6wi/5RLf8xrUhZbQ1MkfjrbNUjBKveY=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -13,6 +13,14 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.2 h1:wkK/mYJ3nIhongTkG3t0QgV4ADdgOYJYVSAF2AHnh8Y=
github.com/gofiber/template/html/v2 v2.1.2/go.mod h1:E98Z/FzvpaSib06aWEgYk6GXNf3ctoyaJH8yW5ay5ak=
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand All @@ -34,6 +42,13 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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.83 h1:W4Kokksvlz3OKf3OqIlzDNKd4MERlC2oN8YptwJ0+GA=
Expand All @@ -44,6 +59,9 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
Expand All @@ -66,6 +84,14 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
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.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
Expand All @@ -74,6 +100,8 @@ golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
Expand Down
19 changes: 19 additions & 0 deletions internal/s3manager/handle_bucket_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package s3manager

import (
"fmt"

"github.com/gofiber/fiber/v2"
)

// HandleBucketList renders all buckets as an HTML list.
func (s *S3Manager) HandleBucketList(c *fiber.Ctx) error {
buckets, err := s.s3.ListBuckets(c.Context())
if err != nil {
return fmt.Errorf("error listing buckets: %w", err)
}

return c.Render("bucket-list", fiber.Map{
"Buckets": buckets,
})
}
80 changes: 80 additions & 0 deletions internal/s3manager/handle_bucket_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package s3manager_test

import (
"context"
"errors"
"io"
"net/http"
"strings"
"testing"

"github.com/cloudlena/s3manager/internal/s3manager"
"github.com/cloudlena/s3manager/internal/s3manager/mocks"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
"github.com/matryer/is"
"github.com/minio/minio-go/v7"
)

func TestHandleBucketList(t *testing.T) {
t.Parallel()

cases := []struct {
it string
listBucketsVal []minio.BucketInfo
listBucketsErr error
expectedStatusCode int
expectedBodyContains string
}{
{
it: "renders a list of buckets",
listBucketsVal: []minio.BucketInfo{{Name: "BUCKET-NAME"}},
expectedStatusCode: http.StatusOK,
expectedBodyContains: "BUCKET-NAME",
},
{
it: "renders placeholder if no buckets",
expectedStatusCode: http.StatusOK,
expectedBodyContains: "No buckets yet",
},
{
it: "returns error if there is an S3 error",
listBucketsErr: errors.New("mocked s3 error"),
expectedStatusCode: http.StatusInternalServerError,
expectedBodyContains: "mocked s3 error",
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.it, func(t *testing.T) {
t.Parallel()
is := is.New(t)

s3 := &mocks.S3Mock{
ListBucketsFunc: func(ctx context.Context) ([]minio.BucketInfo, error) {
return tc.listBucketsVal, tc.listBucketsErr
},
}
server := s3manager.New(s3, true, "", "")

engine := html.New("../../views", ".html.gotmpl")
app := fiber.New(fiber.Config{
Views: engine,
})
app.Get("/bucket-list", server.HandleBucketList)

req, err := http.NewRequest(fiber.MethodGet, "/bucket-list", nil)
is.NoErr(err)

resp, err := app.Test(req)
is.NoErr(err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
is.NoErr(err)

is.Equal(resp.StatusCode, tc.expectedStatusCode) // status code
is.True(strings.Contains(string(body), tc.expectedBodyContains)) // body
})
}
}
10 changes: 10 additions & 0 deletions internal/s3manager/handle_buckets_view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package s3manager

import (
"github.com/gofiber/fiber/v2"
)

// HandleBucketsView renders all buckets on an HTML page.
func (s *S3Manager) HandleBucketsView(c *fiber.Ctx) error {
return c.Render("buckets", fiber.Map{})
}
57 changes: 57 additions & 0 deletions internal/s3manager/handle_buckets_view_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package s3manager_test

import (
"io"
"net/http"
"strings"
"testing"

"github.com/cloudlena/s3manager/internal/s3manager"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
"github.com/matryer/is"
)

func TestHandleBucketsView(t *testing.T) {
t.Parallel()

cases := []struct {
it string
expectedStatusCode int
expectedBodyContains string
}{
{
it: "renders the buckets page",
expectedStatusCode: http.StatusOK,
expectedBodyContains: "S3 Manager",
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.it, func(t *testing.T) {
t.Parallel()
is := is.New(t)

server := s3manager.New(nil, true, "", "")

engine := html.New("../../views", ".html.gotmpl")
app := fiber.New(fiber.Config{
Views: engine,
})
app.Get("/buckets", server.HandleBucketsView)

req, err := http.NewRequest(fiber.MethodGet, "/buckets", nil)
is.NoErr(err)

resp, err := app.Test(req)
is.NoErr(err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
is.NoErr(err)

is.Equal(resp.StatusCode, tc.expectedStatusCode) // status code
is.True(strings.Contains(string(body), tc.expectedBodyContains)) // body
})
}
}
25 changes: 25 additions & 0 deletions internal/s3manager/handle_create_bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package s3manager

import (
"fmt"
"strings"

"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
)

// HandleCreateBucket creates a new bucket.
func (s *S3Manager) HandleCreateBucket(c *fiber.Ctx) error {
name := c.FormValue("name")
if strings.TrimSpace(name) == "" {
return fiber.NewError(fiber.StatusBadRequest, "name is required")
}

err := s.s3.MakeBucket(c.Context(), name, minio.MakeBucketOptions{})
if err != nil {
return fmt.Errorf("error making bucket: %w", err)
}

c.Response().Header.Set("HX-Trigger", "bucketListChanged")
return c.SendStatus(fiber.StatusCreated)
}
Loading

0 comments on commit 1b4bcae

Please sign in to comment.