No description
  • Go 87.1%
  • HTML 6.4%
  • JavaScript 3.2%
  • CSS 2.1%
  • PowerShell 0.6%
  • Other 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Carsten Abele 7fa2618bac MobileUploader: Foto-Upload auf Netzwerkshares als PWA
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/.
2026-10-07 13:44:44 +02:00
deploy MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
internal MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
.gitattributes MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
.gitignore MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
build.ps1 MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
dev-env.ps1 MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
go.mod MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
go.sum MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
Makefile MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00
README.md MobileUploader: Foto-Upload auf Netzwerkshares als PWA 2026-10-07 13:44:44 +02:00

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:

  1. Verbindung, Anmeldung und Zugriff auf die Freigabe bzw. den Bucket
  2. Basisordner lesbar
  3. Basisordner anlegbar
  4. Schreiben einer Testdatei
  5. Zurücklesen und Vergleich des Inhalts
  6. 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

  1. Route auflösen und Zugriffsschutz prüfen
  2. Gesamtgröße des Requests begrenzen (FU_MAX_UPLOAD)
  3. Multipart-Stream lesen: Dateien wandern in temporäre Dateien, Textfelder werden gesammelt — dabei entstehen Größe und SHA256
  4. Vorgang und Kommentare festhalten
  5. Je Datei: Endung prüfen, Ablagepfad rendern, freien Namen finden, Ordner anlegen, schreiben, Zusatzdatei daneben legen
  6. 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. localhost ist 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.