Add support for subdirectory

This commit is contained in:
Tri Vo 2023-06-13 22:37:57 +07:00 committed by Lena Fuhrimann
parent 79eccc5e95
commit 17280f9304
4 changed files with 104 additions and 23 deletions

View file

@ -6,32 +6,44 @@ import (
"io/fs"
"net/http"
"path"
"regexp"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/minio/minio-go/v7"
)
// HandleBucketView shows the details page of a bucket.
func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bool) http.HandlerFunc {
type objectWithIcon struct {
Info minio.ObjectInfo
Key string
Size int64
LastModified time.Time
Owner string
Icon string
IsFolder bool
DisplayName string
}
type pageData struct {
BucketName string
Objects []objectWithIcon
AllowDelete bool
Paths []string
}
return func(w http.ResponseWriter, r *http.Request) {
bucketName := mux.Vars(r)["bucketName"]
regex := regexp.MustCompile(`\/buckets\/([^\/]*)\/?(.*)`)
matches := regex.FindStringSubmatch(r.RequestURI)
bucketName := matches[1]
path := matches[2]
var objs []objectWithIcon
doneCh := make(chan struct{})
defer close(doneCh)
opts := minio.ListObjectsOptions{
Recursive: listRecursive,
Prefix: path,
}
objectCh := s3.ListObjects(r.Context(), bucketName, opts)
for object := range objectCh {
@ -39,13 +51,23 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo
handleHTTPError(w, fmt.Errorf("error listing objects: %w", object.Err))
return
}
obj := objectWithIcon{Info: object, Icon: icon(object.Key)}
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), "/"),
}
objs = append(objs, obj)
}
data := pageData{
BucketName: bucketName,
Objects: objs,
AllowDelete: allowDelete,
Paths: removeEmptyStrings(strings.Split(path, "/")),
}
t, err := template.ParseFS(templates, "layout.html.tmpl", "bucket.html.tmpl")
@ -63,6 +85,10 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo
// icon returns an icon for a file type.
func icon(fileName string) string {
if strings.HasSuffix(fileName, "/") {
return "folder"
}
e := path.Ext(fileName)
switch e {
case ".tgz", ".gz", ".zip":
@ -75,3 +101,13 @@ func icon(fileName string) string {
return "insert_drive_file"
}
func removeEmptyStrings(input []string) []string {
var result []string
for _, str := range input {
if str != "" {
result = append(result, str)
}
}
return result
}

View file

@ -25,6 +25,7 @@ func TestHandleBucketView(t *testing.T) {
it string
listObjectsFunc func(context.Context, string, minio.ListObjectsOptions) <-chan minio.ObjectInfo
bucketName string
path string
expectedStatusCode int
expectedBodyContains string
}{
@ -123,6 +124,32 @@ func TestHandleBucketView(t *testing.T) {
expectedStatusCode: http.StatusInternalServerError,
expectedBodyContains: http.StatusText(http.StatusInternalServerError),
},
{
it: "renders a bucket with folder",
listObjectsFunc: func(context.Context, string, minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objCh := make(chan minio.ObjectInfo)
go func() {
objCh <- minio.ObjectInfo{Key: "AFolder/"}
close(objCh)
}()
return objCh
},
bucketName: "BUCKET-NAME",
expectedStatusCode: http.StatusOK,
expectedBodyContains: "folder",
},
{
it: "renders a bucket with path",
listObjectsFunc: func(context.Context, string, minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objCh := make(chan minio.ObjectInfo)
close(objCh)
return objCh
},
bucketName: "BUCKET-NAME",
path: "abc/def",
expectedStatusCode: http.StatusOK,
expectedBodyContains: "def",
},
}
for _, tc := range cases {
@ -137,12 +164,12 @@ func TestHandleBucketView(t *testing.T) {
templates := os.DirFS(filepath.Join("..", "..", "..", "web", "template"))
r := mux.NewRouter()
r.Handle("/buckets/{bucketName}", s3manager.HandleBucketView(s3, templates, true, true)).Methods(http.MethodGet)
r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, true, true)).Methods(http.MethodGet)
ts := httptest.NewServer(r)
defer ts.Close()
resp, err := http.Get(fmt.Sprintf("%s/buckets/%s", ts.URL, tc.bucketName))
resp, err := http.Get(fmt.Sprintf("%s/buckets/%s/%s", ts.URL, tc.bucketName, tc.path))
is.NoErr(err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)

View file

@ -111,7 +111,7 @@ func main() {
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, allowDelete)).Methods(http.MethodGet)
r.Handle("/buckets/{bucketName}", s3manager.HandleBucketView(s3, templates, allowDelete, listRecursive)).Methods(http.MethodGet)
r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, allowDelete, listRecursive)).Methods(http.MethodGet)
r.Handle("/api/buckets", s3manager.HandleCreateBucket(s3)).Methods(http.MethodPost)
if allowDelete {
r.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucket(s3)).Methods(http.MethodDelete)

