160 lines
3.3 KiB
Go
160 lines
3.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"cmp"
|
|
_ "embed"
|
|
"html/template"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/data"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils"
|
|
"github.com/docker/go-units"
|
|
"github.com/valyala/fasthttp"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
const (
|
|
dateFormat = "02-01-2006 15:04"
|
|
attrOID, attrCreated, attrFileName, attrSize = "OID", "Created", "FileName", "Size"
|
|
)
|
|
|
|
type (
|
|
BrowsePageData struct {
|
|
BucketName,
|
|
Prefix string
|
|
Objects []ResponseObject
|
|
}
|
|
ResponseObject struct {
|
|
OID string
|
|
Created string
|
|
FileName string
|
|
Size string
|
|
}
|
|
)
|
|
|
|
//go:embed templates/index.gotmpl
|
|
var defaultTemplate string
|
|
|
|
func parseTimestamp(tstamp string) (time.Time, error) {
|
|
millis, err := strconv.ParseInt(tstamp, 10, 64)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
|
|
return time.UnixMilli(millis), nil
|
|
}
|
|
|
|
func NewResponseObject(nodes map[string]string) ResponseObject {
|
|
return ResponseObject{
|
|
OID: nodes[attrOID],
|
|
Created: nodes[attrCreated],
|
|
FileName: nodes[attrFileName],
|
|
Size: nodes[attrSize],
|
|
}
|
|
}
|
|
|
|
func formatTimestamp(strdate string) string {
|
|
date, err := parseTimestamp(strdate)
|
|
if err != nil || date.IsZero() {
|
|
return ""
|
|
}
|
|
|
|
return date.Format(dateFormat)
|
|
}
|
|
|
|
func formatSize(strsize string) string {
|
|
size, err := strconv.ParseFloat(strsize, 64)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return units.HumanSize(size)
|
|
}
|
|
|
|
func trimPrefix(encPrefix string) string {
|
|
prefix, err := url.PathUnescape(encPrefix)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
slashIndex := strings.LastIndex(encPrefix, "/")
|
|
if slashIndex == -1 {
|
|
return ""
|
|
}
|
|
return prefix[:slashIndex]
|
|
}
|
|
|
|
func urlencode(prefix, filename, size string) string {
|
|
res := prefix
|
|
|
|
if prefix != "" {
|
|
res += "/"
|
|
}
|
|
res += filename
|
|
|
|
if size == "" {
|
|
res += "/"
|
|
}
|
|
|
|
return url.PathEscape(res)
|
|
}
|
|
|
|
func (h *Handler) browseObjects(c *fasthttp.RequestCtx, bucketInfo *data.BucketInfo, prefix string) {
|
|
var log = h.log.With(zap.String("bucket", bucketInfo.Name))
|
|
ctx := utils.GetContextFromRequest(c)
|
|
nodes, err := h.listObjects(ctx, bucketInfo, prefix)
|
|
if err != nil {
|
|
logAndSendBucketError(c, log, err)
|
|
return
|
|
}
|
|
|
|
respObjects := make([]ResponseObject, len(nodes))
|
|
for i, node := range nodes {
|
|
respObjects[i] = NewResponseObject(node)
|
|
}
|
|
|
|
slices.SortFunc(respObjects, func(a, b ResponseObject) int {
|
|
aIsDir := a.Size == ""
|
|
bIsDir := b.Size == ""
|
|
|
|
// return root object first
|
|
if a.FileName == "" {
|
|
return -1
|
|
} else if b.FileName == "" {
|
|
return 1
|
|
}
|
|
|
|
// dirs objects go first
|
|
if aIsDir && !bIsDir {
|
|
return -1
|
|
} else if !aIsDir && bIsDir {
|
|
return 1
|
|
}
|
|
return cmp.Compare(a.FileName, b.FileName)
|
|
})
|
|
|
|
indexTemplate := h.config.IndexPageTemplate()
|
|
if indexTemplate == "" {
|
|
indexTemplate = defaultTemplate
|
|
}
|
|
tmpl, err := template.New("index").Funcs(template.FuncMap{
|
|
"formatTimestamp": formatTimestamp,
|
|
"formatSize": formatSize,
|
|
"trimPrefix": trimPrefix,
|
|
"urlencode": urlencode,
|
|
}).Parse(indexTemplate)
|
|
if err != nil {
|
|
logAndSendBucketError(c, log, err)
|
|
return
|
|
}
|
|
if err = tmpl.Execute(c, &BrowsePageData{
|
|
BucketName: bucketInfo.Name,
|
|
Prefix: prefix,
|
|
Objects: respObjects,
|
|
}); err != nil {
|
|
logAndSendBucketError(c, log, err)
|
|
return
|
|
}
|
|
}
|