INNORIX

INNORIX PLATFORM

INNORIX Platform시스템 전체의 파일 전송을 하나의 플랫폼에서 운영합니다.
  • 파일 전송 자동화
  • 서버 간 파일 전송
  • 객체 스토리지 간 전송
  • 파일 배포 및 수집

WEB

Exabyter웹에서 대용량 파일을 빠르고 안정적으로 업로드하고 다운로드합니다.
  • 웹 대용량 파일 업로드 & 다운로드
  • 개발자 가이드

DEVELOPERS

앱 임베디드 파일 전송기존 애플리케이션에 파일 전송 기능을 통합합니다.
  • 앱 임베디드 파일 전송

도구

전송 빌더원하는 전송을 직접 구성합니다.전송 파인더업무에 맞는 전송을 찾습니다.

성능과 규모

  • 고속 파일 전송
  • 대용량 파일 전송
  • 대량 파일 전송

현대화

  • FTP, SFTP 마이그레이션

새로운 전송 환경

  • AI Data Delivery
  • Dynamic Endpoint Transfer

글로벌 대규모 전송

Hyperlane수백 TB에서 PB까지 국가와 대륙을 넘어 전송합니다.
개발자리소스고객사
무료 시작하기
INNORIX

LET FILES
MOVE THEMSELVES

INNORIX는 모든 시스템과 환경에서 파일 이동과 자동화를 제공하는 엔터프라이즈 파일 인프라 기업입니다.
5,000개 이상의 기업 및 공공기관에서 사용하고 있습니다.

파일 전송

INNORIX PLATFORM

INNORIX Platform
  • 파일 전송 자동화
  • 서버 간 파일 전송
  • 객체 스토리지 간 전송
  • 파일 배포 및 수집

PERFORMANCE & SCALE

  • 고속 파일 전송
  • 대용량 파일 전송
  • 대량 파일 전송

MODERNIZATION

  • FTP, SFTP 마이그레이션

WEB

Exabyter
  • 웹 대용량 파일 업로드 & 다운로드
  • 개발자 가이드

DEVELOPERS

  • 앱 임베디드 파일 전송

NEW WORKLOADS

  • AI Data Delivery
  • Dynamic Endpoint Transfer

HYPERLANE

  • 서비스 소개
  • 서비스 문의

개발자

  • 개발자 센터
  • 구현 예제
  • API 빠른 시작
  • 개발자 가이드
  • API 레퍼런스
  • GitHub

도구

  • 전송 빌더
  • 전송 파인더

리소스

  • 리소스 센터
  • 제품 가이드
  • 외부 서비스 연동
  • 배포 및 관리
  • 도움말 센터

고객사

  • 정부
  • 공공부문
  • 제조
  • 엔지니어링
  • 금융
  • 유통
  • IT/통신
  • 미디어
  • 의료
  • 교육

플랜

  • 가격 및 플랜

회사

INNORIX 소개

비전 AI 제품

Al.bert — 스마트 교통 AI

글로벌 오피스

  • 미국 뉴욕
  • 대한민국 서울
  • 베트남 호찌민
  • 오피스 위치 보기→

(C)2026 INNORIX. All rights reserved.

  • 보안
  • 상태
  • 이용약관
  • 개인정보 처리방침
  • 쿠키 정책
  1. 개발자
  2. 구현 예제
  3. 시스템 간 파일 동기화

시스템 간 파일 동기화

INNORIX Transfer Builder를 사용해 파일 동기화 흐름을 구성합니다. 연결된 시스템과 동기화 방식을 설정하고 구성을 생성합니다.

배포 및 관리
  • 한 곳으로 파일 보내기
  • 여러 곳으로 파일 보내기
  • 여러 곳에서 파일 수집
  • 시스템 간 파일 동기화
  • 웹 업로드, 다운로드

