Aller au contenu

Téléversement de données dans un Data Bundle

Ce guide explique comment téléverser les fichiers d’entrée bruts d’une capture dans un Data Bundle via la RealityConnect API.

Tous les points de terminaison de téléversement requièrent le scope write:bundle ; la liste des sessions requiert read:bundle.


Un Data Bundle contient les entrées brutes d’une seule capture : un nuage de points, une trajectoire, des images, des métadonnées par image, etc. L’ensemble exact de fichiers acceptés par un bundle dépend du périphérique pour lequel le bundle a été créé.

Les fichiers sont téléversés dans des sessions de téléversement. Une session est un conteneur créé par le serveur pour un lot de fichiers du bundle. Lorsque vous l’ouvrez, vous déclarez le nombre de fichiers que le lot contiendra (expectedFileCount) ; la session se complète automatiquement une fois ce nombre de fichiers finalisés.

Chaque fichier individuel est téléversé avec un téléversement S3 multipart :

  1. Ouvrir une session sur le bundle, en déclarant le nombre total de fichiers que vous allez envoyer.
  2. Pour chaque fichier : initier le téléversement, PUT ses parties directement vers les URL retournées, puis finaliser.
  3. Lorsque le nombre de fichiers finalisés atteint expectedFileCount, la session est marquée completed.
sequenceDiagram
  participant Client as Client
  participant RCAPI as RCAPI
  participant S3 as S3

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

  loop pour chaque fichier
    Client->>RCAPI: POST initiate file
    RCAPI-->>Client: fileId + presigned part URLs
    loop pour chaque partie
      Client->>S3: PUT part
      S3-->>Client: ETag
    end
    Client->>RCAPI: POST finalize
    RCAPI-->>Client: fichier complété + sessionStatus
  end

Les requirements de téléversement (retournés lorsque vous ouvrez une session) indiquent exactement quels types de fichiers le bundle accepte, quels types sont obligatoires et comment ils dépendent les uns des autres.

Un bundle ZF SLAM est un bon exemple car il accepte quatre types de fichiers différents avec une dépendance inter-types. Une capture ZF complète consiste en :