View file

@ -1,7 +1,13 @@
{{ define "content" }}
<nav>
<style>
.breadcrumb:before {
content: '/';
}
</style>
<nav class="nav-extended">
<div class="nav-wrapper container">
<span href="#" class="brand-logo"><i class="material-icons">folder_open</i>{{ .BucketName }}</span>
<a href="/buckets/{{$.BucketName}}" class="brand-logo center"><i class="material-icons">folder_open</i>{{ .BucketName }}</a>
{{ if not .Objects }}
<ul class="right">
<li>
@ -12,18 +18,25 @@
</ul>
{{ end }}
</div>
<div class="nav-wrapper container">
<a href="/buckets" class="breadcrumb"><i class="material-icons">arrow_back</i> buckets </a>
{{ $url := printf "/buckets/%s/" $.BucketName }}
<a href="{{ $url }}" class="breadcrumb">{{ $.BucketName }}</a>
{{ range $index, $path := .Paths }}
{{ $url = printf "%s%s/" $url $path }}
<a href="{{ $url }}" class="breadcrumb">{{ $path }}</a>
{{ end }}
</div>
</div>
</nav>
<div class="section">
<a href="/buckets" style="padding-left:25px;vertical-align:middle;">
<i class="material-icons" style="vertical-align: middle;">arrow_back</i> Buckets
</a>
<div class="section" style="margin: 10px;">
{{ if .Objects }}
<table class="striped">
<thead>
<tr>
<th style="width:75px;"></th>
<th>Key</th>
<th>Size</th>
<th>Owner</th>
@ -35,20 +48,25 @@
<tbody>
{{ range $index, $object := .Objects }}
<tr>
<td style="padding-left:25px;"><i class="material-icons">{{ $object.Icon }}</i></td>
<td>{{ $object.Info.Key }}</td>
<td>{{ $object.Info.Size }} bytes</td>
<td>{{ $object.Info.Owner }}</td>
<td>{{ $object.Info.LastModified }}</td>
<td
{{ if $object.IsFolder }}
onclick="location.href='/buckets/{{ $.BucketName }}/{{ $object.Key }}'"}
style="cursor:pointer;"
{{ end }}>
<i class="material-icons">{{ $object.Icon }}</i> {{ $object.DisplayName }}
</td>
<td>{{ $object.Size }} bytes</td>
<td>{{ $object.Owner }}</td>
<td>{{ $object.LastModified }}</td>
<td>
<button class="dropdown-trigger waves-effect waves-teal btn" data-target="actions-dropdown-{{ $index }}">
Actions <i class="material-icons right">arrow_drop_down</i>
</button>
<!-- Dropdown Structure -->
<ul id="actions-dropdown-{{ $index }}" class="dropdown-content">
<li><a target="_blank" href="/api/buckets/{{ $.BucketName }}/objects/{{ $object.Info.Key }}">Download</a></li>
<li><a target="_blank" href="/api/buckets/{{ $.BucketName }}/objects/{{ $object.Key }}">Download</a></li>
{{- if $.AllowDelete }}
<li><a href="#" onclick="deleteObject({{ $.BucketName }}, {{ $object.Info.Key }})">Delete</a></li>
<li><a href="#" onclick="deleteObject({{ $.BucketName }}, {{ $object.Key }})">Delete</a></li>
{{- end }}
</ul>
</td>