webdav: replace Python WsgiDAV with Go server
ci/woodpecker/push/woodpecker Pipeline was successful
Details
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:
parent
6536aa4c2a
commit
245d852f87
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
7
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
|
||||
)
|
||||
|
|
|
|||
7
go.sum
7
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=
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
wsgidav>=4.0.0
|
||||
cheroot>=10.0.0
|
||||
Loading…
Reference in New Issue