diff --git a/cmd/webdav/main.go b/cmd/webdav/main.go
new file mode 100644
index 0000000..31c5c9d
--- /dev/null
+++ b/cmd/webdav/main.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "golang.org/x/net/webdav"
+)
+
+const (
+ rootDir = "/data"
+ addr = ":80"
+)
+
+func main() {
+ if err := os.MkdirAll(rootDir, 0o777); err != nil {
+ log.Fatalf("mkdir %s: %v", rootDir, err)
+ }
+ abs, err := filepath.Abs(rootDir)
+ if err != nil {
+ log.Fatalf("abs %s: %v", rootDir, err)
+ }
+
+ h := &webdav.Handler{
+ FileSystem: webdav.Dir(abs),
+ LockSystem: webdav.NewMemLS(),
+ }
+ // Wrap so GET / returns a simple page instead of 403 for directory
+ http.Handle("/", &rootGETWrapper{Handler: h})
+
+ log.Printf("WebDAV listening on %s (root %s)", addr, abs)
+ if err := http.ListenAndServe(addr, nil); err != nil {
+ log.Fatalf("listen: %v", err)
+ }
+}
+
+// rootGETWrapper serves a minimal HTML page for GET / and passes everything else to WebDAV.
+type rootGETWrapper struct {
+ *webdav.Handler
+}
+
+func (w *rootGETWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodGet && (r.URL.Path == "" || r.URL.Path == "/") {
+ rw.Header().Set("Content-Type", "text/html; charset=utf-8")
+ rw.WriteHeader(http.StatusOK)
+ rw.Write([]byte(`
WebDAVWebDAV
Connect in Finder: Go → Connect to Server → ` + r.Host + `
`))
+ return
+ }
+ w.Handler.ServeHTTP(rw, r)
+}
diff --git a/docker-compose-macmini.yml b/docker-compose-macmini.yml
index 6790d12..528fcf5 100644
--- a/docker-compose-macmini.yml
+++ b/docker-compose-macmini.yml
@@ -1,8 +1,7 @@
# Macmini WebDAV stack — exposes https://macmini.nixc.us (auth at tunnel, not in Python).
# Server (ingress) is assumed configured for this domain and key.
#
-# Connect in Finder: Go → Connect to Server → https://macmini.nixc.us/dav (user genghis, pass genghis).
-# Root path / returns empty PROPFIND; the share is at /dav so Finder can mount it.
+# Connect in Finder: Go → Connect to Server → https://macmini.nixc.us (user genghis, pass genghis).
#
# What works:
# - WebDAV: no auth in app; uploads go to ~/dev/piconfigurator/bin. Rebuild after app changes: --build.
@@ -12,7 +11,9 @@
#
services:
webdav:
- build: ./webdav
+ build:
+ context: .
+ dockerfile: webdav/Dockerfile
restart: always
volumes:
- ${HOME}/dev/piconfigurator/bin:/data
diff --git a/go.mod b/go.mod
index 9b5a62b..a980bdb 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,9 @@ go 1.24.0
toolchain go1.24.13
-require golang.org/x/crypto v0.47.0
+require golang.org/x/crypto v0.48.0
-require golang.org/x/sys v0.40.0 // indirect
+require (
+ golang.org/x/net v0.50.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+)
diff --git a/go.sum b/go.sum
index d8a4490..31319b3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,13 @@
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
+golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
+golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
+golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
diff --git a/webdav/Dockerfile b/webdav/Dockerfile
index 1fa2612..5bff958 100644
--- a/webdav/Dockerfile
+++ b/webdav/Dockerfile
@@ -1,11 +1,15 @@
-FROM python:3.12-slim
-
+# Build from repo root so go.mod and cmd/webdav are available (build context: .).
+FROM golang:1.24-alpine AS build
WORKDIR /app
-RUN pip install --no-cache-dir wsgidav cheroot
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+RUN go build -o /webdav ./cmd/webdav
-COPY app.py .
-COPY entrypoint.sh /entrypoint.sh
+FROM alpine:3.20
+RUN apk add --no-cache ca-certificates
+COPY webdav/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
-
+COPY --from=build /webdav /webdav
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]
diff --git a/webdav/app.py b/webdav/app.py
deleted file mode 100644
index d80b9d0..0000000
--- a/webdav/app.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""Minimal WebDAV server. Serves /data for uploads. Auth at reverse tunnel; app allows anonymous."""
-import os
-import sys
-import traceback
-
-from wsgidav.fs_dav_provider import FilesystemProvider
-from wsgidav.wsgidav_app import WsgiDAVApp
-from wsgidav.dir_browser import WsgiDavDirBrowser
-from wsgidav.error_printer import ErrorPrinter
-from wsgidav.request_resolver import RequestResolver
-from wsgidav.http_authenticator import HTTPAuthenticator
-from wsgidav.mw.cors import Cors
-from cheroot.wsgi import Server as WSGIServer
-
-ROOT = "/data"
-PORT = 80
-def ensure_data_dir_writable() -> None:
- os.makedirs(ROOT, exist_ok=True)
- probe = os.path.join(ROOT, ".write_probe")
- try:
- with open(probe, "w") as f:
- f.write("")
- os.remove(probe)
- except OSError as e:
- print(f"FATAL: cannot write to {ROOT}: {e}", file=sys.stderr)
- sys.exit(1)
-
-
-def catch_all(app):
- """Catch uncaught exceptions so one bad request doesn't kill the server."""
-
- def wrapper(environ, start_response):
- try:
- return app(environ, start_response)
- except Exception:
- traceback.print_exc()
- start_response("500 Internal Server Error", [("Content-Type", "text/plain")])
- return [b"Internal Server Error"]
-
- return wrapper
-
-
-ensure_data_dir_writable()
-
-config = {
- "host": "0.0.0.0",
- "port": PORT,
- # Mount at /dav so root / is not special-cased (Finder PROPFIND needs real XML)
- "provider_mapping": {"/dav": FilesystemProvider(ROOT, readonly=False)},
- "middleware_stack": [
- Cors,
- ErrorPrinter,
- HTTPAuthenticator,
- WsgiDavDirBrowser,
- RequestResolver,
- ],
- "http_authenticator": {
- "domain_controller": None,
- "accept_basic": True,
- "accept_digest": True,
- "default_to_digest": False,
- },
- "simple_dc": {
- "user_mapping": {"*": True},
- },
- "hotfixes": {
- "emulate_win32_lastmod": True,
- "treat_root_options_as_asterisk": True,
- },
- "add_header_MS_Author_Via": True,
- "property_manager": True, # Required for PROPFIND to return XML (Finder mount)
- "verbose": 3,
-}
-
-app = WsgiDAVApp(config)
-app = catch_all(app)
-
-if __name__ == "__main__":
- server = WSGIServer((config["host"], config["port"]), app)
- # Avoid Finder/hung clients holding connections forever
- if hasattr(server, "connection_limit"):
- server.connection_limit = 20
- if hasattr(server, "timeout"):
- server.timeout = 30
- server.start()
diff --git a/webdav/entrypoint.sh b/webdav/entrypoint.sh
index c8f309f..ee14060 100644
--- a/webdav/entrypoint.sh
+++ b/webdav/entrypoint.sh
@@ -2,8 +2,6 @@
set -e
mkdir -p /data
chmod 1777 /data
-# Fix perms on existing content so host user can read/write
chmod -R a+rwX /data 2>/dev/null || true
-# New files/dirs world-readable/writable so host user can manage them
umask 0000
-exec python -u app.py
+exec /webdav
diff --git a/webdav/requirements.txt b/webdav/requirements.txt
deleted file mode 100644
index d85f535..0000000
--- a/webdav/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-wsgidav>=4.0.0
-cheroot>=10.0.0