시스템 간 파일 동기화는 폴더를 감시하다가 파일이 생기거나 바뀌면 자동으로 전송합니다. 한 번 만들어 두면 계속 동작하는 상주형 자동화이며, 요청에서 눈여겨볼 점은 두 가지입니다.

  • 요청의 transferType 이 "sync" 입니다.
  • 사용자가 시작 시점을 선택하지 않습니다. 다만 API 요청 형식상 schedules 필드는 필요하므로 now 기본값을 그대로 전달합니다. 실제 실행 여부는 스케줄이 아니라 폴더 감시가 결정합니다.

시작하기#

준비물#

  1. API Key — 제품 좌측 하단 프로필 메뉴 → Developer 에서 발급합니다. 같은 화면에 Workspace ID 도 함께 표시됩니다. API 로 발급하려면 POST /api/auth/api-keys (Bearer 액세스 토큰, 바디 없음) → data.apiKey.
  2. deviceId 2개 — 감시할 쪽(Source)과 반영될 쪽(Target). 제품의 Devices 에서 디바이스를 선택하면 우측 상단에 표시되는 ID 입니다.
  3. 경로 — 감시할 폴더(sourceItem[].filePath)와 반영될 폴더(targetPath). 둘 다 슬래시(/)로 구분한 절대 경로를 쓰고, 감시 대상은 반드시 폴더이므로 isDir: true 입니다. targetPath 는 비어 있거나 / 이면 안 됩니다.

인증은 다음 두 가지 중 하나를 사용합니다.

http
x-api-key: <API Key>                  # long-lived key (recommended)
Authorization: Bearer <accessToken>   # short-lived token from login

워크스페이스를 명시해야 하는 경우에만 헤더를 하나 더 추가합니다. 이 헤더는 인증 수단이 아니라 대상 워크스페이스 지정용입니다.

http
x-workspace-id: <Workspace ID>        # optional

기본 주소는 https://app.innorix.com 입니다.

빠른 시작#

빌더에서 Get API Code 로 받은 번들을 그대로 실행하는 순서입니다.

  1. 전송 빌더에서 옵션을 고르고 Get API Code → 언어 선택 → zip 다운로드
  2. 압축을 풀고 .env 를 열어 INNORIX_API_KEY, SOURCE_ID, TARGET_ID, 감시 폴더(SOURCE_PATH)와 반영 폴더(TARGET_PATH)를 채웁니다
  3. 아래 명령으로 실행합니다
  4. 출력된 automationId 로 전송 상태를 조회합니다
언어 요구 사항 실행
Python Python 3.8+ pip install requests → python combo_builder.py
Node.js Node.js 18+ (의존성 없음) node combo_builder.js
Java JDK 11+ (의존성 없음) java ComboBuilder.java 또는 javac ComboBuilder.java && java ComboBuilder
C# .NET 8+ dotnet run

