Add support for subdirectory
This commit is contained in:
parent
79eccc5e95
commit
17280f9304
4 changed files with 104 additions and 23 deletions
|
@ -6,32 +6,44 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleBucketView shows the details page of a bucket.
|
// 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) http.HandlerFunc {
|
||||||
type objectWithIcon struct {
|
type objectWithIcon struct {
|
||||||
Info minio.ObjectInfo
|
Key string
|
||||||
|
Size int64
|
||||||
|
LastModified time.Time
|
||||||
|
Owner string
|
||||||
Icon string
|
Icon string
|
||||||
|
IsFolder bool
|
||||||
|
DisplayName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type pageData struct {
|
type pageData struct {
|
||||||
BucketName string
|
BucketName string
|
||||||
Objects []objectWithIcon
|
Objects []objectWithIcon
|
||||||
AllowDelete bool
|
AllowDelete bool
|
||||||
|
Paths []string
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
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
|
var objs []objectWithIcon
|
||||||
doneCh := make(chan struct{})
|
doneCh := make(chan struct{})
|
||||||
defer close(doneCh)
|
defer close(doneCh)
|
||||||
opts := minio.ListObjectsOptions{
|
opts := minio.ListObjectsOptions{
|
||||||
Recursive: listRecursive,
|
Recursive: listRecursive,
|
||||||
|
Prefix: path,
|
||||||
}
|
}
|
||||||
objectCh := s3.ListObjects(r.Context(), bucketName, opts)
|
objectCh := s3.ListObjects(r.Context(), bucketName, opts)
|
||||||
for object := range objectCh {
|
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))
|
handleHTTPError(w, fmt.Errorf("error listing objects: %w", object.Err))
|
||||||
return
|
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)
|
objs = append(objs, obj)
|
||||||
}
|
}
|
||||||
data := pageData{
|
data := pageData{
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
Objects: objs,
|
Objects: objs,
|
||||||
AllowDelete: allowDelete,
|
AllowDelete: allowDelete,
|
||||||
|
Paths: removeEmptyStrings(strings.Split(path, "/")),
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := template.ParseFS(templates, "layout.html.tmpl", "bucket.html.tmpl")
|
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.
|
// icon returns an icon for a file type.
|
||||||
func icon(fileName string) string {
|
func icon(fileName string) string {
|
||||||
|
if strings.HasSuffix(fileName, "/") {
|
||||||
|
return "folder"
|
||||||
|
}
|
||||||
|
|
||||||
e := path.Ext(fileName)
|
e := path.Ext(fileName)
|
||||||
switch e {
|
switch e {
|
||||||
case ".tgz", ".gz", ".zip":
|
case ".tgz", ".gz", ".zip":
|
||||||
|
@ -75,3 +101,13 @@ func icon(fileName string) string {
|
||||||
|
|
||||||
return "insert_drive_file"
|
return "insert_drive_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeEmptyStrings(input []string) []string {
|
||||||
|
var result []string
|
||||||
|
for _, str := range input {
|
||||||
|
if str != "" {
|
||||||
|
result = append(result, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ func TestHandleBucketView(t *testing.T) {
|
||||||
it string
|
it string
|
||||||
listObjectsFunc func(context.Context, string, minio.ListObjectsOptions) <-chan minio.ObjectInfo
|
listObjectsFunc func(context.Context, string, minio.ListObjectsOptions) <-chan minio.ObjectInfo
|
||||||
bucketName string
|
bucketName string
|
||||||
|
path string
|
||||||
expectedStatusCode int
|
expectedStatusCode int
|
||||||
expectedBodyContains string
|
expectedBodyContains string
|
||||||
}{
|
}{
|
||||||
|
@ -123,6 +124,32 @@ func TestHandleBucketView(t *testing.T) {
|
||||||
expectedStatusCode: http.StatusInternalServerError,
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
expectedBodyContains: http.StatusText(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 {
|
for _, tc := range cases {
|
||||||
|
@ -137,12 +164,12 @@ func TestHandleBucketView(t *testing.T) {
|
||||||
|
|
||||||
templates := os.DirFS(filepath.Join("..", "..", "..", "web", "template"))
|
templates := os.DirFS(filepath.Join("..", "..", "..", "web", "template"))
|
||||||
r := mux.NewRouter()
|
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)
|
ts := httptest.NewServer(r)
|
||||||
defer ts.Close()
|
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)
|
is.NoErr(err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
|
2
main.go
2
main.go
|
@ -111,7 +111,7 @@ func main() {
|
||||||
r.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet)
|
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.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", 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)
|
r.Handle("/api/buckets", s3manager.HandleCreateBucket(s3)).Methods(http.MethodPost)
|
||||||
if allowDelete {
|
if allowDelete {
|
||||||
r.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucket(s3)).Methods(http.MethodDelete)
|
r.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucket(s3)).Methods(http.MethodDelete)
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<nav>
|
<style>
|
||||||
|
.breadcrumb:before {
|
||||||
|
content: '/';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<nav class="nav-extended">
|
||||||
<div class="nav-wrapper container">
|
<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 }}
|
{{ if not .Objects }}
|
||||||
<ul class="right">
|
<ul class="right">
|
||||||
<li>
|
<li>
|
||||||
|
@ -12,18 +18,25 @@
|
||||||
</ul>
|
</ul>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</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>
|
</nav>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section" style="margin: 10px;">
|
||||||
<a href="/buckets" style="padding-left:25px;vertical-align:middle;">
|
|
||||||
<i class="material-icons" style="vertical-align: middle;">arrow_back</i> Buckets
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ if .Objects }}
|
{{ if .Objects }}
|
||||||
<table class="striped">
|
<table class="striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:75px;"></th>
|
|
||||||
<th>Key</th>
|
<th>Key</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
<th>Owner</th>
|
<th>Owner</th>
|
||||||
|
@ -35,20 +48,25 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{{ range $index, $object := .Objects }}
|
{{ range $index, $object := .Objects }}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-left:25px;"><i class="material-icons">{{ $object.Icon }}</i></td>
|
<td
|
||||||
<td>{{ $object.Info.Key }}</td>
|
{{ if $object.IsFolder }}
|
||||||
<td>{{ $object.Info.Size }} bytes</td>
|
onclick="location.href='/buckets/{{ $.BucketName }}/{{ $object.Key }}'"}
|
||||||
<td>{{ $object.Info.Owner }}</td>
|
style="cursor:pointer;"
|
||||||
<td>{{ $object.Info.LastModified }}</td>
|
{{ 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>
|
<td>
|
||||||
<button class="dropdown-trigger waves-effect waves-teal btn" data-target="actions-dropdown-{{ $index }}">
|
<button class="dropdown-trigger waves-effect waves-teal btn" data-target="actions-dropdown-{{ $index }}">
|
||||||
Actions <i class="material-icons right">arrow_drop_down</i>
|
Actions <i class="material-icons right">arrow_drop_down</i>
|
||||||
</button>
|
</button>
|
||||||
<!-- Dropdown Structure -->
|
<!-- Dropdown Structure -->
|
||||||
<ul id="actions-dropdown-{{ $index }}" class="dropdown-content">
|
<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 }}
|
{{- 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 }}
|
{{- end }}
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
|
|
Loading…
Add table
Reference in a new issue