Files
uncensored-send/internal/store/meta.go
T
2026-09-12 23:26:07 +02:00

77 lines
2.4 KiB
Go

package store
import (
"strings"
"time"
"unicode/utf8"
)
// Meta is the flat per-object record stored alongside the blob. Its presence on
// disk is what makes an object visible; an object directory without one is
// either mid-upload or crash debris.
type Meta struct {
ID string `json:"id"`
Filename string `json:"filename"`
Size int64 `json:"size"`
SHA256 string `json:"sha256"`
Created time.Time `json:"created"`
Expires *time.Time `json:"expires"` // nil means never
Owner string `json:"owner"` // "" means anonymous
Vanity bool `json:"vanity"`
// DeleteHash is the SHA-256 of the delete token handed to the uploader.
// The token itself is shown once and never stored.
DeleteHash string `json:"delete_hash"`
}
// Expired reports whether the object's lifetime has run out.
func (m *Meta) Expired(now time.Time) bool {
return m.Expires != nil && !now.Before(*m.Expires)
}
const fallbackFilename = "download.bin"
// maxFilenameBytes matches the common filesystem limit; the name is only ever
// metadata here, but keeping it bounded keeps headers and pages sane.
const maxFilenameBytes = 255
// SanitizeFilename reduces a caller-supplied filename to something safe to put
// in a Content-Disposition header and to show on a page.
//
// The result is never used to build a path - paths come from CleanID alone -
// so this guards against header injection and display confusion rather than
// traversal. Separators are stripped regardless, so that a name surviving to
// some future code path cannot carry a directory with it.
func SanitizeFilename(name string) string {
// Take the last element under either separator convention: browsers on
// Windows have historically sent full paths.
if i := strings.LastIndexAny(name, `/\`); i >= 0 {
name = name[i+1:]
}
if !utf8.ValidString(name) {
name = strings.ToValidUTF8(name, "")
}
name = strings.Map(func(r rune) rune {
switch {
case r < 0x20, r == 0x7f: // control characters, CR and LF included
return -1
case r == '/', r == '\\', r == 0:
return -1
}
return r
}, name)
name = strings.TrimSpace(name)
if len(name) > maxFilenameBytes {
name = name[:maxFilenameBytes]
// Do not leave a partial rune at the end.
for len(name) > 0 && !utf8.ValidString(name) {
name = name[:len(name)-1]
}
}
if name == "" || name == "." || name == ".." {
return fallbackFilename
}
return name
}