Pular para o conteúdo

Enviando Dados para um Data Bundle

Este guia explica como enviar os arquivos de entrada brutos de uma captura para um data bundle pela RealityConnect API.

Todos os endpoints de envio exigem o escopo write:bundle; listar sessões exige read:bundle.


Um data bundle guarda as entradas brutas de uma única captura: uma nuvem de pontos, uma trajetória, imagens, metadados por imagem e assim por diante. O conjunto exato de arquivos que um bundle aceita depende do dispositivo para o qual o bundle foi criado.

Os arquivos são enviados em sessões de envio. Uma sessão é um contêiner criado pelo servidor para um lote de arquivos de um bundle. Ao abri-la, você declara quantos arquivos o lote conterá (expectedFileCount); a sessão é concluída automaticamente quando essa quantidade de arquivos tiver sido finalizada.

Cada arquivo individual é enviado com um upload multipart do S3:

  1. Abra uma sessão no bundle, declarando o número total de arquivos que você enviará.
  2. Para cada arquivo: inicie o envio, faça o PUT de suas partes diretamente nas URLs retornadas e, em seguida, finalize-o.
  3. Quando o número de arquivos finalizados atingir expectedFileCount, a sessão é marcada como completed.
sequenceDiagram
  participant Client as Cliente
  participant RCAPI as RCAPI
  participant S3 as S3

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

  loop para cada arquivo
    Client->>RCAPI: POST initiate file
    RCAPI-->>Client: fileId + presigned part URLs
    loop para cada parte
      Client->>S3: PUT part
      S3-->>Client: ETag
    end
    Client->>RCAPI: POST finalize
    RCAPI-->>Client: file completed + sessionStatus
  end

Os requirements de envio (retornados quando você abre uma sessão) informam exatamente quais tipos de arquivo o bundle aceita, quais são obrigatórios e como eles dependem uns dos outros.

Um bundle ZF SLAM é um bom exemplo porque aceita quatro tipos de arquivo diferentes com uma dependência entre tipos. Uma captura ZF completa consiste em:

