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"
|
||||
"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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
2
main.go
2
main.go
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue