Skip to content

Commit 62621d5

Browse files
author
novnc
committed
refactor: improve slug generation and error handling in upload process
1 parent 497970b commit 62621d5

File tree

2 files changed

+104
-106
lines changed

2 files changed

+104
-106
lines changed

handlers/paste.go

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,50 @@ import (
1616
"github.com/johnwmail/nclip/utils"
1717
)
1818

19+
// Helper: generateUniqueSlug tries to generate a unique slug using batch logic
20+
func (h *PasteHandler) generateUniqueSlug() (string, error) {
21+
batchSize := 5
22+
lengths := []int{5, 6, 7}
23+
var slug string
24+
for _, length := range lengths {
25+
candidates, err := utils.GenerateSlugBatch(batchSize, length)
26+
if err != nil {
27+
return "", fmt.Errorf("failed to generate slug")
28+
}
29+
for _, candidate := range candidates {
30+
existing, err := h.store.Get(candidate)
31+
if err != nil || existing == nil || existing.IsExpired() {
32+
slug = candidate
33+
return slug, nil
34+
}
35+
}
36+
}
37+
return "", fmt.Errorf("failed to generate unique slug after 3 batches")
38+
}
39+
40+
// Helper: parseBurnTTL parses TTL for burn-after-read
41+
func (h *PasteHandler) parseBurnTTL(c *gin.Context) (time.Time, error) {
42+
ttlStr := c.GetHeader("X-TTL")
43+
if ttlStr != "" {
44+
d, err := time.ParseDuration(ttlStr)
45+
minTTL := time.Hour
46+
maxTTL := 7 * 24 * time.Hour
47+
if utils.IsDebugEnabled() {
48+
log.Printf("[DEBUG] Parsed X-TTL duration: %v (raw: %s)", d, ttlStr)
49+
}
50+
if err != nil || d < minTTL || d > maxTTL {
51+
return time.Time{}, fmt.Errorf("X-TTL must be between 1h and 7d")
52+
}
53+
return time.Now().Add(d), nil
54+
}
55+
return time.Now().Add(h.config.DefaultTTL), nil
56+
}
57+
58+
// Helper: respondError sends a JSON error response
59+
func respondError(c *gin.Context, status int, msg string) {
60+
c.JSON(status, gin.H{"error": msg})
61+
}
62+
1963
// PasteHandler handles paste-related operations
2064
type PasteHandler struct {
2165
store storage.PasteStore
@@ -279,94 +323,51 @@ func (h *PasteHandler) UploadBurn(c *gin.Context) {
279323
var filename string
280324
var err error
281325

282-
// Check if it's a multipart form (file upload)
283-
if c.Request.Header.Get("Content-Type") != "" &&
284-
strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
285-
326+
// Extract content
327+
if c.Request.Header.Get("Content-Type") != "" && strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
286328
file, header, err := c.Request.FormFile("file")
287329
if err != nil {
288-
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
330+
respondError(c, http.StatusBadRequest, "No file provided")
289331
return
290332
}
291-
defer func() { _ = file.Close() }() // Ignore close errors in defer
292-
333+
defer func() { _ = file.Close() }()
293334
filename = header.Filename
294335
content, err = io.ReadAll(io.LimitReader(file, h.config.BufferSize))
295336
if err != nil {
296-
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file"})
337+
respondError(c, http.StatusInternalServerError, "Failed to read file")
297338
return
298339
}
299340
} else {
300-
// Raw content upload
301341
content, err = io.ReadAll(io.LimitReader(c.Request.Body, h.config.BufferSize))
302342
if err != nil {
303-
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read content"})
343+
respondError(c, http.StatusInternalServerError, "Failed to read content")
304344
return
305345
}
306346
}
307-
308347
if len(content) == 0 {
309-
c.JSON(http.StatusBadRequest, gin.H{"error": "Empty content"})
348+
respondError(c, http.StatusBadRequest, "Empty content")
310349
return
311350
}
312351

313-
// Generate unique slug using batch logic (lengths: 5, 6, 7)
314-
batchSize := 5
315-
lengths := []int{5, 6, 7}
316-
var slug string
317-
var lastCandidates []string
318-
var lastCollisions []string
319-
found := false
320-
for _, length := range lengths {
321-
candidates, err := utils.GenerateSlugBatch(batchSize, length)
322-
if err != nil {
323-
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate slug"})
324-
return
325-
}
326-
lastCandidates = candidates
327-
lastCollisions = nil
328-
for _, candidate := range candidates {
329-
existing, err := h.store.Get(candidate)
330-
if err != nil || existing == nil || existing.IsExpired() {
331-
slug = candidate
332-
found = true
333-
break
334-
} else {
335-
lastCollisions = append(lastCollisions, candidate)
336-
}
337-
}
338-
if found {
339-
break
340-
}
341-
}
342-
if !found {
343-
log.Printf("[ERROR] Could not generate unique slug after 3 batches. Last candidates: %v. Collisions: %v", lastCandidates, lastCollisions)
344-
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate unique slug after 3 batches"})
352+
// Generate unique slug
353+
slug, err := h.generateUniqueSlug()
354+
if err != nil {
355+
log.Printf("[ERROR] %v", err)
356+
respondError(c, http.StatusInternalServerError, err.Error())
345357
return
346358
}
347359

348360
// Detect content type
349361
contentType := utils.DetectContentType(filename, content)
350362

351-
// Support custom TTL via X-TTL header
352-
ttlStr := c.GetHeader("X-TTL")
353-
var expiresAt time.Time
354-
if ttlStr != "" {
355-
d, err := time.ParseDuration(ttlStr)
356-
minTTL := time.Hour
357-
maxTTL := 7 * 24 * time.Hour
358-
if utils.IsDebugEnabled() {
359-
log.Printf("[DEBUG] Parsed X-TTL duration: %v (raw: %s)", d, ttlStr)
360-
}
361-
if err != nil || d < minTTL || d > maxTTL {
362-
log.Printf("[ERROR] X-TTL out of range or invalid: %v (raw: %s)", d, ttlStr)
363-
c.JSON(http.StatusBadRequest, gin.H{"error": "X-TTL must be between 1h and 7d"})
364-
return
365-
}
366-
expiresAt = time.Now().Add(d)
367-
} else {
368-
expiresAt = time.Now().Add(h.config.DefaultTTL)
363+
// Parse TTL
364+
expiresAt, err := h.parseBurnTTL(c)
365+
if err != nil {
366+
log.Printf("[ERROR] %v", err)
367+
respondError(c, http.StatusBadRequest, err.Error())
368+
return
369369
}
370+
370371
paste := &models.Paste{
371372
ID: slug,
372373
CreatedAt: time.Now(),
@@ -379,20 +380,19 @@ func (h *PasteHandler) UploadBurn(c *gin.Context) {
379380

380381
// Store content and metadata
381382
if err := h.store.StoreContent(slug, content); err != nil {
382-
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store content"})
383+
respondError(c, http.StatusInternalServerError, "Failed to store content")
383384
return
384385
}
385386
if err := h.store.Store(paste); err != nil {
386-
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store metadata"})
387+
respondError(c, http.StatusInternalServerError, "Failed to store metadata")
387388
return
388389
}
389390

390391
// Generate URL
391392
pasteURL := h.generatePasteURL(c, slug)
392393

393394
// Return URL as plain text for cli tools compatibility
394-
if h.isCli(c) ||
395-
c.Request.Header.Get("Accept") == "text/plain" {
395+
if h.isCli(c) || c.Request.Header.Get("Accept") == "text/plain" {
396396
c.String(http.StatusOK, pasteURL+"\n")
397397
return
398398
}

utils/mime.go

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ import (
77
"strings"
88
)
99

10+
// extensionMap holds common mime type to extension mappings
11+
var extensionMap = map[string]string{
12+
"application/zip": ".zip",
13+
"application/x-zip-compressed": ".zip",
14+
"application/x-zip": ".zip",
15+
"application/x-tar": ".tar",
16+
"application/tar": ".tar",
17+
"application/x-gzip": ".gz",
18+
"application/gzip": ".gz",
19+
"application/x-7z-compressed": ".7z",
20+
"application/7z": ".7z",
21+
"application/x-bzip2": ".bz2",
22+
"application/bzip2": ".bz2",
23+
"application/x-xz": ".xz",
24+
"application/xz": ".xz",
25+
"application/x-rar-compressed": ".rar",
26+
"application/vnd.rar": ".rar",
27+
"application/rar": ".rar",
28+
"application/pdf": ".pdf",
29+
"image/jpeg": ".jpg",
30+
"image/png": ".png",
31+
"image/gif": ".gif",
32+
"image/webp": ".webp",
33+
"image/svg+xml": ".svg",
34+
"application/octet-stream": ".bin",
35+
"application/x-binary": ".bin",
36+
"application/bin": ".bin",
37+
}
38+
39+
func extensionByMimeMap(mimeType string) string {
40+
ext, ok := extensionMap[strings.ToLower(mimeType)]
41+
if ok {
42+
return ext
43+
}
44+
return ""
45+
}
46+
1047
// DetectContentType attempts to detect the MIME type of content
1148
// It first tries to detect from the filename, then from the content itself
1249
func DetectContentType(filename string, content []byte) string {
@@ -53,50 +90,11 @@ func ExtensionByMime(mimeType string) string {
5390
if mimeType == "" {
5491
return ""
5592
}
56-
5793
if base, _, err := mime.ParseMediaType(mimeType); err == nil {
5894
mimeType = base
5995
}
60-
6196
if exts, _ := mime.ExtensionsByType(mimeType); len(exts) > 0 && exts[0] != "" {
6297
return exts[0]
6398
}
64-
65-
switch strings.ToLower(mimeType) {
66-
case "application/zip", "application/x-zip-compressed", "application/x-zip":
67-
return ".zip"
68-
case "application/x-tar", "application/tar":
69-
return ".tar"
70-
case "application/x-gzip", "application/gzip":
71-
return ".gz"
72-
case "application/x-7z-compressed", "application/7z":
73-
return ".7z"
74-
case "application/x-bzip2", "application/bzip2":
75-
return ".bz2"
76-
case "application/x-xz", "application/xz":
77-
return ".xz"
78-
case "application/x-rar-compressed", "application/vnd.rar", "application/rar":
79-
return ".rar"
80-
case "application/pdf":
81-
return ".pdf"
82-
case "image/jpeg":
83-
return ".jpg"
84-
case "image/png":
85-
return ".png"
86-
case "image/gif":
87-
return ".gif"
88-
case "image/webp":
89-
return ".webp"
90-
case "image/svg+xml":
91-
return ".svg"
92-
case "application/octet-stream", "application/x-binary", "application/bin":
93-
return ".bin"
94-
}
95-
96-
// fallback: only treat as binary for true binary types
97-
lower := strings.ToLower(mimeType)
98-
if lower == "application/octet-stream" || lower == "application/x-binary" || lower == "application/bin" {
99-
return ".bin"
100-
}
101-
return ""
99+
return extensionByMimeMap(mimeType)
102100
}

0 commit comments

Comments
 (0)