ℹ️ 위 요구 사항은 번들 예제 기준입니다. 이 문서에 실린 Java 발췌 코드는 가독성을 위해 텍스트 블록(""")을 사용하므로 JDK 17+ 가 필요합니다. 번들의 ComboBuilder.java 는 JDK 11+ 에서 동작합니다.

ℹ️ 번들의 combo_builder.* 는 같은 폴더의 .env 를 직접 읽습니다(별도 라이브러리 없이). 반면 이 문서에 실린 발췌 코드는 환경 변수에서 값을 읽으므로, 그대로 복사해 실행할 때는 아래처럼 값을 내보낸 뒤 실행하세요.

macOS, Linux

bash
export INNORIX_API_KEY=your-api-key
export SOURCE_ID=device-source-01
export SOURCE_PATH=D:/hotfolder        # folder to watch
export TARGET_ID=device-target-01
export TARGET_PATH=E:/mirror
export SYNC_DIRECTION=one_way          # one_way | two_way
export SYNC_WATCH=file_created         # file_created | file_modified

Windows PowerShell (CMD 에서는 set INNORIX_API_KEY=your-api-key 형식)

powershell
$env:INNORIX_API_KEY="your-api-key"
$env:SOURCE_ID="device-source-01"
$env:SOURCE_PATH="D:/hotfolder"
$env:TARGET_ID="device-target-01"
$env:TARGET_PATH="E:/mirror"
$env:SYNC_DIRECTION="one_way"
$env:SYNC_WATCH="file_created"

동기화 만들기#

동기화 설정#

동기화 동작은 transferOptions 의 두 값으로 정합니다.

방향 — syncType

값 동작
1 단방향(One-Way) — Source → Target
2 양방향(Two-Way) — 양쪽 변경을 서로 반영

감시 대상 — watchFolderType

값 동작
1 새로 생긴 파일만
2 수정된 파일만
3 생성, 수정 모두 — 일부 서버 버전에서만 지원

ℹ️ 연동 대상 서버가 3 을 지원하지 않으면 요청이 거부됩니다. 지원 여부가 확인되지 않았다면 1 또는 2 로 동기화를 두 개 만드는 방식을 쓰세요.

동기화 만들기#

json
{
  "name": "hot-folder-sync",
  "flowName": "hot-folder-sync",
  "transferType": "sync",
  "timezone": "Asia/Seoul",
  "details": [
    {
      "senderId": "<sourceDeviceId>",
      "receiverId": "<targetDeviceId>",
      "sourceItem": [{ "filePath": "D:/hotfolder", "isDir": true }],
      "targetPath": "E:/mirror",
      "step": 1,
      "transferOptions": {
        "noSchedule": false,
        "target-action": "overwrite",
        "syncType": 1,
        "watchFolderType": 1,
        "checkIntegrity": true
      }
    }
  ],
  "schedules": [
    { "type": "none", "startDateType": "now", "startDate": "2026-09-14T02:00:00.000Z", "timezone": "Asia/Seoul" }
  ],
  "step": 1,
  "isUpcoming": false
}
  • syncType, watchFolderType 은 위 동기화 설정의 값입니다.
  • transferOptions.target-action 은 이름이 겹칠 때의 동작입니다. overwrite(덮어쓰기), numbering(이름 뒤에 번호), nosend(건너뛰기) 중 하나를 넣습니다.
  • startDate 는 예시 값입니다. 요청 시점의 현재 UTC 시각을 넣으세요 (아래 예제 코드는 실행할 때마다 현재 시각을 계산합니다).

schedules 에는 위에서 설명한 대로 now 기본값을 그대로 넣습니다.

# pip install requests
import os, time, requests

BASE = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
HEADERS = {"x-api-key": os.environ["INNORIX_API_KEY"], "Content-Type": "application/json"}
# Every setting comes from an environment variable (second argument is the default).
TZ          = os.getenv("SCHEDULE_TZ", "Asia/Seoul")
SOURCE_ID   = os.environ["SOURCE_ID"]
SOURCE_PATH = os.getenv("SOURCE_PATH", "D:/hotfolder")   # folder to watch
TARGET_ID   = os.environ["TARGET_ID"]
TARGET_PATH = os.getenv("TARGET_PATH", "E:/mirror")

SYNC_TYPE = {"one_way": 1, "two_way": 2}
WATCH = {"file_created": 1, "file_modified": 2, "both": 3}   # both(3) is supported on some server versions only
SYNC_DIRECTION = os.getenv("SYNC_DIRECTION", "one_way")
SYNC_WATCH     = os.getenv("SYNC_WATCH", "file_created")

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

def call(method, path, body=None, params=None):
    r = requests.request(method, BASE + path, headers=HEADERS,
                         json=body, params=params, timeout=30)
    if not r.ok:
        raise RuntimeError(f"API {r.status_code}: {r.text[:500]}")
    return (r.json() or {}).get("data")

body = {
    "name": "hot-folder-sync",
    "flowName": "hot-folder-sync",
    "transferType": "sync",                       # "sync", not "normal"
    "timezone": TZ,
    "details": [{
        "senderId": SOURCE_ID,
        "receiverId": TARGET_ID,
        "sourceItem": [{"filePath": SOURCE_PATH, "isDir": True}],   # watched folder (always a folder)
        "targetPath": TARGET_PATH,
        "step": 1,
        "transferOptions": {
            "noSchedule": False,
            "target-action": "overwrite",
            "syncType": SYNC_TYPE[SYNC_DIRECTION],      # 1=One-Way, 2=Two-Way
            "watchFolderType": WATCH[SYNC_WATCH],       # 1=created, 2=modified, 3=both
            "checkIntegrity": True,
        },
    }],
    # Required by the request format; folder watching is what triggers a run.
    "schedules": [{"type": "none", "startDateType": "now",
                   "startDate": now_iso(), "timezone": TZ}],
    "step": 1,
    "isUpcoming": False,
}

automation_id = call("POST", "/api/automations", body)["automationId"]
print(f"sync automation created: {automation_id}  (one-way, on file create)")
// JDK 17+ (no external dependencies)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class FolderSync {

    static final String BASE = env("INNORIX_BASE_URL", "https://app.innorix.com");
    static final String API_KEY = env("INNORIX_API_KEY", "");
    static final HttpClient HTTP = HttpClient.newHttpClient();

    // Every setting comes from an environment variable (second argument is the default).
    // syncType        : 1=One-Way, 2=Two-Way
    // watchFolderType : 1=File Created, 2=File Modified, 3=Both (some server versions only)
    static final int SYNC_TYPE = env("SYNC_DIRECTION", "one_way").equals("two_way") ? 2 : 1;
    static final int WATCH_FOLDER_TYPE = switch (env("SYNC_WATCH", "file_created")) {
        case "file_modified" -> 2;
        case "both" -> 3;
        default -> 1;
    };

    /** Reads an environment variable, falling back to the default when it is empty. */
    static String env(String key, String dflt) {
        String v = System.getenv(key);
        return (v == null || v.isBlank()) ? dflt : v;
    }

    /** Extracts one string field from the response. Like the downloaded example, it needs no
     *  external JSON library. Use Jackson or Gson when you need to walk deeper structures. */
    static String jsonString(String json, String key) {
        Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
        return m.find() ? m.group(1) : null;
    }

    /** Extracts one integer field from the response. */
    static int jsonInt(String json, String key, int dflt) {
        Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*(-?\\d+)").matcher(json);
        return m.find() ? Integer.parseInt(m.group(1)) : dflt;
    }

    static String call(String method, String path, String body) throws Exception {
        HttpRequest.BodyPublisher pub = body == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(body);
        HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
                .header("Content-Type", "application/json")
                .header("x-api-key", API_KEY)
                .method(method, pub)
                .build();
        HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() >= 400) throw new RuntimeException("API " + res.statusCode() + ": " + res.body());
        return res.body();
    }

    public static void main(String[] args) throws Exception {
        String sourceId   = env("SOURCE_ID", "");
        String sourcePath = env("SOURCE_PATH", "D:/hotfolder");
        String targetId   = env("TARGET_ID", "");
        String targetPath = env("TARGET_PATH", "E:/mirror");
        String nowIso = Instant.now().truncatedTo(ChronoUnit.SECONDS).toString().replace("Z", ".000Z");

        String body = """
                {
                  "name": "hot-folder-sync",
                  "flowName": "hot-folder-sync",
                  "transferType": "sync",
                  "timezone": "Asia/Seoul",
                  "details": [{
                    "senderId": "%s",
                    "receiverId": "%s",
                    "sourceItem": [{ "filePath": "%s", "isDir": true }],
                    "targetPath": "%s",
                    "step": 1,
                    "transferOptions": {
                      "noSchedule": false,
                      "target-action": "overwrite",
                      "syncType": %d,
                      "watchFolderType": %d,
                      "checkIntegrity": true
                    }
                  }],
                  "schedules": [{
                    "type": "none", "startDateType": "now",
                    "startDate": "%s", "timezone": "Asia/Seoul"
                  }],
                  "step": 1,
                  "isUpcoming": false
                }
                """.formatted(sourceId, targetId, sourcePath, targetPath,
                              SYNC_TYPE, WATCH_FOLDER_TYPE, nowIso);

        String res = call("POST", "/api/automations", body);
        System.out.println("sync automation created: " + jsonString(res, "automationId"));
    }
}
// Node.js 18+ (uses the built-in fetch)
const BASE = process.env.INNORIX_BASE_URL || 'https://app.innorix.com';
const HEADERS = {
  'x-api-key': process.env.INNORIX_API_KEY,
  'Content-Type': 'application/json',
};

// Every setting comes from an environment variable (the value after || is the default).
const TZ = process.env.SCHEDULE_TZ || 'Asia/Seoul';
const SOURCE_ID = process.env.SOURCE_ID;
const SOURCE_PATH = process.env.SOURCE_PATH || 'D:/hotfolder';   // folder to watch
const TARGET_ID = process.env.TARGET_ID;
const TARGET_PATH = process.env.TARGET_PATH || 'E:/mirror';

const SYNC_TYPE = { one_way: 1, two_way: 2 };
const WATCH = { file_created: 1, file_modified: 2, both: 3 };   // both(3) is supported on some server versions only
const SYNC_DIRECTION = process.env.SYNC_DIRECTION || 'one_way';
const SYNC_WATCH = process.env.SYNC_WATCH || 'file_created';

const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z');

async function call(method, path, body, params) {
  let url = BASE + path;
  if (params) url += '?' + new URLSearchParams(params).toString();
  const res = await fetch(url, {
    method,
    headers: HEADERS,
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const payload = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`API ${res.status}: ${JSON.stringify(payload)}`);
  return payload.data;
}

const { automationId } = await call('POST', '/api/automations', {
  name: 'hot-folder-sync',
  flowName: 'hot-folder-sync',
  transferType: 'sync',                       // 'sync', not 'normal'
  timezone: TZ,
  details: [{
    senderId: SOURCE_ID,
    receiverId: TARGET_ID,
    sourceItem: [{ filePath: SOURCE_PATH, isDir: true }],   // watched folder
    targetPath: TARGET_PATH,
    step: 1,
    transferOptions: {
      noSchedule: false,
      'target-action': 'overwrite',
      syncType: SYNC_TYPE[SYNC_DIRECTION],
      watchFolderType: WATCH[SYNC_WATCH],
      checkIntegrity: true,
    },
  }],
  // Required by the request format; folder watching is what triggers a run.
  schedules: [{ type: 'none', startDateType: 'now', startDate: nowIso(), timezone: TZ }],
  step: 1,
  isUpcoming: false,
});

console.log('sync automation created:', automationId);
// .NET 8+ (standard library only)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

static class FolderSync
{
    // Every setting comes from an environment variable (second argument is the default).
    static string Env(string key, string dflt = "") =>
        Environment.GetEnvironmentVariable(key) is { Length: > 0 } v ? v : dflt;

    static readonly string Base = Env("INNORIX_BASE_URL", "https://app.innorix.com");
    static readonly HttpClient Http = new();

    const int OneWay = 1, TwoWay = 2;                     // syncType
    const int OnCreate = 1, OnModify = 2, OnBoth = 3;     // watchFolderType (3: some server versions only)
    static readonly int SyncType = Env("SYNC_DIRECTION", "one_way") == "two_way" ? TwoWay : OneWay;
    static readonly int WatchFolderType = Env("SYNC_WATCH", "file_created") switch
    {
        "file_modified" => OnModify,
        "both" => OnBoth,
        _ => OnCreate,
    };

    static async Task<JsonElement> Call(string method, string path, object? body = null)
    {
        var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
        req.Headers.Add("x-api-key", Env("INNORIX_API_KEY"));
        if (body != null)
            req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");

        var res = await Http.SendAsync(req);
        var text = await res.Content.ReadAsStringAsync();
        if (!res.IsSuccessStatusCode) throw new Exception(
quot;API {(int)res.StatusCode}: {text}"
); return JsonDocument.Parse(text).RootElement.GetProperty("data"); } static async Task Main() { var tz = Env("SCHEDULE_TZ", "Asia/Seoul"); var nowIso = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.000Z"); var transferOptions = new Dictionary<string, object> { ["noSchedule"] = false, ["target-action"] = "overwrite", ["syncType"] = SyncType, ["watchFolderType"] = WatchFolderType, ["checkIntegrity"] = true, }; var data = await Call("POST", "/api/automations", new { name = "hot-folder-sync", flowName = "hot-folder-sync", transferType = "sync", timezone = tz, details = new[] { new { senderId = Env("SOURCE_ID"), receiverId = Env("TARGET_ID"), sourceItem = new[] { new { filePath = Env("SOURCE_PATH", "D:/hotfolder"), isDir = true } }, targetPath = Env("TARGET_PATH", "E:/mirror"), step = 1, transferOptions, } }, schedules = new[] { new { type = "none", startDateType = "now", startDate = nowIso, timezone = tz } }, step = 1, isUpcoming = false, }); Console.WriteLine("sync automation created: " + data.GetProperty("automationId").GetString()); } }

동작 확인#

동기화는 파일이 생길 때마다 전송이 발생하므로, 조회 시점에 진행 중인 전송이 없을 수 있습니다. 감시 폴더에 파일을 하나 넣고 목록을 조회하면 전송 행이 나타납니다.

http
GET /api/transfers?automationId=<automationId>     -> rows in data.data[] whose type is not automation|history|flow
GET /api/transfers/<monitorId>                     → status, percent, isTerminal

동기화 중인 전송의 상태 코드는 12(syncing) 로 표시됩니다. 2(complete) 로 끝나면 그 파일 하나의 반영이 끝났다는 뜻이고, 자동화 자체는 계속 감시합니다.

SKIP_ROW_TYPES = {"automation", "history", "flow"}
STATUS = {-1: "queued", 0: "waiting", 1: "started", 2: "complete", 3: "paused",
          4: "error", 5: "cancelled", 6: "transferring", 7: "skipped", 8: "retry",
          9: "partial-complete", 11: "virus-scanning", 12: "syncing", 99: "fail"}

def watch(automation_id, seconds=60, interval=5):
    """Drop a file into the watched folder, then follow the transfers it triggers."""
    deadline, seen = time.time() + seconds, set()
    while time.time() < deadline:
        result = call("GET", "/api/transfers", params={"automationId": automation_id})
        records = result.get("data") if isinstance(result, dict) else result
        for r in records or []:
            if r.get("type") in SKIP_ROW_TYPES:
                continue
            mid = r.get("monitorId") or r.get("id")
            detail = call("GET", f"/api/transfers/{mid}") or {}
            status = detail.get("status")
            key = (mid, status)
            if key not in seen:
                seen.add(key)
                print(f"  {mid}: {STATUS.get(status, status)} ({detail.get('percent', 0)}%)")
        time.sleep(interval)

watch(automation_id)
// Reuses call(), jsonString() and jsonInt() from the example above.
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99);

/** Collects every monitorId in the list response.
 *  Summary rows (type=automation/history/flow) carry no monitorId, so they drop out. */
static List<String> monitorIds(String automationId) throws Exception {
    String json = call("GET", "/api/transfers?automationId=" + automationId, null);
    List<String> ids = new ArrayList<>();
    Matcher m = Pattern.compile("\"monitorId\"\\s*:\\s*\"([^\"]+)\"").matcher(json);
    while (m.find()) if (!ids.contains(m.group(1))) ids.add(m.group(1));
    return ids;
}

/** Drop a file into the watched folder, then follow the transfers it triggers.
 *  status 12=syncing, 2=complete, 4=error */
static void watch(String automationId, int seconds) throws Exception {
    long deadline = System.currentTimeMillis() + seconds * 1000L;
    Set<String> seen = new java.util.HashSet<>();
    while (System.currentTimeMillis() < deadline) {
        for (String monitorId : monitorIds(automationId)) {
            String json = call("GET", "/api/transfers/" + monitorId, null);
            int status = jsonInt(json, "status", -1);
            if (seen.add(monitorId + ":" + status)) {
                System.out.println("  " + monitorId + ": status=" + status
                        + " (" + jsonInt(json, "percent", 0) + "%)");
            }
        }
        Thread.sleep(5000);
    }
}

// Usage
watch(automationId, 60);
const SKIP_ROW_TYPES = new Set(['automation', 'history', 'flow']);
const STATUS = { 2: 'complete', 4: 'error', 6: 'transferring', 12: 'syncing' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function watch(automationId, seconds = 60, intervalMs = 5000) {
  const deadline = Date.now() + seconds * 1000;
  const seen = new Set();
  while (Date.now() < deadline) {
    const result = await call('GET', '/api/transfers', undefined, { automationId });
    const records = Array.isArray(result) ? result : result?.data || [];
    for (const r of records) {
      if (SKIP_ROW_TYPES.has(r.type)) continue;
      const mid = r.monitorId || r.id;
      const detail = (await call('GET', `/api/transfers/${mid}`)) || {};
      const key = `${mid}:${detail.status}`;
      if (seen.has(key)) continue;
      seen.add(key);
      console.log(`  ${mid}: ${STATUS[detail.status] ?? detail.status} (${detail.percent ?? 0}%)`);
    }
    await sleep(intervalMs);
  }
}

await watch(automationId);
static readonly HashSet<string> SkipRowTypes = new() { "automation", "history", "flow" };

static async Task Watch(string automationId, int seconds = 60, int intervalMs = 5000)
{
    var deadline = DateTime.UtcNow.AddSeconds(seconds);
    var seen = new HashSet<string>();

    while (DateTime.UtcNow < deadline)
    {
        var result = await Call("GET", "/api/transfers?automationId=" + automationId);
        var records = result.ValueKind == JsonValueKind.Object && result.TryGetProperty("data", out var inner)
            ? inner : result;

        foreach (var r in records.EnumerateArray())
        {
            var type = r.TryGetProperty("type", out var t) ? t.GetString() : null;
            if (type != null && SkipRowTypes.Contains(type)) continue;

            var mid = r.TryGetProperty("monitorId", out var m) ? m.GetString()
                    : r.TryGetProperty("id", out var i) ? i.GetString() : null;
            if (mid == null) continue;

            var detail = await Call("GET", "/api/transfers/" + mid);
            var status = detail.GetProperty("status").GetInt32();   // 12=syncing, 2=complete
            if (seen.Add(
quot;{mid}:{status}"
)) Console.WriteLine(
quot; {mid}: status={status}"
); } await Task.Delay(intervalMs); } }

전송 옵션#

파일 옵션#

동기화에서도 details[].transferOptions 의 파일 옵션과 processors[] 는 그대로 쓸 수 있습니다.

항목 키 비고
확장자 필터 send-fileoption.extension 특정 확장자만 동기화할 때 유용
크기 필터 send-fileoption.fileSize 임시 파일 제외에 활용
이름 제외 send-fileoption.fileName .tmp, ~$ 같은 작업 중 파일 제외
중복 처리 target-action 동기화는 overwrite 가 일반적
무결성 검증 checkIntegrity 파일마다 검증

파일 옵션의 값 형식은 다음과 같습니다.

json
{
  "noSchedule": false,
  "target-action": "overwrite",
  "syncType": 1,
  "watchFolderType": 1,
  "checkIntegrity": true,
  "send-fileoption": {
    "extension": { "extension": ["pdf", "xlsx"], "allow": true },
    "fileSize": { "size": 1048576, "over": true, "equal": true },
    "fileName": { "name": "tmp", "allow": false }
  }
}

target-action 은 이름이 겹칠 때의 동작입니다 — overwrite(덮어쓰기), numbering(이름 뒤에 번호), nosend(건너뛰기).

ℹ️ 감시 폴더에 작업 중인 임시 파일이 많다면 이름, 확장자 필터를 먼저 걸어 두세요. 필터가 없으면 저장 도중인 파일까지 전송 대상이 될 수 있습니다.

전송 후 동작#

전송이 끝난 뒤의 동작은 두 갈래로 나뉩니다.

① 자동화에 붙는 프로세서 — 바디의 processors[]

json
{
  "processors": [
    { "events": "Run", "type": "https", "method": "POST",
      "url": "https://api.example.com/webhook", "body": "{\"event\":\"done\"}" },
    { "category": "monitoring", "type": "grafana", "name": "builder-grafana",
      "config": { "baseUrl": "https://grafana.company.com", "apiToken": "***" },
      "notificationConfig": { "events": { "completed": true, "error": true } } }
  ]
}
  • Run API — 전송마다 호출되는 HTTP 훅입니다.
  • Monitoring(Grafana, Datadog, Prometheus 등) — 워크스페이스 전역이 아니라 이 자동화에 붙습니다. 선택 가능한 이벤트는 started, completed, paused, recovered, deviceConnected, deviceDisconnected 입니다.

② 워크스페이스 전역 연동 — POST /api/integrations

Message(Slack, Teams, Discord …), Virus scan(ClamAV, Microsoft Defender …), Email(SES, SendGrid) 은 전송이 아니라 워크스페이스에 등록됩니다.

json
{
  "name": "builder-slack",
  "type": "slack",
  "category": "notification",
  "config": { "webhookUrl": "https://hooks.slack.com/services/XXX", "channel": "#transfers" },
  "notificationConfig": { "events": { "completed": true, "error": true } }
}

category 는 Message → notification, Virus scan → security, Email → email 입니다. 제공자별 필수 설정 항목은 GET /api/integrations/rules/{type} 으로 확인할 수 있습니다. 이벤트 이름은 started, completed, paused, resumed, recovered, canceled, error, skipped 입니다.

참고#

빌더 UI ↔ .env ↔ API 매핑#

빌더 UI .env API
탭 = Sync TRANSFER_TYPE=sync transferType: "sync"
From 디바이스 SOURCE_ID details[].senderId
감시 폴더 SOURCE_PATH details[].sourceItem[].filePath (isDir: true)
To 디바이스 TARGET_ID details[].receiverId
To 경로 TARGET_PATH details[].targetPath
One-Way / Two-Way SYNC_DIRECTION=one_way|two_way transferOptions.syncType = 1 / 2
File Created / Modified / Both SYNC_WATCH=file_created|file_modified|both transferOptions.watchFolderType = 1 / 2 / 3
(Start 없음) START_WHEN 무시됨 schedules 는 기본값

자주 겪는 오류#

증상 원인과 해결
파일을 넣어도 전송이 안 생김 isDir 이 false 이면 감시가 동작하지 않습니다. 감시 대상은 항상 폴더입니다.
watchFolderType: 3 거부 Both 는 서버 지원이 필요합니다. 1(생성) 또는 2(수정)로 나눠 만드세요.
Two-Way 인데 한쪽만 반영 양쪽 에이전트가 모두 온라인이어야 합니다. 대상 디바이스 연결 상태를 확인하세요.
임시 파일까지 전송됨 send-fileoption.fileName 으로 tmp 등을 제외하거나 확장자 허용 목록을 지정하세요.
스케줄을 바꿔도 동작이 같음 동기화는 스케줄로 실행되지 않습니다. schedules 값은 무시됩니다.
전송 상태가 계속 12 12(syncing) 는 정상 동작 상태입니다. 파일 단위로 2(complete) 가 찍힙니다.
이전여러 곳에서 파일 수집다음웹 업로드, 다운로드

이 페이지에서

  • 시작하기
  • 준비물
  • 빠른 시작
  • 동기화 만들기
  • 동기화 설정
  • 동기화 만들기
  • 동작 확인
  • 전송 옵션
  • 파일 옵션
  • 전송 후 동작
  • 참고
  • 빌더 UI ↔ .env ↔ API 매핑
  • 자주 겪는 오류