INNORIX
Transfer BuilderTransfer FinderDevelopersResourcesCustomers
Start Free
INNORIX

LET FILES
MOVE THEMSELVES

INNORIX provides enterprise file infrastructure for moving and automating files across every system and environment.
Trusted by more than 5,000 enterprise and government agencies.

START HERE

  • Build the Transfer You Need
  • Find the Transfer You Need

POPULAR TRANSFERS

  • Sync Team Folders
  • Send Large Files to Clients
  • Explore Files Across Systems
  • Migrate FTP, SFTP, SCP & rsync
  • Add Transfer to Any App
  • Add Web Upload & Download
  • Build AI & Data Workflows
  • Browse All Transfers→

DEVELOPERS

  • Developer Center
  • Examples
  • API Quickstart
  • Developer Guide
  • API Reference
  • GitHub

RESOURCES

  • Resource Center
  • Product Guide
  • Integrations
  • Deploy & Manage
  • Help Center

CUSTOMERS

  • Government
  • Public Sector
  • Manufacturing
  • Engineering
  • Finance
  • Distribution
  • IT/Telecom
  • Media
  • Healthcare
  • Education

PLANS

  • Pricing

COMPANY

About Us

OTHER INNORIX PRODUCT

Al.bert — Smart Traffic AI

GLOBAL OFFICES

  • New York, USA
  • Seoul, South Korea
  • Ho Chi Minh City, Vietnam
  • View Office Locations→

(C)2026 INNORIX. All rights reserved.

  • Security
  • Status
  • Terms
  • Privacy
  • Cookies
  1. Guide
  2. Distribute Software and Firmware to Multiple Devices and Review Results

Distribute Software and Firmware to Multiple Devices and Review Results

Distribute software and firmware files to multiple targets with version paths, deployment rules, results, and failure recovery.

IT EngineersDevelopers
  • Automating the End-to-End Workflow for AI Training Data and Model Files
  • Deploy AI Model Files to Multiple Edge Devices
  • Securely Importing and Exporting Files in Air-Gapped and Network-Segmented Environments
  • Simultaneously Distributing Approved Files to Multiple Locations
  • Transfer Files from Azure Blob to an On-Premises Server
  • Automatically Transferring DB Dumps, Backup, and Archive Files to Remote Storage
  • Centrally Collecting Files from Branches, Factories, and Edge Devices
  • Deploy CI/CD Build Artifacts to Multiple Servers
  • Move Files Between Different Cloud Storage Services
  • Transfer Files Between S3 Buckets in Different AWS Accounts
  • Provide Customer-Specific File Workspaces
  • Provide Customers with Large-File Download Links That Expire
  • Let Customers Upload Large Files Through a Browser
  • Send Large Files Directly to a Customer Workspace
  • Store Database Backup Files in Object Storage
  • Receive File Transfer Failure and Recovery Alerts in Datadog
  • Add File Transfer Capabilities to Web, App, and Business Systems
  • Run Validation, Conversion, and Follow-Up Tasks After File Arrival
  • Migrate Large Volumes of Files from NAS and File Servers to the Cloud
  • Convert FTP Batch Jobs to Managed File Flows
  • Transfer Directly from Google Cloud Storage to Amazon S3
  • Create a File Transfer Status Dashboard in Grafana
  • Automatically Verify File Integrity Using Hash Values
  • Automatically Transfer New Files to a Designated Destination
  • Send Result Files from Kubernetes to Object Storage
  • Automatically Transfer Large Files and Build Artifacts Outside Git
  • Automatically Deliver Files That Are Difficult to Manage with Git
  • Collect Logs and Diagnostic Files from Distributed Servers into a Central Location
  • Automatically Transfer Media Sources and Processing Results Step by Step
  • Scan Received Files with Microsoft Defender and Perform Follow-up Processing
  • Automate Multi-Step File Transfers in a Single Workflow
  • Automatically Transfer Media Sources and Processing Results Step by Step
  • Automatically Route Partner Files to Internal Business Systems
  • Exchange Files Regularly with Partners and Supply Chain Systems
  • Automating Recurring File Transfers Daily and Weekly
  • Automatically Resume File Transfers After a Network Interruption
  • Convert rsync Jobs to Managed File Flows
  • Schedule Recurring File Transfers
  • Distribute Software and Firmware to Multiple Devices and Review Results
  • Distributing Software Packages to Multiple Servers and Branches
  • Automatically Apply Team Folder Changes Across Multiple Devices
  • Automatically Reflect a Team Work Folder Across Multiple PCs
  • Find and Transfer Files Across Multiple Devices from One Place
  • Add Large File Upload and Download to Your Website

Getting Started#

Basic Concept#

Distribute installation files and firmware by device and version

Software and firmware files are used across servers, branch systems, production equipment, edge devices, and other environments.

Software and firmware distribution prepares installation packages, build artifacts, and firmware files, then configures deployment jobs based on the target devices and versions to apply.

You can build deployment flows that match your operating environment, whether distributing one file to multiple devices or applying different files and versions by device group.

Deployment Structure

Deployment File
    │
    ▼
Set Version Criteria
    │
    ▼
