애플리케이션에 시스템 간 파일 전송

즉시 전송

즉시 전송은 예약이나 자동화 없이, API 한 번 호출로 소스 디바이스에서 타깃 디바이스로 파일·폴더를 바로 보내는 방식입니다.

개요

즉시 전송이란

소스 디바이스의 특정 파일/폴더를 타깃 디바이스의 지정 경로로 한 번에 전송합니다. 전송을 생성하면 monitorId가 발급되고, 이 ID로 진행 상태 조회와 일시정지·재개·취소·재시도 같은 제어를 수행합니다.

공통 준비

Base URL

https://app.innorix.com

인증 헤더 — 모든 요청에 다음 헤더가 필요합니다.

헤더설명
Authorization: Bearer {accessToken}로그인으로 발급받은 액세스 토큰(JWT)
x-workspace-id: {workspaceId}작업 대상 워크스페이스 ID
Content-Type: application/json요청 본문 형식

액세스 토큰은 POST /api/auth/login(email, password)으로 발급받으며 응답의 data.user.accessToken을 사용합니다. 만료 시 POST /api/auth/token/refresh(X-Refresh-Token 헤더)로 갱신하고, 장기 연동에는 POST /api/auth/api-keys로 API 키를 발급할 수 있습니다.

디바이스 개념 — 전송의 소스와 타깃은 모두 에이전트가 설치된 디바이스입니다. GET /api/devices로 목록을 조회해 deviceId를 얻습니다. 각 디바이스는 os(windows·linux·mac), 온라인 여부(status) 등의 속성을 가집니다.

전송 대상 지정(sourceItem) — 보낼 항목은 sourceItem 배열로 지정합니다.

필드타입설명
hashstring항목 식별자 — {deviceId}_ino_{base64(UTF-8 경로)}
isDirboolean폴더 여부

폴더 하위 전체를 보낼 때는 sendAllFolder를 사용합니다.

장비 조회 — GET /api/devices/resolve — 이름·IP·MAC으로 deviceId를 즉시 조회합니다(name·ip·mac 중 최소 1개).

json
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "matchCount": 1,
    "devices": [
      {
        "deviceId": "dev_01H8...",
        "name": "OfficePC",
        "ipAddress": "192.168.0.9",
        "osType": "windows",
        "state": 1, "stateName": "CONNECTED", "stateLabel": "연결됨",
        "isConnected": true
      }
    ]
  }
}

장비명이 여러 개 매칭되면 409 + data.candidates[]로 응답하므로, 모호하지 않은 이름을 사용합니다.

폴더 조회(비스트리밍) — GET /api/devices/{deviceId}/files — 폴더 직계 항목을 JSON으로 조회합니다(재귀 검색은 files/search SSE 사용). 응답의 pathfileToken이 함께 제공되며, fileToken은 전송·파일 작업의 항목 식별자로 그대로 사용할 수 있습니다.

json
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "path": "/data", "total": 128, "page": 1, "size": 50, "lastPage": 3,
    "items": [
      {
        "name": "보고서.pdf",
        "path": "/data/보고서.pdf",
        "fileToken": "L2RhdGEv...",
        "isDir": false, "size": 20480,
        "modifiedAt": "2026-08-01T09:12:00Z"
      }
    ]
  }
}

열거형 값 병기 — 단건·상세 응답의 열거형 필드는 정수값과 함께 상수명·라벨을 제공합니다(state/stateName/stateLabel). 연결 여부 같은 파생 플래그(isConnected)도 함께 제공됩니다.

핵심 엔드포인트

목적MethodEndpoint
로그인POST/api/auth/login
토큰 갱신POST/api/auth/token/refresh
디바이스 목록GET/api/devices
디바이스 조회(이름·IP·MAC)GET/api/devices/resolve
경로 용량 미리보기GET/api/devices/{deviceId}/path-stats
소스 파일 검색POST/api/devices/{deviceId}/files/search
폴더 조회(비스트리밍)GET/api/devices/{deviceId}/files
경로 사전 검증POST/api/transfers/validate-path
즉시 전송 생성POST/api/transfers/manual
전송 파일 조회GET/api/transfers/{monitorId}/files
전송 제어POST/api/transfers/{monitorId}/pause · resume · cancel · retry

기본 흐름

  1. 로그인해 액세스 토큰 확보
  2. GET /api/devicessourceId, targetId 확인
  3. (선택) 파일 검색으로 sourceItem 구성
  4. (선택) validate-path로 경로 검증
  5. POST /api/transfers/manual로 전송 생성 → monitorId 수신
  6. monitorId로 진행 모니터링·제어

1:1 전송

설명

하나의 소스 디바이스에서 하나의 타깃 디바이스로 지정한 파일을 즉시 전송하는 가장 기본 패턴입니다. sourceId·targetIdsourceItem(항목 hash)으로 소스·타깃과 보낼 항목을 지정하고, monitorId로 완료까지 상태를 확인합니다.

사용 API

목적MethodEndpoint
로그인POST/api/auth/login
전송 생성POST/api/transfers/manual
상태 조회GET/api/transfers/{monitorId}

Request

POST /api/transfers/manual

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/data/incoming",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }],
  "sendAllFolder": false
}
  • sourceId·targetIdGET /api/devices로 얻은 deviceId입니다.
  • sourceItemhash{deviceId}_ino_{base64(UTF-8 경로)} 형식입니다.
  • sendAllFolder는 불리언입니다.

ℹ️ sourceId/targetId에는 GET /api/devices로 얻은 deviceId를 지정합니다. 이름·IP로 조회하려면 GET /api/devices/resolve를 사용하세요.

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

처리 순서

  1. POST /api/auth/login으로 액세스 토큰 확보(data.user.accessToken)
  2. POST /api/transfers/manual 호출 — sourceId, targetId, targetPath, sourceItemdata.monitorId 수신
  3. GET /api/transfers/{monitorId}를 폴링해 status 확인 — 2=완료, 4·5·9·99=실패로 종료

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
    for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
        if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
            return detail;
        }
        await new Promise((resolve) => setTimeout(resolve, 2000));
    }
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 보낼 항목은 sourceItem의 hash로 지정한다.
    const sourceItem = [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }];
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem,
        sendAllFolder: false,
    });
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

파일 수집

설명

여러 소스(지점)의 폴더를 하나의 타깃 경로로 모으는 패턴입니다. 지점마다 대상 폴더를 검색해 전송을 생성하고, 모든 전송의 완료를 함께 집계합니다.

사용 API

목적MethodEndpoint
로그인POST/api/auth/login
장비 조회(이름)GET/api/devices/resolve
폴더 조회(비스트리밍)GET/api/devices/{deviceId}/files
전송 생성POST/api/transfers/manual
상태 조회GET/api/transfers/{monitorId}

Request

POST /api/transfers/manual (소스마다 반복)

json
{
  "sourceId": "dev_01H8BRANCH...",
  "targetId": "dev_01H8HQ...",
  "targetPath": "/collect/logs",
  "sourceItem": [{ "hash": "dev_01H8BRANCH..._ino_L3Zhci9sb2cvYXBwLWxvZw==", "isDir": true }],
  "sendAllFolder": true,
  "transferOptions": { "target-action": "numbering" }
}
  • sourceId만 지점마다 바꾸고 targetId·targetPath는 고정합니다.
  • transferOptionstarget-action: numbering으로 같은 경로에 모을 때 이름 충돌 시 번호를 붙입니다.
  • 폴더 전체 수집이므로 sendAllFoldertrue입니다.

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

