INNORIX
전송 빌더전송 파인더개발자리소스고객사
무료 시작하기
INNORIX

LET FILES
MOVE THEMSELVES

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

시작하기

  • 필요한 전송 만들기
  • 필요한 전송 찾기

주요 전송

  • 팀 업무 폴더 동기화
  • 고객에게 대용량 파일 전송
  • 여러 시스템 파일 탐색
  • FTP·SFTP·SCP·rsync 전환
  • 앱에 파일 전송 추가
  • 웹 업로드·다운로드 적용
  • AI·데이터 워크플로 구축
  • 모든 전송 둘러보기→

개발자

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

리소스

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

고객사

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

플랜

  • 가격 및 플랜

회사

INNORIX 소개

Exabyter를 찾고 계신가요?

이제 INNORIX Platform에 통합되었습니다

기타 INNORIX 제품

Al.bert — 스마트 교통 AI

글로벌 오피스

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

(C)2026 INNORIX. All rights reserved.

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

시스템 간 파일 동기화

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

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

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

  • 요청의 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 매핑
  • 자주 겪는 오류