Select Target Devices
    │
    ├────────→ Server Group
    ├────────→ Branch Devices
    └────────→ Edge Devices
                    │
                    ▼
                 Review Results

This process lets you manage the files to distribute, target devices, and application criteria in a single flow.

Deployment Flow#

Move from file preparation to deployment and result verification

A deployment job starts by preparing the installation file or firmware and defining the devices and versions that will receive it.

When the deployment runs under the specified conditions, files are transferred to each target device, where you can review per-device progress and file application results.

① Prepare deployment files

↓

② Configure target devices

↓

③ Set version and execution criteria

↓

④ Distribute files to targets

↓

⑤ Review application results

If needed, you can rerun the deployment for a specific device or device group.

Deployment Benefits#

Manage multi-device file distribution and version control in one flow

When applying software and firmware across multiple devices, teams need to manage the deployment files, targets, versions, and execution results together.

Software and firmware distribution lets you manage deployment work by device in a single operational flow and track which files were delivered to which targets.

Category Per-Device Management Deployment Flow
Deployment Targets Review targets device by device Manage by device and group
Deployment Files Check applicable targets for each file Configure files as part of the deployment job
Version Application Review version status by device Deploy according to version criteria
Execution Results Review results for each device separately Review results across targets together
Redeployment Reselect required devices Rerun by target or group

This creates a single operational flow from software and firmware distribution through result verification for each target.

IT Engineers#

Deployment Files#

Prepare installation packages and firmware files for deployment

First, prepare the installation packages, build artifacts, or firmware files to distribute.

A file can be used across multiple targets in a single deployment job, or different files can be assigned to different device groups depending on the operating environment.

For example, the following file types can be configured for deployment.

File Type Example Use
Installation Package Application installer
Build Artifact Latest build files and deployment packages
Firmware Firmware for devices and edge systems
Update File Update files for existing environments

After preparing the deployment files, configure the devices and groups that should receive them.

Target Setup#

Organize servers, branch systems, and edge devices by group and device

Deployment targets can be configured as individual devices or grouped together.

For example, targets can be organized around operating criteria such as headquarters servers, nationwide branch systems, specific production equipment, or edge-device groups.

                Deployment Targets
                    │
        ┌───────────┼───────────┐
        ▼           ▼           ▼
      Server Group  Branch Group   Edge Group
        │           │           │
      Device A      Device D       Device G
      Device B      Device E       Device H
      Device C      Device F       Device I

This lets you select multiple devices in a single deployment job or limit the deployment scope to specific groups and devices.

Deployment Criteria#

Set file versions and execution conditions by device

After configuring the targets, define which files should be deployed and under what conditions.

Link deployment-file versions to target devices, then configure execution to run immediately, on a schedule, or after another task completes.

Criteria to Configure

Setting Configuration
Deployment File Specify the installation package or firmware
Applied Version Specify the file version to deploy to target devices
Target Scope Select all devices, a group, or individual devices
Execution Condition Run immediately, on a schedule, or after another task completes
File Path Set the storage or application location on the target device

For example, you can deploy newly prepared firmware to a specific device group or distribute build artifacts to designated servers after a build job completes.

Multi-Device Deployment#

Transfer files to multiple target devices in one deployment workflow

After defining the deployment criteria, configure a multi-device flow that sends one file to multiple targets.

In Flow Canvas, connect one deployment source to multiple target devices to distribute the same file across servers, branch systems, edge devices, and other environments.

                  Deployment File
                      │
                      ▼
                 Deployment Job
            ┌─────────┼─────────┐
            ▼         ▼         ▼
         Server      Branch     Edge
            │         │         │
            ▼         ▼         ▼
          Verify       Verify      Verify

You can process all target devices together or select specific groups and devices based on operational requirements.

Result Management#

Review per-target status and rerun deployment for required devices

When a deployment runs, use Runs and the job details to review the overall deployment status and results for each target.

During review, you can check each device's progress, processed files, execution time, and related details.

Deployment Result Flow

Run Deployment
    │
    ▼
Review Overall Job Status
    │
    ├── Completed
    │      │
    │      └── Review per-target application results
    │
    ├── Running
    │      │
    │      └── Review progress and processing status
    │
    └── Needs Review
           │
           ▼
       Review Details
           │
           ▼
       Select Target Device
           │
           ▼
       Rerun Deployment

Item Details
Target Device Server or device that received the files
Progress Status Current processing status of the deployment job
File Result File application result for each target device
Execution Time Deployment start and completion times
Redeployment Target Device or group to rerun

If a deployment result for a specific device needs to be reviewed again, rerun the required deployment for that target or device group.

Developers#

Distribute one artifact to multiple devices and aggregate the application results by device

Integration Setup#

Prepare shared API calls and status values

import os
import requests

BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")   # optional; falls back to the current workspace

STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99}          # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}


def api(method, path, body=None, params=None):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TOKEN}",
    }

    if WORKSPACE_ID:
        headers["x-workspace-id"] = WORKSPACE_ID

    response = requests.request(
        method, BASE_URL + path,
        headers=headers, json=body, params=params, timeout=30,
    )

    payload = response.json() if response.content else {}

    if not response.ok:
        raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")

    return payload.get("data")


