diff --git a/chfs-ng/.dockerignore b/chfs-ng/.dockerignore new file mode 100644 index 0000000..c82fbf7 --- /dev/null +++ b/chfs-ng/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +bin +data +*.log +*.tmp +.DS_Store diff --git a/chfs-ng/Dockerfile b/chfs-ng/Dockerfile new file mode 100644 index 0000000..e2cd3ee --- /dev/null +++ b/chfs-ng/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.24-alpine AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +ARG VERSION=dev +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" -o /out/chfs-ng ./cmd/chfs + +FROM alpine:3.22 +RUN apk add --no-cache ca-certificates tzdata +COPY --from=build /out/chfs-ng /usr/local/bin/chfs-ng +RUN mkdir -p /data && chmod 755 /data +VOLUME ["/data"] +EXPOSE 80 +ENTRYPOINT ["/usr/local/bin/chfs-ng"] +CMD ["-path", "/data", "-port", "80"] +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget -q -O - http://127.0.0.1/healthz >/dev/null || exit 1 diff --git a/chfs-ng/Makefile b/chfs-ng/Makefile new file mode 100644 index 0000000..d43e0ac --- /dev/null +++ b/chfs-ng/Makefile @@ -0,0 +1,29 @@ +APP := chfs-ng +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +LDFLAGS := -s -w -X main.version=$(VERSION) + +.PHONY: build test race vet check run docker-build docker-run + +build: + CGO_ENABLED=0 go build -trimpath -ldflags '$(LDFLAGS)' -o bin/$(APP) ./cmd/chfs + +test: + go test ./... + +race: + go test -race ./... + +vet: + go vet ./... + +check: test race vet + git diff --check + +run: + go run -ldflags '$(LDFLAGS)' ./cmd/chfs + +docker-build: + docker build --build-arg VERSION=$(VERSION) -t $(APP):$(VERSION) -t $(APP):latest . + +docker-run: + docker run --rm -p 8080:80 -v $(PWD)/data:/data $(APP):latest -path /data -port 80 diff --git a/chfs-ng/cmd/chfs/main.go b/chfs-ng/cmd/chfs/main.go index 0718976..52a7b03 100644 --- a/chfs-ng/cmd/chfs/main.go +++ b/chfs-ng/cmd/chfs/main.go @@ -16,7 +16,10 @@ import ( "chfs-ng/internal/storage" ) +var version = "dev" + func main() { + showVersion := flag.Bool("version", false, "print version and exit") file := flag.String("file", "", "configuration file") pathFlag := flag.String("path", "/data", "shared path") port := flag.Int("port", 80, "listen port") @@ -24,6 +27,11 @@ func main() { var rules multiFlag flag.Var(&rules, "rule", "access rule (repeatable)") flag.Parse() + if *showVersion { + println(version) + return + } + api.Version = version cfg := config.Default() if *file != "" { diff --git a/chfs-ng/deploy/docker-compose.yml b/chfs-ng/deploy/docker-compose.yml new file mode 100644 index 0000000..e7a618a --- /dev/null +++ b/chfs-ng/deploy/docker-compose.yml @@ -0,0 +1,13 @@ +services: + chfs: + build: + context: .. + args: + VERSION: ${VERSION:-dev} + image: chfs-ng:${VERSION:-latest} + restart: unless-stopped + ports: + - "8080:80" + volumes: + - ./data:/data + command: ["-path", "/data", "-port", "80"] diff --git a/chfs-ng/internal/api/health.go b/chfs-ng/internal/api/health.go new file mode 100644 index 0000000..37c7f52 --- /dev/null +++ b/chfs-ng/internal/api/health.go @@ -0,0 +1,24 @@ +package api + +import ( + "encoding/json" + "net/http" +) + +type healthResponse struct { + Status string `json:"status"` + Version string `json:"version"` +} + +var Version = "dev" + +func (s *Server) health(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.Header().Set("Allow", "GET, HEAD") + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + _ = json.NewEncoder(w).Encode(healthResponse{Status: "ok", Version: Version}) +} diff --git a/chfs-ng/internal/api/root_handler_test.go b/chfs-ng/internal/api/root_handler_test.go index ddf4b6d..0fded73 100644 --- a/chfs-ng/internal/api/root_handler_test.go +++ b/chfs-ng/internal/api/root_handler_test.go @@ -33,3 +33,23 @@ func TestRootEscapesPlainNotice(t *testing.T) { t.Fatal("plain notice was not escaped") } } + +func TestHealthz(t *testing.T) { + s := &Server{} + old := Version + Version = "test" + defer func() { Version = old }() + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/healthz", nil) + s.Handler().ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rr.Code, rr.Body.String()) + } + if got := rr.Header().Get("Cache-Control"); got != "no-store" { + t.Fatalf("cache-control=%q", got) + } + if !strings.Contains(rr.Body.String(), `"status":"ok"`) || !strings.Contains(rr.Body.String(), `"version":"test"`) { + t.Fatalf("unexpected body=%s", rr.Body.String()) + } +} diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 32407ca..34a2a14 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -68,6 +68,7 @@ type fileInfo struct { func (s *Server) Handler() http.Handler { mux := http.NewServeMux() + mux.HandleFunc("/healthz", s.health) mux.HandleFunc("/webdav/", s.webdav) mux.HandleFunc("/webdav", s.webdav) mux.HandleFunc("/chfs/files", s.files)