From 881619dfacd904048e5e514e3ac41835cf44142a Mon Sep 17 00:00:00 2001 From: = <=> Date: Fri, 14 Mar 2025 07:14:33 -0400 Subject: [PATCH] refactored code, added HTML template for rich embeds --- main.go | 412 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 271 insertions(+), 141 deletions(-) diff --git a/main.go b/main.go index 81e5b6c..fa810b4 100644 --- a/main.go +++ b/main.go @@ -1,36 +1,41 @@ package main - import ( - "crypto/rand" - "encoding/hex" - "flag" - "fmt" - "html/template" - "io" - "log" - "mime" - "net/http" - "os" - "path" - "path/filepath" - "strings" - "time" - - "github.com/msteinert/pam" + "crypto/rand" + "encoding/hex" + "flag" + "fmt" + "html/template" + "io" + "log" + "mime" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" + "github.com/msteinert/pam" ) - type Config struct { - Listen string - StorageDir string - BaseURL string - MaxFileSize int64 - ExpireHours int - IndexHTML string + Listen string + StorageDir string + BaseURL string + MaxFileSize int64 + ExpireHours int + IndexHTML string +} + +type ImagePageData struct { + Title string + ImageURL string + BaseURL string + Description string + FullImageURL string } var config Config - var indexHTML string +var imagePageTemplate string func loadIndexHTML() { content, err := os.ReadFile(config.IndexHTML) @@ -38,141 +43,266 @@ func loadIndexHTML() { panic(err) } indexHTML = string(content) + + // HTML template for rendering images + imagePageTemplate = ` + + + + + + {{.Title}} + + + + + + + + + + + + + + + + + +
+

{{.Title}}

+ {{.Title}}
+ Download Image +
+ +` } func init() { - flag.StringVar(&config.Listen, "listen", ":8080", "Address to listen on") - flag.StringVar(&config.StorageDir, "storage", "/tmp/share", "Directory to store uploaded files") - flag.StringVar(&config.BaseURL, "baseurl", "http://localhost:8080", "Base URL for generated file links") - flag.Int64Var(&config.MaxFileSize, "maxsize", 10*1024*1024, "Maximum allowed file size in bytes") - flag.IntVar(&config.ExpireHours, "expire", 24, "Number of hours before files are deleted") - flag.StringVar(&config.IndexHTML, "index", "index.html", "Path to html file to serve as index") + flag.StringVar(&config.Listen, "listen", ":8080", "Address to listen on") + flag.StringVar(&config.StorageDir, "storage", "/tmp/share", "Directory to store uploaded files") + flag.StringVar(&config.BaseURL, "baseurl", "http://localhost:8080", "Base URL for generated file links") + flag.Int64Var(&config.MaxFileSize, "maxsize", 10*1024*1024, "Maximum allowed file size in bytes") + flag.IntVar(&config.ExpireHours, "expire", 24, "Number of hours before files are deleted") + flag.StringVar(&config.IndexHTML, "index", "index.html", "Path to html file to serve as index") } func authenticateUser(username, password string) bool { - t, err := pam.StartFunc("system-auth", username, func(s pam.Style, msg string) (string, error) { - switch s { - case pam.PromptEchoOff: - return password, nil - } - return "", fmt.Errorf("unsupported PAM style") - }) - if err != nil { - return false - } - err = t.Authenticate(0) - return err == nil + t, err := pam.StartFunc("system-auth", username, func(s pam.Style, msg string) (string, error) { + switch s { + case pam.PromptEchoOff: + return password, nil + } + return "", fmt.Errorf("unsupported PAM style") + }) + if err != nil { + return false + } + err = t.Authenticate(0) + return err == nil } func generateRandomFilename() (string, error) { - bytes := make([]byte, 16) - if _, err := rand.Read(bytes); err != nil { - return "", err - } - return hex.EncodeToString(bytes), nil + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil } func cleanupOldFiles() { - ticker := time.NewTicker(1 * time.Hour) - for range ticker.C { - now := time.Now() - err := filepath.Walk(config.StorageDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && now.Sub(info.ModTime()) > time.Duration(config.ExpireHours)*time.Hour { - os.Remove(path) - } - return nil - }) - if err != nil { - log.Printf("Cleanup error: %v", err) - } - } + ticker := time.NewTicker(1 * time.Hour) + for range ticker.C { + now := time.Now() + err := filepath.Walk(config.StorageDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && now.Sub(info.ModTime()) > time.Duration(config.ExpireHours)*time.Hour { + os.Remove(path) + } + return nil + }) + if err != nil { + log.Printf("Cleanup error: %v", err) + } + } +} + +func isImageFile(filePath string) bool { + file, err := os.Open(filePath) + if err != nil { + log.Printf("Error opening file for MIME detection: %v", err) + return false + } + defer file.Close() + + buffer := make([]byte, 512) + _, err = file.Read(buffer) + if err != nil && err != io.EOF { + log.Printf("Error reading file for MIME detection: %v", err) + return false + } + + _, err = file.Seek(0, 0) + if err != nil { + log.Printf("Error seeking file after MIME detection: %v", err) + return false + } + + contentType := http.DetectContentType(buffer) + log.Printf("Detected content type for %s: %s", filePath, contentType) + + return strings.HasPrefix(contentType, "image/") } func handleUpload(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPut { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - username, password, ok := r.BasicAuth() - if !ok || !authenticateUser(username, password) { - w.Header().Set("WWW-Authenticate", `Basic realm="Upload"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if r.ContentLength > config.MaxFileSize { - http.Error(w, "File too large", http.StatusRequestEntityTooLarge) - return - } - - ext := path.Ext(r.URL.Path) - if ext == "" { - contentType := r.Header.Get("Content-Type") - exts, _ := mime.ExtensionsByType(contentType) - if len(exts) > 0 { - ext = exts[0] - } - } - - randomName, err := generateRandomFilename() - if err != nil { - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - filename := randomName + ext - filepath := path.Join(config.StorageDir, filename) - - f, err := os.Create(filepath) - if err != nil { - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - defer f.Close() - - _, err = io.Copy(f, r.Body) - if err != nil { - os.Remove(filepath) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - fileURL := strings.TrimRight(config.BaseURL, "/") + "/" + filename - fmt.Fprintf(w, "%s\n", fileURL) + if r.Method != http.MethodPut { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + username, password, ok := r.BasicAuth() + if !ok || !authenticateUser(username, password) { + w.Header().Set("WWW-Authenticate", `Basic realm="Upload"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + if r.ContentLength > config.MaxFileSize { + http.Error(w, "File too large", http.StatusRequestEntityTooLarge) + return + } + ext := path.Ext(r.URL.Path) + if ext == "" { + contentType := r.Header.Get("Content-Type") + exts, _ := mime.ExtensionsByType(contentType) + if len(exts) > 0 { + ext = exts[0] + } + } + randomName, err := generateRandomFilename() + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + filename := randomName + ext + filepath := path.Join(config.StorageDir, filename) + f, err := os.Create(filepath) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer f.Close() + _, err = io.Copy(f, r.Body) + if err != nil { + os.Remove(filepath) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + fileURL := strings.TrimRight(config.BaseURL, "/") + "/" + filename + fmt.Fprintf(w, "%s\n", fileURL) } func handleIndex(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.ServeFile(w, r, path.Join(config.StorageDir, r.URL.Path)) - return - } - - tmpl, err := template.New("index").Parse(indexHTML) - if err != nil { - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - tmpl.Execute(w, config) + if r.URL.Path == "/" { + tmpl, err := template.New("index").Parse(indexHTML) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + tmpl.Execute(w, config) + return + } + + requestPath := strings.TrimPrefix(r.URL.Path, "/") + filePath := path.Join(config.StorageDir, requestPath) + + log.Printf("Requested path: %s, File path: %s", r.URL.Path, filePath) + + fileInfo, err := os.Stat(filePath) + if err != nil { + log.Printf("File not found: %s, Error: %v", filePath, err) + http.NotFound(w, r) + return + } + + if r.URL.Query().Get("raw") == "true" || r.URL.Query().Get("download") == "true" { + if r.URL.Query().Get("download") == "true" { + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(requestPath))) + } + + log.Printf("Serving file directly: %s", filePath) + http.ServeFile(w, r, filePath) + return + } + + if !isImageFile(filePath) { + log.Printf("File is not an image, serving directly: %s", filePath) + http.ServeFile(w, r, filePath) + return + } + + tmpl, err := template.New("imagePage").Parse(imagePageTemplate) + if err != nil { + log.Printf("Template parsing error: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + baseURL := strings.TrimRight(config.BaseURL, "/") + + data := ImagePageData{ + Title: "Filename: " + filepath.Base(requestPath), + ImageURL: r.URL.Path, // Use the original request path for the image URL + BaseURL: baseURL, + Description: fmt.Sprintf("Image shared on %s, size: %d bytes", fileInfo.ModTime().Format("Jan 2, 2006"), fileInfo.Size()), + FullImageURL: baseURL + r.URL.Path + "?raw=true", // Full URL for the image with raw parameter + } + + log.Printf("Serving image template with data: %+v", data) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + tmpl.Execute(w, data) } func main() { - flag.Parse() - loadIndexHTML() - - if err := os.MkdirAll(config.StorageDir, 0700); err != nil { - log.Fatalf("Failed to create storage directory: %v", err) - } - - go cleanupOldFiles() - - http.HandleFunc("/", handleIndex) - http.HandleFunc("/upload", handleUpload) - - log.Printf("Starting server on %s", config.Listen) - log.Fatal(http.ListenAndServe(config.Listen, nil)) + flag.Parse() + loadIndexHTML() + if err := os.MkdirAll(config.StorageDir, 0700); err != nil { + log.Fatalf("Failed to create storage directory: %v", err) + } + go cleanupOldFiles() + http.HandleFunc("/", handleIndex) + http.HandleFunc("/upload", handleUpload) + log.Printf("Starting server on %s", config.Listen) + log.Printf("Storage directory: %s", config.StorageDir) + log.Printf("Base URL: %s", config.BaseURL) + log.Fatal(http.ListenAndServe(config.Listen, nil)) }