package main import ( "archive/zip" "bytes" "embed" "encoding/json" "fmt" "io" "log" "mime/multipart" "net/http" "os" "time" "git.hafen.run/lukas/timeshare/pages" "github.com/a-h/templ" "github.com/gabriel-vasile/mimetype" "github.com/google/uuid" "github.com/valkey-io/valkey-go" ) //go:embed assets var assetsFS embed.FS type File struct { FileKey string File *multipart.FileHeader } func main() { valkeyAddr := os.Getenv("VALKEY_ADDR") if valkeyAddr == "" { valkeyAddr = "127.0.0.1:6379" } client, err := valkey.NewClient(valkey.ClientOption{InitAddress: []string{valkeyAddr}}) if err != nil { log.Fatalln("unable to connect to valkey instance: ", err.Error()) } http.Handle("/assets/", http.FileServer(http.FS(assetsFS))) http.Handle("GET /upload", templ.Handler(pages.Upload([]pages.Expiry{ {DurationCode: "24h", DurationName: "24 Hours"}, {DurationCode: "36h", DurationName: "36 Hours"}, {DurationCode: "48h", DurationName: "48 Hours"}, {DurationCode: "168h", DurationName: "1 Week"}, }, ""))) http.Handle("POST /upload", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(125_000_000) // 1G max memory usage if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } form := r.MultipartForm expiry_values := form.Value["expiry"] expiry, err := time.ParseDuration(expiry_values[0]) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } form_files := form.File["files"] files := make([]string, len(form_files)) for i := range form_files { fid := uuid.New().String() files[i] = fid f, err := form_files[i].Open() if err != nil { continue } buf := new(bytes.Buffer) _, err = io.Copy(buf, f) if err != nil { continue } err = client.Do(r.Context(), client.B().Set().Key(fid+":filename").Value(form_files[i].Filename).Ex(expiry).Build()).Error() if err != nil { continue } err = client.Do(r.Context(), client.B().Set().Key(fid+":contents").Value(valkey.BinaryString(buf.Bytes())).Ex(expiry).Build()).Error() if err != nil { continue } } uid := uuid.New() files_b, _ := json.Marshal(files) err = client.Do(r.Context(), client.B().Set().Key(uid.String()).Value(string(files_b)).Ex(expiry).Build()).Error() templ.Handler(pages.Upload([]pages.Expiry{ {DurationCode: "24h", DurationName: "24 Hours"}, {DurationCode: "36h", DurationName: "36 Hours"}, {DurationCode: "48h", DurationName: "48 Hours"}, {DurationCode: "168h", DurationName: "1 Week"}, }, fmt.Sprintf("https://share.lukaswerner.com/%s", uid.String()))).ServeHTTP(w, r) })) http.Handle("/{upload_id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upload_id := r.PathValue("upload_id") if upload_id == "" { w.WriteHeader(http.StatusBadRequest) return } files_json, err := client.Do(r.Context(), client.B().Get().Key(upload_id).Build()).ToString() if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } var files []string err = json.Unmarshal([]byte(files_json), &files) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if len(files) == 1 { file := files[0] filename, err := client.Do(r.Context(), client.B().Get().Key(file+":filename").Build()).ToString() if err != nil { return } fileContents, err := client.Do(r.Context(), client.B().Get().Key(file+":contents").Build()).AsBytes() if err != nil { return } w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) mime := mimetype.Detect(fileContents) w.Header().Add("mimetype", mime.String()) w.Write(fileContents) return } zipBuf := new(bytes.Buffer) zipWriter := zip.NewWriter(zipBuf) for _, file := range files { filename, err := client.Do(r.Context(), client.B().Get().Key(file+":filename").Build()).ToString() if err != nil { continue } fileContents, err := client.Do(r.Context(), client.B().Get().Key(file+":contents").Build()).AsBytes() if err != nil { continue } w, err := zipWriter.Create(filename) if err != nil { continue } w.Write(fileContents) } zipWriter.Close() w.Header().Add("mimetype", "application/zip") w.Header().Add("Content-Disposition", `attachment; filename="timeshare-download.zip"`) io.Copy(w, zipBuf) })) http.Handle("/", templ.Handler(pages.Index())) log.Println(http.ListenAndServe(":8080", nil)) }