calculate_digest()

Berechnet lokal denselben Digest, den enaio für ein Dokument mit genau diesen Dateien speichert — ohne Serverkontakt und ohne die Dateien hochzuladen.

Damit lässt sich vor einem Upload feststellen, ob eine Datei bereits archiviert ist: den Wert berechnen und über objects_by_digest() nachschlagen oder direkt mit table_digest aus digest() vergleichen. (Ticket #16111)

1. Berechnungsvorschrift

enaio bildet den Digest eines Dokuments nicht über den aneinandergehängten Dateiinhalt, sondern über die Digests der einzelnen Dateien:

  1. SHA256 über jede einzelne Datei.

  2. Jeder dieser Digests wird als GROSSGESCHRIEBENER Hex-String dargestellt und in Dateireihenfolge zu einem ASCII-String konkateniert.

  3. SHA256 über die ASCII-Bytes dieses Strings.

SHA256( "".join(SHA256(datei).hexdigest().upper() for datei in dateien).encode("ascii") )
Auch ein Dokument mit nur einer Datei ist damit nicht der einfache SHA256 dieser Datei — der Hex-String durchläuft die zweite SHA256-Runde ebenfalls.

Die Reihenfolge der Dateien geht in den Digest ein. Sie muss der Reihenfolge entsprechen, in der die Dateien an insert() übergeben wurden.

Der Rückgabewert ist grossgeschrieben, wie enaio den Wert speichert und zurückgibt; er lässt sich damit ohne Normalisierung direkt mit ECMDocumentDigest.table_digest vergleichen.

2. Signatur

  • Sync

  • Async

ecm.dms.calculate_digest(
    files: ECMDigestSource | Iterable[ECMDigestSource],
    *,
    chunk_size: int = 65536,
) -> str
await ecm.dms.calculate_digest(
    files: ECMDigestSource | Iterable[ECMDigestSource],
    *,
    chunk_size: int = 65536,
) -> str
Die asynchrone Variante sendet ebenfalls keinen Job. Sie ist eine Coroutine, damit sie sich in den Aufrufstil einfügt und — wichtiger — damit Lesen und Hashen über asyncio.to_thread in einem Worker-Thread laufen und den Event-Loop bei grossen Dateien nicht blockieren.

3. Parameter

Parameter Typ Standard Beschreibung

files

ECMDigestSource | Iterable[ECMDigestSource]

—

Eine Dateiquelle oder eine iterierbare Folge davon, in der Reihenfolge, in der die Dateien zum Dokument gehören. Eine einzelne Quelle muss nicht in eine Liste verpackt werden.

chunk_size

int

65536

Anzahl Bytes pro Lesevorgang bei gestreamten Quellen. Beeinflusst nur den Speicherbedarf, nicht das Ergebnis.

3.1. Zulässige Dateiquellen

Typ Bedeutung

bytes / bytearray / memoryview

Der Dateiinhalt.

str / os.PathLike

Ein Pfad, aus dem der Inhalt gelesen wird. Ein str ist immer ein Pfad, nie Inhalt — für Inhalt text.encode() übergeben.

JobRequestFile

Dasselbe Objekt, das an insert() oder checkin() übergeben wird. Ein JobRequestFileFromReader wird dabei verbraucht und kann anschliessend nicht mehr hochgeladen werden.

JobResponseFile

Wie von files() zurückgegeben.

Offener Binärstream

Wird ab der aktuellen Position bis zum Ende gelesen.

4. Rückgabewert

Der Dokument-Digest als 64-stelliger, grossgeschriebener Hex-String.

5. Ausnahmen

Ausnahme Bedingung

ValueError

files ist leer, oder chunk_size ist nicht positiv.

TypeError

files oder einer seiner Einträge ist keine unterstützte Quelle.

FileNotFoundError

Ein als Pfad übergebener Eintrag existiert nicht.

6. Beispiele

6.1. Prüfen, ob eine Datei bereits archiviert ist

  • Sync

  • Async

local = ecm.dms.calculate_digest("rechnung.pdf")
if ecm.dms.objects_by_digest(local):
    print("bereits abgelegt")
else:
    ecm.dms.insert(RechnungsDokument(Titel="Rechnung"), files=[RequestFileFromPath("rechnung.pdf")])
local = await ecm.dms.calculate_digest("rechnung.pdf")
if await ecm.dms.objects_by_digest(local):
    print("bereits abgelegt")
else:
    await ecm.dms.insert(RechnungsDokument(Titel="Rechnung"), files=[RequestFileFromPath("rechnung.pdf")])

6.2. Gegen ein bekanntes Dokument vergleichen

  • Sync

  • Async

if ecm.dms.calculate_digest("rechnung.pdf") == ecm.dms.digest(doc).table_digest:
    print("identisch mit dem archivierten Dokument")
if await ecm.dms.calculate_digest("rechnung.pdf") == (await ecm.dms.digest(doc)).table_digest:
    print("identisch mit dem archivierten Dokument")

6.3. Mehrere Dateien — die Reihenfolge zählt

# Die Reihenfolge muss der beim insert() entsprechen
digest = ecm.dms.calculate_digest(["seite1.png", "seite2.png"])

6.4. Direkt aus den Dateien, die eingefügt werden sollen

from ecmind_blue_client.rpc import JobRequestFileFromPath

files = [JobRequestFileFromPath("seite1.png"), JobRequestFileFromPath("seite2.png")]
if not ecm.dms.objects_by_digest(ecm.dms.calculate_digest(files)):
    ecm.dms.insert(MeinDokument(Name="Scan"), files=files)

6.5. Heruntergeladene Dateien nachrechnen

# Integritätsprüfung ohne den Server rechnen zu lassen
downloaded = ecm.dms.files(doc)
assert ecm.dms.calculate_digest(downloaded) == ecm.dms.digest(doc).table_digest

7. Hinweise

  • Die Berechnung liest Dateien in Blöcken. Ein mehrere Gigabyte grosses Dokument kostet einen Block Speicher, nicht seine Grösse.

  • Die serverseitige Suche akzeptiert Gross- und Kleinschreibung. Ein Digest aus einem Fremdsystem muss also nicht umgestellt werden, bevor er nachgeschlagen wird.

  • Verifiziert gegen einen 12.0-Server für Dokumente mit einer, zwei und drei Dateien.

8. Siehe auch

  • objects_by_digest() — Dokumente zu einem Digest suchen

  • digest() — den serverseitig gespeicherten Digest lesen

  • files() — Dateien eines Dokuments herunterladen