diff --git a/sia-share/main.go b/sia-share/main.go
index 73fdf85..b28011e 100644
--- a/sia-share/main.go
+++ b/sia-share/main.go
@@ -427,9 +427,27 @@ func (h *handler) get(w http.ResponseWriter, r *http.Request) {
notFound(w)
return
}
+ if wantsPage(r) {
+ filePage(w, l, l.Files[0])
+ return
+ }
h.stream(w, r, l, l.Files[0])
}
+// A person opening a single-file link in a browser gets a page with the
+// file and where it came from; players, tags, download tools and the
+// page's own preview (?raw) get the file itself.
+func wantsPage(r *http.Request) bool {
+ q := r.URL.Query()
+ if r.Method != http.MethodGet || q.Has("raw") || q.Has("dl") {
+ return false
+ }
+ if d := r.Header.Get("Sec-Fetch-Dest"); d != "" {
+ return d == "document"
+ }
+ return strings.Contains(r.Header.Get("Accept"), "text/html")
+}
+
func notFound(w http.ResponseWriter) {
w.Header().Set("content-type", "text/html; charset=utf-8")
w.WriteHeader(404)
@@ -503,7 +521,11 @@ func (h *handler) stream(w http.ResponseWriter, r *http.Request, l *link, f file
hd.Set("content-type", ct)
hd.Set("accept-ranges", "bytes")
hd.Set("content-length", strconv.FormatInt(length, 10))
- hd.Set("content-disposition", mime.FormatMediaType("inline", map[string]string{"filename": f.Name}))
+ disp := "inline"
+ if r.URL.Query().Has("dl") {
+ disp = "attachment"
+ }
+ hd.Set("content-disposition", mime.FormatMediaType(disp, map[string]string{"filename": f.Name}))
hd.Set("content-security-policy", "sandbox")
hd.Set("x-content-type-options", "nosniff")
hd.Set("cache-control", "public, max-age=300")
@@ -543,6 +565,52 @@ func fmtSize(n int64) string {
return fmt.Sprintf("%.1f %s", f, units[i])
}
+// Where a visitor goes from a shared link: the Pithos site, the Pithos app
+// and Theseus Navigator (which opens pithos.sia itself).
+const pageLinks = ``
+
+const pageStyle = `body{margin:0;background:#0b100e;color:#e6ece9;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:860px;margin:0 auto;padding:28px 18px 120px}
+a{color:#3ddc97;text-decoration:none}a:hover{text-decoration:underline}.owner{color:#8e9c96;font-size:13px}h1{font-size:20px;margin:4px 0 18px;font-weight:600;overflow-wrap:anywhere}
+footer{color:#5f6b66;font-size:12.5px;margin-top:22px;padding-top:14px;border-top:1px solid rgba(255,255,255,.06)}.more{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:8px;font-size:14px}`
+
+func mediaKind(n string) string {
+ switch strings.ToLower(path.Ext(n)) {
+ case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif":
+ return "img"
+ case ".mp3", ".m4a", ".aac", ".ogg", ".oga", ".opus", ".flac", ".wav":
+ return "audio"
+ case ".mp4", ".webm", ".mov", ".m4v":
+ return "video"
+ }
+ return ""
+}
+
+func filePage(w http.ResponseWriter, l *link, f file) {
+ preview := ""
+ switch mediaKind(f.Name) {
+ case "img":
+ preview = `
`
+ case "audio":
+ preview = ``
+ case "video":
+ preview = ``
+ }
+ w.Header().Set("content-type", "text/html; charset=utf-8")
+ w.Header().Set("content-security-policy", "default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; media-src 'self'; frame-ancestors 'none'")
+ w.Header().Set("x-content-type-options", "nosniff")
+ w.Header().Set("cache-control", "no-store")
+ name := html.EscapeString(f.Name)
+ fmt.Fprintf(w, `