Centrally Collecting Files from Branches, Factories, and Edge Devices

Getting Started

Core Concept

Bring files generated across multiple sites into a single central environment

Business systems at branches, production equipment in factories, and edge devices in the field generate various files, including business documents, production data, logs, and result files.

Branch, factory, and edge data collection connects file-generation locations at each site to a central collection environment and automatically transfers files to headquarters servers or cloud storage according to configured conditions.

text
Branch A ──────┐
Branch A ──────┐
Factory B ──────┼────→ Central Collection Environment ────→ Headquarters Server
             │              │
Factory B ──────┼────→ Central Collection Environment ────→ Headquarters Server

Edge Device C ─┘ └──────────→ Cloud

Connecting files from each site into a single central collection flow lets you gather data generated across distributed environments into designated storage locations for subsequent analysis and business processing.

Collection Flow

Detect files generated at sites and automatically move them to the central storage location

When a file is created or changed at a site, the configured collection conditions are checked and the files in scope are transferred to a central server or cloud.

text
Site File Creation
      │
      ▼
File Change Detection
      │
      ▼
Check Collection Conditions
      │
      ▼
Transfer to Central Environment
      │
      ▼
Review Collection Result

You can configure collection jobs to start when files are created or changed, or according to a defined schedule. Collected files can then be used for analysis and follow-up work in the central environment.

Operational Benefits

Manage data flows from multiple sites together from a central location

Each site may use different equipment, file-generation locations, and collection times. Connecting them to a central collection environment lets you manage files generated across multiple sites as a single flow.

Site EnvironmentGenerated FilesCentral Collection LocationUse
Branchtext text · textHeadquarters ServerBusiness Review
FactoryProduction Data · Inspection ResultsCloudtext · Quality Management
Edge DeviceSensor Data · textCentral Analysis EnvironmentData Processing

IT Engineer

Collection Environment

Connect branches, factories, edge devices, and file-generation locations

First, connect the branches, production equipment, and edge devices from which files will be collected to the central management environment.

Specify the folders or storage locations where files are generated on each device to configure the targets and file paths that collection jobs should monitor.

text
Branch-A
└── /data/report

Factory-01
└── /production/result

Edge-Server-01
└── /logs/device

Collection Paths

Connect site-specific file locations to the central storage environment

After connecting the collection targets, configure each site's file path and central storage location as a single collection path.

You can gather files from each site to the headquarters server, or transfer them to cloud storage or an analysis environment according to the data type and intended use.

text
Branch A ────────┐
               │
Factory B ────────┼──→ Central Collection ───→ Headquarters Server
               │         │
Edge Device C ───┘         └───────→ Cloud Storage
collection sourcefile pathcentral storage text
Branch A/report/daily/data/branch
Factory B/production/result/data/factory
Edge C/logs/device/data/edge

Collection Conditions

Start collection jobs based on file events and schedules

You can start collection jobs when files are created or changed, or configure them to collect required files according to a defined schedule.

You can define the collection scope based on file paths and types so that only the required data is collected from files generated at each site.

text
              Collection Start Condition
               Collection Start Condition
       ┌────────────┼────────────┐
       ▼            ▼            ▼
     File Creation      File Change      Recurring Schedule
       │            │            │
       └────────────┼────────────┘
                    ▼
                 File Collection

Collection Automation

Connect central collection to data processing and result storage

After collecting data from multiple sites centrally, collection jobs can continue into analysis, transformation, or separate storage operations.

text
Branch Data ────┐
                │
Factory Data ────┼──→ Central Collection ───→ Data Processing
                │                       │
Edge Data ───────┘                       ▼
                                    Result Storage

By connecting collected files to processing equipment, you can build an automated flow from site data collection → central transfer → data processing → result storage.

Collection Status

Review collection jobs and processing results from multiple sites centrally

Runstext Dataset text text text Branchtext Factory, Edge Devicetext collection jobtext text reviewtext text text.

devicetext execution statustext text, processingtext file text, text execution resulttext text text Collection Statustext text.

text
Branch-A
├── Status      Completed
├── Files       128
└── Last Run    Completed

Factory-01
├── Status      Running
Branch-A
└── Last Run    In Progress

Edge-Server-01
├── Status      Completed
└── Files       2,431

text text text sitetext File Collection jobtext text reviewtext text text, central text text text devicetext processing statustext text text text text.

Operational Response

site textresult file processing statustext reviewtext required job again executiontext

