Zum Inhalt springen

Daten in ein Data Bundle hochladen

Dieser Leitfaden erklärt, wie Sie die Roh-Eingabedateien einer Erfassung in ein Data Bundle über die RealityConnect API hochladen.

Alle Upload-Endpunkte erfordern den Scope write:bundle; zum Auflisten von Sessions ist read:bundle erforderlich.


Ein Data Bundle enthält die Roh-Eingaben für eine einzelne Erfassung: eine Punktwolke, eine Trajektorie, Bilder, Metadaten pro Bild und so weiter. Die genaue Menge an Dateien, die ein Bundle akzeptiert, hängt vom Gerät ab, für das das Bundle erstellt wurde.

Dateien werden in Upload-Sessions hochgeladen. Eine Session ist ein serverseitig erstellter Container für einen Stapel von Dateien eines Bundles. Beim Öffnen geben Sie an, wie viele Dateien der Stapel enthalten wird (expectedFileCount); die Session wird automatisch abgeschlossen, sobald so viele Dateien finalisiert wurden.

Jede einzelne Datei wird mit einem S3-Multipart-Upload hochgeladen:

  1. Session öffnen auf dem Bundle und die Gesamtzahl der Dateien angeben, die Sie senden werden.
  2. Für jede Datei: Upload initiieren, Teile direkt an die zurückgegebenen URLs PUTen, dann finalisieren.
  3. Wenn die Anzahl finalisierter Dateien expectedFileCount erreicht, wird die Session als completed markiert.
sequenceDiagram
  participant Client as Client
  participant RCAPI as RCAPI
  participant S3 as S3

  Client->>RCAPI: POST upload-sessions
  RCAPI-->>Client: sessionId + requirements

  loop for each file
    Client->>RCAPI: POST initiate file
    RCAPI-->>Client: fileId + presigned part URLs
    loop for each part
      Client->>S3: PUT part
      S3-->>Client: ETag
    end
    Client->>RCAPI: POST finalize
    RCAPI-->>Client: file completed + sessionStatus
  end

Die Upload-requirements (beim Öffnen einer Session zurückgegeben) geben genau an, welche Dateitypen das Bundle akzeptiert, welche Pflicht sind und wie sie voneinander abhängen.

Das ausgearbeitete Beispiel: eine ZF-SLAM-Erfassung

Abschnitt betitelt „Das ausgearbeitete Beispiel: eine ZF-SLAM-Erfassung“

Ein ZF-SLAM-Bundle ist ein gutes Beispiel, weil es vier verschiedene Dateitypen mit einer typübergreifenden Abhängigkeit akzeptiert. Eine vollständige ZF-Erfassung besteht aus:

