Files
Artifacto/cmd/artifacto/main.go
dtoro c7d7ee287c initial commit: Artifacto v0.1
Self-hosted HTML artifact publisher. Single Go binary, SQLite metadata,
HTML on disk. Features: password-protected artifacts, per-artifact expiration,
admin dashboard with view metrics (privacy-preserving daily-salt visitor hash,
sparklines, 30-day SVG chart, top referrers), rate-limited unlock endpoint.

Packaged as a Docker image (two-stage, CGO_ENABLED=0, pure-Go SQLite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:45:57 +02:00

114 lines
2.7 KiB
Go

package main
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"git.hubris.network/dtoro/artifacto/internal/auth"
"git.hubris.network/dtoro/artifacto/internal/server"
"git.hubris.network/dtoro/artifacto/internal/store"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel()}))
slog.SetDefault(logger)
dataDir := envOr("DATA_DIR", "/data")
bindAddr := envOr("BIND_ADDR", ":3000")
baseURL := envOr("BASE_URL", "http://localhost:3000")
adminPw := os.Getenv("ADMIN_PASSWORD")
sessionSecret := os.Getenv("SESSION_SECRET")
maxMB, _ := strconv.ParseInt(envOr("MAX_UPLOAD_MB", "5"), 10, 64)
if sessionSecret == "" {
// For dev convenience: generate an ephemeral secret if unset.
buf := make([]byte, 32)
_, _ = rand.Read(buf)
sessionSecret = hex.EncodeToString(buf)
logger.Warn("SESSION_SECRET not set; using ephemeral (sessions will not persist across restarts)")
}
s, err := store.Open(dataDir)
if err != nil {
logger.Error("open store", "err", err)
os.Exit(1)
}
defer s.Close()
secure := strings.HasPrefix(baseURL, "https://")
admin, err := auth.NewAdmin(adminPw, sessionSecret, secure)
if err != nil {
logger.Error("init admin auth", "err", err)
os.Exit(1)
}
artifactAuth := auth.NewArtifact(sessionSecret, secure)
srv, err := server.New(server.Config{
BaseURL: strings.TrimRight(baseURL, "/"),
MaxUploadMB: maxMB,
Secure: secure,
Logger: logger,
}, s, admin, artifactAuth)
if err != nil {
logger.Error("init server", "err", err)
os.Exit(1)
}
httpSrv := &http.Server{
Addr: bindAddr,
Handler: srv.Handler(),
ReadHeaderTimeout: 10 * time.Second,
}
stop := make(chan struct{})
go srv.RollupLoop(stop)
// Graceful shutdown
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
logger.Info("shutting down")
close(stop)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = httpSrv.Shutdown(ctx)
}()
logger.Info("artifacto starting", "addr", bindAddr, "base_url", baseURL, "data_dir", dataDir)
if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("http server", "err", err)
os.Exit(1)
}
}
func envOr(k, d string) string {
if v := os.Getenv(k); v != "" {
return v
}
return d
}
func logLevel() slog.Level {
switch strings.ToLower(os.Getenv("LOG_LEVEL")) {
case "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}