def is_terminal(detail):
    return detail.get("isTerminal", detail.get("status") in TERMINAL)
// InnorixClient.java
public static final String BASE_URL =
        env("INNORIX_BASE_URL", "https://app.innorix.com").replaceAll("/+
quot;
, ""); public static final String WORKSPACE_ID = env("INNORIX_WORKSPACE_ID", null); public static final int STATUS_COMPLETE = 2; // States the transfer no longer moves out of public static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99); // Terminal states that are not a full success public static final Set<Integer> NOT_SUCCEEDED = Set.of(4, 5, 9, 99); private HttpRequest.Builder headers(HttpRequest.Builder builder) { builder.header("Content-Type", "application/json") .header("Authorization", "Bearer " + session.accessToken()); // When omitted the account's current workspace is used. if (workspaceId != null) builder.header("x-workspace-id", workspaceId); return builder; } /** Unwraps and returns data from the response. Throws ApiError on failure. */ public Object api(String method, String path, Object body, Map<String, Object> params) { Resp response = request(method, path, body, params); Object payload = null; try { payload = Json.parse(response.text()); } catch (RuntimeException ignored) { payload = null; } if (!response.ok()) { Map<String, Object> map = Json.asObj(payload); String message = Json.str(map, "message", Json.str(map, "error", "unknown error")); throw new ApiError(response.status, message, map); } return Json.get(payload, "data"); } /** Use the server flag when present, otherwise fall back to the status code. */ public static boolean isTerminal(Map<String, Object> record) { Boolean flag = Json.boolOrNull(record, "isTerminal"); if (flag != null) return flag; Integer status = Json.intOrNull(record, "status"); return status != null && TERMINAL.contains(status); }
// innorix-client.js
const BASE_URL = (process.env.INNORIX_BASE_URL
  || "https://app.innorix.com").replace(/\/+$/, "");
const TOKEN = process.env.INNORIX_ACCESS_TOKEN;
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID || null;

export const STATUS_COMPLETE = 2;
export const TERMINAL = new Set([2, 4, 5, 9, 99]);       // complete / error / cancelled / partial / failed
export const NOT_SUCCEEDED = new Set([4, 5, 9, 99]);

