package handler import ( "cmp" _ "embed" "html/template" "net/url" "strconv" "strings" "time" "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(prefix string) string { slashIndex := strings.LastIndex(prefix, "/") if slashIndex == -1 { return "" } return prefix[:slashIndex] } func urlencode(prefix string) string { escaped := url.QueryEscape(prefix) replaced := strings.ReplaceAll(escaped, ".", "%2E") return replaced } func (h *Handler) browseObjects(c *fasthttp.RequestCtx, bucketName, prefix string) { var log = h.log.With(zap.String("bucket", bucketName)) ctx := utils.GetContextFromRequest(c) nodes, err := h.listObjects(ctx, bucketName, 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 == "" // prefix 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: bucketName, Prefix: prefix, Objects: respObjects, }); err != nil { logAndSendBucketError(c, log, err) return } }