처리 순서

  1. POST /api/auth/login으로 액세스 토큰 확보
  2. 지점마다 반복:
    • GET /api/devices/resolve?name=으로 지점 sourceId 확보
    • GET /api/devices/{deviceId}/files로 대상 폴더 검색 → 경로 확보
    • POST /api/transfers/manual 호출(targetId=타깃, sourceItem=폴더 항목) → monitorId 수집
  3. 모든 monitorIdGET /api/transfers/{monitorId}로 폴링해 완료 집계

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const BRANCH_DEVICES = (process.env.INNORIX_BRANCH_DEVICES || "branch-pc-01,branch-pc-02,branch-pc-03").split(",");
const HQ_DEVICE = process.env.INNORIX_HQ_DEVICE || "headquarters-server";
const SOURCE_ROOT = process.env.INNORIX_SOURCE_ROOT || "/var/log";
const SEARCH = process.env.INNORIX_LOG_FOLDER_SEARCH || "log";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/collect/logs";
async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}
async function resolveDevice(name, token) {
    const data = await api("GET", `/api/devices/resolve?name=${encodeURIComponent(name)}`, token);
    if (data.matchCount !== 1) throw new Error(`Device must resolve uniquely: ${name}`);
    return data.devices[0].deviceId;
}
function encodePath(deviceId, path) {
    // UTF-8 Base64 인코딩 전에 경로 구분자를 정규화한다.
    const normalizedPath = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalizedPath, "utf8").toString("base64")}`;
}

async function main() {
    // 세 지점의 로그 폴더를 검색한 뒤 각각 독립된 전송을 생성한다.
    const login = await api("POST", "/api/auth/login", null, {
        email: process.env.INNORIX_EMAIL,
        password: process.env.INNORIX_PASSWORD,
    });
    const token = login.user.accessToken;
    const targetId = await resolveDevice(HQ_DEVICE, token);
    const monitorIds = new Map();
    for (const rawName of BRANCH_DEVICES) {
        const branchName = rawName.trim();
        const sourceId = await resolveDevice(branchName, token);
        const query = new URLSearchParams({ path: SOURCE_ROOT, type: "dir", search: SEARCH, size: "500" });
        const listing = await api("GET", `/api/devices/${sourceId}/files?${query}`, token);
        if (!listing.items.length) throw new Error(`Log folder not found: ${branchName}`);
        const folder = listing.items[0];
        const transfer = await api("POST", "/api/transfers/manual", token, {
            sourceId,
            targetId,
            targetPath: TARGET_PATH,
            sourceItem: [{ hash: encodePath(sourceId, folder.path), isDir: true }],
            sendAllFolder: true,
            transferOptions: { "target-action": "numbering" },
        });
        monitorIds.set(branchName, transfer.monitorId);
    }

    // 모든 monitorId가 종료될 때까지 병렬 작업의 전체 완료 상태를 집계한다.
    const pending = new Map(monitorIds);
    while (pending.size) {
        await Promise.all(
            [...pending].map(async ([branchName, monitorId]) => {
                const detail = await api("GET", `/api/transfers/${monitorId}`, token);
                console.log(`${branchName}: status=${detail.status} percent=${detail.percent || 0}`);
                if ([2, 4, 5, 9, 99].includes(detail.status)) {
                    if (detail.status !== 2) throw new Error(`${branchName}: ${detail.errorCode || "failed"}`);
                    pending.delete(branchName);
                }
            })
        );
        if (pending.size) await new Promise((resolve) => setTimeout(resolve, 2000));
    }
    console.log(`Collected logs from ${monitorIds.size} branches`);
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

파일 배포

설명

하나의 소스 파일 세트를 여러 타깃 디바이스로 배포하는 패턴입니다. 소스에서 배포 폴더를 한 번 조회한 뒤, 각 타깃으로 동일 패키지를 전송하고 성공·실패를 집계합니다.

사용 API

목적MethodEndpoint
로그인POST/api/auth/login
장비 조회(이름)GET/api/devices/resolve
폴더 조회(비스트리밍)GET/api/devices/{deviceId}/files
전송 생성POST/api/transfers/manual
상태 조회GET/api/transfers/{monitorId}

Request

POST /api/transfers/manual (타깃마다 반복)

json
{
  "sourceId": "dev_01H8SRC...",
  "targetId": "dev_01H8BRANCH01...",
  "targetPath": "/deploy",
  "sourceItem": [{ "hash": "dev_01H8SRC..._ino_L2RlcGxveS9kZXBsb3ltZW50LXBhY2thZ2U=", "isDir": true }],
  "sendAllFolder": true,
  "transferOptions": { "target-action": "overwrite" }
}
  • targetId만 지점마다 바꾸고 sourceId·sourceItem은 고정합니다.
  • transferOptionstarget-action: overwrite로 타깃의 기존 파일을 덮어씁니다.
  • 폴더 전체 배포이므로 sendAllFoldertrue입니다.

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

처리 순서

  1. POST /api/auth/login으로 액세스 토큰 확보
  2. GET /api/devices/resolve?name=으로 소스 deviceId 확보
  3. GET /api/devices/{deviceId}/files로 배포 패키지 폴더를 검색해 경로 확보
  4. 각 타깃마다 POST /api/transfers/manual 호출(targetId=지점, sourceItem=패키지 항목) → 타깃 수만큼 monitorId
  5. monitorIdGET /api/transfers/{monitorId}로 폴링해 성공·실패 집계

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_IDS = (process.env.INNORIX_TARGET_IDS || "device-target-01,device-target-02,device-target-03").split(",");
const SOURCE_ROOT = process.env.INNORIX_SOURCE_ROOT || "/deploy";
const PACKAGE_NAME = process.env.INNORIX_PACKAGE_NAME || "deployment-package";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/deploy";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 본사에서 배포 패키지 폴더를 한 번 검색한다.
    const query = new URLSearchParams({ path: SOURCE_ROOT, type: "dir", search: PACKAGE_NAME, size: "500" });
    const listing = await api("GET", `/api/devices/${SOURCE_ID}/files?${query}`, token);
    const packageFolder = listing.items.find((item) => item.name === PACKAGE_NAME);
    if (!packageFolder) throw new Error(`Package folder not found: ${PACKAGE_NAME}`);
    const sourceItem = [{ hash: encodePath(SOURCE_ID, packageFolder.path), isDir: true }];

    // 동일한 패키지를 여러 타깃으로 각각 전송한다.
    const pending = new Map(),
        results = new Map();
    for (const rawId of TARGET_IDS) {
        const targetId = rawId.trim();
        const transfer = await api("POST", "/api/transfers/manual", token, {
            sourceId: SOURCE_ID,
            targetId,
            targetPath: TARGET_PATH,
            sourceItem,
            sendAllFolder: true,
            transferOptions: { "target-action": "overwrite" },
        });
        pending.set(targetId, transfer.monitorId);
    }

    // 종료된 타깃을 성공과 실패 목록으로 나누어 집계한다.
    while (pending.size) {
        await Promise.all(
            [...pending].map(async ([targetId, monitorId]) => {
                const detail = await api("GET", `/api/transfers/${monitorId}`, token);
                if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
                    results.set(targetId, detail.status === TRANSFER_STATUS.transferComplete ? "success" : detail.errorCode || "failed");
                    pending.delete(targetId);
                }
            })
        );
        if (pending.size) await new Promise((resolve) => setTimeout(resolve, 2000));
    }
    const succeeded = [...results].filter(([, value]) => value === "success").map(([name]) => name);
    const failed = Object.fromEntries([...results].filter(([, value]) => value !== "success"));
    console.log({ succeeded, failed });
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

전송 규칙

설명

하나의 전송이 무엇을 / 어디로 / 어떻게 보낼지 결정하는 요청 구성 요소를 정리합니다. 세부 항목은 이어지는 절에서 다룹니다.

구분관련 필드세부
무엇을(소스)sourceItem, sendAllFolder, filter소스 필터
어디로(타깃)targetId, targetPath대상 옵션
배치(경로)pathMapping, sendAllFolder경로 매핑
겹칠 때transferOptions.target-action충돌 처리

사용 API

목적MethodEndpoint
경로 검증POST/api/transfers/validate-path
전송 생성POST/api/transfers/manual

Request

POST /api/transfers/validate-path

json
{
  "sourceId": "device-src-001",
  "targetId": "device-dst-002",
  "targetPath": "/data/incoming",
  "sourceItem": [{ "hash": "device-src-001_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }]
}

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": { "valid": true }
}

처리 순서

  1. POST /api/transfers/validate-path로 소스·타깃·경로 유효성 확인
  2. 규칙(sourceItem/filter/transferOptions.pathMapping/transferOptions.target-action)을 구성해 POST /api/transfers/manual 호출
  3. 응답 data.monitorId로 후속 처리

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
    for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
        if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
            return detail;
        }
        await new Promise((resolve) => setTimeout(resolve, 2000));
    }
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;
    const sourceItem = [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }];

    // 1) 보내기 전에 소스·타깃 경로가 유효한지 먼저 검증한다.
    const validation = await api("POST", "/api/transfers/validate-path", token, { sourceId: SOURCE_ID, targetId: TARGET_ID, sourceItem, targetPath: TARGET_PATH });
    console.log("validate-path", validation);

    // 2) 필터·경로매핑·충돌정책을 한 요청에 조합해 전송한다.
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem,
        sendAllFolder: false,
        transferOptions: {
            "send-filetype-cus": "\\.(log|csv)$", // 필터: log·csv 확장자만
            savepath: true, // 경로 매핑: 소스 폴더 구조 유지
            optionPath: "relative",
            "target-action": "numbering", // 충돌 정책: 이름 뒤 (1) 부여
        },
    });
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    // 3) 완료될 때까지 확인한다.
    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

소스 필터

설명

소스에서 보낼 항목을 조건으로 선별합니다. sourceItem으로 항목을 직접 지정하는 것 외에, 전송 요청에 filter 옵션을 더하면 확장자·크기·수정시각 기준으로 서버 측에서 걸러냅니다. sendAllFolder와 함께 쓰면 폴더 하위를 필터 조건에 맞게 재귀 수집합니다.

사용 API

목적MethodEndpoint
파일 검색POST/api/devices/{deviceId}/files/search
필터 전송 생성POST/api/transfers/filtered

filter 옵션 필드:

필드타입설명
filter.includeExtensionsstring[]포함할 확장자 목록 (예: ["log","csv"])
filter.excludePatternsstring[]제외할 glob 패턴 (예: ["*.tmp"])
filter.minSizenumber최소 크기(바이트)
filter.maxSizenumber최대 크기(바이트)
filter.modifiedAfterstring이 시각 이후 수정 파일만 (ISO 8601)
filter.modifiedBeforestring이 시각 이전 수정 파일만 (ISO 8601)
filter.recursiveboolean하위 폴더 재귀 탐색 여부

Request

POST /api/transfers/filtered

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/data/incoming",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvbG9ncw==", "isDir": true }],
  "sendAllFolder": true,
  "filter": {
    "recursive": true,
    "includeExtensions": ["log", "csv"],
    "excludePatterns": ["*.tmp"],
    "minSize": 1024,
    "modifiedAfter": "2026-08-01T00:00:00Z"
  }
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

응답의 data.monitorId로 이후 상태 조회(GET /api/transfers/{monitorId}/files)와 제어(pause·resume·cancel·retry)를 수행합니다.

처리 순서

  1. (선택) POST /api/devices/{deviceId}/files/search로 후보 목록 확인
  2. POST /api/transfers/filteredfilter를 포함해 호출
  3. 응답 data.monitorId로 진행 상태 조회

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/var/log";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/collect/logs";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
    for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
        if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
            return detail;
        }
        await new Promise((resolve) => setTimeout(resolve, 2000));
    }
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 어제 이후 수정된 *.log·*.csv 중 1KB 이상만, *.tmp는 제외하고 하위 폴더까지 재귀 전송한다.
    const yesterday = new Date(Date.now() - 86400000).toISOString();
    const transfer = await api("POST", "/api/transfers/filtered", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: true }],
        sendAllFolder: true,
        filter: {
            recursive: true,
            includeExtensions: ["log", "csv"],
            excludePatterns: ["*.tmp"],
            minSize: 1024,
            modifiedAfter: yesterday,
        },
    });
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

대상 옵션

설명

타깃을 지정하는 방법입니다. 타깃은 디바이스(targetId)와 도착 경로(targetPath)로 정해집니다. 타깃 폴더가 없으면 사전에 생성할 수 있습니다.

사용 API

목적MethodEndpoint
타깃 폴더 생성POST/api/devices/{deviceId}/files/folders
경로 검증POST/api/transfers/validate-path
용량 확인GET/api/devices/{deviceId}/capacity
전송 생성POST/api/transfers/manual

Request

POST /api/transfers/manual

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/data/incoming/2026",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }]
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

응답의 data.monitorId로 이후 상태 조회(GET /api/transfers/{monitorId}/files)와 제어(pause·resume·cancel·retry)를 수행합니다.

처리 순서

  1. (선택) POST /api/devices/{deviceId}/files/folders로 타깃 폴더 생성
  2. GET /api/devices/{deviceId}/capacity로 도착 경로 여유 용량 확인
  3. POST /api/transfers/manual 호출(sourceId, targetId, targetPath)
  4. 응답 data.monitorId 수신

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming/2026/08";
const REQUIRED_BYTES = process.env.INNORIX_SOURCE_SIZE || "107374182400";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
    for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
        if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
            return detail;
        }
        await new Promise((resolve) => setTimeout(resolve, 2000));
    }
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 1) 타깃에 없는 날짜 폴더를 만든다.
    await api("POST", `/api/devices/${TARGET_ID}/files/folders`, token, { path: TARGET_PATH });

    // 2) 남은 디스크 용량이 충분한지 확인하고, 부족하면 전송 전에 실패시킨다.
    const capacity = await api("GET", `/api/devices/${TARGET_ID}/capacity?path=${encodeURIComponent(TARGET_PATH)}&requiredBytes=${REQUIRED_BYTES}`, token);
    if (capacity.sufficient !== true) throw new Error("Target capacity was not confirmed");

    // 3) 용량이 확인되면 전송한다.
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }],
        sendAllFolder: false,
    });
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

경로 매핑

설명

소스 항목이 타깃의 어느 위치에 놓이는지 결정합니다. 도착 기준은 targetPath이며, transferOptions.pathMapping 옵션으로 폴더 구조 보존, 접두 경로 제거, 파일명 규칙을 지정합니다. 항목 경로는 sourceItemhash로 지정합니다.

사용 API

목적MethodEndpoint
전송 생성POST/api/transfers/manual
배치 결과 조회GET/api/transfers/{monitorId}/files

transferOptions.pathMapping 옵션 필드:

필드타입설명
pathMapping.preserveStructureboolean소스 폴더 구조 유지(false면 평탄화)
pathMapping.removePrefixstring소스 경로에서 제거할 접두 경로
pathMapping.targetRootstring배치 기준이 되는 타깃 루트 경로
pathMapping.fileNameTemplatestring파일명 템플릿 (예: {name}, 날짜 접두)
pathMapping.createTargetFoldersboolean없는 타깃 폴더 자동 생성

Request

POST /api/transfers/manual

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/archive",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvMjAyNi9yZXBvcnRz", "isDir": true }],
  "sendAllFolder": true,
  "transferOptions": {
    "pathMapping": {
      "preserveStructure": true,
      "removePrefix": "/data/2026",
      "targetRoot": "/archive",
      "fileNameTemplate": "20260825_{name}",
      "createTargetFolders": true
    }
  }
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

응답의 data.monitorId로 이후 상태 조회(GET /api/transfers/{monitorId}/files)와 제어(pause·resume·cancel·retry)를 수행합니다.

처리 순서

  1. POST /api/transfers/manualpathMapping을 포함해 호출
  2. 응답 data.monitorId 수신
  3. GET /api/transfers/{monitorId}/files로 실제 배치 결과 확인

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/2026/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/archive";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
    for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
        if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
            return detail;
        }
        await new Promise((resolve) => setTimeout(resolve, 2000));
    }
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 파일명 앞에 붙일 전송일자(YYYYMMDD)를 만든다.
    const today = new Date().toISOString().slice(0, 10).replaceAll("-", "");

    // 소스 폴더 구조는 유지하되 /data/2026 접두 경로를 떼고,
    // 파일명 앞에 전송일자를 붙여 /archive 아래에 배치한다.
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }],
        sendAllFolder: false,
        transferOptions: {
            pathMapping: {
                preserveStructure: true,
                removePrefix: "/data/2026",
                targetRoot: "/archive",
                fileNameTemplate: `${today}_{name}`,
                createTargetFolders: true,
            },
        },
    });
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

충돌 처리

설명

타깃 경로에 같은 이름의 파일이 이미 있을 때의 처리 방식입니다. transferOptionstarget-action으로 번호 부여(numbering)·건너뛰기(skip)·덮어쓰기(overwrite)·실패(fail) 중 하나를 지정합니다. 전송 중 실패한 파일은 재시도로 이어받습니다.

사용 API

목적MethodEndpoint
전송 생성POST/api/transfers/manual
실패 재시도POST/api/transfers/{monitorId}/retry

transferOptions.target-action 값:

설명
numbering이름 충돌 시 번호를 붙여 둘 다 보존 ((1), (2))
skip이미 있으면 건너뜀
overwrite기존 파일을 덮어씀
fail충돌 시 해당 파일 실패 처리

Request

POST /api/transfers/manual

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/data/incoming",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }],
  "transferOptions": { "target-action": "numbering" }
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

응답의 data.monitorId로 이후 상태 조회(GET /api/transfers/{monitorId}/files)와 제어(pause·resume·cancel·retry)를 수행합니다.

처리 순서

  1. POST /api/transfers/manualtransferOptions.target-action을 포함해 호출
  2. 응답 data.monitorId 수신
  3. 실패한 파일은 POST /api/transfers/{monitorId}/retry로 재전송

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
// 충돌 정책을 바꿔가며 결과가 어떻게 달라지는지 확인한다.
const CONFLICT_POLICIES = ["numbering", "skip", "overwrite", "fail"];
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForTerminal(monitorId, token) {
    // fail 정책은 의도적으로 실패하므로 완료가 아닌 '종료'까지만 기다린다.
    for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) return detail;
        await new Promise((resolve) => setTimeout(resolve, 2000));
    }
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    const sourceItem = [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }];
    for (const policy of CONFLICT_POLICIES) {
        // 같은 파일을 같은 위치로 보내되 충돌 정책만 바꾼다.
        const transfer = await api("POST", "/api/transfers/manual", token, {
            sourceId: SOURCE_ID,
            targetId: TARGET_ID,
            targetPath: TARGET_PATH,
            sourceItem,
            sendAllFolder: false,
            transferOptions: { "target-action": policy },
        });
        const detail = await waitForTerminal(transfer.monitorId, token);

        // 정책별로 파일 단위 결과를 조회한다.
        const files = await api("GET", `/api/transfers/${transfer.monitorId}/files?state=any&size=100`, token);
        console.log(`policy=${policy} status=${detail.status}`, files.items?.map((file) => ({ name: file.name, state: file.state })));
    }
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

전송 자동화

사람이 매번 호출하지 않아도, 일정·파일 이벤트·워크플로에 따라 전송이 자동으로 실행되도록 구성합니다.

반복 자동화

설명

일정(스케줄)에 따라 같은 전송을 반복 실행합니다. 자동화를 생성하면 automationId가 발급되고, 일시정지·상세 조회로 운영합니다.

사용 API

목적MethodEndpoint
자동화 생성POST/api/automations
자동화 목록GET/api/automations
자동화 상세GET/api/automations/{automationId}/details
자동화 일시정지POST/api/automations/{automationId}/pause

Request

POST /api/automations

json
{
  "name": "Daily Settlement Transfer",
  "transferType": "normal",
  "timezone": "Asia/Seoul",
  "details": [
    {
      "senderId": "device-src-001",
      "receiverId": "device-hq-001",
      "targetPath": "/collect/logs",
      "sourceItem": [{ "hash": "device-src-001_ino_...", "isDir": false }],
      "step": 1,
      "transferOptions": { "noSchedule": false, "target-action": "numbering", "send-fileoption": {} }
    }
  ],
  "schedules": [
    { "type": "day", "startDateType": "now", "hour": "02", "minute": "00", "ampm": "am", "startDate": "2026-08-25T00:00:00Z", "timezone": "Asia/Seoul" }
  ]
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "automationId": "auto-abc123" }
}

처리 순서

  1. POST /api/automations 호출 — name, schedules(일정 객체 배열), details(전송 정의 배열), timezone, transferType
  2. 응답 data.automationId 수신
  3. GET /api/automations/{automationId}/details로 세부·진행 확인
  4. 필요 시 POST /api/automations/{automationId}/pause로 일시정지

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TIMEZONE = process.env.INNORIX_TIMEZONE || "UTC";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 매일 새벽 2시에 전일 정산 파일을 본사 서버로 보내는 일정.
    const schedule = { type: "day", startDateType: "now", hour: "02", minute: "00", ampm: "am", startDate: new Date().toISOString(), timezone: TIMEZONE };

    // 1) 일정 등록
    const created = await api("POST", "/api/automations", token, {
        name: "Daily Settlement Transfer",
        details: [{ sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }], targetPath: TARGET_PATH, senderId: SOURCE_ID, receiverId: TARGET_ID, step: 1, transferOptions: { noSchedule: false, "target-action": "numbering", "send-fileoption": {} } }],
        transferType: "normal",
        timezone: TIMEZONE,
        schedules: [schedule],
    });
    const automationId = created.automationId;
    console.log("automation created", automationId);

    // 2) 일정 조회
    console.log("detail", await api("GET", `/api/automations/${automationId}`, token));

    // 3) 일정 수정
    await api("PATCH", `/api/automations/${automationId}`, token, { name: "Daily Settlement Transfer", isUpdateSchedule: true, schedules: [schedule] });

    // 4) 지난 실행 이력 조회
    console.log("executions", await api("GET", `/api/automations/${automationId}/executions`, token));

    // 5) 일시 중지
    await api("POST", `/api/automations/${automationId}/pause`, token, { pause: true });

    // 6) 삭제 (예제 정리)
    await api("DELETE", `/api/automations/${automationId}`, token);
    console.log("automation deleted", automationId);
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

Webhook

설명

전송·자동화 상태 변화를 외부 시스템으로 통지합니다. 통합(integration)을 webhook 모드로 등록하거나, 자동화의 callbackURL로 완료 콜백을 받습니다.

사용 API

목적MethodEndpoint
웹훅 생성POST/api/webhooks
전달 이력 조회GET/api/webhooks/{webhookId}/deliveries
전달 재시도POST/api/webhooks/{webhookId}/deliveries/{deliveryId}/retry

Request

POST /api/webhooks

json
{
  "url": "https://internal.example.com/innorix",
  "events": ["transfer.succeeded", "transfer.failed"],
  "active": true,
  "retryPolicy": { "maxAttempts": 5, "initialDelaySeconds": 30 }
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "webhookId": "wh-abc123" }
}

처리 순서

  1. POST /api/webhooks 호출 — url, events, active, retryPolicy
  2. 응답 data.webhookId 수신
  3. 이후 전송 이벤트 발생 시 등록한 URL로 통지(서명은 HMAC-SHA256으로 검증)
  4. 전달 이력은 GET /api/webhooks/{webhookId}/deliveries, 실패 전달은 .../retry로 재시도

구현 예제

javascript
const { createHmac, timingSafeEqual } = require("node:crypto");

const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const WEBHOOK_URL = process.env.INNORIX_WEBHOOK_URL || "https://internal.example.com/innorix";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function verifySignature(secret, timestamp, rawBody, receivedSignature) {
    // 통보가 위조되지 않았는지 (timestamp.body)를 HMAC-SHA256으로 검증한다.
    const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
    const a = Buffer.from(expected);
    const b = Buffer.from(receivedSignature || "");
    return a.length === b.length && timingSafeEqual(a, b);
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 1) 전송 완료·실패 이벤트를 통보받을 웹훅을 등록한다.
    const created = await api("POST", "/api/webhooks", token, {
        url: WEBHOOK_URL,
        events: ["transfer.succeeded", "transfer.failed"],
        active: true,
        retryPolicy: { maxAttempts: 5, initialDelaySeconds: 30 }, // 우리 서버가 죽어 있었으면 재통보
    });
    const webhookId = created.webhookId;
    console.log("webhook created", webhookId);

    // 2) 수신 측에서 받은 통보의 서명을 검증한다. (환경변수로 실제 값 주입)
    if (process.env.INNORIX_WEBHOOK_SECRET) {
        const ok = verifySignature(process.env.INNORIX_WEBHOOK_SECRET, process.env.INNORIX_WEBHOOK_TIMESTAMP, process.env.INNORIX_WEBHOOK_PAYLOAD, process.env.INNORIX_WEBHOOK_SIGNATURE);
        if (!ok) throw new Error("Invalid webhook signature");
        console.log("Webhook signature verified");
    }

    // 3) 통보 전송 이력을 조회한다.
    const deliveries = await api("GET", `/api/webhooks/${webhookId}/deliveries?limit=20`, token);
    console.log("deliveries", deliveries);

    // 4) 실패한 통보를 재전송한다.
    const failed = deliveries.items?.find((item) => item.status !== "delivered");
    if (failed) await api("POST", `/api/webhooks/${webhookId}/deliveries/${failed.deliveryId}/retry`, token);
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

워크플로

설명

여러 단계를 이어 하나의 자동화 흐름으로 구성합니다. 자동화를 플로우 방식으로 만들고(isFlowDesign), 각 단계는 자동화 컴포넌트로 연결합니다.

사용 API

목적MethodEndpoint
자동화 생성(플로우)POST/api/automations
플로우 상세 조회GET/api/automations/{automationId}/details
실행 이력 조회GET/api/automations/{automationId}/executions

Request

POST /api/automations

json
{
  "name": "A to B to C Workflow",
  "flowName": "A-B-C Workflow",
  "transferType": "normal",
  "timezone": "Asia/Seoul",
  "details": [
    { "senderId": "device-a", "receiverId": "device-b", "targetPath": "/data/incoming", "sourceItem": [{ "hash": "device-a_ino_...", "isDir": false }], "step": 1, "transferOptions": { "target-action": "numbering" } },
    { "senderId": "device-b", "receiverId": "device-c", "targetPath": "/data/incoming", "sourceItem": [{ "hash": "device-b_ino_...", "isDir": false }], "step": 2, "transferOptions": { "target-action": "numbering" } }
  ]
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "automationId": "auto-flow-abc123" }
}

처리 순서

  1. POST /api/automations로 플로우 자동화 생성 — flowName, details(단계 배열, 각 step)
  2. 응답 data.automationId 수신
  3. GET /api/automations/{automationId}/details로 구성 확인
  4. 자동화 실행 시 detailsstep 순서대로 처리, 이력은 .../executions

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01"; // A
const MIDDLE_ID = process.env.INNORIX_MIDDLE_ID || "device-middle-01"; // B
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01"; // C
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const MIDDLE_PATH = process.env.INNORIX_MIDDLE_PATH || SOURCE_PATH;
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TIMEZONE = process.env.INNORIX_TIMEZONE || "UTC";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // A→B→C 체인 전송을 서버에 정의한다. B→C는 step 순서에 따라
    // A→B 완료 후 서버가 자동으로 이어서 실행한다. (클라이언트 폴링 조립 아님)
    const created = await api("POST", "/api/automations", token, {
        name: "A to B to C Workflow",
        flowName: "A-B-C Workflow",
        details: [
            { sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }], targetPath: TARGET_PATH, senderId: SOURCE_ID, receiverId: MIDDLE_ID, step: 1, transferOptions: { noSchedule: false, "target-action": "numbering", "send-fileoption": {} } },
            { sourceItem: [{ hash: encodePath(MIDDLE_ID, MIDDLE_PATH), isDir: false }], targetPath: TARGET_PATH, senderId: MIDDLE_ID, receiverId: TARGET_ID, step: 2, transferOptions: { noSchedule: false, "target-action": "numbering", "send-fileoption": {} } },
        ],
        transferType: "normal",
        timezone: TIMEZONE,
        schedules: [{ type: "none", startDateType: "now", hour: "00", minute: "00", ampm: "am", startDate: new Date().toISOString(), timezone: TIMEZONE }],
    });
    const automationId = created.automationId;
    console.log("workflow created", automationId);

    // 단계별 정의와 전체 실행 상태를 조회한다.
    console.log("steps", await api("GET", `/api/automations/${automationId}/details`, token));
    console.log("executions", await api("GET", `/api/automations/${automationId}/executions`, token));
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

파일 동기화

두 위치의 파일 상태를 지속적으로 일치시킵니다. 실시간 감시는 핫 폴더로, 주기적 동기화는 자동화로 구성합니다.

동기화 개요

설명

동기화는 POST /api/sync/jobs로 동기화 작업(job)을 만들어 구성합니다. mode로 방향(단방향·양방향), realTime으로 트리거(실시간·주기), incremental 계열 옵션으로 범위(전체·증분)를 조합해 정책을 만듭니다.

사용 API

목적MethodEndpoint
동기화 작업 생성POST/api/sync/jobs
작업 상태 조회GET/api/sync/jobs/{jobId}
일시정지·재개POST/api/sync/jobs/{jobId}/pause · resume
작업 삭제DELETE/api/sync/jobs/{jobId}

처리 순서

  1. 동기화 방향(mode)·트리거(realTime)·범위 결정
  2. POST /api/sync/jobs로 작업 생성 → jobId
  3. GET /api/sync/jobs/{jobId}로 상태 확인, pause·resume으로 운영
  4. 완료·불필요 시 DELETE /api/sync/jobs/{jobId}

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/master";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/mirror";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 동기화 작업의 생명주기: 생성 → 조회 → 일시중지 → 재개 → 삭제.
    // (즉시 전송과 달리 작업이 지속되며 상태를 가진다.)
    const created = await api("POST", "/api/sync/jobs", token, {
        source: { deviceId: SOURCE_ID, path: SOURCE_PATH },
        target: { deviceId: TARGET_ID, path: TARGET_PATH },
        mode: "one-way",
    });
    const jobId = created.jobId;
    console.log("sync job created", jobId);
    console.log("detail", await api("GET", `/api/sync/jobs/${jobId}`, token));
    await api("POST", `/api/sync/jobs/${jobId}/pause`, token);
    console.log("paused", jobId);
    await api("POST", `/api/sync/jobs/${jobId}/resume`, token);
    console.log("resumed", jobId);
    await api("DELETE", `/api/sync/jobs/${jobId}`, token);
    console.log("deleted", jobId);
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

단방향

설명

소스의 변경을 타깃으로만 반영합니다(소스 → 타깃). 타깃의 변경은 소스로 되돌아가지 않습니다.

사용 API

목적MethodEndpoint
동기화 작업 생성POST/api/sync/jobs

Request

POST /api/sync/jobs

json
{
  "source": { "deviceId": "device-src-001", "path": "/sync/source" },
  "target": { "deviceId": "device-dst-002", "path": "/sync/target" },
  "mode": "one-way",
  "targetChangePolicy": "restore-source"
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "jobId": "job-oneway-01" }
}

처리 순서

  1. POST /api/sync/jobsmode: one-way로 작업 등록
  2. 소스 변경 발생 시 타깃으로 자동 반영(타깃 변경은 targetChangePolicy로 처리)
  3. GET /api/sync/jobs/{jobId}로 상태 확인

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/master";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/mirror";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 본사 마스터 폴더를 지점 PC에 그대로 미러링한다(단방향).
    // 지점에서 임의로 바꾼 파일은 본사 기준으로 되돌린다(restore-source).
    const created = await api("POST", "/api/sync/jobs", token, {
        source: { deviceId: SOURCE_ID, path: SOURCE_PATH },
        target: { deviceId: TARGET_ID, path: TARGET_PATH },
        mode: "one-way",
        targetChangePolicy: "restore-source",
    });
    const jobId = created.jobId;
    console.log("sync job created", jobId);
    console.log("detail", await api("GET", `/api/sync/jobs/${jobId}`, token));
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

양방향

설명

두 위치의 변경을 서로 반영합니다(소스 ↔ 타깃). 양쪽을 각각 감시해 어느 쪽 변경이든 상대편으로 전송합니다.

사용 API

목적MethodEndpoint
동기화 작업 생성POST/api/sync/jobs

처리 순서

  1. POST /api/sync/jobsmode: two-way로 작업 등록
  2. 변경 감지 기준은 changeDetection(예: sha256)로 지정
  3. 어느 쪽 변경이든 상대편으로 반영
  4. 양쪽 동시 변경은 conflictPolicy(예: newest-wins)로 처리

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/shared-a";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/shared-b";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 두 작업장의 공유 폴더를 양쪽 모두 최신 상태로 맞춘다(양방향).
    // 변경 판정은 해시(sha256)로, 충돌은 최신 파일 우선으로 처리한다.
    const created = await api("POST", "/api/sync/jobs", token, {
        source: { deviceId: SOURCE_ID, path: SOURCE_PATH },
        target: { deviceId: TARGET_ID, path: TARGET_PATH },
        mode: "two-way",
        changeDetection: "sha256",
        conflictPolicy: "newest-wins",
    });
    const jobId = created.jobId;
    console.log("sync job created", jobId);
    console.log("detail", await api("GET", `/api/sync/jobs/${jobId}`, token));
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

실시간

설명

파일 변경을 즉시 감지해 지연 없이 동기화합니다. 일정이 아니라 파일 이벤트가 트리거입니다.

사용 API

목적MethodEndpoint
동기화 작업 생성(실시간)POST/api/sync/jobs
이벤트 전달POST/api/sync/jobs/{jobId}/events

처리 순서

  1. POST /api/sync/jobsrealTime: true로 작업 등록
  2. 파일 생성·변경·삭제·이름 변경 즉시 반영(propagateDeletes·propagateRenames)
  3. 에이전트 이벤트는 POST /api/sync/jobs/{jobId}/events로 전달

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/shared";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/shared";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 폴더 변경을 몇 초 내로 반대편에 반영하는 실시간 동기화.
    // 생성·수정뿐 아니라 삭제·이름변경 이벤트도 그대로 전파한다.
    const created = await api("POST", "/api/sync/jobs", token, {
        source: { deviceId: SOURCE_ID, path: SOURCE_PATH },
        target: { deviceId: TARGET_ID, path: TARGET_PATH },
        mode: "two-way",
        realTime: true,
        propagateDeletes: true,
        propagateRenames: true,
        targetDelaySeconds: 3,
    });
    const jobId = created.jobId;
    console.log("sync job created", jobId);

    // 에이전트가 전달하는 이름변경(renamed) 이벤트 페이로드 예시.
    // (삭제는 eventType:"deleted" + path, 생성/수정은 "created"/"modified")
    await api("POST", `/api/sync/jobs/${jobId}/events`, token, {
        eventId: "event-20260825-001",
        eventType: "renamed",
        deviceId: SOURCE_ID,
        occurredAt: new Date().toISOString(),
        path: "/shared/old-name.txt",
        newPath: "/shared/new-name.txt",
        isDirectory: false,
    });
    console.log("rename event sent");
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

증분

설명

전체를 매번 보내지 않고, 마지막 동기화 이후 변경된 파일만 전송합니다. 반복 자동화에 변경 기준을 두어 구성합니다.

사용 API

목적MethodEndpoint
증분 전송 생성POST/api/transfers/manual (incremental: true)
주기 동기화POST/api/automations
상태 조회GET/api/transfers/{monitorId}

처리 순서

  1. POST /api/transfers/manualincremental: true로 변경분만 전송(주기화는 POST /api/automations)
  2. 마지막 동기화 이후 변경된 파일만 선별 전송
  3. GET /api/transfers/{monitorId}로 반영 결과 확인

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/bigfolder";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function encodePath(deviceId, path) {
    // 경로 구분자를 정규화한 뒤 UTF-8 기준 Base64로 인코딩한다.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
    for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
        if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
            return detail;
        }
        await new Promise((resolve) => setTimeout(resolve, 2000));
    }
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 큰 폴더를 매일 동기화하되 바뀐 파일만 보낸다.
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: true }],
        sendAllFolder: true,
        incremental: true, // 변경분만 전송
    });
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

확장 활용

즉시 전송·자동화·동기화를 조합해 실제 개발 시나리오에 적용하는 유스케이스입니다.

Git 밖 파일

설명

대용량 바이너리·미디어·데이터셋처럼 Git으로 관리하기 어려운 파일을 디바이스 간에 전송·동기화합니다.

사용 API

목적MethodEndpoint
전송 생성POST/api/transfers/manual
주기 동기화POST/api/automations
상태 조회GET/api/transfers/{monitorId}/files

처리 순서

  1. 관리 대상 폴더/파일을 sourceItem으로 지정
  2. 1회성은 POST /api/transfers/manual, 지속 관리는 POST /api/automations
  3. GET /api/transfers/{monitorId}/files로 반영 확인

빌드 결과물

설명

CI/CD 파이프라인이 만든 빌드 산출물을 배포 대상 디바이스로 전달합니다. 파이프라인에서 전송을 트리거하고, 완료를 웹훅으로 통지받습니다.

사용 API

목적MethodEndpoint
전송 생성POST/api/transfers/manual
자동 배포POST/api/automations
완료 통지POST/api/webhooks

처리 순서

  1. 빌드 완료 후 파이프라인에서 POST /api/transfers/manual 호출(다수 대상은 파일 배포 패턴)
  2. 응답 data.monitorId 수신
  3. POST /api/webhooks로 등록한 웹훅으로 배포 완료 통지 수신

AI·데이터

설명

학습 데이터셋·추론 결과 같은 대용량 데이터를 수집 서버나 처리 노드로 이동합니다. 이동 완료를 트리거로 후속 파이프라인을 연결합니다.

사용 API

목적MethodEndpoint
데이터 수집POST/api/transfers/manual
주기 수집POST/api/automations
후속 트리거POST/api/webhooks

처리 순서

  1. 데이터 위치를 sourceItem으로 지정해 POST /api/transfers/manual(수집은 파일 수집 패턴)
  2. 완료 후 data.monitorId로 상태 확인
  3. POST /api/webhooks 웹훅으로 후속 처리(학습·추론) 트리거

결과·운영

전송이 생성된 뒤 상태 확인, 오류 대응, 중단 복구, 모니터링, 기록 관리를 다룹니다.

상태·결과

설명

진행 중 전송의 파일별 상태와, 완료된 전송의 결과를 조회합니다.

사용 API

목적MethodEndpoint
전송 상태·진행률GET/api/transfers/{monitorId}
진행 중 파일 조회GET/api/transfers/{monitorId}/files
완료 이력 상세GET/api/transfer-history/{monitorId}
완료 이력 파일GET/api/transfers/{monitorId}/files?state=history

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": {
    "monitorId": "mon-abc123",
    "status": 2, "statusName": "COMPLETED", "statusLabel": "완료",
    "files": [
      {
        "path": "/data/report.pdf",
        "fileToken": "L2RhdGEv...",
        "status": 2, "statusName": "DONE", "statusLabel": "완료",
        "size": 20480
      }
    ]
  }
}

처리 순서

  1. GET /api/transfers/{monitorId}로 전송 상태·진행률 확인 — status 2=완료, 4·5·9·99=실패로 종료. 파일별 진행은 GET /api/transfers/{monitorId}/files
  2. 완료 후 GET /api/transfer-history/{monitorId}로 결과 요약 확인
  3. 파일 단위 상세는 GET /api/transfers/{monitorId}/files?state=history

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const DEVICE_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const PAGE_SIZE = 50;
const TRANSFER_STATUS = Object.freeze({ transferError: 4, transferFail: 99 });

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 지난 7일간 특정 디바이스로 간 전송 중 실패(error·fail)한 건만 조회한다.
    const startDate = new Date(Date.now() - 7 * 86400000).toISOString();
    const statusFilter = [TRANSFER_STATUS.transferError, TRANSFER_STATUS.transferFail].join(",");

    // 페이징을 돌며 실패 목록을 모두 모은다.
    const failures = [];
    for (let offset = 0; ; offset += PAGE_SIZE) {
        const page = await api("GET", `/api/transfer-history?deviceId=${DEVICE_ID}&statusFilter=${statusFilter}&startDate=${startDate}&offset=${offset}&limit=${PAGE_SIZE}`, token);
        failures.push(...page.items);
        if (page.items.length < PAGE_SIZE) break;
    }

    console.log(`Failed transfers in last 7 days: ${failures.length}`);
    for (const item of failures) {
        console.log({ monitorId: item.monitorId, status: item.status, targetPath: item.targetPath, finishedAt: item.finishedAt });
    }
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

오류·재시도

설명

전송 중 실패한 파일만 골라 재전송하거나, 여러 전송을 한 번에 취소합니다.

사용 API

목적MethodEndpoint
실패 재시도POST/api/transfers/{monitorId}/retry
다중 취소POST/api/transfers/bulk/cancel
단건 취소POST/api/transfers/{monitorId}/cancel

Request

POST /api/transfers/{monitorId}/retry

json
{
  "filesRetry": ["/data/report.pdf", "/data/image.png"]
}

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": { "monitorId": "mon-abc123", "retried": 2 }
}

처리 순서

  1. GET /api/transfers/{monitorId}/files로 실패 파일 확인
  2. POST /api/transfers/{monitorId}/retryfilesRetry로 재전송
  3. 다수 전송 정리는 POST /api/transfers/bulk/cancel(monitorIds)

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
// 실패 원인 분류: 코드 앞자리로 네트워크·권한·용량을 구분한다.
const ERROR_CATEGORY = {
    NETWORK: "네트워크",
    PERMISSION: "권한",
    CAPACITY: "용량",
};

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

function categorize(errorCode) {
    // 에러 코드를 사람이 읽을 수 있는 원인으로 매핑한다.
    return ERROR_CATEGORY[errorCode?.split("_")[0]] || "기타";
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 1) 가장 최근에 실패한 전송 한 건을 찾는다.
    const history = await api("GET", "/api/transfer-history?statusFilter=4,99&limit=1", token);
    if (!history.items.length) {
        console.log("No failed transfers found");
        return;
    }
    const monitorId = history.items[0].monitorId;

    // 2) 전송 상세에서 실패 원인 코드를 확인한다.
    const detail = await api("GET", `/api/transfers/${monitorId}`, token);
    console.log({ monitorId, status: detail.status, errorCode: detail.errorCode, cause: categorize(detail.errorCode) });

    // 3) 실패한 파일 목록과 각각의 실패 사유를 조회한다.
    const files = await api("GET", `/api/transfers/${monitorId}/files?state=any&size=100`, token);
    for (const file of files.items) {
        console.log({ name: file.name, state: file.state, errorCode: file.errorCode, cause: categorize(file.errorCode) });
    }
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

중단 복구

설명

일시정지되었거나 중단된 전송을 이어서 재개하고, 과거 전송을 동일 설정으로 다시 실행합니다.

사용 API

목적MethodEndpoint
재개POST/api/transfers/{monitorId}/resume
다시 실행POST/api/transfers/{monitorId}/replay
다시 실행 설정 조회GET/api/transfers/{monitorId}/replay-data

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": { "monitorId": "mon-abc123", "status": "in_progress" }
}

처리 순서

  1. 일시정지 전송은 POST /api/transfers/{monitorId}/resume로 중단 지점부터 재개
  2. 과거 전송을 재실행하려면 GET /api/transfers/{monitorId}/replay-data로 설정 확인
  3. POST /api/transfers/{monitorId}/replay로 동일 설정 재실행

무결성 검증

설명

전송된 파일이 소스와 동일한지 확인합니다. 원본과 사본의 체크섬(sha256)을 비교해 무결성을 검증하고, 불일치 파일 목록을 받습니다.

사용 API

목적MethodEndpoint
검증 시작POST/api/transfers/{monitorId}/verify
검증 상태 조회GET/api/transfers/{monitorId}/verify/{verificationId}

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": {
    "verificationId": "vrf-abc123",
    "state": "completed",
    "checkedCount": 128,
    "totalCount": 128,
    "mismatches": []
  }
}

처리 순서

  1. 완료된 전송의 monitorIdPOST /api/transfers/{monitorId}/verify(algorithm: sha256) 호출 → verificationId
  2. GET /api/transfers/{monitorId}/verify/{verificationId}statecompleted가 될 때까지 조회
  3. mismatches에 불일치 파일이 있으면 재전송으로 보정

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
// 검증 대상은 이미 완료된 전송이므로 그 monitorId를 입력받는다.
const MONITOR_ID = process.env.INNORIX_MONITOR_ID;

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

async function main() {
    if (!MONITOR_ID) throw new Error("INNORIX_MONITOR_ID is required");
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 1) 원본과 사본의 체크섬 비교(무결성 검증)를 요청한다. 비동기 작업이므로 ID를 받는다.
    const started = await api("POST", `/api/transfers/${MONITOR_ID}/verify`, token, { algorithm: "sha256" });
    const verificationId = started.verificationId;

    // 2) 검증이 끝날 때까지 상태를 조회한다.
    let result;
    for (;;) {
        result = await api("GET", `/api/transfers/${MONITOR_ID}/verify/${verificationId}`, token);
        console.log({ verificationId, state: result.state, checked: result.checkedCount, total: result.totalCount });
        if (result.state === "completed") break;
        await new Promise((resolve) => setTimeout(resolve, 2000));
    }

    // 3) 불일치 파일이 있으면 목록으로 출력한다.
    const mismatches = result.mismatches || [];
    if (!mismatches.length) {
        console.log("All files verified: source and copy match");
        return;
    }
    console.log(`Mismatched files: ${mismatches.length}`);
    for (const file of mismatches) {
        console.log({ path: file.path, sourceChecksum: file.sourceChecksum, targetChecksum: file.targetChecksum });
    }
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

모니터링

설명

전송·자동화·디바이스 상태를 지속적으로 관찰합니다.

사용 API

목적MethodEndpoint
전송 진행GET/api/transfers/{monitorId}/files
자동화 상세GET/api/automations/{automationId}/details
디바이스 연결 상태GET/api/devices/{deviceId}/connectivity

처리 순서

  1. 진행 전송은 GET /api/transfers/{monitorId}/files로 주기 폴링
  2. 자동화는 GET /api/automations/{automationId}/details로 진행률 확인
  3. 디바이스는 GET /api/devices/{deviceId}/connectivity로 온라인 여부 확인

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const POLL_COUNT = 10; // 폴링 횟수
const POLL_INTERVAL_MS = 2000; // 폴링 간격 (부하를 줄이려면 늘린다)

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 진행 중인 전송들의 진행률·속도·예상 완료 시각을 주기적으로 조회해 표시한다.
    for (let tick = 1; tick <= POLL_COUNT; tick++) {
        const active = await api("GET", "/api/transfers?limit=50", token);
        console.log(`--- poll ${tick}/${POLL_COUNT}: ${active.items.length} active transfers ---`);
        for (const transfer of active.items) {
            console.log({
                monitorId: transfer.monitorId,
                percent: transfer.percent || 0,
                speed: transfer.speed, // bytes/sec
                eta: transfer.eta, // 예상 완료 시각
            });
        }
        if (tick < POLL_COUNT) await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
    }
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});

감사 기록

설명

전송 이력을 조회·내보내기하여 운영 기록으로 남깁니다.

사용 API

목적MethodEndpoint
이력 상세GET/api/transfer-history/{monitorId}
이력 내보내기(CSV)GET/api/transfer-history/export

처리 순서

  1. 개별 전송 기록은 GET /api/transfer-history/{monitorId}로 조회
  2. 기간·상태·키워드로 필터해 GET /api/transfer-history/export로 CSV 내보내기
    • 쿼리: periodDays, status, searchKeyword, page, size, sort

구현 예제

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
// 감사 대상 장비 (예: device-branch-01)
const DEVICE_ID = process.env.INNORIX_SOURCE_ID || "device-branch-01";
const PAGE_SIZE = 50;

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
        headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
    return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
    const token = login.user.accessToken;

    // 특정 장비가 지난 30일간 보낸 전송 이력을 페이징하며 모두 조회한다.
    const startDate = new Date(Date.now() - 30 * 86400000).toISOString();
    const records = [];
    for (let offset = 0; ; offset += PAGE_SIZE) {
        const page = await api("GET", `/api/devices/${DEVICE_ID}/transfer-history?startDate=${startDate}&offset=${offset}&limit=${PAGE_SIZE}`, token);
        records.push(...page.items);
        if (page.items.length < PAGE_SIZE) break;
    }

    // 언제 어떤 파일을 어느 디바이스로 보냈는지 확인한다.
    console.log(`Transfers by ${DEVICE_ID} in last 30 days: ${records.length}`);
    for (const record of records) {
        console.log({
            startedAt: record.startedAt,
            source: record.sourcePath,
            targetDevice: record.targetDevice,
            targetPath: record.targetPath,
            fileCount: record.fileCount,
            status: record.status,
        });
    }
}

main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
});