Skip to content

Commit

Permalink
refactor code.
Browse files Browse the repository at this point in the history
  • Loading branch information
dunkbing committed Mar 4, 2024
1 parent 536074b commit ea6e302
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 277 deletions.
198 changes: 198 additions & 0 deletions converter/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package handlers

import (
"encoding/json"
"fmt"
"github.com/dunkbing/tinyimg/converter/config"
"github.com/dunkbing/tinyimg/converter/image"
"github.com/google/uuid"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)

type RequestBody struct {
Files []string `json:"files"`
}

func getContentType(fileName string) string {
switch filepath.Ext(fileName) {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".gif":
return "image/gif"
case ".webp":
return "image/webp"
default:
return "application/octet-stream"
}
}

func isImage(mimeType string) bool {
mimeType = strings.ToLower(mimeType)
return strings.HasPrefix(mimeType, "image/")
}

func Upload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}

var sizeLimit int64 = 10 * 1024 * 1024
r.Body = http.MaxBytesReader(w, r.Body, sizeLimit)

startTime := time.Now()
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "Error retrieving the file. The file may be too large (max 10MB)", http.StatusInternalServerError)
return
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
http.Error(w, "Error reading the file", http.StatusInternalServerError)
return
}

mimeType := http.DetectContentType(data)
if !isImage(mimeType) {
http.Error(w, "Invalid file format. Only images are allowed.", http.StatusBadRequest)
return
}
fmt.Println("content type", mimeType, header.Filename)
fileType, _ := image.GetFileType(mimeType)
filename := filepath.Base(header.Filename)
filename = strings.ReplaceAll(filename, " ", "_")

ext := filepath.Ext(filename)
filename = strings.Replace(filename, ext, fmt.Sprintf(".%s", fileType), 1)
id := uuid.New()
filename = fmt.Sprintf("%s_%s", id.String(), filename)

ext = fmt.Sprintf(".%s", fileType)

c := config.GetConfig()
dest := filepath.Join(c.App.InDir, filename)
slog.Info("Upload", "dest", dest)
err = os.WriteFile(dest, data, 0644)
if err != nil {
http.Error(w, "Error writing the file", http.StatusInternalServerError)
return
}
took := time.Since(startTime).Seconds()
fmt.Println("Write to file took", took, "seconds")
formatStr := r.FormValue("formats")
formats := make([]string, 0)
if formatStr != "" {
formats = strings.Split(formatStr, ",")
} else {
formats = append(formats, fileType)
}

f := image.File{
Data: data,
Ext: ext,
MimeType: mimeType,
Name: filename,
Size: header.Size,
Formats: formats,
InputFileDest: dest,
}

fileManager := image.NewFileManager()
err = fileManager.HandleFile(&f)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
results, files, errs := fileManager.Convert()
strErrs := make([]string, len(errs))
for i, err := range errs {
strErrs[i] = err.Error()
}

// Success
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
"data": results,
"files": files,
"errors": strErrs,
})
}

func DownloadAll(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}

var body RequestBody
err := json.NewDecoder(r.Body).Decode(&body)
if err != nil {
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}

fm := image.NewFileManager()
zippedPath, err := fm.ZipFiles(body.Files)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
zipFileName := filepath.Base(zippedPath)
zipFile, err := os.Open(zippedPath)
if err != nil {
http.Error(w, "Error opening zip file", http.StatusInternalServerError)
return
}
defer zipFile.Close()

w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipFileName))
_, err = io.Copy(w, zipFile)
if err != nil {
http.Error(w, "Error serving zip file", http.StatusInternalServerError)
return
}
}

func ServeImg(w http.ResponseWriter, r *http.Request) {
fileName := r.URL.Query().Get("f")
if fileName == "" {
http.Error(w, "File not found", http.StatusBadRequest)
return
}

filePath := filepath.Join("output", fileName)

_, err := os.Stat(filePath)
if os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}

file, err := os.Open(filePath)
if err != nil {
http.Error(w, "Error opening file", http.StatusInternalServerError)
return
}
defer file.Close()

contentType := getContentType(fileName)
w.Header().Set("Content-Type", contentType)

_, err = io.Copy(w, file)
if err != nil {
http.Error(w, "Error serving file", http.StatusInternalServerError)
return
}
}
82 changes: 82 additions & 0 deletions converter/handlers/limiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package handlers

//yanked from here: https://www.alexedwards.net/blog/how-to-rate-limit-http-requests

import (
"fmt"
"golang.org/x/time/rate"
"log"
"net"
"net/http"
"sync"
"time"
)

// Create a custom visitor struct which holds the rate limiter for each
// visitor and the last time that the visitor was seen.
type visitor struct {
limiter *rate.Limiter
lastSeen time.Time
}

// Change the map to hold values of the type visitor.
var visitors = make(map[string]*visitor)
var mu sync.Mutex

// Run a background goroutine to remove old entries from the visitors map.
func init() {
go cleanupVisitors()
}

func getVisitor(ip string) *rate.Limiter {
mu.Lock()
defer mu.Unlock()

v, exists := visitors[ip]
fmt.Println(ip)
if !exists {
limiter := rate.NewLimiter(15, 30)
// Include the current time when creating a new visitor.
visitors[ip] = &visitor{limiter, time.Now()}
return limiter
}

// Update the last seen time for the visitor.
v.lastSeen = time.Now()
return v.limiter
}

// Every minute check the map for visitors that haven't been seen for
// more than 3 minutes and delete the entries.
func cleanupVisitors() {
for {
time.Sleep(time.Minute)

mu.Lock()
for ip, v := range visitors {
if time.Since(v.lastSeen) > 3*time.Minute {
delete(visitors, ip)
}
}
mu.Unlock()
}
}

func Limit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
log.Print(err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

limiter := getVisitor(ip)
if !limiter.Allow() {
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
return
}

next.ServeHTTP(w, r)
})
}
Loading

0 comments on commit ea6e302

Please sign in to comment.