upload-service/main.go

130 lines
3.4 KiB
Go

package main
import (
"context"
"embed"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jeffemmett/upload-service/internal/cleanup"
"github.com/jeffemmett/upload-service/internal/config"
"github.com/jeffemmett/upload-service/internal/db"
"github.com/jeffemmett/upload-service/internal/handler"
"github.com/jeffemmett/upload-service/internal/middleware"
"github.com/jeffemmett/upload-service/internal/r2"
"github.com/jeffemmett/upload-service/internal/store"
)
//go:embed web
var webFS embed.FS
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
database, err := db.Open(cfg.DBPath)
if err != nil {
log.Fatalf("db: %v", err)
}
defer database.Close()
s := store.New(database)
r2Client := r2.NewClient(cfg)
h := handler.New(s, r2Client, cfg)
handler.InitTemplates(webFS)
// CLI script
cliScript, _ := fs.ReadFile(webFS, "web/static/upload.sh")
mux := http.NewServeMux()
// Web UI
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
data, _ := fs.ReadFile(webFS, "web/index.html")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
w.Write(data)
})
// Static assets
staticFS, _ := fs.Sub(webFS, "web/static")
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
// API
mux.HandleFunc("POST /upload", h.Upload)
mux.HandleFunc("GET /f/{id}", h.Download)
mux.HandleFunc("GET /f/{id}/dl", h.DirectDownload)
mux.HandleFunc("GET /f/{id}/view", h.ViewFile)
mux.HandleFunc("GET /f/{id}/info", h.Info)
mux.HandleFunc("GET /f/{id}/auth", h.AuthPage)
mux.HandleFunc("POST /f/{id}/auth", h.AuthSubmit)
mux.HandleFunc("DELETE /f/{id}", h.Delete)
mux.HandleFunc("GET /b/{id}", h.Batch)
// Favicon (prevent 404)
mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
// Health
mux.HandleFunc("GET /health", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
})
// CLI download
mux.HandleFunc("GET /cli", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="upload.sh"`)
w.Write(cliScript)
})
// Middleware chain
rl := middleware.NewRateLimiter(cfg.RateLimit, cfg.RateBurst)
var chain http.Handler = mux
chain = rl.Middleware(chain)
chain = middleware.Security(chain)
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: chain,
ReadHeaderTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
// No read/write timeout — large uploads can take a long time
}
// Cleanup goroutine
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go cleanup.Start(ctx, s, r2Client)
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("shutting down...")
cancel()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
srv.Shutdown(shutdownCtx)
}()
log.Printf("listening on :%s", cfg.Port)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server: %v", err)
}
}