When a collection job requires review, use the execution details and Activity Log to check the site's device connection, file path, access scope, and processing result.

text
Collection Job Execution
      │
      ▼
Execution Status Check
      │
 ┌────┴─────┐
 ▼          ▼
Completed  Review Required
 │          │
 ▼          ▼
Review Result  Check Device · Path · File Status
                │
             Adjust Configuration
             Adjust Configuration
                │
             Rerun Operation
              Rerun Operation
                │
              Review Result
               Review Result

Developer

Register collection automation for each site and aggregate connection status and collection metrics

Integration Preparation

Prepare common request code and path representation

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)
import base64
import time


def encode_path(device_id, raw_path):
    normalized = str(raw_path or "").replace("\\", "/")
    token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
    return f"{device_id}_ino_{token}"


def now_iso():
    return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())

Determine the transfer status using the values below. There are five terminal states, and the successful value is Complete (2).

Status ValueMeaningTerminal
2CompleteYes
4ErrorYes
5CancelledYes
9Partially CompleteYes
99FailedYes
1 · 6 · 12 · 13Started · Transferring · Synchronizing · ReceivingNo

Configure Site List

Keep site information as data and create collection jobs in bulk

When there are dozens of sites, creating them one by one in the UI is difficult. Keep the site list as data and iterate through it.

SITES = [
    {"device": "branch-seoul", "path": "/report/daily", "target": "/data/branch"},
    {"device": "branch-busan", "path": "/report/daily", "target": "/data/branch"},
    {"device": "factory-01", "path": "/production/result", "target": "/data/factory"},
    {"device": "edge-line-01", "path": "/logs/device", "target": "/data/edge"},
]

CENTRAL = "device-hq-01"


def site_target(site):
    # sites reuse the same file names, so put the site id in the target path
    return f"{site['target']}/{site['device']}"

If destination paths are not separated, files with the same name, such as result.csv, will overwrite one another between sites.

Register Collection Automation

Create a collection job for each site and define its execution conditions

def build_collection(site, central, schedule=None, options=None):
    name = f"collect {site['device']}"
    sync = schedule is None

    body = {
        "name": name,
        "flowName": name,
        "transferType": "sync" if sync else "normal",
        "timezone": "Asia/Seoul",
        "step": 1,
        "isUpcoming": False,
        "details": [
            {
                "senderId": site["device"],
                "receiverId": central,
                "sourceItem": [
                    {
                        "hash": encode_path(site["device"], site["path"]),
                        "filePath": site["path"],
                        "isDir": True,
                    }
                ],
                "targetPath": encode_path(central, site_target(site)),
                "step": 1,
                "transferOptions": {
                    "noSchedule": sync,
                    "target-action": "numbering",
                    "send-fileoption": {},
                    **({"syncType": 1} if sync else {}),
                    **(options or {}),
                },
            }
        ],
        "schedules": [schedule or {
            "type": "none", "startDateType": "now",
            "startDate": now_iso(), "timezone": "Asia/Seoul",
        }],
    }

    return body


DAILY = {
    "type": "day", "startDateType": "now",
    "hour": "02", "minute": "00", "ampm": "am",
    "startDate": now_iso(), "timezone": "Asia/Seoul",
}

collectors = {
    site["device"]: api("POST", "/api/automations",
                        build_collection(site, CENTRAL, DAILY))["automationId"]
    for site in SITES
}

There are four items that must be followed in an automation request.

ItemSpecification
isUpcomingMust be false. The server default true ignores the schedule in the request and replaces it with a five-minute one-time schedule. Steps with triggerAutomation are forced to false by the server, so specify it directly only on the first step without a trigger.
stepInclude it at both the top level and in details. It represents the hop position in the flow.
sourceItemInclude both hash (path token) and filePath (plain-text path).
syncTypePut it inside transferOptions. 1 is one-way and 2 is bidirectional.

All four items can be omitted and registration will still succeed, but behavior changes at execution time. If a recurring schedule was registered but runs only once and stops, check isUpcoming first.

Execution ModeConfigurationSuitable Site
Scheduled ExecutiontransferType: normal + scheduleBranches where files are created at a fixed time
Creation DetectiontransferType: sync + transferOptions.syncTypeProduction equipment and edge devices where files are created intermittently

Because file-generation times differ by site, you do not have to standardize on a single method.

Preventing Duplicate Registration

Prevent the same collection job from being created twice

A new automation is created even when an automation with the same name already exists. Iterating through the site list again would execute collection twice.

