즉시 전송
즉시 전송은 예약이나 자동화 없이, 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 배열로 지정합니다.
| 필드 | 타입 | 설명 |
|---|---|---|
hash | string | 항목 식별자 — {deviceId}_ino_{base64(UTF-8 경로)} |
isDir | boolean | 폴더 여부 |
폴더 하위 전체를 보낼 때는 sendAllFolder를 사용합니다.
장비 조회 — GET /api/devices/resolve — 이름·IP·MAC으로 deviceId를 즉시 조회합니다(name·ip·mac 중 최소 1개).
{
"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 사용). 응답의 path와 fileToken이 함께 제공되며, fileToken은 전송·파일 작업의 항목 식별자로 그대로 사용할 수 있습니다.
{
"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)도 함께 제공됩니다.
핵심 엔드포인트
| 목적 | Method | Endpoint |
|---|---|---|
| 로그인 | 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 |
기본 흐름
- 로그인해 액세스 토큰 확보
GET /api/devices로sourceId,targetId확인- (선택) 파일 검색으로
sourceItem구성 - (선택)
validate-path로 경로 검증 POST /api/transfers/manual로 전송 생성 →monitorId수신monitorId로 진행 모니터링·제어
1:1 전송
설명
하나의 소스 디바이스에서 하나의 타깃 디바이스로 지정한 파일을 즉시 전송하는 가장 기본 패턴입니다. sourceId·targetId와 sourceItem(항목 hash)으로 소스·타깃과 보낼 항목을 지정하고, monitorId로 완료까지 상태를 확인합니다.
사용 API
| 목적 | Method | Endpoint |
|---|---|---|
| 로그인 | POST | /api/auth/login |
| 전송 생성 | POST | /api/transfers/manual |
| 상태 조회 | GET | /api/transfers/{monitorId} |
Request
POST /api/transfers/manual
{
"sourceId": "device-source-01",
"targetId": "device-target-01",
"targetPath": "/data/incoming",
"sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }],
"sendAllFolder": false
}
sourceId·targetId는GET /api/devices로 얻은deviceId입니다.sourceItem의hash는{deviceId}_ino_{base64(UTF-8 경로)}형식입니다.sendAllFolder는 불리언입니다.
ℹ️
sourceId/targetId에는GET /api/devices로 얻은deviceId를 지정합니다. 이름·IP로 조회하려면GET /api/devices/resolve를 사용하세요.
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
처리 순서
POST /api/auth/login으로 액세스 토큰 확보(data.user.accessToken)POST /api/transfers/manual호출 —sourceId,targetId,targetPath,sourceItem→data.monitorId수신GET /api/transfers/{monitorId}를 폴링해status확인 —2=완료,4·5·9·99=실패로 종료
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 로그인 | 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 (소스마다 반복)
{
"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는 고정합니다.transferOptions의target-action: numbering으로 같은 경로에 모을 때 이름 충돌 시 번호를 붙입니다.- 폴더 전체 수집이므로
sendAllFolder는true입니다.
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
처리 순서
POST /api/auth/login으로 액세스 토큰 확보- 지점마다 반복:
GET /api/devices/resolve?name=으로 지점sourceId확보GET /api/devices/{deviceId}/files로 대상 폴더 검색 → 경로 확보POST /api/transfers/manual호출(targetId=타깃,sourceItem=폴더 항목) →monitorId수집
- 모든
monitorId를GET /api/transfers/{monitorId}로 폴링해 완료 집계
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 로그인 | 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 (타깃마다 반복)
{
"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은 고정합니다.transferOptions의target-action: overwrite로 타깃의 기존 파일을 덮어씁니다.- 폴더 전체 배포이므로
sendAllFolder는true입니다.
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
처리 순서
POST /api/auth/login으로 액세스 토큰 확보GET /api/devices/resolve?name=으로 소스deviceId확보GET /api/devices/{deviceId}/files로 배포 패키지 폴더를 검색해 경로 확보- 각 타깃마다
POST /api/transfers/manual호출(targetId=지점,sourceItem=패키지 항목) → 타깃 수만큼monitorId - 각
monitorId를GET /api/transfers/{monitorId}로 폴링해 성공·실패 집계
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 경로 검증 | POST | /api/transfers/validate-path |
| 전송 생성 | POST | /api/transfers/manual |
Request
POST /api/transfers/validate-path
{
"sourceId": "device-src-001",
"targetId": "device-dst-002",
"targetPath": "/data/incoming",
"sourceItem": [{ "hash": "device-src-001_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }]
}
Response
{
"status_code": 200,
"message": "success",
"data": { "valid": true }
}
처리 순서
POST /api/transfers/validate-path로 소스·타깃·경로 유효성 확인- 규칙(
sourceItem/filter/transferOptions.pathMapping/transferOptions.target-action)을 구성해POST /api/transfers/manual호출 - 응답
data.monitorId로 후속 처리
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 파일 검색 | POST | /api/devices/{deviceId}/files/search |
| 필터 전송 생성 | POST | /api/transfers/filtered |
filter 옵션 필드:
| 필드 | 타입 | 설명 |
|---|---|---|
filter.includeExtensions | string[] | 포함할 확장자 목록 (예: ["log","csv"]) |
filter.excludePatterns | string[] | 제외할 glob 패턴 (예: ["*.tmp"]) |
filter.minSize | number | 최소 크기(바이트) |
filter.maxSize | number | 최대 크기(바이트) |
filter.modifiedAfter | string | 이 시각 이후 수정 파일만 (ISO 8601) |
filter.modifiedBefore | string | 이 시각 이전 수정 파일만 (ISO 8601) |
filter.recursive | boolean | 하위 폴더 재귀 탐색 여부 |
Request
POST /api/transfers/filtered
{
"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
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
응답의 data.monitorId로 이후 상태 조회(GET /api/transfers/{monitorId}/files)와 제어(pause·resume·cancel·retry)를 수행합니다.
처리 순서
- (선택)
POST /api/devices/{deviceId}/files/search로 후보 목록 확인 POST /api/transfers/filtered에filter를 포함해 호출- 응답
data.monitorId로 진행 상태 조회
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 타깃 폴더 생성 | 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
{
"sourceId": "device-source-01",
"targetId": "device-target-01",
"targetPath": "/data/incoming/2026",
"sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }]
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
응답의 data.monitorId로 이후 상태 조회(GET /api/transfers/{monitorId}/files)와 제어(pause·resume·cancel·retry)를 수행합니다.
처리 순서
- (선택)
POST /api/devices/{deviceId}/files/folders로 타깃 폴더 생성 GET /api/devices/{deviceId}/capacity로 도착 경로 여유 용량 확인POST /api/transfers/manual호출(sourceId,targetId,targetPath)- 응답
data.monitorId수신
구현 예제
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 옵션으로 폴더 구조 보존, 접두 경로 제거, 파일명 규칙을 지정합니다. 항목 경로는 sourceItem의 hash로 지정합니다.
사용 API
| 목적 | Method | Endpoint |
|---|---|---|
| 전송 생성 | POST | /api/transfers/manual |
| 배치 결과 조회 | GET | /api/transfers/{monitorId}/files |
transferOptions.pathMapping 옵션 필드:
| 필드 | 타입 | 설명 |
|---|---|---|
pathMapping.preserveStructure | boolean | 소스 폴더 구조 유지(false면 평탄화) |
pathMapping.removePrefix | string | 소스 경로에서 제거할 접두 경로 |
pathMapping.targetRoot | string | 배치 기준이 되는 타깃 루트 경로 |
pathMapping.fileNameTemplate | string | 파일명 템플릿 (예: {name}, 날짜 접두) |
pathMapping.createTargetFolders | boolean | 없는 타깃 폴더 자동 생성 |
Request
POST /api/transfers/manual
{
"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
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
응답의 data.monitorId로 이후 상태 조회(GET /api/transfers/{monitorId}/files)와 제어(pause·resume·cancel·retry)를 수행합니다.
처리 순서
POST /api/transfers/manual에pathMapping을 포함해 호출- 응답
data.monitorId수신 GET /api/transfers/{monitorId}/files로 실제 배치 결과 확인
구현 예제
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;
});
충돌 처리
설명
타깃 경로에 같은 이름의 파일이 이미 있을 때의 처리 방식입니다. transferOptions의 target-action으로 번호 부여(numbering)·건너뛰기(skip)·덮어쓰기(overwrite)·실패(fail) 중 하나를 지정합니다. 전송 중 실패한 파일은 재시도로 이어받습니다.
사용 API
| 목적 | Method | Endpoint |
|---|---|---|
| 전송 생성 | POST | /api/transfers/manual |
| 실패 재시도 | POST | /api/transfers/{monitorId}/retry |
transferOptions.target-action 값:
| 값 | 설명 |
|---|---|
numbering | 이름 충돌 시 번호를 붙여 둘 다 보존 ((1), (2)) |
skip | 이미 있으면 건너뜀 |
overwrite | 기존 파일을 덮어씀 |
fail | 충돌 시 해당 파일 실패 처리 |
Request
POST /api/transfers/manual
{
"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
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
응답의 data.monitorId로 이후 상태 조회(GET /api/transfers/{monitorId}/files)와 제어(pause·resume·cancel·retry)를 수행합니다.
처리 순서
POST /api/transfers/manual에transferOptions.target-action을 포함해 호출- 응답
data.monitorId수신 - 실패한 파일은
POST /api/transfers/{monitorId}/retry로 재전송
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 자동화 생성 | POST | /api/automations |
| 자동화 목록 | GET | /api/automations |
| 자동화 상세 | GET | /api/automations/{automationId}/details |
| 자동화 일시정지 | POST | /api/automations/{automationId}/pause |
Request
POST /api/automations
{
"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
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-abc123" }
}
처리 순서
POST /api/automations호출 —name,schedules(일정 객체 배열),details(전송 정의 배열),timezone,transferType- 응답
data.automationId수신 GET /api/automations/{automationId}/details로 세부·진행 확인- 필요 시
POST /api/automations/{automationId}/pause로 일시정지
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 웹훅 생성 | POST | /api/webhooks |
| 전달 이력 조회 | GET | /api/webhooks/{webhookId}/deliveries |
| 전달 재시도 | POST | /api/webhooks/{webhookId}/deliveries/{deliveryId}/retry |
Request
POST /api/webhooks
{
"url": "https://internal.example.com/innorix",
"events": ["transfer.succeeded", "transfer.failed"],
"active": true,
"retryPolicy": { "maxAttempts": 5, "initialDelaySeconds": 30 }
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "webhookId": "wh-abc123" }
}
처리 순서
POST /api/webhooks호출 —url,events,active,retryPolicy- 응답
data.webhookId수신 - 이후 전송 이벤트 발생 시 등록한 URL로 통지(서명은 HMAC-SHA256으로 검증)
- 전달 이력은
GET /api/webhooks/{webhookId}/deliveries, 실패 전달은.../retry로 재시도
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 자동화 생성(플로우) | POST | /api/automations |
| 플로우 상세 조회 | GET | /api/automations/{automationId}/details |
| 실행 이력 조회 | GET | /api/automations/{automationId}/executions |
Request
POST /api/automations
{
"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
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-flow-abc123" }
}
처리 순서
POST /api/automations로 플로우 자동화 생성 —flowName,details(단계 배열, 각step)- 응답
data.automationId수신 GET /api/automations/{automationId}/details로 구성 확인- 자동화 실행 시
details의step순서대로 처리, 이력은.../executions
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 동기화 작업 생성 | POST | /api/sync/jobs |
| 작업 상태 조회 | GET | /api/sync/jobs/{jobId} |
| 일시정지·재개 | POST | /api/sync/jobs/{jobId}/pause · resume |
| 작업 삭제 | DELETE | /api/sync/jobs/{jobId} |
처리 순서
- 동기화 방향(
mode)·트리거(realTime)·범위 결정 POST /api/sync/jobs로 작업 생성 →jobIdGET /api/sync/jobs/{jobId}로 상태 확인,pause·resume으로 운영- 완료·불필요 시
DELETE /api/sync/jobs/{jobId}
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 동기화 작업 생성 | POST | /api/sync/jobs |
Request
POST /api/sync/jobs
{
"source": { "deviceId": "device-src-001", "path": "/sync/source" },
"target": { "deviceId": "device-dst-002", "path": "/sync/target" },
"mode": "one-way",
"targetChangePolicy": "restore-source"
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "jobId": "job-oneway-01" }
}
처리 순서
POST /api/sync/jobs에mode: one-way로 작업 등록- 소스 변경 발생 시 타깃으로 자동 반영(타깃 변경은
targetChangePolicy로 처리) GET /api/sync/jobs/{jobId}로 상태 확인
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 동기화 작업 생성 | POST | /api/sync/jobs |
처리 순서
POST /api/sync/jobs에mode: two-way로 작업 등록- 변경 감지 기준은
changeDetection(예:sha256)로 지정 - 어느 쪽 변경이든 상대편으로 반영
- 양쪽 동시 변경은
conflictPolicy(예:newest-wins)로 처리
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 동기화 작업 생성(실시간) | POST | /api/sync/jobs |
| 이벤트 전달 | POST | /api/sync/jobs/{jobId}/events |
처리 순서
POST /api/sync/jobs에realTime: true로 작업 등록- 파일 생성·변경·삭제·이름 변경 즉시 반영(
propagateDeletes·propagateRenames) - 에이전트 이벤트는
POST /api/sync/jobs/{jobId}/events로 전달
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 증분 전송 생성 | POST | /api/transfers/manual (incremental: true) |
| 주기 동기화 | POST | /api/automations |
| 상태 조회 | GET | /api/transfers/{monitorId} |
처리 순서
POST /api/transfers/manual에incremental: true로 변경분만 전송(주기화는POST /api/automations)- 마지막 동기화 이후 변경된 파일만 선별 전송
GET /api/transfers/{monitorId}로 반영 결과 확인
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 전송 생성 | POST | /api/transfers/manual |
| 주기 동기화 | POST | /api/automations |
| 상태 조회 | GET | /api/transfers/{monitorId}/files |
처리 순서
- 관리 대상 폴더/파일을
sourceItem으로 지정 - 1회성은
POST /api/transfers/manual, 지속 관리는POST /api/automations GET /api/transfers/{monitorId}/files로 반영 확인
빌드 결과물
설명
CI/CD 파이프라인이 만든 빌드 산출물을 배포 대상 디바이스로 전달합니다. 파이프라인에서 전송을 트리거하고, 완료를 웹훅으로 통지받습니다.
사용 API
| 목적 | Method | Endpoint |
|---|---|---|
| 전송 생성 | POST | /api/transfers/manual |
| 자동 배포 | POST | /api/automations |
| 완료 통지 | POST | /api/webhooks |
처리 순서
- 빌드 완료 후 파이프라인에서
POST /api/transfers/manual호출(다수 대상은 파일 배포 패턴) - 응답
data.monitorId수신 POST /api/webhooks로 등록한 웹훅으로 배포 완료 통지 수신
AI·데이터
설명
학습 데이터셋·추론 결과 같은 대용량 데이터를 수집 서버나 처리 노드로 이동합니다. 이동 완료를 트리거로 후속 파이프라인을 연결합니다.
사용 API
| 목적 | Method | Endpoint |
|---|---|---|
| 데이터 수집 | POST | /api/transfers/manual |
| 주기 수집 | POST | /api/automations |
| 후속 트리거 | POST | /api/webhooks |
처리 순서
- 데이터 위치를
sourceItem으로 지정해POST /api/transfers/manual(수집은 파일 수집 패턴) - 완료 후
data.monitorId로 상태 확인 POST /api/webhooks웹훅으로 후속 처리(학습·추론) 트리거
결과·운영
전송이 생성된 뒤 상태 확인, 오류 대응, 중단 복구, 모니터링, 기록 관리를 다룹니다.
상태·결과
설명
진행 중 전송의 파일별 상태와, 완료된 전송의 결과를 조회합니다.
사용 API
| 목적 | Method | Endpoint |
|---|---|---|
| 전송 상태·진행률 | GET | /api/transfers/{monitorId} |
| 진행 중 파일 조회 | GET | /api/transfers/{monitorId}/files |
| 완료 이력 상세 | GET | /api/transfer-history/{monitorId} |
| 완료 이력 파일 | GET | /api/transfers/{monitorId}/files?state=history |
Response
{
"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
}
]
}
}
처리 순서
GET /api/transfers/{monitorId}로 전송 상태·진행률 확인 —status2=완료,4·5·9·99=실패로 종료. 파일별 진행은GET /api/transfers/{monitorId}/files- 완료 후
GET /api/transfer-history/{monitorId}로 결과 요약 확인 - 파일 단위 상세는
GET /api/transfers/{monitorId}/files?state=history
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 실패 재시도 | POST | /api/transfers/{monitorId}/retry |
| 다중 취소 | POST | /api/transfers/bulk/cancel |
| 단건 취소 | POST | /api/transfers/{monitorId}/cancel |
Request
POST /api/transfers/{monitorId}/retry
{
"filesRetry": ["/data/report.pdf", "/data/image.png"]
}
Response
{
"status_code": 200,
"message": "success",
"data": { "monitorId": "mon-abc123", "retried": 2 }
}
처리 순서
GET /api/transfers/{monitorId}/files로 실패 파일 확인POST /api/transfers/{monitorId}/retry에filesRetry로 재전송- 다수 전송 정리는
POST /api/transfers/bulk/cancel(monitorIds)
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 재개 | POST | /api/transfers/{monitorId}/resume |
| 다시 실행 | POST | /api/transfers/{monitorId}/replay |
| 다시 실행 설정 조회 | GET | /api/transfers/{monitorId}/replay-data |
Response
{
"status_code": 200,
"message": "success",
"data": { "monitorId": "mon-abc123", "status": "in_progress" }
}
처리 순서
- 일시정지 전송은
POST /api/transfers/{monitorId}/resume로 중단 지점부터 재개 - 과거 전송을 재실행하려면
GET /api/transfers/{monitorId}/replay-data로 설정 확인 POST /api/transfers/{monitorId}/replay로 동일 설정 재실행
무결성 검증
설명
전송된 파일이 소스와 동일한지 확인합니다. 원본과 사본의 체크섬(sha256)을 비교해 무결성을 검증하고, 불일치 파일 목록을 받습니다.
사용 API
| 목적 | Method | Endpoint |
|---|---|---|
| 검증 시작 | POST | /api/transfers/{monitorId}/verify |
| 검증 상태 조회 | GET | /api/transfers/{monitorId}/verify/{verificationId} |
Response
{
"status_code": 200,
"message": "success",
"data": {
"verificationId": "vrf-abc123",
"state": "completed",
"checkedCount": 128,
"totalCount": 128,
"mismatches": []
}
}
처리 순서
- 완료된 전송의
monitorId로POST /api/transfers/{monitorId}/verify(algorithm: sha256) 호출 →verificationId GET /api/transfers/{monitorId}/verify/{verificationId}로state가completed가 될 때까지 조회mismatches에 불일치 파일이 있으면 재전송으로 보정
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 전송 진행 | GET | /api/transfers/{monitorId}/files |
| 자동화 상세 | GET | /api/automations/{automationId}/details |
| 디바이스 연결 상태 | GET | /api/devices/{deviceId}/connectivity |
처리 순서
- 진행 전송은
GET /api/transfers/{monitorId}/files로 주기 폴링 - 자동화는
GET /api/automations/{automationId}/details로 진행률 확인 - 디바이스는
GET /api/devices/{deviceId}/connectivity로 온라인 여부 확인
구현 예제
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
| 목적 | Method | Endpoint |
|---|---|---|
| 이력 상세 | GET | /api/transfer-history/{monitorId} |
| 이력 내보내기(CSV) | GET | /api/transfer-history/export |
처리 순서
- 개별 전송 기록은
GET /api/transfer-history/{monitorId}로 조회 - 기간·상태·키워드로 필터해
GET /api/transfer-history/export로 CSV 내보내기- 쿼리:
periodDays,status,searchKeyword,page,size,sort
- 쿼리:
구현 예제
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;
});