FichierTypeNotes
scan.lasnuage de pointsexactement un
…laser_0.txttrajectoireexactement un
panorama/*.jpgimages panoramaplusieurs, chacune associée à des métadonnées
panorama/*.jsonmétadonnées par imageplusieurs, une par image

Un lot ZF complet est donc 1 nuage de points + 1 trajectoire + N images + N fichiers de métadonnées. Nous téléverserons tous ces fichiers dans une seule session.

Tous les chemins sont relatifs à votre api_url régionale. Le bundle est adressé par son propre id (bundleId).

MéthodeCheminScopeObjectif
POST/v1/bundles/{bundleId}/upload-sessionswrite:bundleOuvrir une session
GET/v1/bundles/{bundleId}/upload-sessionsread:bundleLister les sessions (reprise / progression)
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/fileswrite:bundleInitier un téléversement de fichier
GET/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex}write:bundleRéémettre des URL de parties expirées
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizewrite:bundleFinaliser un téléversement de fichier

Pour téléverser des données dans un Data Bundle, vous avez besoin de :

  • un flux OAuth terminé avec un access_token et votre api_url régionale
  • un Data Bundle existant (bundleId)

Si vous n’avez pas encore un bundle, créez-en un en suivant Création d’un Data Bundle. Ce flux obtient l’arbre d’entrée du périphérique, choisit un dbuPath feuille et crée le bundle sous un nœud parent.

Déclarez le nombre total de fichiers que le lot contiendra. Pour une capture ZF complète avec 13 images, c’est 1 + 1 + 13 + 13 = 28.

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

La réponse retourne le sessionId et les requirements du bundle :

{
"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"] }
]
}

Comprendre requirements et les dépendances croisées

Section intitulée « Comprendre requirements et les dépendances croisées »

Chaque entrée dans requirements décrit un type de fichier que le bundle accepte :

ChampSignification
typeL’identifiant que vous transmettez comme type lors de l’initiation d’un téléversement de fichier.
allowedExtensionsExtensions de fichier acceptées pour ce type.
optionalSi false, le bundle ne peut pas être traité sans au moins un fichier de ce type.
allowMultiplesSi false, un seul fichier de ce type peut être téléversé.
dependenciesAutres types qui doivent également être présents lorsque ce type est présent.

Pour l’exemple ZF :

  • pointclouds et trajectories sont obligatoires (optional: false) et uniques (allowMultiples: false). Chaque bundle ZF a besoin d’exactement un fichier de chaque type.
  • pictures et picturesMetadata sont facultatifs et acceptent plusieurs fichiers.
  • Les deux types d’images dépendent l’un de l’autre : pictures.dependencies = ["picturesMetadata"] et picturesMetadata.dependencies = ["pictures"].

Une dépendance signifie : si un type est présent, chaque type qu’il liste doit également être présent. Comme les types d’images ZF référencent l’un l’autre, ils sont tout ou rien en paire. Vous pouvez téléverser les images et leurs métadonnées ensemble, ou ignorer les deux, mais vous ne pouvez pas téléverser l’un sans l’autre.

Lots ZF valides :

  • Minimal : 1 nuage de points + 1 trajectoire (sans images). expectedFileCount = 2.
  • Complet : 1 nuage de points + 1 trajectoire + N pictures + N picturesMetadata. expectedFileCount = 2 + 2N.

Non valide :

  • Images sans leurs métadonnées (ou inversement), car la dépendance croisée n’est pas satisfaite.
  • Un lot sans nuage de points ou sans trajectoire, car un type obligatoire est manquant.

Avant d’ouvrir une session, comptez chaque fichier que vous prévoyez de téléverser et définissez ce total comme expectedFileCount. Les types facultatifs ne comptent que si vous les incluez dans le lot. La session se complète lorsque ce nombre de fichiers est finalisé, donc le nombre déclaré doit correspondre à ce que vous téléversez réellement.

Répétez les trois appels suivants pour chaque fichier du lot.

Transmettez le nom du fichier, sa taille en octets et son type (l’un des types de requirements) :

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" }

La réponse décrit le téléversement multipart : le nombre de parts à envoyer et une url pré-signée par partie.

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

Divisez le fichier en parts blocs séquentiels de ceil(fileSize / parts) octets et effectuez un PUT de chaque bloc vers son url pré-signée. Aucun en-tête d’authentification n’est envoyé sur ces requêtes ; l’URL est déjà signée. Conservez l’en-tête de réponse ETag de chaque partie, vous en aurez besoin pour finaliser.

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

Transmettez l’ETag exactement comme retourné, y compris ses guillemets.

Envoyez les parties collectées pour compléter le fichier. Le point de terminaison retourne 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…\"" } ] }

La réponse renvoie le fichier et indique le statut de la session :

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

sessionStatus reste pending jusqu’à la finalisation du dernier fichier attendu, moment auquel il devient completed.

Les URL pré-signées expirent. Si un téléversement prend longtemps et qu’un PUT commence à retourner 403, réémettez les URL depuis la partie où vous avez arrêté et continuez :

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?…" } ] }

Le fichier n’a pas besoin d’être ré-initié ; seules les URL de parties sont renouvelées.

Listez les sessions d’un bundle pour vérifier la progression ou reprendre un lot interrompu sans garder le sessionId en mémoire. La liste est paginée et triable, avec un filtre status facultatif.

Paramètres de requête : page (défaut 1), limit (défaut 20, maximum 20), sortBy (createdAt ou status, défaut createdAt), sortDir (ASC ou DESC, défaut ASC), status (filtre facultatif : pending ou 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 par rapport à expectedFileCount indique combien de fichiers restent. Pour reprendre, initiez et finalisez uniquement les fichiers qui ne sont pas encore complétés.

Lisez ceci attentivement, car cela détermine ce dont vous êtes responsable lors de l’utilisation de la RealityConnect API pour téléverser des données.

  • Les points de terminaison de téléversement enregistrent chaque fichier sous le type que vous fournissez et complètent la session uniquement sur le nombre : une fois que finalizedFileCount atteint expectedFileCount, la session est completed. Au moment du téléversement, le serveur ne vérifie pas que le type est l’un des requirements, que l’extension du fichier est autorisée, que les types obligatoires sont présents ou que les dépendances croisées sont satisfaites.
  • Toutes les règles (types obligatoires, allowMultiples et dependencies entre types) sont appliquées après que le bundle est envoyé pour traitement. Un lot qui ignore les requirements se téléversera avec succès mais échouera au traitement.
  • La validation profonde du contenu n’est jamais effectuée par l’API de téléversement. Qu’un .las soit un nuage de points ZF authentique, ou qu’un .json référence correctement son image, n’est vérifié qu’au traitement.

Traitez requirements comme le contrat. Respectez les types, extensions, optionnalité, multiplicité et dépendances dans votre lot de téléversement pour que le bundle soit traitable une fois téléversé. Les échecs de traitement causés par une structure de téléversement incorrecte ne sont pas remboursables.

Mise en pratique pour la capture ZF. files est la liste que vous construisez depuis votre jeu de données local, chaque entrée associant un chemin à son type issu de 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"

Une fois chaque fichier téléversé et la session completed, continuez vers Traitement et suivi pour transformer les entrées brutes en sorties visualisables.