def find_automation(name):
    # the name we send is stored as flowName in the response
    # automationName is a server generated id like T4037-8500-1815, not the name we set.
    for page in range(1, 6):
        result = api("GET", "/api/automations",
                     params={"page": page, "size": 100, "search": name}) or {}

        items = [item
                 for flow in result.get("automations") or []
                 for item in flow.get("automations") or []]

        for item in items:
            if item.get("flowName") == name:
                return item

        if len(items) < 100:
            return None

    return None


def ensure_collection(site, central, schedule=None):
    name = f"collect {site['device']}"

    if find_automation(name):
        return None

    return api("POST", "/api/automations",
               build_collection(site, central, schedule))["automationId"]

The automation list is returned nested by flow group, so you must iterate through the inner arrays as well. Server search uses partial matching, so select only the item whose name exactly matches the name received.

Offline Site Response

Find disconnected sites and send queued files after recovery

Site equipment can experience unstable network connectivity. The response differs depending on whether collection failed or the device was disconnected.

def site_state(device_id):
    state = api("GET", f"/api/devices/{device_id}/connectivity") or {}
    return bool(state.get("isConnected")), state.get("stateLabel")


for site in SITES:
    connected, label = site_state(site["device"])

    if not connected:
        print(f"{site['device']:20} {label}")

The connection status in the response is isConnected. The accompanying stateLabel can be used directly in the UI.

When the connection returns, send the files that accumulated during the outage in one batch. Compare the source and target file lists and select only missing files.

def list_files(device_id, path):
    found, page = {}, 1

    while True:
        result = api("GET", f"/api/devices/{device_id}/files", params={
            "path": path, "page": page, "size": 200, "type": "file",
        }) or {}

        for item in result.get("items") or []:
            found[item["name"]] = item.get("size")

        if page >= (result.get("lastPage") or 1):
            return found

        page += 1


def catch_up(site, central):
    source_files = list_files(site["device"], site["path"])
    missing = sorted(set(source_files) - set(list_files(central, site_target(site))))

    if not missing:
        return None

    # send file lists through sourceItem; sourcePaths treats every path as a folder
    return api("POST", "/api/transfers/manual", {
        "sourceDevice": site["device"],
        "targetDevice": central,
        "targetPath": site_target(site),
        "sourceItem": [
            {"path": f"{site['path'].rstrip('/')}/{name}",
             "isDir": False,
             "fileSize": source_files[name]}
            for name in missing
        ],
        "sendAllFolder": False,
        "transferOptions": {"target-action": "numbering"},
    })["monitorId"]

Passing the file size along lets the server skip querying the size of each item again, improving performance.

When the destination policy is numbering, files accumulate with numbered names. In that configuration, find files based on transfer history rather than comparing names.

Aggregate Collection Status

Review collection results from multiple sites at once

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
from collections import Counter
from datetime import datetime, timedelta, timezone


def site_summary(device_id, days=1):
    end = datetime.now(timezone.utc)
    fmt = "%Y-%m-%dT%H:%M:%SZ"

    rows = list(paginate(f"/api/devices/{device_id}/transfer-history", params={
        "startDate": (end - timedelta(days=days)).strftime(fmt),
        "endDate": end.strftime(fmt),
    }))

    failed = [r for r in rows if r.get("status") in NOT_SUCCEEDED]
    return {"total": len(rows), "failed": len(failed)}


header = "{:20} {:^6} {:>6} {:>6}".format("site", "up", "collect", "fail")
print(header)
print("-" * len(header))

for site in SITES:
    connected, _ = site_state(site["device"])
    summary = site_summary(site["device"])

    print(f"{site['device']:20} {'O' if connected else 'X':^6}"
          f" {summary['total']:>6} {summary['failed']:>6}")

Transfer history is returned in the data.data array, and pagination information is returned in data.pagination.

Exception Handling

Review failed collections and rerun them

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)
for device, automation_id in collectors.items():
    runs = api("GET", f"/api/automations/{automation_id}/executions") or []

    if not runs:
        print(f"{device}: no run history - check registration and start condition")
        continue

    latest = runs[0]

    if latest["status"] != STATUS_COMPLETE:
        print(f"{device}: retried {retry_failed(latest['monitorId'])} files")

execution text text text text text text. text text text text text text executiontext text text isUpcomingtext Started conditiontext reviewtext.

review Itemreview text
site deviceisConnectedtext stateLabel
collection jobsitetext text execution condition
text pathsite text text storage text
Collection Statussitetext collection text Failed
text filetext text text file