- Go 87.1%
- HTML 6.4%
- JavaScript 3.2%
- CSS 2.1%
- PowerShell 0.6%
- Other 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Go-Anwendung, die Fotos vom Handy entgegennimmt und über SMB, SFTP, S3 oder einen lokalen Pfad ablegt. Ausgeliefert wird ein einzelnes Binary mit eingebettetem Frontend - ohne Docker und ohne Laufzeitabhängigkeiten. Funktionsumfang: - Routen als eigene Adressen wie /Wareneingang, jeweils mit Zielordner, Ablage-Template, Kommentar-Vorgaben und optionalem PIN- oder Token-Schutz - Verwaltungsbereich hinter argon2id-Passwort: Routen, Ablageziele und Upload-Protokoll mit Suche und CSV-Export - Ablageziele mit Verbindungstest: Verbindung, Schreiben, Zurücklesen - Kommentare als Zusatzdatei neben dem Foto, als JSON oder Text - Aufnahmezeit aus den Bilddaten, ersatzweise die Upload-Zeit - PWA mit Manifest, Service Worker und zur Laufzeit gezeichneten Icons Sicherheit: Geheimnisse liegen AES-GCM-verschlüsselt in der Datenbank, Pfad-Traversal wird in Vorlage und Ablageziel abgewiesen, PIN und Token werden nie aus dem Multipart-Body gelesen. Verifiziert mit gofmt, go vet und go test über 13 Pakete sowie HTTP-Durchläufen gegen das gebaute Binary. Nicht gegen ein echtes SMB-Share getestet; der Integrationstest ist im README beschrieben. Deployment: systemd-Unit und Installationsskript unter deploy/. |
||
| deploy | ||
| internal | ||
| .gitattributes | ||
| .gitignore | ||
| build.ps1 | ||
| dev-env.ps1 | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
MobileUploader
Foto-Upload für Handys auf Netzwerkshares — als progressive Web App, bedient von einem einzigen Go-Binary ohne Docker und ohne Laufzeitabhängigkeiten.
Mitarbeiter öffnen am Handy https://fotouploader.domain.local/Wareneingang,
wählen die zutreffenden Kommentare aus und fotografieren. Die Bilder landen
strukturiert auf dem Share, die Kommentare als Sidecar-Datei daneben.
Funktionsumfang
Geplant und in Umsetzung:
- Mehrere Routen als eigene URLs:
/Wareneingang,/Abnahme, beliebig viele - Pro Route ein eigener Zielordner mit konfigurierbarem Ablage-Template
- Zielsysteme: SMB (NTLMv2, optional mit Domäne), SFTP, S3 und lokaler Pfad. Jedes Ziel lässt sich im Admin-Bereich anlegen, testen und der Zielordner erstellen.
- Kommentare: pro Route eine vordefinierte Liste als Checkboxen plus Freitextfeld. Die Kommentare gelten für den Upload-Vorgang und werden als Sidecar-Datei neben jedes Foto geschrieben.
- Admin-Bereich hinter Passwort: Routen, Ziele, Kommentar-Vorgaben, Upload-Log mit Suche und CSV-Export
- Routen-Schutz optional pro Route: offen, PIN oder Link-Token
- Bewusst schlank: keine clientseitige Bildkompression, keine Offline-Warteschlange, keine HEIC-Konvertierung. Originale bleiben Originale.
Ablageziele
Ein Ziel beschreibt, wohin die Fotos gehen. Alle Typen teilen dieselbe Schnittstelle, sodass Routen und Upload-Logik den Typ nicht kennen müssen.
| Typ | Beschreibung | Pfadtrenner |
|---|---|---|
local |
Verzeichnis im Dateisystem, auch ein bereits eingebundener CIFS/NFS-Mount | / |
smb |
SMB2/3-Freigabe, Anmeldung per NTLMv2 mit optionaler Domäne | \ intern |
sftp |
SFTP über SSH, Passwort und/oder privater Schlüssel | / |
s3 |
S3-kompatibler Objektspeicher (MinIO, Ceph, Wasabi, AWS) | / |
Jedes Ziel kennt Test(), EnsureDir(), Put(), Get(), Exists() und
Delete(). Verbindung testen im Admin-Bereich führt aus:
- Verbindung, Anmeldung und Zugriff auf die Freigabe bzw. den Bucket
- Basisordner lesbar
- Basisordner anlegbar
- Schreiben einer Testdatei
- Zurücklesen und Vergleich des Inhalts
- Aufräumen
Alle Schritte laufen durch, auch wenn einer fehlschlägt — so sieht man in einem Durchgang alles, was nicht stimmt. Die Testdatei wird anschließend entfernt.
Zugangsdaten
Geheime Felder der Zielkonfiguration (Passwörter, Schlüssel, Access Keys)
liegen ausschließlich AES-GCM-verschlüsselt in der Datenbank und enden auf
_enc. Beschreibung() gibt Ziele für Logs aus und enthält niemals
Geheimnisse — weder im Klartext noch als Geheimtext. Das ist als Test
abgesichert.
Pfadsicherheit
Alle Backends arbeiten mit Pfaden relativ zur Zielwurzel. PfadNormalisieren
weist absolute Pfade, ..-Segmente, Null-Bytes und Laufwerksangaben ab; die
Local-Variante prüft nach dem Zusammensetzen zusätzlich, dass das Ergebnis die
Basis nicht verlässt. Das ist die zentrale Absicherung gegen Pfad-Traversal,
denn die Ablage-Templates enthalten Dateinamen aus dem Browser.
HEIC und EXIF
HEIC/HEIF wird unverändert abgelegt — es findet keine Konvertierung statt.
.heic und .heif stehen deshalb in der Standardliste der erlaubten
Endungen, sonst würden iPhone-Fotos abgelehnt.
Aufnahmezeit aus den Bilddaten
Für die Ablage zählt die Aufnahmezeit, nicht die Upload-Zeit: Ein Foto vom Samstag landet im Samstagsordner, auch wenn es am Montag hochgeladen wird. Gelesen werden JPEG, TIFF, PNG, die RAW-Formate gängiger Kameras sowie HEIF/HEIC.
Der Zeitstempel steht auch in der Zusatzdatei, zusammen mit seiner Herkunft:
"zeitpunkt": "2024-03-15T08:05:00Z",
"hochgeladen_am": "2026-10-07T09:16:46Z",
"aufnahmezeit_quelle": "exif"
aufnahmezeit_quelle ist exif, wenn die Zeit aus den Bilddaten stammt, und
upload, wenn die Datei keine verwertbaren Metadaten enthielt — etwa ein Scan
oder ein bearbeitetes Bild. In der Textvariante steht die Herkunft als
Klammerzusatz hinter der Zeit.
Unplausible Werte werden verworfen. Kameras mit falsch gestellter Uhr liefern gelegentlich Zeitstempel aus der Zukunft oder aus dem Jahr 1900; solche Werte würden die Ablage in absurde Ordner schreiben. Sie werden verworfen und durch die Upload-Zeit ersetzt. Als plausibel gilt alles ab 1980 bis einen Tag in der Zukunft.
In der Spalte exif_zeit des Protokolls steht nur eine tatsächlich
gelesene Aufnahmezeit. Ist keine vorhanden, bleibt sie leer — dort die
Upload-Zeit einzutragen wäre irreführend.
Die Formaterkennung stammt aus der Bibliothek
imagemeta. Getestet ist die Auswertung mit JPEG; für HEIF ist sie laut Bibliothek vorgesehen, wurde hier aber nicht mit einer echten iPhone-Datei nachgestellt.
SMB-Integrationstest
Für SMB gibt es keinen Go-Server, deshalb läuft dieser Test nur gegen ein echtes Share:
$env:MU_SMB_HOST = 'fileserver'
$env:MU_SMB_SHARE = 'fotos'
$env:MU_SMB_USER = 'foto'
$env:MU_SMB_PASS = '...'
$env:MU_SMB_DOMAIN = 'FIRMA' # optional
$env:MU_SMB_BASIS = 'mobileuploader-test' # optional
go test ./internal/backend -run TestSMBGegenEchtenServer -v
Ohne MU_SMB_HOST wird der Test übersprungen.
Aktueller Stand
| Meilenstein | Inhalt | Status |
|---|---|---|
| M0 | Gerüst, Konfiguration, SQLite-Schema, HTTP-Server, Deployment | fertig |
| M1 | Storage-Backends (SMB, SFTP, S3, Local) mit Test-Funktion | fertig |
| M2 | Admin-Auth und Admin-UI, Ziele-Verwaltung | fertig |
| M3 | Routen- und Kommentar-Verwaltung, Template-Engine | fertig |
| M4 | Upload-API, Sidecar, Upload-Log | fertig |
| M5 | PWA-Oberfläche | fertig |
| M6 | EXIF-Zeitstempel, Rate-Limits, Release-Build | fertig |
Schnellstart
# Entwicklungsumgebung: Go-Caches bleiben im Projekt
. .\dev-env.ps1
go run ./cmd/fotouploader -data-dir .\data -listen 127.0.0.1:8080 -log-level debug
Danach im Browser: http://127.0.0.1:8080
# Tests, Vet und Format
go test ./...
go vet ./...
gofmt -l ./cmd ./internal
Release bauen
.\build.ps1 # linux/amd64, linux/arm64, windows/amd64
.\build.ps1 -Version 1.0.0 # mit Versionskennung
.\build.ps1 -NurHost # nur das aktuelle System
make release # dasselbe unter Linux oder macOS
Die Artefakte liegen in dist/ und sind rund 18 bis 19 MiB groß. Version,
Commit und Build-Zeit werden über ldflags eingesetzt und erscheinen in
/healthz.
Konfiguration
Vorrang: Kommandozeilen-Flag > Umgebungsvariable > Default.
| Flag | Umgebungsvariable | Default | Bedeutung |
|---|---|---|---|
-listen |
FU_LISTEN |
127.0.0.1:8080 |
Adresse des HTTP-Listeners |
-data-dir |
FU_DATA_DIR |
data |
SQLite, master.key, Upload-Temp |
-base-url |
FU_BASE_URL |
– | externe URL, steuert Links und Secure-Cookies |
-master-key |
FU_MASTER_KEY |
– | Schlüssel für die Zugangsdaten der Ziele |
-trust-proxy |
FU_TRUST_PROXY |
false |
X-Forwarded-* auswerten |
-log-level |
FU_LOG_LEVEL |
info |
debug, info, warn, error |
-log-format |
FU_LOG_FORMAT |
text |
text oder json |
-max-upload |
FU_MAX_UPLOAD |
536870912 |
maximale Request-Größe in Bytes |
-temp-ttl |
– | 24h |
Aufräumalter verwaister Temp-Dateien |
.\fotouploader.exe -h zeigt dieselbe Übersicht.
Master-Key
Die Zugangsdaten der Storage-Ziele liegen AES-GCM-verschlüsselt in der
Datenbank. Den Schlüssel dafür holt sich der Dienst aus FU_MASTER_KEY oder
aus <data-dir>/master.key, die er beim ersten Start mit den Rechten 0600
selbst anlegt.
openssl rand -base64 32
Für Backups ist ein expliziter FU_MASTER_KEY praktischer: Geht die
master.key verloren, sind die gespeicherten Zugangsdaten unwiederbringlich
und müssen neu erfasst werden. Der Dienst weist beschädigte Schlüsseldateien
ausdrücklich zurück, statt stillschweigend einen falschen Schlüssel
abzuleiten.
Routen
Eine Route ist eine öffentliche Adresse, unter der Fotos hochgeladen werden —
etwa /Wareneingang oder /Abnahme. Sie bündelt alles, was für einen
Upload-Ort gilt:
| Angabe | Wirkung |
|---|---|
| Adresse | Der Slug in der URL. Wird er leer gelassen, wird er aus dem Titel abgeleitet. Groß- und Kleinschreibung spielt keine Rolle. |
| Ablageziel | Wohin die Dateien gehen. Ohne Ziel kann die Route nichts ablegen. |
| Unterordner | Basisordner innerhalb des Ziels. |
| Ablage-Template | Der Pfad innerhalb des Ziels, siehe unten. |
| Zusatzdatei | Ob und wie die Kommentare neben dem Foto abgelegt werden: JSON, Text oder gar nicht. |
| Erlaubte Endungen | Whitelist. heic und heif gehören dazu, sonst werden iPhone-Fotos abgelehnt. |
| Maximale Dateigröße | Eigene Obergrenze pro Datei, 0 bedeutet keine. |
| Kommentar-Vorgaben | Ein Eintrag pro Zeile, erscheint am Handy als Auswahlfeld. |
| Zugriffsschutz | Offen, PIN oder Link-Token. |
Reservierte Adressen (admin, static, healthz, api) werden abgewiesen,
damit eine Route nicht mit der Anwendung selbst kollidiert.
Ablage-Template
Das Template bestimmt den Pfad im Zielordner.
| Platzhalter | Bedeutung | Beispiel |
|---|---|---|
{jahr} {monat} {tag} |
Datum in Teilen | 2026 10 07 |
{datum} |
Datum vollständig | 2026-10-07 |
{zeit} {stunde} {minute} {sekunde} |
Uhrzeit | 14-30-05 |
{route} |
Slug der Route | wareneingang |
{vorgang} |
Kennung des Upload-Vorgangs | 20261007-143005-a1b2c3 |
{datei} |
Dateiname ohne Endung | IMG_1234 |
{ext} |
Endung ohne Punkt, klein | jpg |
{original} |
vollständiger Dateiname | IMG_1234.JPG |
{n} |
laufende Nummer im Vorgang | 1 |
Vorgabe: {jahr}/{monat}/{datum}/{vorgang}_{n}.{ext}
Das Admin-UI zeigt zu jedem Template eine Vorschau mit drei Beispieldateien. Vorschau und Speichern sind getrennte Schaltflächen — die Vorschau legt nichts an.
Pfadsicherheit
Die Dateinamen kommen aus dem Browser und sind nicht vertrauenswürdig. Deshalb
werden die Platzhalterwerte vor dem Einsetzen bereinigt, nicht erst danach:
Ein Dateiname wie ../../etc/passwd kann so keine zusätzlichen Pfadebenen
einschleusen.
Zusätzlich gilt für jeden Namensbestandteil:
..-Segmente, absolute Pfade und Null-Bytes werden abgewiesen- Windows-Verbotszeichen (
< > : " / \ | ? *) und Steuerzeichen werden ersetzt - führende und abschließende Punkte und Leerzeichen entfallen
- mehrfache Punkte werden zusammengefasst
- reservierte Gerätenamen (
CON,NUL,LPT1, …) werden entschärft - zu lange Namen werden gekürzt, die Endung bleibt erhalten
Umlaute und Leerzeichen innerhalb eines Namens bleiben erhalten.
Upload
Ein Upload läuft als ein Vorgang (POST /api/upload/<slug>) und kann
mehrere Fotos enthalten. Die Kommentare gelten für den gesamten Vorgang und
werden bei jedem Foto als Zusatzdatei mitgeschrieben.
Ablauf
- Route auflösen und Zugriffsschutz prüfen
- Gesamtgröße des Requests begrenzen (
FU_MAX_UPLOAD) - Multipart-Stream lesen: Dateien wandern in temporäre Dateien, Textfelder werden gesammelt — dabei entstehen Größe und SHA256
- Vorgang und Kommentare festhalten
- Je Datei: Endung prüfen, Ablagepfad rendern, freien Namen finden, Ordner anlegen, schreiben, Zusatzdatei daneben legen
- Ergebnis je Datei zurückmelden
Das Zwischenspeichern kostet einen zusätzlichen Schreibvorgang, hat aber drei
Vorteile: die Reihenfolge der Formularfelder spielt keine Rolle, die Größe
ist vor dem Schreiben bekannt (SMB und S3 brauchen sie), und der SHA256 fällt
beim Lesen nebenbei ab. Das temporäre Verzeichnis wird nach dem Vorgang
vollständig entfernt; Reste abgebrochener Vorgänge räumt der Dienst beim Start
nach der Frist aus -temp-ttl weg (Vorgabe 24 Stunden).
Antwort
{
"vorgang": "20261007-143005-a1b2c3",
"erfolgreich": 2,
"fehlgeschlagen": 0,
"ergebnisse": [
{"datei": "IMG_0001.JPG", "ok": true,
"ziel_pfad": "2026/10/2026-10-07/20261007-143005-a1b2c3_1.jpg",
"sidecar": "2026/10/2026-10-07/20261007-143005-a1b2c3_1.jpg.json",
"groesse": 15, "sha256": "773a9a47…"}
]
}
Eine einzelne abgelehnte Datei bricht den Vorgang nicht ab: Die übrigen werden weiterhin abgelegt und der Fehler steht im Ergebnis und im Protokoll.
Namenskonflikte
Enthält das Ablage-Template {vorgang}, sind die Namen von sich aus
eindeutig. Fehlt es, ergäben zwei Vorgänge denselben Pfad. Statt eine
vorhandene Datei zu überschreiben oder am Widerstand des Ziels zu scheitern,
wird ein Zähler angehängt: foto_1.jpg, foto_1_2.jpg, foto_1_3.jpg.
Zugriffsschutz im Upload
| Modus | Übergabe |
|---|---|
| offen | keine |
| PIN | Kopfzeile X-Upload-Pin oder ?pin= |
| Link-Token | Kopfzeile X-Upload-Token oder ?t= |
PIN und Token werden nie aus dem Formular gelesen: Das würde den Multipart-Body parsen und damit den gesamten Upload in den Speicher laden. Nach zehn Fehlversuchen ist eine Route für die jeweilige Quelladresse 15 Minuten gesperrt — gezählt wird pro Adresse und Route, damit eine blockierte Route andere nicht mitzieht.
Mengenbegrenzung
Je Quelladresse sind 60 Upload-Vorgänge in 10 Minuten erlaubt. Der Wert ist
großzügig genug für eifrige Nutzung und bremst zugleich Skripte, die den Dienst
fluten. Die Prüfung greift vor allem anderen — noch bevor die Route aufgelöst
und der Zugriffsschutz geprüft wird. Ist die Grenze erreicht, antwortet der
Dienst mit 429 und der Restdauer.
Gezählt wird im Speicher und pro Adresse, nicht pro Route: Ein einzelner Vielnutzer kann damit nicht alle Routen für alle sperren.
Zusatzdatei
Die Zusatzdatei trägt Route, Vorgang, Zeitpunkt, Originalname, Zielpfad,
Größe, SHA256, Ziel, Kommentare, Freitext und Quelladresse. Sie liegt als
<foto>.<ext>.json oder .txt daneben, je nach Einstellung der Route.
Schlägt nur die Zusatzdatei fehl, gilt der Upload trotzdem als erfolgreich: Das Foto liegt am Ziel, und sein Fehlen wird protokolliert statt den Vorgang scheitern zu lassen.
Upload-Protokoll
Unter /admin/uploads steht jeder Upload mit Filter nach Route, Status,
Zeitraum und Volltextsuche über Dateiname, Zielpfad, Kommentare, Freitext und
Fehlermeldung. Einzelne Einträge haben eine Detailseite.
Der Eintrag entsteht vor dem Schreiben. Ein abgebrochener Vorgang bleibt dadurch als „offen" sichtbar, statt eine Lücke im Protokoll zu hinterlassen.
Der CSV-Export (/admin/uploads.csv) übernimmt die gesetzten Filter, verwendet
Semikolon als Trennzeichen und beginnt mit einem Byte-Order-Mark — beides,
damit Excel in deutscher Einstellung die Datei korrekt öffnet.
Oberfläche am Handy
https://fotouploader.domain.local/Wareneingang zeigt die Upload-Seite der
Route. Sie ist auf die Bedienung mit dem Daumen ausgelegt: große
Berührflächen, keine verschachtelten Menüs, alles auf einer Seite.
Ablauf: Kommentare auswählen → optional eine Bemerkung tippen → Fotos aufnehmen oder aus der Galerie wählen → hochladen. Der Fortschritt läuft als Balken mit Prozentangabe und übertragener Datenmenge. Danach steht je Foto, ob es abgelegt wurde und unter welchem Pfad.
Zwei Wege zur Kamera. „Foto aufnehmen" öffnet direkt die Kamera
(capture="environment"), „Aus Galerie wählen" die Fotomediathek mit
Mehrfachauswahl. Ein einzelner Knopf mit capture würde die Galerie
verschließen — für Nachzügler unbrauchbar.
Nach dem Upload: Erfolgreich abgelegte Fotos verschwinden aus der Liste,
fehlgeschlagene bleiben für einen zweiten Versuch stehen. Die Kommentare
werden erst geleert, wenn nichts mehr offen ist. Ein beforeunload-Hinweis
warnt vor dem Verlassen der Seite, solange noch Fotos ausstehen.
PIN wird in der sessionStorage gehalten, nicht in der localStorage:
Sie verschwindet mit dem Schließen des Tabs und bleibt nicht dauerhaft auf
dem Gerät.
Installierbar als App
Manifest, Icons und Service Worker macht der Dienst selbst:
| Adresse | Inhalt |
|---|---|
/manifest.webmanifest |
Manifest; mit ?start=<slug> startet die installierte App direkt in dieser Route |
/sw.js |
Service Worker |
/static/icon-192.png |
Icon, zur Laufzeit gezeichnet |
Die Icons werden zur Laufzeit gezeichnet statt als Dateien mitgeliefert. Dadurch enthält das Repository keine Binärdateien, und das Programm bleibt ein einzelnes Binary.
Der Service Worker speichert die Oberfläche (app.css, upload.js, Icons)
und zeigt Seiten bei fehlender Verbindung aus dem Zwischenspeicher.
Ausdrücklich nicht gespeichert werden /admin und /api/: Dort stehen
Testergebnisse und Protokolle, die aktuell sein müssen.
Eine Offline-Warteschlange für Uploads gibt es bewusst nicht. Ein Upload, der scheinbar gelingt und dessen Foto nie ankommt, wäre der schlimmste denkbare Fehler in diesem Anwendungsfall. Die Seite meldet stattdessen ehrlich, dass die Verbindung fehlt.
HTTPS ist Pflicht, sobald vom Handy fotografiert oder die App installiert werden soll: Browser geben Kamera und Service Worker nur in einem sicheren Kontext frei.
localhostist die einzige Ausnahme.
Verwaltungsbereich
Erreichbar unter /admin/. Beim ersten Start führt der Bereich zur
Einrichtung, in der ein Passwort vergeben wird; danach ist nur noch die
Anmeldung möglich.
- Übersicht: Kennzahlen und Hinweise, etwa wenn noch kein Ablageziel existiert
- Routen: anlegen, bearbeiten, löschen, Ablage-Vorschau
- Ablageziele: anlegen, bearbeiten, löschen und die Verbindung testen
- Uploads: Protokoll mit Filter, Suche und CSV-Export
- Passwort ändern: beendet alle laufenden Sitzungen
Anmeldung und Sitzung
Das Passwort liegt als argon2id-Hash (64 MiB, 2 Durchläufe, PHC-Format) in der
Tabelle settings. Die Sitzung ist ein signiertes, zustandsloses Cookie: es
enthält nur Ausstellungs- und Ablaufzeit und ist mit einem eigenen
Sitzungsgeheimnis per HMAC-SHA256 gesichert. Das Cookie ist HttpOnly,
SameSite=Lax und bei HTTPS zusätzlich Secure.
Beim Ändern des Passworts wird das Sitzungsgeheimnis gewechselt. Dadurch werden alle ausgestellten Cookies wertlos — auch die einer möglicherweise gestohlenen Sitzung. Die laufende Sitzung endet dabei ebenfalls.
Nach zehn Fehlversuchen von derselben Adresse wird die Anmeldung für 15 Minuten gesperrt. Die Zählung liegt im Speicher und ist nach einem Neustart wieder leer.
Formulare
Jedes Formular trägt ein CSRF-Token, das als Cookie gesetzt und im Formular mitgeschickt wird. Fehlt es oder passt es nicht, wird die Anfrage mit 403 abgewiesen — unabhängig davon, ob sie sonst gültig wäre.
Umgang mit Zugangsdaten
Geheimnisse werden im Formular nie zurückgegeben, auch nicht als Geheimtext. Stattdessen erscheint der Hinweis, dass bereits ein Wert hinterlegt ist. Bleibt das Feld beim Speichern leer, bleibt der gespeicherte Wert erhalten — man muss also nicht bei jeder Änderung alle Passwörter neu eintippen.
Die Feldbeschreibungen der vier Zieltypen liegen an einer Stelle im Code
(felderFuer). Daraus entstehen sowohl das Formular als auch die
Verschlüsselung und die Validierung. Ein neuer Zieltyp braucht damit nur eine
Feldliste und ein Backend — kein weiteres Template.
Deployment (Linux, systemd)
Kein Docker. Auf dem Zielsystem liegt genau ein Binary.
# 1. Binary bauen (auf dem Entwicklungsrechner)
make release # oder: .\build.ps1
# 2. Auf den Server kopieren und installieren
scp dist/fotouploader-linux-amd64 server:/tmp/
scp -r deploy server:/tmp/
ssh server 'cd /tmp/deploy && sudo ./install.sh /tmp/fotouploader-linux-amd64'
Das Installationsskript legt einen Systembenutzer an, installiert das Binary
nach /usr/local/bin/fotouploader, bereitet /var/lib/mobileuploader vor und
richtet die systemd-Unit ein. Anschließend /etc/mobileuploader.env anpassen
und systemctl restart mobileuploader.
Die Unit bringt Härtung mit: ProtectSystem=strict, ProtectHome,
NoNewPrivileges, PrivateTmp, leere CapabilityBoundingSet und
Schreibzugriff ausschließlich auf /var/lib/mobileuploader.
Reverse Proxy
TLS terminiert der Proxy; der Dienst selbst spricht HTTP.
nginx
server {
listen 443 ssl;
server_name fotouploader.domain.local;
ssl_certificate /etc/ssl/certs/fotouploader.crt;
ssl_certificate_key /etc/ssl/private/fotouploader.key;
# Muss zu -max-upload passen, sonst bricht nginx große Uploads ab.
client_max_body_size 512m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_request_buffering off;
}
}
Caddy — erledigt TLS automatisch:
fotouploader.domain.local {
reverse_proxy 127.0.0.1:8080
request_body {
max_size 512MB
}
}
Mit FU_TRUST_PROXY=true wertet der Dienst X-Forwarded-For und
X-Forwarded-Proto aus. Das nur aktivieren, wenn tatsächlich ein eigener Proxy
davorsteht — sonst sind diese Kopfzeilen fälschbar.
HTTPS ist Pflicht, sobald vom Handy fotografiert werden soll: Browser geben
Kamera und Service Worker nur in einem sicheren Kontext frei. localhost ist
die einzige Ausnahme.
Entwicklung
cmd/fotouploader/ Einstiegspunkt, Flags, Logging, Graceful Shutdown
internal/config/ Konfiguration aus Flags und Umgebung
internal/secrets/ Master-Key und AES-GCM für Zugangsdaten
internal/backend/ Ablageziele: local, smb, sftp, s3 und gemeinsame Schnittstelle
internal/upload/ Upload-Endpunkt und Oberflaeche am Handy
internal/pwa/ Manifest, Icons (zur Laufzeit gezeichnet), Service Worker
internal/exif/ Aufnahmezeit aus den Bilddaten lesen
internal/ratelimit/ Sperre nach Fehlversuchen, Mengenbegrenzung je Fenster
internal/sidecar/ Zusatzdatei neben dem Foto (JSON oder Text)
internal/admin/ Verwaltungsbereich: Anmeldung, Routen, Ziele, Vorlagen
internal/vorlage/ Ablage-Template: Platzhalter, Bereinigung, Vorschau
internal/passwort/ argon2id-Hashing für Admin-Passwort und Route-PINs
internal/store/ SQLite: Verbindung, Migrationen, Abfragen
internal/testbild/ Hilfspaket für Tests: JPEG mit und ohne EXIF
internal/web/ eingebettete Templates und statische Dateien
internal/server/ Routing, Middleware, Handler
internal/version/ Build-Metadaten (per ldflags gesetzt)
deploy/ systemd-Unit, Installationsskript, Konfigurationsvorlage
CGO ist deaktiviert. SQLite kommt aus modernc.org/sqlite, einem
pure-Go-Treiber. Dadurch entstehen statische Binaries ohne C-Toolchain, und
Cross-Compiling für linux/arm64 funktioniert ohne zusätzliches Werkzeug.
dev-env.ps1 legt GOPATH, GOMODCACHE, GOCACHE und GOTMPDIR in das
Projektverzeichnis. Das ist in eingeschränkten Umgebungen nötig, in denen das
Standard-Cache-Verzeichnis nicht beschreibbar ist, und hält das System sauber.
Tests
Unit-Tests laufen ohne externe Dienste. Für SFTP und S3 startet der Test jeweils einen echten Server im Testprozess — einen SSH-/SFTP-Server und einen S3-Dienst — sodass gegen das reale Protokoll getestet wird statt gegen eine Attrappe. Nur SMB braucht ein echtes Share (siehe Abschnitt SMB-Integrationstest).
go test -race setzt CGO und damit einen C-Compiler voraus. Im
Standardaufbau mit CGO_ENABLED=0 steht der Race-Detektor deshalb nicht zur
Verfügung.
Datenbank
Migrationen liegen in internal/store/migrations/ und sind eingebettet. Sie
werden beim Start der Reihe nach angewandt und in schema_migrations
protokolliert. Eine neue Migration ist eine weitere Datei 0002_....sql — der
Präfix bestimmt die Reihenfolge.
Tabellen: storage_targets, routes, comment_presets, upload_jobs,
uploads, settings, audit_log.
Sicherheit
- Admin-Passwort als argon2id-Hash, Sitzungen über signierte HttpOnly-Cookies, CSRF-Token auf allen Formularen, Sperre nach zu vielen Fehlversuchen
- Zugangsdaten der Ziele ausschließlich verschlüsselt in der Datenbank
- Proxy-Kopfzeilen zählen nur bei ausdrücklichem
FU_TRUST_PROXY - Strenge CSP ohne Inline-Skript und ohne externe Quellen
- Pfad-Traversal-Schutz und Endungs-Whitelist beim Upload
- Route-PINs als argon2id-Hash mit Sperre nach zu vielen Fehlversuchen
- Mengenbegrenzung von 60 Upload-Vorgängen je Adresse und 10 Minuten
Lizenz
Noch nicht festgelegt.