feat: initialize warpbox.dev project structure and backend

Initialize the repository with the core Go backend architecture and a frontend mockup for warpbox.dev, a self-hosted file-sharing application.

- Set up Go backend modules for configuration, HTTP server, middleware, handlers, and templates.
- Add local development scripts, environment templates, and basic project configuration.
- Include a React-based frontend mockup under the docs directory.
This commit is contained in:
2026-05-25 15:36:49 +03:00
parent 84e5aee87c
commit 9b8ef16474
129 changed files with 19863 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
package middleware
import "net/http"
type Middleware func(http.Handler) http.Handler
func Chain(handler http.Handler, middleware ...Middleware) http.Handler {
for i := len(middleware) - 1; i >= 0; i-- {
handler = middleware[i](handler)
}
return handler
}

View File

@@ -0,0 +1,69 @@
package middleware
import (
"compress/gzip"
"io"
"net/http"
"strings"
)
type gzipResponseWriter struct {
http.ResponseWriter
writer io.Writer
wrote bool
}
func (w *gzipResponseWriter) WriteHeader(status int) {
if w.wrote {
return
}
w.wrote = true
header := w.Header()
header.Del("Content-Length")
header.Del("Accept-Ranges")
header.Set("Content-Encoding", "gzip")
header.Add("Vary", "Accept-Encoding")
w.ResponseWriter.WriteHeader(status)
}
func (w *gzipResponseWriter) Write(data []byte) (int, error) {
if !w.wrote {
w.WriteHeader(http.StatusOK)
}
return w.writer.Write(data)
}
func Gzip(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !acceptsGzip(r) || shouldSkipGzip(r) {
next.ServeHTTP(w, r)
return
}
gz := gzip.NewWriter(w)
defer gz.Close()
next.ServeHTTP(&gzipResponseWriter{
ResponseWriter: w,
writer: gz,
}, r)
})
}
func acceptsGzip(r *http.Request) bool {
return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
}
func shouldSkipGzip(r *http.Request) bool {
if r.Method == http.MethodHead || r.Header.Get("Range") != "" {
return true
}
path := r.URL.Path
switch ext := strings.ToLower(path[strings.LastIndex(path, ".")+1:]); ext {
case "br", "gz", "zip", "7z", "rar", "jpg", "jpeg", "png", "gif", "webp", "avif", "mp4", "webm", "mov", "m4v", "mp3", "ogg", "woff", "woff2", "ttf", "otf":
return true
default:
return false
}
}

View File

@@ -0,0 +1,63 @@
package middleware
import (
"io"
"net/http"
"net/http/httptest"
"testing"
)
func TestGzipCompressesEligibleResponses(t *testing.T) {
handler := Gzip(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "11")
_, _ = io.WriteString(w, "hello world")
}))
request := httptest.NewRequest(http.MethodGet, "/page", nil)
request.Header.Set("Accept-Encoding", "gzip")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if got := response.Header().Get("Content-Encoding"); got != "gzip" {
t.Fatalf("Content-Encoding = %q, want gzip", got)
}
if got := response.Header().Get("Content-Length"); got != "" {
t.Fatalf("Content-Length = %q, want empty for gzipped response", got)
}
if got := response.Header().Get("Vary"); got != "Accept-Encoding" {
t.Fatalf("Vary = %q, want Accept-Encoding", got)
}
}
func TestGzipSkipsRangeAndHeadRequests(t *testing.T) {
handler := Gzip(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, "hello world")
}))
tests := []struct {
name string
method string
rangeHeader string
}{
{name: "range", method: http.MethodGet, rangeHeader: "bytes=0-4"},
{name: "head", method: http.MethodHead},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
request := httptest.NewRequest(tt.method, "/asset.css", nil)
request.Header.Set("Accept-Encoding", "gzip")
if tt.rangeHeader != "" {
request.Header.Set("Range", tt.rangeHeader)
}
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if got := response.Header().Get("Content-Encoding"); got != "" {
t.Fatalf("Content-Encoding = %q, want empty", got)
}
})
}
}

View File

@@ -0,0 +1,54 @@
package middleware
import (
"log/slog"
"net/http"
"time"
)
type statusRecorder struct {
http.ResponseWriter
status int
bytes int
}
func (r *statusRecorder) WriteHeader(status int) {
r.status = status
r.ResponseWriter.WriteHeader(status)
}
func (r *statusRecorder) Write(data []byte) (int, error) {
if r.status == 0 {
r.status = http.StatusOK
}
n, err := r.ResponseWriter.Write(data)
r.bytes += n
return n, err
}
func Logger(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
recorder := &statusRecorder{ResponseWriter: w}
next.ServeHTTP(recorder, r)
status := recorder.status
if status == 0 {
status = http.StatusOK
}
logger.Info("http request",
"method", r.Method,
"path", r.URL.Path,
"status", status,
"bytes", recorder.bytes,
"duration_ms", time.Since(start).Milliseconds(),
"request_id", RequestIDFromContext(r.Context()),
"remote_addr", r.RemoteAddr,
"user_agent", r.UserAgent(),
)
})
}
}

View File

@@ -0,0 +1,26 @@
package middleware
import (
"log/slog"
"net/http"
"runtime/debug"
)
func Recoverer(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if recovered := recover(); recovered != nil {
logger.Error("panic recovered",
"error", recovered,
"stack", string(debug.Stack()),
"request_id", RequestIDFromContext(r.Context()),
)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
}

View File

@@ -0,0 +1,38 @@
package middleware
import (
"context"
"crypto/rand"
"encoding/hex"
"net/http"
)
type requestIDKey struct{}
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = newRequestID()
}
w.Header().Set("X-Request-ID", requestID)
ctx := context.WithValue(r.Context(), requestIDKey{}, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func RequestIDFromContext(ctx context.Context) string {
if value, ok := ctx.Value(requestIDKey{}).(string); ok {
return value
}
return ""
}
func newRequestID() string {
var data [16]byte
if _, err := rand.Read(data[:]); err != nil {
return "request-id-unavailable"
}
return hex.EncodeToString(data[:])
}

View File

@@ -0,0 +1,16 @@
package middleware
import "net/http"
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := w.Header()
header.Set("X-Content-Type-Options", "nosniff")
header.Set("X-Frame-Options", "DENY")
header.Set("Referrer-Policy", "strict-origin-when-cross-origin")
header.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
header.Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self'; style-src 'self'; script-src 'self'; base-uri 'self'; frame-ancestors 'none'")
next.ServeHTTP(w, r)
})
}