package server import ( "archive/zip" "crypto/rand" "encoding/hex" "fmt" "io" "mime" "mime/multipart" "net/http" "net/url" "os" "path/filepath" "strings" "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" ) const uploadRoot = "data/uploads" type boxFile struct { Name string Size int64 SizeLabel string MimeType string IconPath string DownloadPath string } func Run(addr string) error { router := gin.Default() router.LoadHTMLGlob("templates/*.html") router.GET("/", func(ctx *gin.Context) { ctx.HTML(http.StatusOK, "index.html", gin.H{}) }) router.GET("/box/:id", func(ctx *gin.Context) { boxID := ctx.Param("id") if !validBoxID(boxID) { ctx.String(http.StatusBadRequest, "Invalid box id") return } files, err := listBoxFiles(boxID) if err != nil { ctx.String(http.StatusNotFound, "Box not found") return } ctx.HTML(http.StatusOK, "box.html", gin.H{ "BoxID": boxID, "Files": files, "FileCount": len(files), "DownloadAll": "/box/" + boxID + "/download", }) }) router.GET("/box/:id/download", func(ctx *gin.Context) { boxID := ctx.Param("id") if !validBoxID(boxID) { ctx.String(http.StatusBadRequest, "Invalid box id") return } files, err := listBoxFiles(boxID) if err != nil { ctx.String(http.StatusNotFound, "Box not found") return } ctx.Header("Content-Type", "application/zip") ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="warpbox-%s.zip"`, boxID)) zipWriter := zip.NewWriter(ctx.Writer) defer zipWriter.Close() for _, file := range files { if err := addFileToZip(zipWriter, boxID, file.Name); err != nil { ctx.Status(http.StatusInternalServerError) return } } }) router.GET("/box/:id/files/:filename", func(ctx *gin.Context) { boxID := ctx.Param("id") filename, ok := safeFilename(ctx.Param("filename")) if !validBoxID(boxID) || !ok { ctx.String(http.StatusBadRequest, "Invalid file") return } path := filepath.Join(boxPath(boxID), filename) if !strings.HasPrefix(path, boxPath(boxID)+string(filepath.Separator)) { ctx.String(http.StatusBadRequest, "Invalid file") return } if _, err := os.Stat(path); err != nil { ctx.String(http.StatusNotFound, "File not found") return } ctx.FileAttachment(path, filename) }) router.POST("/box", func(ctx *gin.Context) { boxID, err := newBoxID() if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create upload box"}) return } if err := os.MkdirAll(boxPath(boxID), 0755); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not prepare upload box"}) return } ctx.JSON(http.StatusOK, gin.H{ "box_id": boxID, "box_url": "/box/" + boxID, }) }) router.POST("/box/:id/upload", func(ctx *gin.Context) { boxID := ctx.Param("id") if !validBoxID(boxID) { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"}) return } file, err := ctx.FormFile("file") if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"}) return } savedFile, err := saveUploadedFile(ctx, boxID, file) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{ "box_id": boxID, "box_url": "/box/" + boxID, "file": savedFile, }) }) router.POST("/upload", func(ctx *gin.Context) { form, err := ctx.MultipartForm() if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "No files received"}) return } files := form.File["files"] if len(files) == 0 { ctx.JSON(http.StatusBadRequest, gin.H{"error": "No files received"}) return } boxID, err := newBoxID() if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create upload box"}) return } if err := os.MkdirAll(boxPath(boxID), 0755); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not prepare upload box"}) return } savedFiles := make([]gin.H, 0, len(files)) for _, file := range files { savedFile, err := saveUploadedFile(ctx, boxID, file) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } savedFiles = append(savedFiles, savedFile) } ctx.JSON(http.StatusOK, gin.H{ "box_id": boxID, "box_url": "/box/" + boxID, "files": savedFiles, }) }) compressed := router.Group("/", gzip.Gzip(gzip.DefaultCompression)) compressed.Static("/static", "./static") return router.Run(addr) } func newBoxID() (string, error) { bytes := make([]byte, 16) if _, err := rand.Read(bytes); err != nil { return "", err } return hex.EncodeToString(bytes), nil } func validBoxID(boxID string) bool { if len(boxID) != 32 { return false } for _, character := range boxID { if !strings.ContainsRune("0123456789abcdef", character) { return false } } return true } func boxPath(boxID string) string { return filepath.Join(uploadRoot, boxID) } func listBoxFiles(boxID string) ([]boxFile, error) { entries, err := os.ReadDir(boxPath(boxID)) if err != nil { return nil, err } files := make([]boxFile, 0, len(entries)) for _, entry := range entries { if entry.IsDir() { continue } info, err := entry.Info() if err != nil { return nil, err } name := entry.Name() mimeType := mimeTypeForFile(filepath.Join(boxPath(boxID), name), name) files = append(files, boxFile{ Name: name, Size: info.Size(), SizeLabel: formatBytes(info.Size()), MimeType: mimeType, IconPath: iconForMimeType(mimeType, name), DownloadPath: "/box/" + boxID + "/files/" + url.PathEscape(name), }) } return files, nil } func addFileToZip(zipWriter *zip.Writer, boxID string, filename string) error { path := filepath.Join(boxPath(boxID), filename) source, err := os.Open(path) if err != nil { return err } defer source.Close() destination, err := zipWriter.Create(filename) if err != nil { return err } _, err = io.Copy(destination, source) return err } func saveUploadedFile(ctx *gin.Context, boxID string, file *multipart.FileHeader) (gin.H, error) { filename, ok := safeFilename(file.Filename) if !ok { return nil, fmt.Errorf("Invalid filename") } boxPath := boxPath(boxID) if err := os.MkdirAll(boxPath, 0755); err != nil { return nil, fmt.Errorf("Could not prepare upload box") } filename = uniqueFilename(boxPath, filename) destination := filepath.Join(boxPath, filename) if err := ctx.SaveUploadedFile(file, destination); err != nil { return nil, fmt.Errorf("Could not save uploaded file") } return gin.H{ "name": filename, "size": file.Size, }, nil } func safeFilename(name string) (string, bool) { filename := filepath.Base(name) filename = strings.TrimSpace(filename) return filename, filename != "" && filename != "." && filename != string(filepath.Separator) } func uniqueFilename(directory string, filename string) string { if _, err := os.Stat(filepath.Join(directory, filename)); os.IsNotExist(err) { return filename } extension := filepath.Ext(filename) base := strings.TrimSuffix(filename, extension) for count := 2; ; count++ { candidate := fmt.Sprintf("%s-%d%s", base, count, extension) if _, err := os.Stat(filepath.Join(directory, candidate)); os.IsNotExist(err) { return candidate } } } func mimeTypeForFile(path string, filename string) string { if mimeType := mime.TypeByExtension(strings.ToLower(filepath.Ext(filename))); mimeType != "" { return mimeType } file, err := os.Open(path) if err != nil { return "application/octet-stream" } defer file.Close() buffer := make([]byte, 512) bytesRead, err := file.Read(buffer) if err != nil && err != io.EOF { return "application/octet-stream" } return http.DetectContentType(buffer[:bytesRead]) } func iconForMimeType(mimeType string, filename string) string { extension := strings.ToLower(filepath.Ext(filename)) switch { case strings.HasPrefix(mimeType, "image/"): return "/static/img/sprites/bitmap.png" case strings.HasPrefix(mimeType, "video/"): return "/static/img/icons/netshow_notransm-1.png" case strings.HasPrefix(mimeType, "audio/"): return "/static/img/icons/netshow_notransm-1.png" case strings.HasPrefix(mimeType, "text/") || extension == ".md": return "/static/img/sprites/notepad_file-1.png" case strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "compressed") || extension == ".rar" || extension == ".7z" || extension == ".tar" || extension == ".gz": return "/static/img/icons/Windows Icons - PNG/zipfldr.dll_14_101-0.png" case extension == ".ttf" || extension == ".otf" || extension == ".woff" || extension == ".woff2": return "/static/img/sprites/font.png" case extension == ".pdf": return "/static/img/sprites/journal.png" case extension == ".html" || extension == ".css" || extension == ".js": return "/static/img/sprites/frame_web-0.png" default: return "/static/img/sprites/freepad.png" } } func formatBytes(bytes int64) string { units := []string{"B", "KB", "MB", "GB"} size := float64(bytes) unitIndex := 0 for size >= 1024 && unitIndex < len(units)-1 { size /= 1024 unitIndex++ } if unitIndex == 0 { return fmt.Sprintf("%d %s", bytes, units[unitIndex]) } return fmt.Sprintf("%.1f %s", size, units[unitIndex]) }