Add 404 page, add Deleted page, fix errors

This commit is contained in:
2026-02-27 19:32:37 +01:00
parent 1b2f2bb942
commit 90a2f43ab6
9 changed files with 375 additions and 36 deletions

View File

@@ -30,6 +30,9 @@ func uploadHandler(c *gin.Context) {
id := uuid.New().String()
delID := uuid.New().String()
cleanName := filepath.Base(header.Filename)
if len(cleanName) > 255 {
cleanName = cleanName[:255]
}
folderPath := filepath.Join("uploads", id)
os.MkdirAll(folderPath, 0755)
@@ -65,6 +68,7 @@ func uploadHandler(c *gin.Context) {
Filename: cleanName,
Path: storagePath,
ExpiresAt: expiry,
Size: written,
DeleteAfterDownload: c.PostForm("once") == "true",
}
@@ -79,14 +83,21 @@ func uploadHandler(c *gin.Context) {
func downloadHandler(c *gin.Context) {
var record FileRecord
if err := db.First(&record, "id = ? AND deleted = ?", c.Param("id"), false).Error; err != nil {
c.String(404, "File not found or expired")
var err = db.First(&record, "id = ? AND deleted = ?", c.Param("id"), false).Error
if err != nil {
c.HTML(200, "fileNotFound.html", gin.H{
"message": "File not found",
})
return
}
if time.Now().After(record.ExpiresAt) {
performDeletion(&record)
c.String(410, "File has expired")
//c.String(410, "File has expired")
c.HTML(404, "fileNotFound.html", gin.H{
"message": "File not found",
})
return
}
@@ -107,7 +118,8 @@ func deleteHandler(c *gin.Context) {
return
}
performDeletion(&record)
c.JSON(200, gin.H{"message": "Deleted successfully"})
//c.JSON(200, gin.H{"message": "Deleted successfully"})
c.HTML(200, "deleted.html", nil)
}
func loginHandler(c *gin.Context) {
@@ -145,3 +157,30 @@ func loginHandler(c *gin.Context) {
c.Redirect(302, "/admin")
}
func adminIndexHandler(c *gin.Context) {
var files []FileRecord
// Pagination parameters
perPage := 20
page := 1
if p := c.Query("page"); p != "" {
if v, err := strconv.Atoi(p); err == nil && v > 0 {
page = v
}
}
var total int64
db.Model(&FileRecord{}).Count(&total)
totalPages := int((total + int64(perPage) - 1) / int64(perPage)) // ceiling division
offset := (page - 1) * perPage
db.Order("created_at desc").Limit(perPage).Offset(offset).Find(&files)
c.HTML(200, "admin.html", gin.H{
"Files": files,
"Page": page,
"TotalPages": totalPages,
})
}

View File

@@ -6,7 +6,6 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
@@ -34,8 +33,9 @@ func main() {
router := gin.Default()
router.MaxMultipartMemory = 10 << 30
router.SetFuncMap(template.FuncMap{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"humanSize": humanSize,
})
router.LoadHTMLGlob("templates/*")
var staticPath = gin.Dir("./static", false)
@@ -43,6 +43,9 @@ func main() {
router.StaticFS("/static", staticPath)
router.NoRoute(func(c *gin.Context) {
c.HTML(404, "error.html", nil)
})
// Public Routes
router.GET("/", func(c *gin.Context) { c.HTML(200, "index.html", nil) })
router.GET("/f/:id", downloadHandler)
@@ -53,34 +56,16 @@ func main() {
admin := router.Group("/admin")
admin.Use(authMiddleware())
admin.GET("/", func(c *gin.Context) {
var files []FileRecord
// Pagination parameters
perPage := 20
page := 1
if p := c.Query("page"); p != "" {
if v, err := strconv.Atoi(p); err == nil && v > 0 {
page = v
}
admin.GET("/", adminIndexHandler)
admin.GET("/delete/fr/:id", func(c *gin.Context) {
var record FileRecord
if err := db.First(&record, "id = ?", c.Param("id")).Error; err == nil {
performActualDeletion(&record)
}
var total int64
db.Model(&FileRecord{}).Count(&total)
totalPages := int((total + int64(perPage) - 1) / int64(perPage)) // ceiling division
offset := (page - 1) * perPage
db.Order("created_at desc").Limit(perPage).Offset(offset).Find(&files)
c.HTML(200, "admin.html", gin.H{
"Files": files,
"Page": page,
"TotalPages": totalPages,
})
c.Redirect(301, "/admin")
})
admin.POST("/delete/:id", func(c *gin.Context) {
admin.GET("/delete/:id", func(c *gin.Context) {
var record FileRecord
if err := db.First(&record, "id = ?", c.Param("id")).Error; err == nil {
performDeletion(&record)

View File

@@ -9,6 +9,7 @@ type FileRecord struct {
Path string `json:"-"`
ExpiresAt time.Time `json:"expires_at"`
DeleteAfterDownload bool `json:"delete_after_download"`
Size int64 `json:"size"`
DownloadCount int `json:"download_count"`
Deleted bool `json:"deleted"`
CreatedAt time.Time `json:"created_at"`

View File

@@ -1,6 +1,9 @@
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
@@ -12,6 +15,19 @@ func performDeletion(r *FileRecord) {
db.Save(r)
}
func performActualDeletion(r *FileRecord) {
folderPath := filepath.Join("uploads", r.ID)
err := os.RemoveAll(folderPath)
if err != nil {
fmt.Println("Error deleting file:", err)
return
}
db.Delete(r)
fmt.Println("Deleted file:", r.Filename)
}
func cleanupWorker() {
for {
time.Sleep(10 * time.Minute)
@@ -38,7 +54,7 @@ func authMiddleware() gin.HandlerFunc {
tokenStr, err := c.Cookie("auth")
if err != nil {
c.Redirect(302, "/")
c.Redirect(302, "/login")
c.Abort()
return
}
@@ -48,7 +64,7 @@ func authMiddleware() gin.HandlerFunc {
})
if err != nil || !token.Valid {
c.Redirect(302, "/")
c.Redirect(302, "/login")
c.Abort()
return
}
@@ -56,3 +72,19 @@ func authMiddleware() gin.HandlerFunc {
c.Next()
}
}
func humanSize(size int64) string {
const unit = 1024
if size < unit {
return fmt.Sprintf("%d B", size)
}
div, exp := int64(unit), 0
for n := size / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(size)/float64(div),
"KMGTPE"[exp],
)
}

View File

@@ -39,6 +39,7 @@
<thead>
<tr>
<th>Filename</th>
<th>Size</th>
<th>Created</th>
<th>Expires</th>
<th>Hits</th>
@@ -53,6 +54,7 @@
<td class="font-mono">
<a href="/admin/download/{{.ID}}" target="_blank">{{.Filename}}</a>
</td>
<td>{{humanSize .Size}}</td>
<td>{{.CreatedAt.Format "Jan 02, 2006 15:04"}}</td>
<td>{{.ExpiresAt.Format "Jan 02, 2006 15:04"}}</td>
<td>{{.DownloadCount}}</td>
@@ -72,10 +74,13 @@
</td>
<td>
{{if not .Deleted}}
<form action="/admin/delete/{{.ID}}" method="POST" onsubmit="return confirm('Kill this file?')">
<form action="/admin/delete/{{.ID}}" method="GET" onsubmit="return confirm('Kill this file?')">
<button type="submit">TERMINATE</button>
</form>
{{end}}
<form action="/admin/delete/fr/{{.ID}}" method="GET" onsubmit="return confirm('Kill this file and the record?')">
<button type="submit">TERMINATE RECORD</button>
</form>
</td>
</tr>
{{end}}

92
templates/deleted.html Normal file
View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Deleted sucessfull</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
* { border-radius: 0 !important; transition: none !important; }
body { font-family: sans-serif; background: #fff; color: #000; }
.box {
border: 3px solid #000;
padding: 20px;
background: #fff;
width: 100%;
}
.title {
font-size: 28px;
font-weight: 900;
border-bottom: 3px solid #000;
padding-bottom: 6px;
margin-bottom: 12px;
text-transform: uppercase;
}
.subtitle {
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 16px;
}
.button {
border: 2px solid #000;
background: #eee;
padding: 6px 12px;
font-weight: bold;
text-decoration: none;
display: inline-block;
}
.button:hover {
background: #000;
color: #fff;
}
.ascii {
font-family: monospace;
font-size: 11px;
border: 2px dashed #000;
padding: 10px;
margin: 10px 0;
text-align: left;
white-space: pre;
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-[520px]">
<div class="box text-center">
<div class="title">
FILE DELETED SUCESSFULL
</div>
<div class="subtitle">
The file has been absolutely obliterated.
</div>
<!-- <div class="ascii">-->
<!-- [ OK ] locating file...-->
<!-- [ OK ] emotionally detaching...-->
<!-- [ OK ] pressing the big red button...-->
<!-- [ OK ] file screaming detected...-->
<!-- [ OK ] scream ignored...-->
<!-- [ OK ] file is now gone forever™-->
<!-- (there is no undo)-->
<!-- </div>-->
<!-- <div class="text-xs font-bold uppercase mb-4">-->
<!-- Congratulations. The electrons have been freed.-->
<!-- </div>-->
<div class="flex flex-col gap-2">
<a href="/" class="button w-full">Pretend Nothing Happened</a>
</div>
</div>
</div>
</body>
</html>

97
templates/error.html Normal file
View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nothing to see here</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
border-radius: 0 !important;
transition: none !important;
}
body {
font-family: sans-serif;
background: #fff;
color: #000;
}
.box {
border: 2px solid #000;
padding: 20px;
background: #fff;
width: 100%;
}
.button {
border: 2px solid #000;
background: #eee;
padding: 4px 12px;
font-weight: bold;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.button:hover {
background: #ccc;
}
.button:active {
background: #000;
color: #fff;
}
.title {
font-size: 28px;
font-weight: 900;
border-bottom: 2px solid #000;
margin-bottom: 10px;
padding-bottom: 4px;
}
.subtitle {
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 16px;
}
.text {
font-size: 12px;
text-transform: uppercase;
margin-bottom: 20px;
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-[493px] flex flex-col items-center">
<div class="box text-center">
<div class="title">
NOTHING TO SEE HERE
</div>
<div class="subtitle">
MOVE ALONG
</div>
<div class="text">
This page is empty,<br>
unavailable, private,<br>
or intentionally left blank.
</div>
<div class="flex flex-col gap-2">
<a href="/" class="button w-full">GO BACK</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 — File Not Found</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
border-radius: 0 !important;
transition: none !important;
}
body {
font-family: sans-serif;
background: #fff;
color: #000;
}
.box {
border: 2px solid #000;
padding: 20px;
background: #fff;
width: 100%;
}
button, .button {
border: 2px solid #000;
background: #eee;
padding: 4px 12px;
font-weight: bold;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
button:hover, .button:hover {
background: #ccc;
}
button:active, .button:active {
background: #000;
color: #fff;
}
.error-code {
font-size: 64px;
font-weight: 900;
border-bottom: 2px solid #000;
margin-bottom: 10px;
}
.error-text {
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-[493px] flex flex-col items-center">
<div class="box text-center">
<div class="error-code">404</div>
<div class="error-text mb-4">
FILE NOT FOUND 💀
</div>
<div class="text-xs mb-6 uppercase">
The requested file does not exist,<br>
has expired, or was destroyed,<br>or my db is fucked.
We'll never know :D
</div>
<div class="flex flex-col gap-2">
<a href="/" class="button w-full">RETURN TO UPLOADER</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -335,6 +335,6 @@
document.execCommand('copy');
}
</script>
<a href="/login" class="fixed bottom-1 right-1 text-[10px] underline">SUDO</a>
<a href="/admin" class="fixed bottom-1 right-1 text-[10px] underline">SUDO</a>
</body>
</html>