-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
286 additions
and
277 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
Oops, something went wrong.