webdav: replace Python WsgiDAV with Go server
ci/woodpecker/push/woodpecker Pipeline was successful Details

- Add cmd/webdav (golang.org/x/net/webdav), serves /data at /
- GET / returns simple HTML; PROPFIND / returns 207 multistatus for Finder
- Multi-stage Dockerfile, entrypoint runs /webdav; compose build from repo root
- Remove webdav/app.py and requirements.txt

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leopere 2026-02-24 16:44:46 -05:00
parent 6536aa4c2a
commit 245d852f87
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
8 changed files with 79 additions and 101 deletions

52
cmd/webdav/main.go Normal file
View File

@ -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(`<!DOCTYPE html><html><head><title>WebDAV</title></head><body><h1>WebDAV</h1><p>Connect in Finder: Go → Connect to Server → <code>` + r.Host + `</code></p></body></html>`))
return
}
w.Handler.ServeHTTP(rw, r)
}

View File

@ -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

7
go.mod
View File

@ -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
)

7
go.sum
View File

@ -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=

View File

@ -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"]

View File

@ -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()

View File

@ -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

View File

@ -1,2 +0,0 @@
wsgidav>=4.0.0
cheroot>=10.0.0