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 Feb 10, 2024
1 parent d4b0288 commit 31b1170
Show file tree
Hide file tree
Showing 40 changed files with 1,780 additions and 590 deletions.
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,28 +3,36 @@ module github.com/cloudlena/s3manager
go 1.21.6

require (
github.com/cloudlena/adapters v0.0.0-20240204125410-209613aec852
github.com/gofiber/fiber/v2 v2.52.0
github.com/gofiber/template/html/v2 v2.1.0
github.com/gorilla/mux v1.8.1
github.com/matryer/is v1.4.1
github.com/minio/minio-go/v7 v7.0.66
github.com/spf13/viper v1.18.2
)

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gofiber/template v1.8.2 // 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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/magiconair/properties v1.8.7 // 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.15 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
Expand All @@ -34,6 +42,9 @@ require (
github.com/spf13/cast v1.6.0 // 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.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
Expand Down
30 changes: 28 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-20240204125410-209613aec852 h1:54K1i3dBMw/oClIAZmSuU7SdqN0B4ME1M7DrlAcT1HU=
github.com/cloudlena/adapters v0.0.0-20240204125410-209613aec852/go.mod h1:+4onzjcERlRT7WZJ94VxaqMxCiSNAbuAj7hpsxLUYJ0=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
Expand All @@ -10,6 +10,14 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk=
github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.0 h1:FjwzqhhdJpnhyCvav60Z1ytnBqOUr5sGO/aTeob9/ng=
github.com/gofiber/template/html/v2 v2.1.0/go.mod h1:txXsRQN/G7Fr2cqGfr6zhVHgreCfpsBS+9+DJyrddJc=
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.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand All @@ -34,6 +42,13 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/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.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
Expand All @@ -52,6 +67,9 @@ github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdU
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
Expand Down Expand Up @@ -83,6 +101,12 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
Expand All @@ -92,7 +116,9 @@ golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/i
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
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 31b1170

Please sign in to comment.