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(`WebDAV

WebDAV

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