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).
|
# Macmini WebDAV stack — exposes https://macmini.nixc.us (auth at tunnel, not in Python).
|
||||||
# Server (ingress) is assumed configured for this domain and key.
|
# 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).
|
# Connect in Finder: Go → Connect to Server → https://macmini.nixc.us (user genghis, pass genghis).
|
||||||
# Root path / returns empty PROPFIND; the share is at /dav so Finder can mount it.
|
|
||||||
#
|
#
|
||||||
# What works:
|
# What works:
|
||||||
# - WebDAV: no auth in app; uploads go to ~/dev/piconfigurator/bin. Rebuild after app changes: --build.
|
# - WebDAV: no auth in app; uploads go to ~/dev/piconfigurator/bin. Rebuild after app changes: --build.
|
||||||
|
|
@ -12,7 +11,9 @@
|
||||||
#
|
#
|
||||||
services:
|
services:
|
||||||
webdav:
|
webdav:
|
||||||
build: ./webdav
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: webdav/Dockerfile
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ${HOME}/dev/piconfigurator/bin:/data
|
- ${HOME}/dev/piconfigurator/bin:/data
|
||||||
|
|
|
||||||
7
go.mod
7
go.mod
|
|
@ -4,6 +4,9 @@ go 1.24.0
|
||||||
|
|
||||||
toolchain go1.24.13
|
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 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
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 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
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
|
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 .
|
FROM alpine:3.20
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
RUN apk add --no-cache ca-certificates
|
||||||
|
COPY webdav/entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
COPY --from=build /webdav /webdav
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
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
|
set -e
|
||||||
mkdir -p /data
|
mkdir -p /data
|
||||||
chmod 1777 /data
|
chmod 1777 /data
|
||||||
# Fix perms on existing content so host user can read/write
|
|
||||||
chmod -R a+rwX /data 2>/dev/null || true
|
chmod -R a+rwX /data 2>/dev/null || true
|
||||||
# New files/dirs world-readable/writable so host user can manage them
|
|
||||||
umask 0000
|
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