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 parentDir(prefix string) string { index := strings.LastIndex(prefix, "/") if index == -1 { return "" } return prefix[index:] } 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, created string) string { res := prefix if prefix != "" { res += "/" } res += filename if size == "" && created == "" { 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, "parentDir": parentDir, }).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 } }