export async function api(method, path, body = null, params = null) {
  const url = new URL(BASE_URL + path);

  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value === undefined || value === null) continue;
      url.searchParams.set(key, String(value));
    }
  }

  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${TOKEN}`,
  };

  // When omitted the account's current workspace is used.
  if (WORKSPACE_ID) headers["x-workspace-id"] = WORKSPACE_ID;

  const response = await fetch(url, {
    method,
    headers,
    body: body === null ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(30_000),
  });

  const payload = await response.json().catch(() => ({}));

  if (!response.ok) {
    throw new Error(payload.message || `HTTP ${response.status}`);
  }

  return payload.data;
}

export function isTerminal(detail) {
  return detail.isTerminal !== undefined
    ? detail.isTerminal
    : TERMINAL.has(detail.status);
}
// InnorixClient.cs
public static readonly string BaseUrl =
    Env("INNORIX_BASE_URL", "https://app.innorix.com").TrimEnd('/');
public static readonly string WorkspaceIdFromEnv = Env("INNORIX_WORKSPACE_ID", null);

public const int StatusComplete = 2;
/// <summary>States the transfer no longer moves out of</summary>
public static readonly HashSet<int> Terminal = new HashSet<int> { 2, 4, 5, 9, 99 };
/// <summary>Terminal states that are not a full success</summary>
public static readonly HashSet<int> NotSucceeded = new HashSet<int> { 4, 5, 9, 99 };

// Applied on every request
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Session.AccessToken);
// When omitted the account's current workspace is used.
if (WorkspaceId != null) request.Headers.TryAddWithoutValidation("x-workspace-id", WorkspaceId);

public async Task<JsonNode> ApiAsync(string method, string path, JsonNode body = null,
    IDictionary<string, object> parameters = null)
{
    Resp response = await RequestAsync(method, path, body, parameters).ConfigureAwait(false);
    JsonNode payload = null;

    try
    {
        payload = J.Parse(response.Text());
    }
    catch (Exception)
    {
        payload = null;
    }

    if (!response.Ok)
    {
        JsonObject map = J.AsObj(payload);
        string message = J.Str(map, "message", J.Str(map, "error", "unknown error"));
        throw new ApiError(response.Status, message, map);
    }

    return J.Get(payload, "data");
}

/// <summary>Use the server flag when present, otherwise fall back to the status code.</summary>
public static bool IsTerminal(JsonObject record)
{
    bool? flag = J.BoolOrNull(record, "isTerminal");
    if (flag != null) return flag.Value;

    int? status = J.IntOrNull(record, "status");
    return status != null && Terminal.Contains(status.Value);
}

Use the following values to determine transfer status. There are five terminal states, and Complete (2) is the successful state.

Status Value Meaning Terminal
2 Complete Yes
4 Error Yes
5 Cancelled Yes
9 Partial Complete Yes
99 Failed Yes
1 · 6 · 12 · 13 Starting · Transferring · Synchronizing · Receiving No

Configure Deployment Targets#

Define device groups as lists and resolve their identifiers

Manage deployment targets as lists. If you work with device names, resolve them to identifiers first.

def resolve_device(name):
    result = api("GET", "/api/devices/resolve", params={"name": name}) or {}
    devices = result.get("devices") or []
    count = result.get("matchCount", len(devices))

    if count != 1 or not devices:
        raise RuntimeError(f"{name}: {count} matches")

    return devices[0]["deviceId"]


GROUPS = {
    "server": ["srv-01", "srv-02", "srv-03"],
    "branch": ["branch-seoul", "branch-busan"],
    "edge":   ["edge-line-01", "edge-line-02"],
}

RESOLVED = {
    group: [resolve_device(name) for name in names]
    for group, names in GROUPS.items()
}
String resolveDevice(String name) {
    Map<String, Object> result = client.apiObj("GET", "/api/devices/resolve", null,
            Json.newObj("name", name));
    List<Object> devices = Json.arrOf(result, "devices");
    int count = Json.intOr(result, "matchCount", devices.size());

    if (count != 1 || devices.isEmpty()) {
        throw new RuntimeException(name + ": " + count + " matches");
    }

    return Json.str(Json.asObj(devices.get(0)), "deviceId");
}

Map<String, List<String>> groups = Map.of(
        "server", List.of("srv-01", "srv-02", "srv-03"),
        "branch", List.of("branch-seoul", "branch-busan"),
        "edge", List.of("edge-line-01", "edge-line-02"));

Map<String, List<String>> resolved = new LinkedHashMap<>();
groups.forEach((group, names) -> {
    List<String> ids = new ArrayList<>();
    for (String n : names) ids.add(resolveDevice(n));
    resolved.put(group, ids);
});
async function resolveDevice(name) {
  const result = (await client.api("GET", "/api/devices/resolve",
    null, { name })) || {};
  const devices = result.devices || [];
  const count = result.matchCount ?? devices.length;

  if (count !== 1 || devices.length === 0) {
    throw new Error(`${name}: ${count} matches`);
  }

  return devices[0].deviceId;
}

const GROUPS = {
  server: ["srv-01", "srv-02", "srv-03"],
  branch: ["branch-seoul", "branch-busan"],
  edge: ["edge-line-01", "edge-line-02"],
};

const RESOLVED = {};
for (const [group, names] of Object.entries(GROUPS)) {
  RESOLVED[group] = [];
  for (const n of names) RESOLVED[group].push(await resolveDevice(n));
}
async Task<string> ResolveDeviceAsync(string name)
{
    JsonObject result = await client.ApiObjAsync("GET", "/api/devices/resolve",
        null, new Dictionary<string, object> { ["name"] = name });
    var devices = J.ArrOf(result, "devices");
    int count = J.Int(result, "matchCount", devices.Count);

    if (count != 1 || devices.Count == 0)
    {
        throw new Exception(
quot;{name}: {count} matches"
); } return J.Str(J.AsObj(devices[0]), "deviceId"); } var groups = new Dictionary<string, string[]> { ["server"] = new[] { "srv-01", "srv-02", "srv-03" }, ["branch"] = new[] { "branch-seoul", "branch-busan" }, ["edge"] = new[] { "edge-line-01", "edge-line-02" }, }; var resolved = new Dictionary<string, List<string>>(); foreach (var (group, names) in groups) { var ids = new List<string>(); foreach (string n in names) ids.Add(await ResolveDeviceAsync(n)); resolved[group] = ids; }

You can also filter the complete device list by condition.

result = api("GET", "/api/devices", params={"page": 1, "size": 200}) or {}

edge_devices = [d["deviceId"] for d in result.get("devices") or []
                if d["name"].startswith("edge-")]
Map<String, Object> result = client.apiObj("GET", "/api/devices", null,
        Json.newObj("page", 1, "size", 200));

List<String> edgeDevices = new ArrayList<>();
for (Object node : Json.arrOf(result, "devices")) {
    Map<String, Object> device = Json.asObj(node);
    if (Json.str(device, "name").startsWith("edge-")) {
        edgeDevices.add(Json.str(device, "deviceId"));
    }
}
const result = (await client.api("GET", "/api/devices", null,
  { page: 1, size: 200 })) || {};

const edgeDevices = (result.devices || [])
  .filter((d) => d.name.startsWith("edge-"))
  .map((d) => d.deviceId);
JsonObject result = await client.ApiObjAsync("GET", "/api/devices", null,
    new Dictionary<string, object> { ["page"] = 1, ["size"] = 200 });

var edgeDevices = J.ArrOf(result, "devices").Select(J.AsObj)
    .Where(d => J.Str(d, "name").StartsWith("edge-"))
    .Select(d => J.Str(d, "deviceId")).ToList();

The device list is returned in the data.devices array.

Design Versioned Paths#

Make the deployment-file version visible in the path

Record the deployed version in the path so you can see which version was applied where.

def package_path(product, version):
    return f"/release/{product}/{version}"


def target_path(product, version):
    return f"/opt/{product}/{version}"


PRODUCT, VERSION = "app", "2.14.0"
String packagePath(String product, String version) {
    return "/release/" + product + "/" + version;
}

String targetPath(String product, String version) {
    return "/opt/" + product + "/" + version;
}

String product = "app", version = "2.14.0";
const packagePath = (product, version) => `/release/${product}/${version}`;
const targetPath = (product, version) => `/opt/${product}/${version}`;

const PRODUCT = "app";
const VERSION = "2.14.0";
string PackagePath(string product, string version) => 
quot;/release/{product}/{version}"
; string TargetPath(string product, string version) =>
quot;/opt/{product}/{version}"
; const string Product = "app"; const string Version = "2.14.0";

Keeping separate version folders preserves previous versions and makes rollback easier if a problem occurs. If every release overwrites the same path, there is no previous version to roll back to.

Multi-Device Deployment#

Send one file to multiple devices

A single transfer handles one target device. Create a transfer for each target and store the returned monitorId by device to track progress and results.

def deploy(source, targets, product, version, target_paths=None):
    src = package_path(product, version)

    # pass a list when targets need different paths; the order must match
    paths = target_paths or [target_path(product, version)] * len(targets)

    return {
        device: api("POST", "/api/transfers/manual", {
            "sourceDevice": source,
            "targetDevice": device,
            "targetPath": path,
            "sourcePaths": [src],
            "sendAllFolder": True,
            "checkIntegrity": True,
            "transferOptions": {"target-action": "overwrite"},
        })["monitorId"]
        for device, path in zip(targets, paths)
    }


transfers = deploy("device-build-01", RESOLVED["edge"], PRODUCT, VERSION)
Map<String, String> deploy(String source, List<String> targets,
                           String product, String version, List<String> targetPaths) {
    String src = packagePath(product, version);

    // pass a list when targets need different paths; the order must match
    List<String> paths = targetPaths;
    if (paths == null) {
        paths = new ArrayList<>();
        for (int i = 0; i < targets.size(); i++) paths.add(targetPath(product, version));
    }

    Map<String, String> transfers = new LinkedHashMap<>();
    for (int i = 0; i < targets.size(); i++) {
        String device = targets.get(i);
        Map<String, Object> transfer = client.apiObj("POST", "/api/transfers/manual", Json.newObj(
                "sourceDevice", source,
                "targetDevice", device,
                "targetPath", paths.get(i),
                "sourcePaths", List.of(src),
                "sendAllFolder", true,
                "checkIntegrity", true,
                "transferOptions", Json.newObj("target-action", "overwrite")));
        transfers.put(device, Json.str(transfer, "monitorId"));
    }

    return transfers;
}

Map<String, String> transfers = deploy("device-build-01", resolved.get("edge"),
        product, version, null);
async function deploy(source, targets, product, version, targetPaths = null) {
  const src = packagePath(product, version);

  // pass a list when targets need different paths; the order must match
  const paths = targetPaths || targets.map(() => targetPath(product, version));

  const transfers = {};
  for (const [i, device] of targets.entries()) {
    const transfer = await client.api("POST", "/api/transfers/manual", {
      sourceDevice: source,
      targetDevice: device,
      targetPath: paths[i],
      sourcePaths: [src],
      sendAllFolder: true,
      checkIntegrity: true,
      transferOptions: { "target-action": "overwrite" },
    });
    transfers[device] = transfer.monitorId;
  }

  return transfers;
}

const transfers = await deploy("device-build-01", RESOLVED.edge, PRODUCT, VERSION);
async Task<Dictionary<string, string>> DeployAsync(string source,
    List<string> targets, string product, string version, List<string> targetPaths = null)
{
    string src = PackagePath(product, version);

    // pass a list when targets need different paths; the order must match
    List<string> paths = targetPaths
        ?? targets.Select(_ => TargetPath(product, version)).ToList();

    var transfers = new Dictionary<string, string>();
    for (int i = 0; i < targets.Count; i++)
    {
        string device = targets[i];
        JsonObject transfer = await client.ApiObjAsync("POST", "/api/transfers/manual",
            new JsonObject
            {
                ["sourceDevice"] = source,
                ["targetDevice"] = device,
                ["targetPath"] = paths[i],
                ["sourcePaths"] = new JsonArray { src },
                ["sendAllFolder"] = true,
                ["checkIntegrity"] = true,
                ["transferOptions"] = new JsonObject { ["target-action"] = "overwrite" },
            });
        transfers[device] = J.Str(transfer, "monitorId");
    }

    return transfers;
}

Dictionary<string, string> transfers = await DeployAsync("device-build-01",
    resolved["edge"], Product, Version);

Within a version folder, redeployments usually send the same files again, so use overwrite. Using numbering creates an additional copy every time you redeploy.

Enable integrity verification. Applying corrupted firmware can prevent a device from booting.

If devices share field network links, apply a rate limit so deployment traffic does not interfere with other work.

json
"transferOptions": {
  "target-action": "overwrite",
  "limitRate": 20480
}

The limitRate unit is KB/s.

Aggregate Results#

Collect and review application status by device

def wait(monitor_id, timeout=3600, interval=3):
    deadline = time.time() + timeout

    while time.time() < deadline:
        detail = api("GET", f"/api/transfers/{monitor_id}")

        if is_terminal(detail):
            return detail

        time.sleep(interval)

    raise TimeoutError(monitor_id)


def failed_files(monitor_id):
    result = api("GET", f"/api/transfers/{monitor_id}/files", params={
        "state": "any", "size": 500,
    }) or {}

    return [r for r in (result.get("children") or [])
            if r.get("status") in NOT_SUCCEEDED]


def retry_failed(monitor_id):
    rows = failed_files(monitor_id)

    if not rows:
        return 0

    api("POST", f"/api/transfers/{monitor_id}/retry", {
        "filesRetry": [
            {"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
            for r in rows
        ]
    })

    return len(rows)
public Map<String, Object> wait(String monitorId, int timeoutSeconds, int intervalSeconds) {
    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;

    while (System.currentTimeMillis() < deadline) {
        Map<String, Object> detail = apiObj("GET", "/api/transfers/" + monitorId);

        if (isTerminal(detail)) return detail;

        sleep(intervalSeconds * 1000L);
    }

    throw new ApiError(0, "timeout waiting for " + monitorId);
}

public List<Map<String, Object>> failedFiles(String monitorId) {
    List<Map<String, Object>> failed = new ArrayList<>();

    for (Map<String, Object> row : transferFiles(monitorId)) {
        Integer status = Json.intOrNull(row, "status");
        if (status != null && NOT_SUCCEEDED.contains(status)) failed.add(row);
    }

    return failed;
}

public int retryFailed(String monitorId) {
    List<Map<String, Object>> rows = failedFiles(monitorId);
    if (rows.isEmpty()) return 0;

    List<Object> filesRetry = new ArrayList<>();

    for (Map<String, Object> row : rows) {
        String path = Json.str(row, "sourceFilePath");
        if (path == null) continue;
        filesRetry.add(Json.newObj("filePath", path, "isDir", Json.bool(row, "isFolder", false)));
    }

    api("POST", "/api/transfers/" + monitorId + "/retry", Json.newObj("filesRetry", filesRetry));

    return rows.size();
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function wait(monitorId, { timeout = 3600, interval = 3 } = {}) {
  const deadline = Date.now() + timeout * 1000;

  while (Date.now() < deadline) {
    const detail = await api("GET", `/api/transfers/${monitorId}`);

    if (isTerminal(detail)) return detail;

    await sleep(interval * 1000);
  }

  throw new Error(`timeout waiting for ${monitorId}`);
}

export async function failedFiles(monitorId) {
  const result = (await api("GET", `/api/transfers/${monitorId}/files`, null, {
    state: "any", size: 500,
  })) || {};

  return (result.children || []).filter((row) => NOT_SUCCEEDED.has(row.status));
}

export async function retryFailed(monitorId) {
  const rows = await failedFiles(monitorId);

  if (rows.length === 0) return 0;

  await api("POST", `/api/transfers/${monitorId}/retry`, {
    filesRetry: rows
      .filter((row) => row.sourceFilePath)
      .map((row) => ({ filePath: row.sourceFilePath, isDir: Boolean(row.isFolder) })),
  });

  return rows.length;
}
public async Task<JsonObject> WaitAsync(string monitorId, int timeoutSeconds = 3600,
    int intervalSeconds = 3)
{
    long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;

    while (Environment.TickCount64 < deadline)
    {
        JsonObject detail = await ApiObjAsync("GET", "/api/transfers/" + monitorId)
            .ConfigureAwait(false);

        if (IsTerminal(detail)) return detail;

        await Task.Delay(intervalSeconds * 1000).ConfigureAwait(false);
    }

    throw new ApiError(0, "timeout waiting for " + monitorId);
}

public async Task<List<JsonObject>> FailedFilesAsync(string monitorId)
{
    List<JsonObject> rows = await TransferFilesAsync(monitorId).ConfigureAwait(false);

    return rows.Where(row =>
    {
        int? status = J.IntOrNull(row, "status");
        return status != null && NotSucceeded.Contains(status.Value);
    }).ToList();
}

/// <summary>Only callable once the transfer has reached a terminal state.</summary>
public async Task<int> RetryFailedAsync(string monitorId)
{
    List<JsonObject> rows = await FailedFilesAsync(monitorId).ConfigureAwait(false);
    if (rows.Count == 0) return 0;

    var filesRetry = new JsonArray();

    foreach (JsonObject row in rows)
    {
        string path = J.Str(row, "sourceFilePath");
        if (path == null) continue;

        filesRetry.Add(new JsonObject
        {
            ["filePath"] = path,
            ["isDir"] = J.Bool(row, "isFolder", false),
        });
    }

    await ApiAsync("POST", "/api/transfers/" + monitorId + "/retry",
        new JsonObject { ["filesRetry"] = filesRetry }).ConfigureAwait(false);

    return rows.Count;
}

Continue checking the remaining devices even if one fails so you can determine how far the deployment was applied.

def deploy_report(transfers, timeout=3600):
    results = {}

    for device, monitor_id in transfers.items():
        try:
            results[device] = wait(monitor_id, timeout=timeout)
        except TimeoutError:
            results[device] = {"status": None}

    succeeded = [d for d, r in results.items()
                 if r.get("status") == STATUS_COMPLETE]
    failed = [d for d in results if d not in succeeded]

    return succeeded, failed, results


succeeded, failed, results = deploy_report(transfers)

print(f"{len(succeeded)} succeeded / {len(failed)} failed")

for device in failed:
    detail = results[device]
    print(f"  {device:20} {detail.get('statusName') or detail.get('status')}")
record DeployResult(List<String> succeeded, List<String> failed,
                    Map<String, Map<String, Object>> results) {}

DeployResult deployReport(Map<String, String> transfers, int timeout) {
    Map<String, Map<String, Object>> results = new LinkedHashMap<>();

    transfers.forEach((device, monitorId) -> {
        try {
            results.put(device, client.await(monitorId, timeout, null));
        } catch (RuntimeException error) {
            results.put(device, Json.newObj("status", null));
        }
    });

    List<String> succeeded = new ArrayList<>();
    List<String> failed = new ArrayList<>();
    results.forEach((device, detail) -> {
        if (Json.intOr(detail, "status", -1) == InnorixClient.STATUS_COMPLETE) succeeded.add(device);
        else failed.add(device);
    });

    return new DeployResult(succeeded, failed, results);
}

DeployResult report = deployReport(transfers, 3600);
System.out.printf("%d succeeded / %d failed%n",
        report.succeeded().size(), report.failed().size());

for (String device : report.failed()) {
    Map<String, Object> detail = report.results().get(device);
    System.out.println("  " + device + " " + Json.str(detail, "statusName"));
}
async function deployReport(transfers, timeout = 3600) {
  const results = {};

  for (const [device, monitorId] of Object.entries(transfers)) {
    try {
      results[device] = await wait(monitorId, { timeout });
    } catch {
      results[device] = { status: null };
    }
  }

  const succeeded = Object.entries(results)
    .filter(([, r]) => r.status === STATUS_COMPLETE).map(([d]) => d);
  const failed = Object.keys(results).filter((d) => !succeeded.includes(d));

  return { succeeded, failed, results };
}

const { succeeded, failed, results } = await deployReport(transfers);
console.log(`${succeeded.length} succeeded / ${failed.length} failed`);

for (const device of failed) {
  console.log(`  ${device} ${results[device].statusName ?? results[device].status}`);
}
async Task<(List<string> Succeeded, List<string> Failed,
    Dictionary<string, JsonObject> Results)> DeployReportAsync(
    Dictionary<string, string> transfers, int timeout = 3600)
{
    var results = new Dictionary<string, JsonObject>();

    foreach (var (device, monitorId) in transfers)
    {
        try
        {
            results[device] = await client.WaitAsync(monitorId, timeout);
        }
        catch
        {
            results[device] = new JsonObject { ["status"] = null };
        }
    }

    var succeeded = results
        .Where(kv => J.IntOrNull(kv.Value, "status") == InnorixClient.StatusComplete)
        .Select(kv => kv.Key).ToList();
    var failed = results.Keys.Where(d => !succeeded.Contains(d)).ToList();

    return (succeeded, failed, results);
}

var (succeeded, failed, results) = await DeployReportAsync(transfers);
Console.WriteLine(
quot;{succeeded.Count} succeeded / {failed.Count} failed"
); foreach (string device in failed) { Console.WriteLine(
quot; {device} {J.Str(results[device], "statusName")}"
); }
Item Details
status Transfer status by device
percent Progress
fileCount · totalSize Number and total size of applied files
endDate Completion time

Redeploy Failed Devices#

Skip successful devices and redeploy only failed targets

Rerunning the entire deployment retransfers files to devices that already succeeded. Select only the failed targets and rerun them.

for device in failed:
    monitor_id = transfers[device]
    rows = failed_files(monitor_id)

    if rows:
        # reached the device, but some files failed
        print(f"{device}: retried {retry_failed(monitor_id)} files")
    else:
        # no per file record means the transfer never started
        state = api("GET", f"/api/devices/{device}/connectivity") or {}
        print(f"{device}: transfer never started - connected {state.get('isConnected')}"
              f" ({state.get('stateLabel')})")
for (String device : failed) {
    String monitorId = transfers.get(device);
    List<Map<String, Object>> rows = client.failedFiles(monitorId);

    if (!rows.isEmpty()) {
        // reached the device, but some files failed
        System.out.println(device + ": retried " + client.retryFailed(monitorId) + " files");
    } else {
        // no per file record means the transfer never started
        Map<String, Object> state = client.apiObj("GET",
                "/api/devices/" + device + "/connectivity");
        System.out.println(device + ": transfer never started - connected "
                + Json.bool(state, "isConnected", false)
                + " (" + Json.str(state, "stateLabel") + ")");
    }
}
for (const device of failed) {
  const monitorId = transfers[device];
  const rows = await failedFiles(monitorId);

  if (rows.length) {
    // reached the device, but some files failed
    console.log(`${device}: retried ${await retryFailed(monitorId)} files`);
  } else {
    // no per file record means the transfer never started
    const state = (await client.api("GET",
      `/api/devices/${device}/connectivity`)) || {};
    console.log(`${device}: transfer never started - connected ${state.isConnected}`
      + ` (${state.stateLabel})`);
  }
}
foreach (string device in failed)
{
    string monitorId = transfers[device];
    List<JsonObject> rows = await client.FailedFilesAsync(monitorId);

    if (rows.Count > 0)
    {
        // reached the device, but some files failed
        Console.WriteLine(
quot;{device}: retried {await client.RetryFailedAsync(monitorId)} files"
); } else { // no per file record means the transfer never started JsonObject state = await client.ApiObjAsync("GET",
quot;/api/devices/{device}/connectivity"
); Console.WriteLine(
quot;{device}: transfer never started - "
+
quot;connected {J.Bool(state, "isConnected", false)} "
+
quot;({J.Str(state, "stateLabel")})"
); } }

If there is no per-file failure record, check device connectivity first. After the connection is restored, rerun the deployment.

if still_offline:
    retry_transfers = deploy("device-build-01", recovered, PRODUCT, VERSION)
    _, still_failed, _ = deploy_report(retry_transfers)
if (stillOffline) {
    Map<String, String> retryTransfers = deploy("device-build-01", recovered,
            product, version, null);
    DeployResult retry = deployReport(retryTransfers, 3600);
}
if (stillOffline) {
  const retryTransfers = await deploy("device-build-01", recovered, PRODUCT, VERSION);
  const { failed: stillFailed } = await deployReport(retryTransfers);
}
if (stillOffline)
{
    Dictionary<string, string> retryTransfers = await DeployAsync(
        "device-build-01", recovered, Product, Version);
    var (_, stillFailed, _) = await DeployReportAsync(retryTransfers);
}

Review Deployment History#

Check which version was applied to each device and when

from datetime import datetime, timedelta, timezone


def paginate(path, params=None, limit=200, max_pages=50):
    query = dict(params or {})
    query["limit"] = limit
    cursor = None

    for _ in range(max_pages):
        if cursor:
            query["cursor"] = cursor

        result = api("GET", path, params=query) or {}

        for record in result.get("data") or []:
            yield record

        pagination = result.get("pagination") or {}

        if not pagination.get("hasMore"):
            return

        cursor = pagination.get("nextCursor")

        if not cursor:
            return


end = datetime.now(timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%SZ"

for row in paginate(f"/api/devices/{device_id}/transfer-history", params={
    "startDate": (end - timedelta(days=30)).strftime(fmt),
    "endDate": end.strftime(fmt),
}):
    print(row.get("startDate"), row.get("statusName"), row.get("monitorId"))
List<Map<String, Object>> history = client.paginate(
        "/api/devices/" + deviceId + "/transfer-history",
        Json.newObj("startDate", start, "endDate", end), 200, 50);

for (Map<String, Object> row : history) {
    System.out.println(Json.str(row, "startDate") + " "
            + Json.str(row, "statusName") + " " + Json.str(row, "monitorId"));
}
for await (const row of paginate(`/api/devices/${deviceId}/transfer-history`, {
  startDate: start,
  endDate: end,
})) {
  console.log(row.startDate, row.statusName, row.monitorId);
}
List<JsonObject> history = await client.PaginateAsync(
    
quot;/api/devices/{deviceId}/transfer-history"
, new Dictionary<string, object> { ["startDate"] = start, ["endDate"] = end }); foreach (JsonObject row in history) { Console.WriteLine(
quot;{J.Str(row, "startDate")} {J.Str(row, "statusName")} "
+
quot;{J.Str(row, "monitorId")}"
); }

Recording the deployment identifier and version together in the application links deployment history to the corresponding version.

db.insert("deployments", {
    "product": PRODUCT,
    "version": VERSION,
    "device": device,
    "monitorId": transfers[device],
    "result": "done" if device in succeeded else "failed",
})
db.insert("deployments", Json.newObj(
        "product", product,
        "version", version,
        "device", device,
        "monitorId", transfers.get(device),
        "result", succeeded.contains(device) ? "done" : "failed"));
await db.insert("deployments", {
  product: PRODUCT,
  version: VERSION,
  device,
  monitorId: transfers[device],
  result: succeeded.includes(device) ? "done" : "failed",
});
await db.InsertAsync("deployments", new
{
    product = Product,
    version = Version,
    device,
    monitorId = transfers[device],
    result = succeeded.Contains(device) ? "done" : "failed",
});
Item Details
Deployment File Transferred package and version
Target Device and group
Result Success or failure by device
Redeployment Targets rerun and their results
History Recent deployment records by device
PreviousSchedule Recurring File TransfersNextDistributing Software Packages to Multiple Servers and Branches

On this page

  • Getting Started
  • Basic Concept
  • Deployment Flow
  • Deployment Benefits
  • IT Engineers
  • Deployment Files
  • Target Setup
  • Deployment Criteria
  • Multi-Device Deployment
  • Result Management
  • Developers
  • Integration Setup
  • Configure Deployment Targets
  • Design Versioned Paths
  • Multi-Device Deployment
  • Aggregate Results
  • Redeploy Failed Devices
  • Review Deployment History