goblin/internal/paste/paste.go

215 lines
5.4 KiB
Go

package paste
import (
"fmt"
"html/template"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
"time"
"goblin/internal/metrics"
"goblin/internal/util"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
var logger = log.WithFields(log.Fields{
"package": "paste",
})
const idLength = 8
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
var (
pasteMap = make(map[string]*Paste)
templateDir string
pasteDir string
pasteTmpl *template.Template // Declare pasteTmpl at the package level
)
func generateUniqueID() string {
idRunes := make([]rune, idLength)
for i := range idRunes {
idRunes[i] = letters[rand.Intn(len(letters))]
}
uniqueID := fmt.Sprintf("%s-%d", string(idRunes), time.Now().UnixNano())
return uniqueID
}
type Paste struct {
ID string
Content string
CreatedAt time.Time
ExpiresAt time.Time
}
func InitializeTemplates() error {
templateDir = viper.GetString("TemplateDir")
pasteTmpl = template.Must(template.ParseFiles(filepath.Join(templateDir, "paste.html")))
return nil
}
func CreatePasteHandler(w http.ResponseWriter, r *http.Request) {
private := viper.GetBool("Private")
// Parse the request body
content, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
metrics.IncrementPasteCreatedCounter("failure")
return
}
// log the event's occurance - not the content
log.Infof("Received request to create a paste (Private mode: %v)", private)
if !private {
// log the content if private mode is not enabled
log.Infof("Received content: %s", content)
// also log the content into a file with the timestamp in the log directory
// check if the log directory exists and create it if it doesn't
if _, err := os.Stat(viper.GetString("LogDir")); os.IsNotExist(err) {
err := os.MkdirAll(viper.GetString("LogDir"), 0755)
if err != nil {
logger.Errorf("Failed to create log directory: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
logFile := filepath.Join(viper.GetString("LogDir"), fmt.Sprintf("%d.log", time.Now().UnixNano()))
f, err := os.Create(logFile)
if err != nil {
logger.Errorf("Failed to create log file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
metrics.IncrementPasteCreatedCounter("failure")
return
}
defer f.Close()
if _, err := f.Write(content); err != nil {
logger.Errorf("Failed to write to log file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
metrics.IncrementPasteCreatedCounter("failure")
return
}
}
// Generate a unique ID for the paste
pasteID := generateUniqueID()
// Save the paste in the map
paste := &Paste{
ID: pasteID,
Content: string(content),
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(24 * time.Hour),
}
pasteMap[pasteID] = paste
// Get the directory path from configuration
pasteDir := viper.GetString("PasteDir")
// Create the paste directory if it doesn't exist
if err := util.CreateDirectoryIfNotExists(pasteDir); err != nil {
logger.Errorf("Failed to create paste directory: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
metrics.IncrementPasteCreatedCounter("failure")
return
}
// Save the paste content to a file
pasteFilePath := filepath.Join(pasteDir, pasteID+".txt")
pasteFile, err := os.Create(pasteFilePath)
if err != nil {
logger.Errorf("Failed to create paste file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
metrics.IncrementPasteCreatedCounter("failure")
return
}
defer pasteFile.Close()
// Write the paste content to the file
_, err = pasteFile.WriteString(string(content))
if err != nil {
logger.Errorf("Failed to write paste content to file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
metrics.IncrementPasteCreatedCounter("failure")
return
}
// Construct the complete paste URL including the protocol
var protocol string
if r.TLS != nil {
protocol = "https"
} else {
protocol = "http"
}
pasteURL := fmt.Sprintf("%s://%s/%s", protocol, r.Host, pasteID)
// Write the paste URL in the response
response := fmt.Sprintf("Paste available at %s", pasteURL)
w.Write([]byte(response))
// Record metrics
metrics.IncrementPasteCreatedCounter("success")
metrics.ObservePasteLength("success", float64(len(content)))
}
func GetPasteHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
pasteID := vars["id"]
// Exclude the "/metrics" path
if pasteID == "metrics" {
http.NotFound(w, r)
return
}
p, exists := pasteMap[pasteID]
if !exists {
http.Error(w, "Paste not found", http.StatusNotFound)
return
}
if time.Now().After(p.ExpiresAt) {
delete(pasteMap, pasteID)
http.Error(w, "Paste has expired", http.StatusGone)
return
}
log.Infof("Template file path: %s", templateDir)
err := pasteTmpl.Execute(w, p)
if err != nil {
log.Errorf("Error executing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
func ExpirePastes() {
// Read the expiration duration from configuration
expirationDuration := viper.GetDuration("ExpirationDuration")
for {
time.Sleep(expirationDuration)
for pasteID, p := range pasteMap {
if time.Now().After(p.ExpiresAt) {
delete(pasteMap, pasteID)
}
}
}
}