From 6949702f5ab20c3c17b6c6c645c26425bb79cc49 Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Fri, 28 Aug 2026 18:41:50 +0200 Subject: [PATCH] Foto-Upload: identische Datei kein zweites Mal ablegen (v1.6.0) [deploy] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Auftrag (PROV105) lagen 34 Fotos, davon 11 exakte Wiederholungen: eine Serie lief bei ausgeschaltetem Display nur halb durch, der zweite Anlauf lud alles noch einmal hoch. - bericht_find_identical_file() vergleicht die hochgeladene Datei gegen den Zielordner (Groesse als Vorfilter, md5 nur bei Kandidaten) - eingebaut in beide Upload-Wege: api/orders.php?action=upload_photo (PWA) und mobile_upload.php (Token-Link) - bei Treffer wird nichts gespeichert, die Antwort traegt duplicate:true UND den relpath der vorhandenen Datei — ohne relpath wertet die PWA den Upload als nicht bestaetigt und wiederholt ihn endlos (lib/api.js) Funktion isoliert geprueft: identischer Inhalt wird gefunden, neuer Inhalt und gleiche Groesse mit anderem Inhalt liefern null (kein Fehlalarm). Co-Authored-By: Claude Opus 5 (1M context) --- ChangeLog.md | 21 +++++++++++++++++++++ api/orders.php | 14 ++++++++++++++ lib/bericht.lib.php | 41 +++++++++++++++++++++++++++++++++++++++++ mobile_upload.php | 9 +++++++++ 4 files changed, 85 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index f43132e..2c77720 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,26 @@ # Changelog +## 1.6.0 — 2026-08-28 + +### Dasselbe Foto kommt kein zweites Mal in den Auftragsordner + +Am 28.08. lagen an Auftrag `(PROV105)` **34 Fotos, davon 11 exakte Wiederholungen** — eine +Serie war bei ausgeschaltetem Display nur halb durchgelaufen, der zweite Anlauf lud alles +noch einmal hoch. Der Upload ist bewusst *at-least-once* (KB #840: lieber ein Duplikat als +ein verlorenes Foto), aber er war eben auch duplikaterzeugend. + +- **`bericht_find_identical_file()`** (lib/bericht.lib.php) vergleicht die hochgeladene Datei + mit dem Inhalt des Zielordners: erst die Dateigroesse, md5 nur bei einem Kandidaten. +- Eingebaut in **beide** Upload-Wege: `api/orders.php?action=upload_photo` (Baustelle-PWA, + JWT) und `mobile_upload.php` (Token-Link). +- Bei einem Treffer wird nichts abgelegt, die Antwort traegt `duplicate: true` — **und + trotzdem den `relpath` der vorhandenen Datei**. Das ist Pflicht: die PWA wertet eine + Antwort ohne `relpath` als „nicht bestaetigt" und haette das Foto sonst endlos + weiterversucht (lib/api.js). + +Damit ist die Wiederholung idempotent statt duplikaterzeugend; die at-least-once-Regel bleibt +als Rueckfallebene bestehen. + ## 1.5.1 — 2026-08-22 Die API-Seite zur Baustelle-PWA holt nach, was der Editor mit 1.5.0 bekommen hat. diff --git a/api/orders.php b/api/orders.php index 2624bf9..e4b9b68 100644 --- a/api/orders.php +++ b/api/orders.php @@ -240,6 +240,20 @@ if ($action === 'upload_photo' && $_SERVER['REQUEST_METHOD'] === 'POST') { $upload_dir = $conf->commande->multidir_output[$cmd->entity].'/'.dol_sanitizeFileName($cmd->ref); if (!is_dir($upload_dir)) dol_mkdir($upload_dir); + // Liegt dasselbe Foto schon im Ordner? Dann kein zweites Mal ablegen. + // Wichtig: trotzdem relpath zurueckgeben — die PWA wertet eine Antwort OHNE relpath + // als "nicht bestaetigt" (lib/api.js) und haette das Foto sonst ewig in der Queue. + $dup = bericht_find_identical_file($upload_dir, $_FILES['file']['tmp_name']); + if ($dup !== null) { + $existing = $upload_dir.'/'.$dup; + api_ok(array( + 'filename' => $dup, + 'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $existing), + 'size' => filesize($existing), + 'duplicate' => true, + )); + } + $filename = 'foto_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext; $target = $upload_dir.'/'.$filename; if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) api_fail('Upload fehlgeschlagen', 500); diff --git a/lib/bericht.lib.php b/lib/bericht.lib.php index 8c057b1..d39d90a 100644 --- a/lib/bericht.lib.php +++ b/lib/bericht.lib.php @@ -1602,3 +1602,44 @@ function bericht_render_cover_internal($template_path, $bericht, $parent, $tempd return null; } } + +/** + * Sucht im Zielordner eine inhaltlich identische Datei zur hochgeladenen Datei. + * + * Hintergrund: Der Foto-Upload ist bewusst at-least-once (KB #840) — geht die Antwort + * nach dem Speichern verloren, wiederholt die PWA den Upload, und dasselbe Foto lag + * danach doppelt im Auftragsordner. Dasselbe passiert, wenn nach einem abgebrochenen + * Durchgang dieselbe Auswahl noch einmal hochgeladen wird (real am 28.08.2026 an + * Auftrag (PROV105): 34 Dateien, davon 11 identische Wiederholungen). + * Mit dem Inhalts-Vergleich wird der Upload idempotent, ohne die Queue-Semantik + * anzutasten: der Aufrufer meldet den Treffer als Erfolg zurueck. + * + * Vergleich zuerst ueber die Dateigroesse (billig), erst bei Gleichstand ueber md5. + * + * @param string $dir Zielverzeichnis (absoluter Pfad) + * @param string $tmpfile Hochgeladene temporaere Datei + * @return string|null Dateiname der bereits vorhandenen identischen Datei, sonst null + */ +function bericht_find_identical_file($dir, $tmpfile) +{ + if (!is_dir($dir) || !is_readable($tmpfile)) return null; + + $size = filesize($tmpfile); + if ($size === false || $size <= 0) return null; + + $files = scandir($dir); + if ($files === false) return null; + + $hash = null; + foreach ($files as $f) { + if ($f === '.' || $f === '..') continue; + $full = $dir.'/'.$f; + if (!is_file($full)) continue; + if (filesize($full) !== $size) continue; // andere Groesse = anderer Inhalt + if ($hash === null) $hash = md5_file($tmpfile); // erst hashen, wenn ein Kandidat da ist + if ($hash === false) return null; + if (md5_file($full) === $hash) return $f; + } + + return null; +} diff --git a/mobile_upload.php b/mobile_upload.php index dff02c8..e7215db 100644 --- a/mobile_upload.php +++ b/mobile_upload.php @@ -26,6 +26,7 @@ if (!$res) die("Include of main fails"); require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once __DIR__.'/class/upload_token.class.php'; +require_once __DIR__.'/lib/bericht.lib.php'; $token = (string) ($_REQUEST['token'] ?? ''); $tok = BerichtUploadToken::fetchValid($db, $token); @@ -62,6 +63,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file']['tmp_name']) if (!is_dir($upload_dir)) dol_mkdir($upload_dir); + // Dasselbe Foto schon da? Als Erfolg melden, aber nichts zusaetzlich ablegen + // (gleiche Regel wie in api/orders.php). + $dup = bericht_find_identical_file($upload_dir, $_FILES['file']['tmp_name']); + if ($dup !== null) { + echo json_encode(array('success' => true, 'filename' => $dup, 'duplicate' => true)); + exit; + } + $filename = 'foto_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext; $target = $upload_dir.'/'.$filename; if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) {