mirror of
https://github.com/Luzifer/preserve.git
synced 2024-11-08 14:20:05 +00:00
181 lines
4.9 KiB
Go
181 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
httpHelpers "github.com/Luzifer/go_helpers/v2/http"
|
|
"github.com/Luzifer/preserve/pkg/storage"
|
|
"github.com/Luzifer/preserve/pkg/storage/gcs"
|
|
"github.com/Luzifer/preserve/pkg/storage/local"
|
|
"github.com/Luzifer/rconfig/v2"
|
|
)
|
|
|
|
var (
|
|
cfg = struct {
|
|
BucketURI string `flag:"bucket-uri" default:"" description:"[gcs] Format: gs://bucket/prefix"`
|
|
Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"`
|
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
|
StorageDir string `flag:"storage-dir" default:"./data/" description:"[local] Where to store cached files"`
|
|
StorageProvider string `flag:"storage-provider" default:"local" description:"Storage providers to use ('list' to print a list)"`
|
|
UserAgent string `flag:"user-agent" default:"" description:"Override user-agent"`
|
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
|
}{}
|
|
|
|
store storage.Storage
|
|
version = "dev"
|
|
)
|
|
|
|
func initApp() error {
|
|
rconfig.AutoEnv(true)
|
|
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
|
return fmt.Errorf("parsing cli options: %w", err)
|
|
}
|
|
|
|
l, err := logrus.ParseLevel(cfg.LogLevel)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing log-level: %w", err)
|
|
}
|
|
logrus.SetLevel(l)
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
var err error
|
|
|
|
if err = initApp(); err != nil {
|
|
logrus.WithError(err).Fatal("initializing app")
|
|
}
|
|
|
|
if cfg.VersionAndExit {
|
|
fmt.Printf("preserve %s\n", version) //nolint:forbidigo // Fine here
|
|
os.Exit(0)
|
|
}
|
|
|
|
switch cfg.StorageProvider {
|
|
case "gcs":
|
|
if store, err = gcs.New(cfg.BucketURI); err != nil {
|
|
logrus.WithError(err).Fatal("creating GCS storage")
|
|
}
|
|
|
|
case "list":
|
|
// Special "provider" to list possible providers
|
|
logrus.Println("Available Storage Providers: gcs, local")
|
|
return
|
|
|
|
case "local":
|
|
store = local.New(cfg.StorageDir)
|
|
|
|
default:
|
|
logrus.Fatalf("invalid storage provider: %q", cfg.StorageProvider)
|
|
}
|
|
|
|
r := mux.NewRouter()
|
|
r.PathPrefix("/latest/").HandlerFunc(handleCacheLatest)
|
|
r.PathPrefix("/").HandlerFunc(handleCacheOnce)
|
|
|
|
r.SkipClean(true)
|
|
|
|
r.Use(httpHelpers.NewHTTPLogHandler)
|
|
r.Use(httpHelpers.GzipHandler)
|
|
|
|
server := http.Server{
|
|
Addr: cfg.Listen,
|
|
Handler: r,
|
|
ReadHeaderTimeout: time.Second,
|
|
}
|
|
|
|
logrus.WithFields(logrus.Fields{"addr": cfg.Listen, "version": version}).Info("preserve starting...")
|
|
if err = server.ListenAndServe(); err != nil {
|
|
logrus.WithError(err).Fatal("running HTTP server")
|
|
}
|
|
}
|
|
|
|
func handleCacheLatest(w http.ResponseWriter, r *http.Request) {
|
|
handleCache(w, r, strings.TrimPrefix(r.RequestURI, "/latest/"), true)
|
|
}
|
|
|
|
func handleCacheOnce(w http.ResponseWriter, r *http.Request) {
|
|
handleCache(w, r, strings.TrimPrefix(r.RequestURI, "/"), false)
|
|
}
|
|
|
|
//revive:disable-next-line:flag-parameter // This is fine in this case
|
|
func handleCache(w http.ResponseWriter, r *http.Request, uri string, update bool) {
|
|
if strings.HasPrefix(uri, "b64:") {
|
|
u, err := base64.URLEncoding.DecodeString(strings.TrimPrefix(uri, "b64:"))
|
|
if err != nil {
|
|
http.Error(w, "decoding base64 URL", http.StatusBadRequest)
|
|
return
|
|
}
|
|
uri = string(u)
|
|
}
|
|
|
|
var (
|
|
cachePath = urlToCachePath(uri)
|
|
cacheHeader = "HIT"
|
|
logger = logrus.WithFields(logrus.Fields{
|
|
"url": uri,
|
|
"path": cachePath,
|
|
})
|
|
)
|
|
|
|
if u, err := url.Parse(uri); err != nil || u.Scheme == "" {
|
|
http.Error(w, "parsing requested URL", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
logger.Debug("Received request")
|
|
|
|
metadata, err := store.LoadMeta(r.Context(), cachePath)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
logrus.WithError(err).Error("loading meta")
|
|
http.Error(w, "accessing entry metadata", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if update || errors.Is(err, fs.ErrNotExist) {
|
|
logger.Debug("updating cache")
|
|
cacheHeader = "MISS"
|
|
|
|
// Using background context to cache the file even in case of the request being aborted
|
|
metadata, err = renewCache(context.Background(), uri) //nolint:contextcheck // See line above
|
|
if err != nil {
|
|
logger.WithError(err).Warn("refreshing file")
|
|
}
|
|
}
|
|
|
|
if metadata == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", metadata.ContentType)
|
|
w.Header().Set("X-Last-Cached", metadata.LastCached.UTC().Format(http.TimeFormat))
|
|
w.Header().Set("X-Cache", cacheHeader)
|
|
|
|
f, err := store.GetFile(r.Context(), cachePath)
|
|
if err != nil {
|
|
logrus.WithError(err).Error("loading cached file")
|
|
http.Error(w, "accessing cache entry", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer func() {
|
|
if err := f.Close(); err != nil {
|
|
logrus.WithError(err).Error("closing storage file (leaked fd)")
|
|
}
|
|
}()
|
|
|
|
http.ServeContent(w, r, "", metadata.LastModified, f)
|
|
}
|