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 }