---
module: fleet.artifacts
title: Artifact Import Guide
tab: Architect
order: 30
audience: dev
---

> How artifact import is built inside **hiveops-fleet**. Scope: the `fleet_artifacts` lifecycle — CDN import, multipart upload, and how an agent ultimately gets the bytes. Cross-link [platform.data-architecture], [platform.kafka].

## Components

| Layer | Class / file | Role |
|-------|--------------|------|
| Frontend | `FleetManagement/FleetArtifacts.svelte` | Table + three CDN import panels (Agent / Hotfix / Software); calls `fleetAPI` (`/artifacts*`, `/cdn-manifest/*`) |
| REST (user) | `FleetApiController` (`/api/fleet`) | Artifact CRUD, manifest proxy, CDN import, enable/disable |
| REST (agent) | `InternalFleetController` (`/api/internal/fleet`, `ROLE_INTERNAL`) | Serves task payload metadata + chunked download to agent-proxy |
| Service | `FleetArtifactService` | All artifact logic: upload+SHA-256, CDN HEAD-probe register, chunk read/store, toggle, delete-guard |
| Persistence | `FleetArtifactRepository`, entity `FleetArtifact`, DTO `FleetArtifactDTO` | JPA over `fleet_artifacts` |

Enum `ArtifactType`: `UPDATE_AGENT`, `UPDATE_HOTFIX`, `UPDATE_SOFTWARE`, `UPDATE_MODULE`, `SCRIPT`.

## Data flow — CDN import (the preferred path)

```
Admin ─▶ FleetArtifacts.svelte
      │  1. GET /api/fleet/cdn-manifest/{agent|hotfix|software}
      ▼
FleetApiController ─▶ FleetArtifactService.fetchCdnManifest(type)
      │  server-side fetch of cdn.bcos.cloud/downloads/{type}/latest.json
      │  (adds X-CDN-Token header if cdn.token set) ── returns raw JSON to UI
      ▼
Admin clicks Import ─▶ POST /api/fleet/artifacts/import-cdn { cdnUrl, name, version, artifactType, description?, institutionKey? }
      ▼
FleetArtifactService.importFromCdn(...)
      │  HEAD probe cdnUrl (headers: X-HiveIQ-Browser:true, X-CDN-Token) → Content-Length only
      │  builds FleetArtifact with cdn_url set, storage_path NULL, sha256 NULL
      ▼
fleet_artifacts row (cdn_url populated)
```

The file itself is never copied into fleet. At task-execution time `InternalFleetController` returns `artifactCdnUrl` in the `tasks/next` payload and the agent downloads the bytes **directly from the CDN**, bypassing fleet. If an agent instead hits the chunk endpoint for a CDN-only artifact, `readChunk` throws `410 GONE`.

## Data flow — multipart upload (self-hosted path)

```
Admin ─▶ POST /api/fleet/artifacts (multipart: file,name,version,artifactType,description?,institutionKey?)
      ▼
FleetArtifactService.uploadArtifact(...)
      │  transferTo FLEET_UPLOAD_DIR/{name}-{version}-{filename}
      │  computeSha256() streamed
      │  builds FleetArtifact with storage_path + sha256_hash, cdn_url NULL
      ▼
fleet_artifacts row
      ▼ (later, at task time)
Agent ─▶ GET /api/internal/fleet/tasks/{taskId}/download?offset=N
      ▼  RandomAccessFile chunked read (CHUNK_SIZE) from storage_path
```

## DB — `fleet_artifacts` (in `hiveiq_fleet`)

Created V1; `cdn_url VARCHAR(1000)` added by **V7__add_artifact_cdn_url.sql**. `ddl-auto=validate`, Flyway-managed. Cross-link [platform.data-architecture].

Columns of note (entity `FleetArtifact`):

| Column | Meaning |
|--------|---------|
| `name`, `version`, `filename` | required identity |
| `artifact_type` | enum, stored as STRING |
| `file_size` | bytes (from upload size, or CDN HEAD Content-Length) |
| `sha256_hash` | **uploads only** — CDN imports leave null |
| `storage_path` | **uploads only** — on-disk path under `FLEET_UPLOAD_DIR` |
| `cdn_url` | **CDN imports only** — agent's direct download URL |
| `institution_key` | null ⇒ Global; else scoped (≤10 chars) |
| `enabled` | default true; toggled via `/toggle-enabled` |
| `uploaded_at` / `created_at` / `updated_at` | timestamps via `@PrePersist`/`@PreUpdate` |

## API surface (artifact-relevant)

| Method Path | Auth | Purpose |
|-------------|------|---------|
| GET `/api/fleet/artifacts` (`?type=`) | read roles | list (optional `ArtifactType` filter) |
| GET `/api/fleet/artifacts/{id}` | read roles | fetch one |
| POST `/api/fleet/artifacts` (multipart) | ADMIN/MSP_ADMIN/BCOS_ADMIN | upload + hash |
| GET `/api/fleet/cdn-manifest/{type}` | MSP_ADMIN/BCOS_ADMIN | proxy CDN `latest.json` |
| POST `/api/fleet/artifacts/import-cdn` | ADMIN/MSP_ADMIN/BCOS_ADMIN | register CDN artifact |
| POST `/api/fleet/artifacts/{id}/toggle-enabled` | ADMIN/MSP_ADMIN/BCOS_ADMIN | enable/disable |
| DELETE `/api/fleet/artifacts/{id}` | ADMIN/MSP_ADMIN/BCOS_ADMIN | delete (guarded) |
| GET `/api/internal/fleet/tasks/{taskId}/download` | ROLE_INTERNAL | chunked payload to agent |

Read roles = `USER,CUSTOMER,ADMIN,MSP_ADMIN,BCOS_ADMIN`. Internal role granted via `X-Internal-Secret` (`INTERNAL_SECRET`) — agent-proxy is the only intended caller.

## Kafka

Artifact import itself publishes **no Kafka event** — it is a synchronous DB write. Cross-link [platform.kafka]. Fleet's producer topic `hiveops.fleet.task.events` (`FleetTaskEventPublisher`) carries *task* lifecycle, not artifact rows; artifacts only reach the executor→Kafka→record-store pattern indirectly, when a fleet task that references an artifact completes. Device targeting for tasks reads the Kafka-synced `device_summary` mirror (`hiveops.device.events`, group `hiveops-fleet-device-sync`), not the artifact table.

## Delete guard (referential safety)

`deleteArtifact` counts tasks referencing the artifact in `PENDING/QUEUED/RUNNING` (`countByArtifactAndStatusIn`); >0 ⇒ `409 CONFLICT`. Otherwise it clears the reference on any remaining (terminal) tasks (`clearArtifactReference`), deletes the on-disk file if `storage_path` is set, then removes the row.

## Config / env

- `CDN_TOKEN` → `cdn.token` — `X-CDN-Token` header on manifest fetch + HEAD probe.
- `FLEET_UPLOAD_DIR` → `fleet.artifact.upload.dir` (default `/app/uploads/fleet`).
- Multipart cap: `spring.servlet.multipart.max-file-size=100MB` (request-size same).
- `INTERNAL_SECRET`, `JWT_SECRET`, `DB_URL` per service standard.
