Skip to content

Commit bc3e2d8

Browse files
authored
Merge pull request #29 from johnwmail/feature/grokcodefast1
2 parents 756caf6 + 939313b commit bc3e2d8

File tree

5 files changed

+618
-10
lines changed

5 files changed

+618
-10
lines changed

handlers/retrieval/handler.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package retrieval
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"net/url"
8+
"strings"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/johnwmail/nclip/config"
12+
"github.com/johnwmail/nclip/internal/services"
13+
"github.com/johnwmail/nclip/storage"
14+
"github.com/johnwmail/nclip/utils"
15+
)
16+
17+
// Handler handles paste retrieval operations
18+
type Handler struct {
19+
service *services.PasteService
20+
store storage.PasteStore
21+
config *config.Config
22+
}
23+
24+
// NewHandler creates a new retrieval handler
25+
func NewHandler(service *services.PasteService, store storage.PasteStore, config *config.Config) *Handler {
26+
return &Handler{
27+
service: service,
28+
store: store,
29+
config: config,
30+
}
31+
}
32+
33+
// isHTTPS detects if the request is over HTTPS
34+
func (h *Handler) isHTTPS(c *gin.Context) bool {
35+
// Check X-Forwarded-Proto header (common with load balancers/proxies)
36+
if c.GetHeader("X-Forwarded-Proto") == "https" {
37+
return true
38+
}
39+
40+
// AWS Lambda Function URLs may use different headers
41+
if c.GetHeader("CloudFront-Forwarded-Proto") == "https" {
42+
return true
43+
}
44+
45+
// Check if the original URL scheme can be detected from request URL
46+
if strings.HasPrefix(c.Request.Header.Get("Referer"), "https://") {
47+
return true
48+
}
49+
50+
return false
51+
}
52+
53+
// isCli detects if the request is from CLI (curl, wget, Invoke-WebRequest, Invoke-RestMethod, etc.)
54+
func (h *Handler) isCli(c *gin.Context) bool {
55+
userAgent := strings.ToLower(c.Request.Header.Get("User-Agent"))
56+
if strings.Contains(userAgent, "curl") ||
57+
strings.Contains(userAgent, "wget") ||
58+
strings.Contains(userAgent, "powershell") {
59+
return true
60+
}
61+
return false
62+
}
63+
64+
// View handles paste viewing via GET /:slug
65+
func (h *Handler) View(c *gin.Context) {
66+
slug := c.Param("slug")
67+
68+
if !utils.IsValidSlug(slug) {
69+
c.HTML(http.StatusBadRequest, "view.html", gin.H{
70+
"Title": "NCLIP - Error",
71+
"Error": "Invalid slug format",
72+
"Version": h.config.Version,
73+
"BuildTime": h.config.BuildTime,
74+
"CommitHash": h.config.CommitHash,
75+
"BaseURL": h.getBaseURL(c),
76+
})
77+
return
78+
}
79+
80+
paste, err := h.service.GetPaste(slug)
81+
if err != nil {
82+
log.Printf("[ERROR] View: %v", err)
83+
c.HTML(http.StatusNotFound, "view.html", gin.H{
84+
"Title": "NCLIP - Not Found",
85+
"Error": "Paste not found or deleted",
86+
"Version": h.config.Version,
87+
"BuildTime": h.config.BuildTime,
88+
"CommitHash": h.config.CommitHash,
89+
"BaseURL": h.getBaseURL(c),
90+
})
91+
return
92+
}
93+
94+
// Increment read count
95+
if err := h.service.IncrementReadCount(slug); err != nil {
96+
// Log error but don't fail the request
97+
fmt.Printf("Failed to increment read count for %s: %v\n", slug, err)
98+
}
99+
100+
content, err := h.service.GetPasteContent(slug)
101+
if err != nil {
102+
log.Printf("[ERROR] View: content not found or deleted for slug %s: %v", slug, err)
103+
c.HTML(http.StatusNotFound, "view.html", gin.H{
104+
"Title": "NCLIP - Not Found",
105+
"Error": "Paste content not found or deleted",
106+
"Version": h.config.Version,
107+
"BuildTime": h.config.BuildTime,
108+
"CommitHash": h.config.CommitHash,
109+
"BaseURL": h.getBaseURL(c),
110+
})
111+
return
112+
}
113+
114+
// If burn-after-read, delete and return 404 if accessed again
115+
if paste.BurnAfterRead {
116+
if err := h.service.DeletePaste(slug); err != nil {
117+
fmt.Printf("Failed to delete burn-after-read paste %s: %v\n", slug, err)
118+
}
119+
}
120+
if h.isCli(c) {
121+
c.Header("Content-Type", paste.ContentType)
122+
c.Header("Content-Length", fmt.Sprintf("%d", paste.Size))
123+
c.Data(http.StatusOK, paste.ContentType, content)
124+
return
125+
}
126+
if strings.Contains(c.Request.Header.Get("Accept"), "text/html") {
127+
c.HTML(http.StatusOK, "view.html", gin.H{
128+
"Title": fmt.Sprintf("NCLIP - Paste %s", paste.ID),
129+
"Paste": paste,
130+
"IsText": utils.IsTextContent(paste.ContentType),
131+
"Content": string(content),
132+
"Version": h.config.Version,
133+
"BuildTime": h.config.BuildTime,
134+
"CommitHash": h.config.CommitHash,
135+
"BaseURL": h.getBaseURL(c),
136+
})
137+
return
138+
}
139+
c.JSON(http.StatusOK, gin.H{
140+
"id": paste.ID,
141+
"created_at": paste.CreatedAt,
142+
"expires_at": paste.ExpiresAt,
143+
"size": paste.Size,
144+
"content_type": paste.ContentType,
145+
"burn_after_read": paste.BurnAfterRead,
146+
"content": string(content),
147+
})
148+
}
149+
150+
// Raw handles raw content download via GET /raw/:slug
151+
func (h *Handler) Raw(c *gin.Context) {
152+
slug := c.Param("slug")
153+
154+
paste, err := h.service.GetPaste(slug)
155+
if err != nil {
156+
log.Printf("[ERROR] Raw: %v", err)
157+
c.JSON(http.StatusNotFound, gin.H{"error": "Paste not found or deleted"})
158+
return
159+
}
160+
161+
// Increment read count
162+
if err := h.service.IncrementReadCount(slug); err != nil {
163+
// Log error but don't fail the request
164+
fmt.Printf("Failed to increment read count for %s: %v\n", slug, err)
165+
}
166+
167+
content, err := h.service.GetPasteContent(slug)
168+
if err != nil {
169+
log.Printf("[ERROR] Raw: content not found or deleted for slug %s: %v", slug, err)
170+
c.JSON(http.StatusNotFound, gin.H{"error": "Paste content not found or deleted"})
171+
return
172+
}
173+
174+
// If burn-after-read, delete and return 404 if accessed again
175+
if paste.BurnAfterRead {
176+
if err := h.service.DeletePaste(slug); err != nil {
177+
fmt.Printf("Failed to delete burn-after-read paste %s: %v\n", slug, err)
178+
}
179+
// After deletion, return 404 and no content
180+
c.JSON(http.StatusNotFound, gin.H{"error": "Paste not found or deleted (burn-after-read)"})
181+
return
182+
}
183+
c.Header("Content-Type", paste.ContentType)
184+
c.Header("Content-Length", fmt.Sprintf("%d", paste.Size))
185+
ext := utils.ExtensionByMime(paste.ContentType)
186+
filename := slug
187+
if ext != "" {
188+
filename = slug + ext
189+
}
190+
escaped := url.PathEscape(filename)
191+
if utils.IsTextContent(paste.ContentType) {
192+
c.Header("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"; filename*=UTF-8''%s", filename, escaped))
193+
} else {
194+
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", filename, escaped))
195+
}
196+
c.Data(http.StatusOK, paste.ContentType, content)
197+
}
198+
199+
// getBaseURL returns the base URL for the application
200+
func (h *Handler) getBaseURL(c *gin.Context) string {
201+
scheme := "http"
202+
if h.isHTTPS(c) {
203+
scheme = "https"
204+
}
205+
return fmt.Sprintf("%s://%s", scheme, c.Request.Host)
206+
}

0 commit comments

Comments
 (0)