mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-30 15:07:56 +01:00
80 lines
1.9 KiB
Go
80 lines
1.9 KiB
Go
|
|
package str
|
||
|
|
|
||
|
|
import (
|
||
|
|
"cmp"
|
||
|
|
"fmt"
|
||
|
|
"mime"
|
||
|
|
"path"
|
||
|
|
"strings"
|
||
|
|
"unicode"
|
||
|
|
"unicode/utf8"
|
||
|
|
|
||
|
|
"github.com/deluan/sanitize"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
maxFilenameBytes = 255
|
||
|
|
maxExtensionBytes = 16
|
||
|
|
fallbackFilename = "download"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ContentDispositionAttachment builds an RFC 6266 attachment header value for a user-controlled filename.
|
||
|
|
// Non-ASCII names also get a filename* parameter, which clients prefer over the ASCII fallback.
|
||
|
|
func ContentDispositionAttachment(filename string) string {
|
||
|
|
stem, ext := splitFilename(filename)
|
||
|
|
header := fmt.Sprintf("attachment; filename=%q", joinFilename(toASCII(stem), toASCII(ext)))
|
||
|
|
name := joinFilename(stem, ext)
|
||
|
|
if isASCII(name) {
|
||
|
|
return header
|
||
|
|
}
|
||
|
|
// FormatMediaType emits a percent-encoded filename* for non-ASCII values
|
||
|
|
extended := mime.FormatMediaType("attachment", map[string]string{"filename": name})
|
||
|
|
return header + strings.TrimPrefix(extended, "attachment")
|
||
|
|
}
|
||
|
|
|
||
|
|
func splitFilename(filename string) (stem, ext string) {
|
||
|
|
name := strings.Map(func(r rune) rune {
|
||
|
|
if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) {
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
return r
|
||
|
|
}, SanitizeFilename(strings.ToValidUTF8(filename, "_")))
|
||
|
|
ext = path.Ext(name)
|
||
|
|
if len(ext) > maxExtensionBytes {
|
||
|
|
ext = ""
|
||
|
|
}
|
||
|
|
stem = strings.TrimSuffix(name, ext)
|
||
|
|
if limit := maxFilenameBytes - len(ext); len(stem) > limit {
|
||
|
|
for limit > 0 && !utf8.RuneStart(stem[limit]) {
|
||
|
|
limit--
|
||
|
|
}
|
||
|
|
stem = stem[:limit]
|
||
|
|
}
|
||
|
|
return stem, ext
|
||
|
|
}
|
||
|
|
|
||
|
|
func joinFilename(stem, ext string) string {
|
||
|
|
stem = cmp.Or(strings.TrimSpace(stem), fallbackFilename)
|
||
|
|
return cmp.Or(strings.TrimRight(stem+ext, " ."), fallbackFilename)
|
||
|
|
}
|
||
|
|
|
||
|
|
func toASCII(s string) string {
|
||
|
|
return strings.Map(func(r rune) rune {
|
||
|
|
switch {
|
||
|
|
case r == ',':
|
||
|
|
return '_'
|
||
|
|
case r >= ' ' && r <= '~':
|
||
|
|
return r
|
||
|
|
}
|
||
|
|
return -1
|
||
|
|
}, sanitize.Accents(SanitizeFilename(Clear(s))))
|
||
|
|
}
|
||
|
|
|
||
|
|
func isASCII(s string) bool {
|
||
|
|
for i := 0; i < len(s); i++ {
|
||
|
|
if s[i] >= utf8.RuneSelf {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|