add view page
This commit is contained in:
@@ -3,6 +3,7 @@ package file
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -64,9 +65,49 @@ func (h *Handler) Upload(c *gin.Context) {
|
|||||||
"filename": record.Filename,
|
"filename": record.Filename,
|
||||||
"size": record.Size,
|
"size": record.Size,
|
||||||
"expires_at": record.ExpiresAt,
|
"expires_at": record.ExpiresAt,
|
||||||
|
"view_key": record.ViewID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) View(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
record, err := h.service.DownloadFile(id)
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusOK, "fileNotFound.html", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, record.Filename))
|
||||||
|
c.Header("X-Content-Type-Options", "nosniff")
|
||||||
|
c.File(record.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeFilename(name string) string {
|
||||||
|
// keep it simple: drop control chars and quotes
|
||||||
|
out := make([]rune, 0, len(name))
|
||||||
|
for _, r := range name {
|
||||||
|
if r < 32 || r == 127 || r == '"' || r == '\\' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return "file"
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isXSSRisk(filename string) bool {
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
switch ext {
|
||||||
|
case ".html", ".htm", ".js", ".css", ".svg":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) Download(c *gin.Context) {
|
func (h *Handler) Download(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|
||||||
@@ -75,7 +116,11 @@ func (h *Handler) Download(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "fileNotFound.html", nil)
|
c.HTML(http.StatusOK, "fileNotFound.html", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, record.Filename))
|
c.Header("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, record.Filename))
|
||||||
|
c.Header("X-Content-Type-Options", "nosniff")
|
||||||
|
//c.Header("Content-Security-Policy", "default-src 'none'; img-src 'self'; media-src 'self'; script-src 'none'; style-src 'none';")
|
||||||
|
//c.Header("Content-Type", "application/octet-stream")
|
||||||
c.File(record.Path)
|
c.File(record.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,21 @@ func (r *Repository) GetByDeletionID(delID string) (*FileRecord, error) {
|
|||||||
return &f, nil
|
return &f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Update(f *FileRecord) error {
|
||||||
|
return r.db.Save(f).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetFileByViewID(viewID string) (*FileRecord, error) {
|
||||||
|
var f FileRecord
|
||||||
|
if err := r.db.First(&f, "view_id = ?", viewID).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrFileNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &f, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) IncrementDownload(f *FileRecord) error {
|
func (r *Repository) IncrementDownload(f *FileRecord) error {
|
||||||
f.DownloadCount++
|
f.DownloadCount++
|
||||||
return r.db.Save(f).Error
|
return r.db.Save(f).Error
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ func RegisterRoutes(r *gin.RouterGroup, h *Handler) {
|
|||||||
files := r.Group("/files")
|
files := r.Group("/files")
|
||||||
|
|
||||||
files.POST("/upload", h.Upload)
|
files.POST("/upload", h.Upload)
|
||||||
files.GET("/download/:id", h.Download)
|
//files.GET("/download/:id", h.Download)
|
||||||
|
|
||||||
|
files.GET("/view/:id", h.View)
|
||||||
files.GET("/delete/:del_id", h.Delete)
|
files.GET("/delete/:del_id", h.Delete)
|
||||||
|
|
||||||
adminRoutes := files.Group("/admin")
|
adminRoutes := files.Group("/admin")
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func (s *Service) UploadFile(filename string, data io.Reader, deleteAfterDownloa
|
|||||||
f := &FileRecord{
|
f := &FileRecord{
|
||||||
ID: folderID,
|
ID: folderID,
|
||||||
DeletionID: uuid.NewString(),
|
DeletionID: uuid.NewString(),
|
||||||
|
ViewID: uuid.NewString(),
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
Path: path,
|
Path: path,
|
||||||
Size: size,
|
Size: size,
|
||||||
@@ -145,6 +146,10 @@ func (s *Service) GetFileByDeletionID(delID string) (*FileRecord, error) {
|
|||||||
return s.repo.GetByDeletionID(delID)
|
return s.repo.GetByDeletionID(delID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetFileByViewID(viewID string) (*FileRecord, error) {
|
||||||
|
return s.repo.GetFileByViewID(viewID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) ImportFiles(records []ImportFileRecord) error {
|
func (s *Service) ImportFiles(records []ImportFileRecord) error {
|
||||||
for _, r := range records {
|
for _, r := range records {
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,25 @@ func (h *Handler) LoginPage(c *gin.Context) {
|
|||||||
c.HTML(200, "login.html", nil)
|
c.HTML(200, "login.html", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) FileView(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
fileRecord, err := h.fileService.GetFileByViewID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(404, "fileNotFound.html", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadKey := fileRecord.ID
|
||||||
|
deleteKey := fileRecord.DeletionID
|
||||||
|
|
||||||
|
c.HTML(200, "complete.html", gin.H{
|
||||||
|
"Filename": fileRecord.Filename,
|
||||||
|
"DownloadID": downloadKey,
|
||||||
|
"DeleteID": deleteKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) AdminPage(c *gin.Context) {
|
func (h *Handler) AdminPage(c *gin.Context) {
|
||||||
pageStr := c.Query("page")
|
pageStr := c.Query("page")
|
||||||
page, err := strconv.Atoi(pageStr)
|
page, err := strconv.Atoi(pageStr)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ func RegisterRoutes(r *gin.Engine, h *Handler, userService *user.Service) {
|
|||||||
//r.GET("/upload", h.UploadPage)
|
//r.GET("/upload", h.UploadPage)
|
||||||
r.GET("/login", h.LoginPage)
|
r.GET("/login", h.LoginPage)
|
||||||
|
|
||||||
|
r.GET("/f/:id", h.FileView)
|
||||||
|
|
||||||
adminRoutes := r.Group("/")
|
adminRoutes := r.Group("/")
|
||||||
adminRoutes.Use(middleware.AuthMiddleware())
|
adminRoutes.Use(middleware.AuthMiddleware())
|
||||||
adminRoutes.Use(middleware.RequireRole("admin"))
|
adminRoutes.Use(middleware.RequireRole("admin"))
|
||||||
|
|||||||
132
templates/complete.html
Normal file
132
templates/complete.html
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Send.it - File Ready</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* No-design brutalist 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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-text {
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #fff;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 2px solid #000;
|
||||||
|
background: #eee;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</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">
|
||||||
|
|
||||||
|
<img src="/static/logo.png" alt="Send.it logo" style="width:50%;" class="mb-2 border-black">
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
|
||||||
|
<header class="mb-6 border-b-2 border-black pb-2 text-center">
|
||||||
|
<h1 class="text-xl font-bold uppercase">FILE READY</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
<div class="bg-black text-white p-2 text-xs font-bold">
|
||||||
|
UPLOAD COMPLETE
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <!– File info –>-->
|
||||||
|
<!-- <div class="text-[10px] uppercase font-bold">-->
|
||||||
|
<!-- example_file.png (2.4 MB)-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<!-- Download -->
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold block">DOWNLOAD LINK</label>
|
||||||
|
<div class="flex">
|
||||||
|
<input id="res-url" readonly class="input-text text-sm">
|
||||||
|
<button onclick="copy('res-url')" class="border-l-0">COPY</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete -->
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold block">DELETION LINK (PRIVATE)</label>
|
||||||
|
<div class="flex">
|
||||||
|
<input id="res-del" readonly class="input-text text-sm text-red-600">
|
||||||
|
<button onclick="copy('res-del')" class="border-l-0">COPY</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="pt-4 flex justify-between">
|
||||||
|
<a href="/" class="text-xs underline">NEW UPLOAD</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-1 text-[10px] uppercase font-bold text-gray-400">
|
||||||
|
A service by Brammie15
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copy(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
el.select();
|
||||||
|
el.setSelectionRange(0, 99999); // mobile support
|
||||||
|
document.execCommand('copy');
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadKey = "{{.DownloadID}}";
|
||||||
|
const deleteKey = "{{.DeleteID}}";
|
||||||
|
|
||||||
|
const base = window.location.origin;
|
||||||
|
|
||||||
|
document.getElementById("res-url").value = `${base}/api/files/view/${downloadKey}`;
|
||||||
|
document.getElementById("res-del").value = `${base}/api/files/delete/${deleteKey}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href="/admin" class="fixed bottom-1 right-1 text-[10px] underline">SUDO</a>
|
||||||
|
<a href="/static/TOS.txt" class="fixed bottom-1 left-1 text-[10px] underline">TOS</a>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -301,21 +301,15 @@
|
|||||||
const data = JSON.parse(xhr.responseText);
|
const data = JSON.parse(xhr.responseText);
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
|
|
||||||
document.getElementById('upload-ui').classList.add('hidden');
|
// Redirect using view key
|
||||||
document.getElementById('success-ui').classList.remove('hidden');
|
window.location.href = "/f/" + data.view_key;
|
||||||
|
|
||||||
const dlUrl = window.location.origin + "/api/files/download/" + data.id;
|
|
||||||
const delUrl = window.location.origin + "/api/files/delete/" + data.deletion_id;
|
|
||||||
|
|
||||||
document.getElementById('res-url').value = dlUrl;
|
|
||||||
document.getElementById('res-del').value = delUrl;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("JSON Parse Error. Server sent:", xhr.responseText);
|
console.error("Invalid response:", xhr.responseText);
|
||||||
alert("Server returned an invalid response");
|
alert("Server error");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Server Error:", xhr.status, xhr.responseText);
|
alert("Upload failed");
|
||||||
alert(`Upload failed with status ${xhr.status}. Check console.`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user