FileTypeNotes
scan.laspoint cloudgenau eine
…laser_0.txttrajectorygenau eine
panorama/*.jpgpanorama imagesviele, jeweils mit Metadaten gepaart
panorama/*.jsonper-image metadataviele, eine pro Bild

Ein vollständiger ZF-Stapel ist also 1 Punktwolke + 1 Trajektorie + N Bilder + N Metadatendateien. Wir laden alle in einer einzigen Session hoch.

Alle Pfade sind relativ zu Ihrer regionalen api_url. Das Bundle wird über seine eigene ID (bundleId) angesprochen.

MethodPathScopePurpose
POST/v1/bundles/{bundleId}/upload-sessionswrite:bundleSession öffnen
GET/v1/bundles/{bundleId}/upload-sessionsread:bundleSessions auflisten (Fortsetzen / Fortschritt)
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/fileswrite:bundleDatei-Upload initiieren
GET/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex}write:bundleAbgelaufene Teil-URLs erneut ausstellen
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizewrite:bundleDatei-Upload finalisieren

Um Daten in ein Data Bundle hochzuladen, benötigen Sie:

  • einen abgeschlossenen OAuth-Flow mit access_token und Ihrer regionalen api_url
  • ein vorhandenes Data Bundle (bundleId)

Wenn Sie noch kein Bundle haben, erstellen Sie zuerst eines, indem Sie Data Bundle erstellen folgen. Dieser Ablauf ruft den Geräte-Input-Baum ab, wählt einen Blatt-dbuPath und erstellt das Bundle unter einem übergeordneten Knoten.

Geben Sie die Gesamtzahl der Dateien an, die der Stapel enthalten wird. Für eine vollständige ZF-Erfassung mit 13 Bildern ist das 1 + 1 + 13 + 13 = 28.

POST {api_url}/v1/bundles/{bundleId}/upload-sessions
Authorization: Bearer {access_token}
Content-Type: application/json
{ "expectedFileCount": 28 }

Die Antwort liefert die sessionId und die requirements des Bundles:

{
"sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b",
"expectedFileCount": 28,
"requirements": [
{ "type": "pointclouds", "allowedExtensions": ["las", "laz"], "optional": false, "allowMultiples": false, "dependencies": [] },
{ "type": "trajectories", "allowedExtensions": ["txt"], "optional": false, "allowMultiples": false, "dependencies": [] },
{ "type": "pictures", "allowedExtensions": ["jpg", "png", "jpeg"], "optional": true, "allowMultiples": true, "dependencies": ["picturesMetadata"] },
{ "type": "picturesMetadata", "allowedExtensions": ["json"], "optional": true, "allowMultiples": true, "dependencies": ["pictures"] }
]
}

requirements und typübergreifende Abhängigkeiten verstehen

Abschnitt betitelt „requirements und typübergreifende Abhängigkeiten verstehen“

Jeder Eintrag in requirements beschreibt einen Dateityp, den das Bundle akzeptiert:

FieldMeaning
typeDie Kennung, die Sie als type beim Initiieren eines Datei-Uploads übergeben.
allowedExtensionsFür diesen Typ akzeptierte Dateierweiterungen.
optionalWenn false, kann das Bundle ohne mindestens eine Datei dieses Typs nicht verarbeitet werden.
allowMultiplesWenn false, darf nur eine Datei dieses Typs hochgeladen werden.
dependenciesAndere Typen, die ebenfalls vorhanden sein müssen, wann immer dieser Typ vorhanden ist.

Für das ZF-Beispiel:

  • pointclouds und trajectories sind Pflicht (optional: false) und einzeln (allowMultiples: false). Jedes ZF-Bundle benötigt genau je eine.
  • pictures und picturesMetadata sind optional und akzeptieren mehrere Dateien.
  • Die beiden Bildtypen hängen voneinander ab: pictures.dependencies = ["picturesMetadata"] und picturesMetadata.dependencies = ["pictures"].

Eine Abhängigkeit bedeutet: Wenn ein Typ vorhanden ist, muss jeder Typ, den er auflistet, ebenfalls vorhanden sein. Da die ZF-Bildtypen aufeinander verweisen, gelten sie als Paar alles oder nichts. Sie können die Bilder und ihre Metadaten zusammen hochladen oder beides weglassen, aber Sie können nicht eines ohne das andere hochladen.

Gültige ZF-Stapel:

  • Minimal: 1 Punktwolke + 1 Trajektorie (keine Bilder). expectedFileCount = 2.
  • Vollständig: 1 Punktwolke + 1 Trajektorie + N pictures + N picturesMetadata. expectedFileCount = 2 + 2N.

Ungültig:

  • Bilder ohne ihre Metadaten (oder umgekehrt), da die typübergreifende Abhängigkeit nicht erfüllt ist.
  • Ein Stapel ohne Punktwolke oder ohne Trajektorie, da ein Pflichttyp fehlt.

Bevor Sie eine Session öffnen, zählen Sie jede Datei, die Sie hochladen möchten, und setzen Sie diese Gesamtzahl als expectedFileCount. Optionale Typen zählen nur, wenn Sie sie in den Stapel aufnehmen. Die Session wird abgeschlossen, wenn so viele Dateien finalisiert sind; die angegebene Zahl muss also dem übereinstimmen, was Sie tatsächlich hochladen.

Wiederholen Sie die folgenden drei Aufrufe für jede Datei im Stapel.

Übergeben Sie den Dateinamen, die Größe in Bytes und den type (einer der requirements-Typen):

POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files
Authorization: Bearer {access_token}
Content-Type: application/json
{ "fileName": "scan.las", "fileSize": 734003200, "type": "pointclouds" }

Die Antwort beschreibt den Multipart-Upload: wie viele parts zu senden sind und eine presignierte url pro Teil.

{
"fileId": "c8c8cb0b-f476-4211-a3a4-72e4e04b91d5",
"parts": 8,
"urls": [
{ "partNumber": 1, "url": "https://s3…/part-1?…" },
{ "partNumber": 2, "url": "https://s3…/part-2?…" }
]
}

Teilen Sie die Datei in parts sequenzielle Blöcke à ceil(fileSize / parts) Bytes und PUTen Sie jeden Block an seine presignierte url. Bei diesen Anfragen wird kein Auth-Header gesendet; die URL ist bereits signiert. Bewahren Sie den ETag-Antwortheader jedes Teils auf, Sie benötigen ihn zum Finalisieren.

import math, requests
def upload_parts(file_path, file_size, urls):
part_size = math.ceil(file_size / len(urls))
parts = []
with open(file_path, "rb") as handle:
for entry in sorted(urls, key=lambda u: u["partNumber"]):
res = requests.put(entry["url"], data=handle.read(part_size))
res.raise_for_status()
parts.append({"partNumber": entry["partNumber"], "etag": res.headers["ETag"]})
return parts

Geben Sie den ETag genau so zurück, wie er geliefert wurde, einschließlich der umgebenden Anführungszeichen.

Senden Sie die gesammelten Teile, um die Datei abzuschließen. Der Endpunkt liefert 200 OK.

POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalize
Authorization: Bearer {access_token}
Content-Type: application/json
{ "parts": [ { "partNumber": 1, "etag": "\"a1b2…\"" }, { "partNumber": 2, "etag": "\"c3d4…\"" } ] }

Die Antwort gibt die Datei zurück und meldet den Session-Status:

{
"fileId": "c8c8cb0b-f476-4211-a3a4-72e4e04b91d5",
"fileName": "scan.las",
"type": "pointclouds",
"size": 734003200,
"status": "completed",
"sessionStatus": "pending"
}

sessionStatus bleibt pending, bis die letzte erwartete Datei finalisiert ist; dann wird er completed.

Presignierte URLs laufen ab. Wenn ein Upload lange dauert und ein PUT 403 zurückgibt, stellen Sie die URLs ab dem Teil neu aus, an dem Sie angehalten haben, und fahren Sie fort:

GET {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/0
Authorization: Bearer {access_token}
{ "startIndex": 0, "urls": [ { "partNumber": 1, "url": "https://s3…/part-1?…" } ] }

Die Datei muss nicht erneut initiiert werden; nur die Teil-URLs werden erneuert.

Listen Sie die Sessions eines Bundles auf, um den Fortschritt zu prüfen oder einen unterbrochenen Stapel fortzusetzen, ohne die sessionId im Speicher zu halten. Die Liste ist paginiert und sortierbar, mit optionalem status-Filter.

Query-Parameter: page (Standard 1), limit (Standard 20, Maximum 20), sortBy (createdAt oder status, Standard createdAt), sortDir (ASC oder DESC, Standard ASC), status (optionaler Filter: pending oder completed).

GET {api_url}/v1/bundles/{bundleId}/upload-sessions?page=1&limit=20&sortBy=createdAt&sortDir=DESC
Authorization: Bearer {access_token}
{
"items": [
{
"sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b",
"status": "pending",
"expectedFileCount": 28,
"finalizedFileCount": 14
}
],
"total": 1
}

finalizedFileCount gegenüber expectedFileCount zeigt, wie viele Dateien noch fehlen. Zum Fortsetzen initiieren und finalisieren Sie nur die Dateien, die noch nicht abgeschlossen sind.

Lesen Sie dies sorgfältig, da es festlegt, wofür Sie verantwortlich sind, wenn Sie die RealityConnect API zum Hochladen von Daten nutzen.

  • Die Upload-Endpunkte erfassen jede Datei unter dem von Ihnen angegebenen type und schließen die Session rein anhand der Anzahl ab: Sobald finalizedFileCount expectedFileCount entspricht, ist die Session completed. Zum Upload-Zeitpunkt prüft der Server nicht, ob der type einer der requirements ist, ob die Dateierweiterung erlaubt ist, ob Pflichttypen vorhanden sind oder ob typübergreifende Abhängigkeiten erfüllt sind.
  • Alle Regeln (Pflichttypen, allowMultiples und die dependencies zwischen Typen) werden erst durchgesetzt, nachdem das Bundle zur Verarbeitung gesendet wurde. Ein Stapel, der die requirements ignoriert, wird erfolgreich hochgeladen, schlägt aber bei der Verarbeitung fehl.
  • Eine tiefe Inhaltsvalidierung erfolgt durch die Upload-API nie. Ob eine .las eine echte ZF-Punktwolke ist oder eine .json korrekt auf ihr Bild verweist, wird erst bei der Verarbeitung geprüft.

Behandeln Sie requirements als Vertrag. Halten Sie Typen, Erweiterungen, Optionalität, Mehrfachheit und Abhängigkeiten in Ihrem Upload-Stapel ein, damit das Bundle nach dem Upload verarbeitbar ist. Verarbeitungsfehler durch eine falsche Upload-Struktur sind nicht erstattungsfähig.

Zusammengefasst für die ZF-Erfassung. files ist die Liste, die Sie aus Ihrem lokalen Datensatz aufbauen; jeder Eintrag verknüpft einen Pfad mit seinem type aus requirements.

import math, requests
API_URL = "https://<your-regional-api-url>"
TOKEN = "<access_token>"
BUNDLE_ID = "<bundleId>"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# (local path, schema type) for every file in the batch
files = [
("scan.las", "pointclouds"),
("2024-07-scan.laser_laser_0.txt", "trajectories"),
("panorama/2024-07-scan-idx40.jpg", "pictures"),
("panorama/2024-07-scan-idx40.json", "picturesMetadata"),
# … remaining image / metadata pairs …
]
def upload_one(session_id, path, file_type):
size = __import__("os").path.getsize(path)
base = f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions/{session_id}/files"
initiated = requests.post(
base, headers=HEADERS,
json={"fileName": path.split("/")[-1], "fileSize": size, "type": file_type},
).json()
part_size = math.ceil(size / initiated["parts"])
parts = []
with open(path, "rb") as handle:
for entry in sorted(initiated["urls"], key=lambda u: u["partNumber"]):
res = requests.put(entry["url"], data=handle.read(part_size))
res.raise_for_status()
parts.append({"partNumber": entry["partNumber"], "etag": res.headers["ETag"]})
return requests.post(
f"{base}/{initiated['fileId']}/finalize", headers=HEADERS, json={"parts": parts},
).json()
# 1. Open the session for the exact number of files we will send.
session = requests.post(
f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions",
headers=HEADERS, json={"expectedFileCount": len(files)},
).json()
# 2. Upload every file under its type.
result = None
for path, file_type in files:
result = upload_one(session["sessionId"], path, file_type)
# 3. The last finalize reports the session as completed.
assert result["sessionStatus"] == "completed"

Sobald jede Datei hochgeladen ist und die Session completed ist, fahren Sie mit Verarbeitung und Überwachung fort, um die Roh-Eingaben in anzeigbare Ausgaben umzuwandeln.