ArquivoTipoObservações
scan.lasnuvem de pontosexatamente uma
…laser_0.txttrajetóriaexatamente uma
panorama/*.jpgimagens panorâmicasvárias, cada uma emparelhada com metadados
panorama/*.jsonmetadados por imagemvários, um por imagem

Portanto, um lote ZF completo é 1 nuvem de pontos + 1 trajetória + N imagens + N arquivos de metadados. Vamos enviar todos eles em uma única sessão.

Todos os caminhos são relativos à sua api_url regional. O bundle é endereçado pelo seu próprio id (bundleId).

MétodoCaminhoEscopoFinalidade
POST/v1/bundles/{bundleId}/upload-sessionswrite:bundleAbrir uma sessão
GET/v1/bundles/{bundleId}/upload-sessionsread:bundleListar sessões (retomar / progresso)
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/fileswrite:bundleIniciar o envio de um arquivo
GET/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex}write:bundleReemitir URLs de partes expiradas
POST/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizewrite:bundleFinalizar o envio de um arquivo

Para enviar dados a um data bundle, você precisa de:

  • um fluxo OAuth concluído com um access_token e sua api_url regional
  • um data bundle existente (bundleId)

Se você ainda não tiver um bundle, crie um primeiro seguindo Criando um Data Bundle. Esse fluxo obtém a árvore de entrada de dispositivos, escolhe um dbuPath de folha e cria o bundle sob um nó pai.

Declare o número total de arquivos que o lote conterá. Para uma captura ZF completa com 13 imagens, isso é 1 + 1 + 13 + 13 = 28.

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

A resposta retorna o sessionId e os requirements do 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"] }
]
}

Entendendo requirements e as dependências entre tipos

Seção intitulada “Entendendo requirements e as dependências entre tipos”

Cada entrada em requirements descreve um tipo de arquivo que o bundle aceita:

CampoSignificado
typeO identificador que você passa como type ao iniciar o envio de um arquivo.
allowedExtensionsExtensões de arquivo aceitas para esse tipo.
optionalSe false, o bundle não pode ser processado sem pelo menos um arquivo desse tipo.
allowMultiplesSe false, apenas um arquivo desse tipo pode ser enviado.
dependenciesOutros tipos que também precisam estar presentes sempre que este tipo estiver presente.

Para o exemplo ZF:

  • pointclouds e trajectories são obrigatórios (optional: false) e únicos (allowMultiples: false). Cada bundle ZF precisa de exatamente um de cada.
  • pictures e picturesMetadata são opcionais e aceitam vários arquivos.
  • Os dois tipos de imagem dependem um do outro: pictures.dependencies = ["picturesMetadata"] e picturesMetadata.dependencies = ["pictures"].

Uma dependência significa: se um tipo está presente, todos os tipos que ele lista também precisam estar presentes. Como os tipos de imagem do ZF se referenciam mutuamente, eles são tudo ou nada, como um par. Você pode enviar as imagens e seus metadados juntos, ou omitir ambos, mas não pode enviar um sem o outro.

Lotes ZF válidos:

  • Mínimo1 pointcloud + 1 trajectory (sem imagens). expectedFileCount = 2.
  • Completo1 pointcloud + 1 trajectory + N pictures + N picturesMetadata. expectedFileCount = 2 + 2N.

Inválido:

  • Imagens sem seus metadados (ou vice-versa), já que a dependência cruzada não é satisfeita.
  • Um lote sem nuvem de pontos ou sem trajetória, já que um tipo obrigatório está ausente.

Antes de abrir uma sessão, conte todos os arquivos que você planeja enviar e defina esse total como expectedFileCount. Os tipos opcionais só contam se você os incluir no lote. A sessão é concluída quando essa quantidade de arquivos é finalizada, portanto o número que você declara precisa corresponder ao que você realmente enviar.

Repita as três chamadas a seguir para cada arquivo do lote.

Passe o nome do arquivo, seu tamanho em bytes e seu type (um dos tipos 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" }

A resposta descreve o upload multipart: quantas parts enviar e uma url pré-assinada por parte.

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

Divida o arquivo em parts blocos sequenciais de ceil(fileSize / parts) bytes e faça o PUT de cada bloco em sua url pré-assinada. Nenhum cabeçalho de autenticação é enviado nessas requisições; a URL já está assinada. Guarde o cabeçalho de resposta ETag de cada parte, pois você precisará dele para finalizar.

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

Passe o ETag de volta exatamente como foi retornado, incluindo as aspas ao redor.

Envie as partes coletadas para concluir o arquivo. O endpoint retorna 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…\"" } ] }

A resposta ecoa o arquivo e informa o status da sessão:

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

sessionStatus permanece pending até que o último arquivo esperado seja finalizado, quando então passa a completed.

As URLs pré-assinadas expiram. Se um envio demorar muito e um PUT começar a retornar 403, reemita as URLs a partir da parte em que você parou e continue:

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

O arquivo não precisa ser reiniciado; apenas as URLs das partes são renovadas.

Liste as sessões de um bundle para verificar o progresso ou retomar um lote interrompido sem manter o sessionId na memória. A lista é paginada e ordenável, com um filtro status opcional.

Parâmetros de consulta: page (padrão 1), limit (padrão 20, máximo 20), sortBy (createdAt ou status, padrão createdAt), sortDir (ASC ou DESC, padrão ASC), status (filtro opcional: 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 em comparação com expectedFileCount informa quantos arquivos faltam. Para retomar, inicie e finalize apenas os arquivos que ainda não foram concluídos.

Leia isto com atenção, pois determina o que é sua responsabilidade ao usar a RealityConnect API para enviar dados.

  • Os endpoints de envio registram cada arquivo sob o type que você fornece e concluem a sessão puramente com base na contagem: assim que finalizedFileCount for igual a expectedFileCount, a sessão é completed. No momento do envio, o servidor não verifica se o type é um dos requirements, se a extensão do arquivo é permitida, se os tipos obrigatórios estão presentes ou se as dependências entre tipos são satisfeitas.
  • Todas as regras (tipos obrigatórios, allowMultiples e as dependencies entre tipos) são aplicadas após o bundle ser enviado para processamento. Um lote que ignore os requirements será enviado com sucesso, mas não será processado.
  • A validação aprofundada do conteúdo nunca é realizada pela API de envio. Se um .las é realmente uma nuvem de pontos ZF, ou se um .json referencia corretamente sua imagem, isso só é verificado no processamento.

Trate requirements como o contrato. Respeite os tipos, extensões, opcionalidade, multiplicidade e dependências no seu lote de envio para que o bundle seja processável depois de enviado. Falhas de processamento causadas por uma estrutura de envio incorreta não são reembolsáveis.

Juntando tudo para a captura ZF. files é a lista que você monta a partir do seu conjunto de dados local, cada entrada emparelhando um caminho com seu type 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"

Depois que todos os arquivos forem enviados e a sessão estiver completed, continue em Processamento e Monitoramento para transformar as entradas brutas em saídas visualizáveis.