diff --git a/internal/api/server.go b/internal/api/server.go index dfe1ac4..5d3de98 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "html/template" + "io" "net/http" "os" "strconv" @@ -443,6 +444,37 @@ func (a *apiServer) handleRaw(w http.ResponseWriter, r *http.Request) { http.Error(w, "not found", 404) return } + // #221: file pastes store the bytes in the blob store, not row.Content. + // Raw on an image paste serves the attachment file itself (inline), the + // same as the paste-view preview link, instead of an empty body. + if row.Content == "" { + att, err := a.store.GetAttachmentForPaste(row.ID) + if err != nil { + http.Error(w, "db error", 500) + return + } + if att == nil { + http.Error(w, "not found", 404) + return + } + blobs := a.store.Blobs() + if blobs == nil { + http.Error(w, "blob storage unavailable", 500) + return + } + blob, err := blobs.Get(att.PasteID + "/" + att.SHA256) + if err != nil { + http.Error(w, "not found", 404) + return + } + defer blob.Close() + w.Header().Set("Content-Type", serveContentType(att.Mime)) + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", att.Filename)) + a.store.IncrementViews(row.ID, "", 0) + io.Copy(w, blob) + return + } // #49 decision: raw reads count against the read budget too, with the // same per-viewer dedupe window as page views. #58: a reader that loses // the burn claim must not receive the content. diff --git a/internal/web/static/app.css b/internal/web/static/app.css index c96034b..98f90c7 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -216,7 +216,8 @@ body { .tag { font-size: 19.8px; color: var(--muted-fg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 2px 9px; } .paste-title-bar { display: flex; align-items: center; gap: 12px; padding: 12px 18px; flex-wrap: wrap; } .paste-title-bar h1 { font-size: 29.2px; font-weight: 600; margin: 0; word-break: normal; overflow-wrap: anywhere; } -.stats-pill { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } +/* #222: overflow:hidden clips the element's own 1px border thin at the radius; inset shadow isn't clipped */ +.stats-pill { border: 0; box-shadow: inset 0 0 0 1px var(--border); border-radius: var(--radius); overflow: hidden; } .stats-head { display: flex; align-items: center; gap: 16px; width: 100%; background: none; border: 0; border-bottom: 1px solid var(--border); color: var(--muted-fg); font: inherit; font-size: 21.6px; padding: 14px 18px; cursor: pointer; text-align: left; } .stats-head:hover { color: var(--fg); background: var(--surface-2); } .stats-chev { width: 18px; height: 18px; flex: none; transition: transform 0.15s ease; } @@ -819,6 +820,17 @@ button[type="submit"]:focus-visible, .attachment-chip:hover { border-color: var(--accent); } .attachment-chip .attachment-size { color: var(--muted-fg); font-size: 19px; } .attachment-preview img { max-width: 480px; max-height: 360px; border-radius: var(--radius); border: 1px solid var(--border); } +/* #221: image pastes scale to fit the available box; no fixed pixel cap */ +.attachment-preview.is-image img { + display: block; + max-width: 100%; + max-height: min(70vh, 720px); + width: auto; + height: auto; + object-fit: contain; + border-radius: var(--radius); + border: 1px solid var(--border); +} /* #139: CSP-safe replacements for inline style attributes (style-src 'self') */ .hidden { display: none; } diff --git a/internal/web/templates/paste.html b/internal/web/templates/paste.html index b907ded..212b35f 100644 --- a/internal/web/templates/paste.html +++ b/internal/web/templates/paste.html @@ -6,9 +6,9 @@