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. 데이터베이스 덤프, 백업, 아카이브 파일을 원격 보관처로 자동 전달하기

데이터베이스 덤프, 백업, 아카이브 파일을 원격 보관처로 자동 전달하기

INNORIX에서 백업 및 아카이브 파일 전송을 구성하고 연결된 시스템 간 파일 작업을 일관되게 운영하는 방법을 알아보세요.

IT 엔지니어개발자
  • AI 학습 데이터와 모델 파일의 전체 흐름 자동화하기
  • AI 모델 파일을 여러 엣지 장비에 배포하기
  • 폐쇄망·망분리 환경에서 파일을 승인 반입·반출하기
  • 승인된 파일을 여러 지점에 동시에 배포하기
  • Azure Blob에서 사내 서버로 파일 전달하기
  • 데이터베이스 덤프, 백업, 아카이브 파일을 원격 보관처로 자동 전달하기
  • 지점·공장·엣지 장비의 파일을 중앙으로 수집하기
  • CI/CD 빌드 결과물을 여러 서버에 배포하기
  • 서로 다른 클라우드 스토리지 사이에서 파일 이동하기
  • 서로 다른 AWS 계정의 S3 버킷 사이에 파일 전송하기
  • 고객별 파일 작업 공간 제공하기
  • 고객에게 만료 조건이 있는 대용량 다운로드 링크 제공하기
  • 고객이 브라우저에서 대용량 파일을 업로드하게 하기
  • 고객 작업 공간으로 대용량 파일 직접 보내기
  • 데이터베이스 백업 파일을 오브젝트 스토리지에 보관하기
  • Datadog으로 파일 전송 실패와 복구 알림 받기
  • 웹, 앱, 업무 시스템에 파일 전송 기능 추가하기
  • 파일 도착 후 검증·변환·후속 작업 실행하기
  • NAS, 파일 서버의 대량 파일을 클라우드로 이전하기
  • FTP 배치 작업을 관리형 파일 흐름으로 전환하기
  • Google Cloud Storage에서 Amazon S3로 직접 전송하기
  • Grafana에서 파일 전송 상태 대시보드 만들기
  • 전송된 파일의 해시값으로 무결성 자동 검증하기
  • 새 파일이 생기면 지정한 곳으로 자동 전송하기
  • Kubernetes에서 오브젝트 스토리지로 결과 파일 보내기
  • Git 밖의 대용량 파일과 빌드 결과물 자동 전달하기
  • Git으로 관리하기 어려운 파일을 자동 전달하기
  • 분산 서버의 로그·진단 파일을 중앙으로 모으기
  • 미디어 원본과 처리 결과를 단계별로 자동 전달하기
  • 수신 파일을 Microsoft Defender로 검사하고 후속 처리하기
  • 여러 단계의 파일 전송을 하나의 흐름으로 자동화하기
  • 승인된 파일을 여러 팀·지점에 자동 배포하기
  • 협력사 파일을 업무 시스템으로 자동 분류하기
  • 협력사·공급망과 정기적으로 파일 교환하기
  • 매일·매주 반복되는 파일 전송 자동화하기
  • 네트워크 중단 후 파일 전송을 자동으로 재개하기
  • rsync 작업을 관리형 파일 흐름으로 전환하기
  • 정해진 시간에 반복 파일 전송 예약하기
  • 소프트웨어·펌웨어를 여러 장비에 배포하고 결과 확인하기
  • 소프트웨어 패키지를 여러 서버·지점에 배포하기
  • 팀 폴더의 변경 파일을 여러 장비에 자동 반영하기
  • 팀 업무 폴더를 여러 PC에 자동 반영하기
  • 여러 장비의 파일을 한곳에서 찾고 직접 전송하기
  • 웹사이트에 대용량 파일 업로드·다운로드 기능 추가하기
  • 채널을 구독해 새 파일을 자동으로 받기
  • Amazon S3에서 Azure Blob으로 직접 전송하기
  • Amazon S3에서 Cloudflare R2로 파일 이전하기
  • Amazon S3 파일을 Linux 서버로 자동 내려받기
  • SCP 셸 스크립트를 CLI 기반 전송으로 전환하기
  • SFTP 계정과 배치 전송을 중앙에서 관리하기
  • Slack·Teams에서 파일 전송을 실행하고 상태 알림 받기
  • Windows 폴더의 파일을 Amazon S3로 자동 업로드하기
  • Windows와 Linux 서버 사이에 파일을 직접 전송하기

시작하기#

기본 개념#

데이터베이스 덤프와 백업, 아카이브 파일을 지정된 위치로 자동 전송하기

데이터베이스와 백업 프로그램에서는 DB 덤프, 전체 백업 파일, 증분 백업 파일, 로그 아카이브 등 다양한 파일이 생성됩니다.

DB, 백업 파일 자동 전달은 생성된 파일을 확인하고, 설정한 조건에 따라 원격 데이터센터나 클라우드 스토리지로 자동 전송하는 방식입니다.

파일이 생성되는 위치와 원격 저장 환경을 연결하면 정기적으로 생성되는 파일을 정해진 흐름에 따라 관리할 수 있습니다.

DB ,  백업 시스템
       │
       ▼
  파일 생성 확인
       │
       ▼
  전송 조건 확인
       │
       ▼
 원격 저장 환경
       │
       ├── 원격 데이터센터
       │
       └── 클라우드 스토리지

자동화 흐름#

백업 파일 생성부터 원격 저장과 결과 확인까지 순서에 따라 연결하기

백업 파일이 생성되거나 백업 작업이 완료되면 설정한 조건에 따라 다음 전송 작업을 실행할 수 있습니다.

지정된 일정에 따라 생성된 파일을 확인하고 원격 저장 환경으로 전송하는 흐름도 구성할 수 있습니다.

① DB 덤프 또는 백업 파일 생성

↓

② 파일 또는 백업 완료 확인

↓

③ 전송 작업 실행

↓

④ 원격 환경에 저장

↓

⑤ 결과 확인

이 흐름을 통해 백업 파일 생성과 원격 저장 작업을 순서에 따라 자동으로 연결할 수 있습니다.

운영 효과#

정기적인 백업 파일 전송과 원격 저장을 하나의 흐름으로 관리하기

DB와 백업 환경에서는 파일 생성, 원격 전송, 저장 위치, 실행 결과를 지속적으로 관리합니다.

자동 전송 흐름을 구성하면 정기적으로 생성되는 파일을 지정된 원격 환경으로 전송하고, 실행 결과와 저장 상태를 함께 확인할 수 있습니다.

구분 개별 관리 자동 전송
파일 확인 생성된 파일을 작업마다 확인 설정한 조건에 따라 확인
전송 실행 파일별로 직접 실행 조건과 일정에 따라 자동 실행
저장 위치 작업마다 대상 지정 파일 유형별 경로 구성
결과 관리 작업별 결과 확인 실행 기록과 저장 결과 통합 확인

이를 통해 DB 덤프와 백업 파일의 생성부터 원격 저장까지 이어지는 작업을 하나의 운영 흐름으로 구성할 수 있습니다.

IT 엔지니어#

원본 연결#

데이터베이스와 백업 장비의 파일 생성 위치 연결하기

먼저 DB 덤프와 백업 파일이 생성되는 장비와 폴더를 전송 환경에 연결합니다.

데이터베이스 서버, 백업 서버 또는 스토리지에서 생성되는 파일 경로를 지정해 전송 원본으로 구성할 수 있습니다.

원본 환경 생성 파일
데이터베이스 서버 DB 덤프와 내보내기 파일
백업 서버 전체, 증분 백업 파일
로그 서버 트랜잭션 로그와 아카이브
스토리지 장기 보관 대상 파일

전송 구성#

원격 저장 위치와 실행 조건을 하나의 전송 흐름으로 구성하기

원본 파일을 연결한 후에는 원격 데이터센터나 클라우드 스토리지를 전송 대상으로 설정합니다.

파일이 생성될 때, 백업 작업이 완료될 때 또는 지정된 일정에 따라 전송 작업을 시작하도록 구성할 수 있습니다.

설정 항목 구성 내용
원격 대상 데이터센터 또는 클라우드 스토리지
저장 위치 파일 유형별 폴더 또는 버킷 경로
실행 조건 파일 생성, 백업 완료, 일정 실행
파일 구분 DB Dump, Backup, Archive 등

예를 들어 일일 백업은 클라우드 저장소로 전송하고, 주간 백업과 장기 보관 파일은 별도의 원격 경로로 전송하는 흐름을 구성할 수 있습니다.

백업 작업
    │
    ▼
백업 완료
    │
    ▼
파일 확인
    │
    ├── DB Dump ────────────→ 원격 데이터센터
    │
    ├── Daily Backup ───────→ 클라우드 저장소
    │
    └── Archive ────────────→ 장기 보관 경로

자동 전송#

생성된 백업 파일을 지정된 원격 환경으로 자동 전송하기

설정한 조건이 충족되면 DB 덤프와 백업 파일을 지정된 원격 환경으로 전송합니다.

파일 유형과 경로에 따라 서로 다른 저장 위치로 분기하거나, 하나의 백업 파일을 여러 원격 환경에 함께 전송하는 방식으로 구성할 수 있습니다.

                백업 파일
                    │
                    ▼
                전송 작업
              ╱           ╲
             ▼             ▼
      원격 데이터센터   클라우드 스토리지
             │             │
             ▼             ▼
        백업 저장소      Bucket / Path

결과 관리#

전송 상태와 저장 결과를 확인하고 필요한 작업 다시 실행하기

전송 작업이 실행되면 Runs와 작업 상세 정보를 통해 파일 처리 상태와 원격 저장 결과를 확인할 수 있습니다.

특정 작업에서 추가 확인이 필요한 경우에는 Activity Log와 실행 정보를 통해 원본 파일, 장비 연결, 대상 경로와 접근 범위를 확인한 후 필요한 작업을 다시 실행할 수 있습니다.

전송 실행
    │
    ▼
작업 상태 확인
    │
    ├── Running
    │      │
    │      └── 진행률 확인
    │
    ├── Completed
    │      │
    │      └── 저장 결과 확인
    │
    └── 확인 필요
           │
           ▼
       상세 기록 확인
           │
           ▼
 원본 ,  연결 ,  저장 경로 확인
           │
           ▼
       필요한 작업 재실행
           │
           ▼
        결과 다시 확인
확인 항목 확인 내용
원본 파일 전송된 DB 덤프와 백업 파일
대상 위치 원격 서버 또는 클라우드 저장 경로
진행 상태 현재 작업 상태와 진행률
처리 결과 전송된 파일 수와 전체 용량
실행 기록 파일 처리 단계와 작업 결과

개발자#

백업 완료 시점에 덤프를 원격으로 전송하고 체크섬으로 검증하기

연동 준비#

공통 호출 코드와 경로 표기 준비하기

import os
import requests

BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")   # optional; falls back to the current workspace

STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99}          # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}


def api(method, path, body=None, params=None):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TOKEN}",
    }

    if WORKSPACE_ID:
        headers["x-workspace-id"] = WORKSPACE_ID

    response = requests.request(
        method, BASE_URL + path,
        headers=headers, json=body, params=params, timeout=30,
    )

    payload = response.json() if response.content else {}

    if not response.ok:
        raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")

    return payload.get("data")


def is_terminal(detail):
    return detail.get("isTerminal", detail.get("status") in TERMINAL)
// InnorixClient.java
public static final String BASE_URL =
        env("INNORIX_BASE_URL", "https://app.innorix.com").replaceAll("/+
quot;
, ""); public static final String WORKSPACE_ID = env("INNORIX_WORKSPACE_ID", null); public static final int STATUS_COMPLETE = 2; // States the transfer no longer moves out of public static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99); // Terminal states that are not a full success public static final Set<Integer> NOT_SUCCEEDED = Set.of(4, 5, 9, 99); private HttpRequest.Builder headers(HttpRequest.Builder builder) { builder.header("Content-Type", "application/json") .header("Authorization", "Bearer " + session.accessToken()); // When omitted the account's current workspace is used. if (workspaceId != null) builder.header("x-workspace-id", workspaceId); return builder; } /** Unwraps and returns data from the response. Throws ApiError on failure. */ public Object api(String method, String path, Object body, Map<String, Object> params) { Resp response = request(method, path, body, params); Object payload = null; try { payload = Json.parse(response.text()); } catch (RuntimeException ignored) { payload = null; } if (!response.ok()) { Map<String, Object> map = Json.asObj(payload); String message = Json.str(map, "message", Json.str(map, "error", "unknown error")); throw new ApiError(response.status, message, map); } return Json.get(payload, "data"); } /** Use the server flag when present, otherwise fall back to the status code. */ public static boolean isTerminal(Map<String, Object> record) { Boolean flag = Json.boolOrNull(record, "isTerminal"); if (flag != null) return flag; Integer status = Json.intOrNull(record, "status"); return status != null && TERMINAL.contains(status); }
// innorix-client.js
const BASE_URL = (process.env.INNORIX_BASE_URL
  || "https://app.innorix.com").replace(/\/+$/, "");
const TOKEN = process.env.INNORIX_ACCESS_TOKEN;
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID || null;

export const STATUS_COMPLETE = 2;
export const TERMINAL = new Set([2, 4, 5, 9, 99]);       // complete / error / cancelled / partial / failed
export const NOT_SUCCEEDED = new Set([4, 5, 9, 99]);

export async function api(method, path, body = null, params = null) {
  const url = new URL(BASE_URL + path);

  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value === undefined || value === null) continue;
      url.searchParams.set(key, String(value));
    }
  }

  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${TOKEN}`,
  };

  // When omitted the account's current workspace is used.
  if (WORKSPACE_ID) headers["x-workspace-id"] = WORKSPACE_ID;

  const response = await fetch(url, {
    method,
    headers,
    body: body === null ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(30_000),
  });

  const payload = await response.json().catch(() => ({}));

  if (!response.ok) {
    throw new Error(payload.message || `HTTP ${response.status}`);
  }

  return payload.data;
}

export function isTerminal(detail) {
  return detail.isTerminal !== undefined
    ? detail.isTerminal
    : TERMINAL.has(detail.status);
}
// InnorixClient.cs
public static readonly string BaseUrl =
    Env("INNORIX_BASE_URL", "https://app.innorix.com").TrimEnd('/');
public static readonly string WorkspaceIdFromEnv = Env("INNORIX_WORKSPACE_ID", null);

public const int StatusComplete = 2;
/// <summary>States the transfer no longer moves out of</summary>
public static readonly HashSet<int> Terminal = new HashSet<int> { 2, 4, 5, 9, 99 };
/// <summary>Terminal states that are not a full success</summary>
public static readonly HashSet<int> NotSucceeded = new HashSet<int> { 4, 5, 9, 99 };

// Applied on every request
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Session.AccessToken);
// When omitted the account's current workspace is used.
if (WorkspaceId != null) request.Headers.TryAddWithoutValidation("x-workspace-id", WorkspaceId);

public async Task<JsonNode> ApiAsync(string method, string path, JsonNode body = null,
    IDictionary<string, object> parameters = null)
{
    Resp response = await RequestAsync(method, path, body, parameters).ConfigureAwait(false);
    JsonNode payload = null;

    try
    {
        payload = J.Parse(response.Text());
    }
    catch (Exception)
    {
        payload = null;
    }

    if (!response.Ok)
    {
        JsonObject map = J.AsObj(payload);
        string message = J.Str(map, "message", J.Str(map, "error", "unknown error"));
        throw new ApiError(response.Status, message, map);
    }

    return J.Get(payload, "data");
}

/// <summary>Use the server flag when present, otherwise fall back to the status code.</summary>
public static bool IsTerminal(JsonObject record)
{
    bool? flag = J.BoolOrNull(record, "isTerminal");
    if (flag != null) return flag.Value;

    int? status = J.IntOrNull(record, "status");
    return status != null && Terminal.Contains(status.Value);
}
import base64
import time


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


def now_iso():
    return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())
public static String encodePath(String deviceId, String rawPath) {
    String normalized = (rawPath == null ? "" : rawPath).replace("\\", "/");
    return deviceId + "_ino_"
            + Base64.getEncoder().encodeToString(normalized.getBytes(StandardCharsets.UTF_8));
}

public static String nowIso() {
    return Instant.now().truncatedTo(ChronoUnit.SECONDS).toString().replace("Z", ".000Z");
}
export function encodePath(deviceId, rawPath) {
  const normalized = String(rawPath ?? "").replace(/\\/g, "/");
  const token = Buffer.from(normalized, "utf8").toString("base64");
  return `${deviceId}_ino_${token}`;
}

export function nowIso() {
  return new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z");
}
public static string EncodePath(string deviceId, string rawPath)
{
    string normalized = (rawPath ?? "").Replace("\\", "/");
    return deviceId + "_ino_" + Convert.ToBase64String(Encoding.UTF8.GetBytes(normalized));
}

public static string NowIso()
{
    return DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'.000Z'",
        System.Globalization.CultureInfo.InvariantCulture);
}

전송 상태는 아래 값으로 판단합니다. 종료 상태는 다섯 개이고 성공에 해당하는 값은 완료(2)입니다.

상태 값 의미 종료
2 완료 예
4 오류 예
5 취소 예
9 부분 완료 예
99 실패 예
1 , 6 , 12 , 13 시작, 전송 중, 동기화 중, 수신 중 아니오

백업 완료 후 전송#

백업 스크립트 끝에서 전송을 이어 실행하기

백업 프로그램이 파일을 만든 직후에 전송을 호출하면 파일이 준비된 시점을 정확히 맞출 수 있습니다.

파일을 보낼 때는 sourceItem에 isDir: false로 명시합니다. sourcePaths는 모든 경로를 폴더로 취급하므로, 덤프 파일을 넣으면 서버가 폴더로 스캔하려 해 느려지거나 시간이 초과됩니다.

import glob
import os


def latest_backup(directory, pattern="*.dump"):
    files = glob.glob(os.path.join(directory, pattern))

    if not files:
        raise FileNotFoundError(directory)

    return max(files, key=os.path.getmtime)


def send_backup(source, target, backup_path, target_path):
    transfer = api("POST", "/api/transfers/manual", {
        "sourceDevice": source,
        "targetDevice": target,
        "targetPath": target_path,
        "sourceItem": [{
            "path": backup_path,
            "isDir": False,
            "fileSize": os.path.getsize(backup_path),
        }],
        "sendAllFolder": False,
        "checkIntegrity": True,
        "transferOptions": {"target-action": "numbering"},
    })

    return transfer["monitorId"]
String latestBackup(String directory, String pattern) throws IOException {
    Path dir = Path.of(directory);
    try (var stream = Files.newDirectoryStream(dir, pattern)) {
        Path latest = null;
        for (Path p : stream) {
            if (latest == null || Files.getLastModifiedTime(p)
                    .compareTo(Files.getLastModifiedTime(latest)) > 0) latest = p;
        }
        if (latest == null) throw new FileNotFoundException(directory);
        return latest.toString();
    }
}

String sendBackup(String source, String target, String backupPath, String targetPath)
        throws IOException {
    Map<String, Object> transfer = client.apiObj("POST", "/api/transfers/manual", Json.newObj(
            "sourceDevice", source,
            "targetDevice", target,
            "targetPath", targetPath,
            "sourceItem", List.of(Json.newObj(
                    "path", backupPath, "isDir", false,
                    "fileSize", Files.size(Path.of(backupPath)))),
            "sendAllFolder", false,
            "checkIntegrity", true,
            "transferOptions", Json.newObj("target-action", "numbering")));

    return Json.str(transfer, "monitorId");
}
import { readdirSync, statSync } from "node:fs";
import { join } from "node:path";

function latestBackup(directory, pattern = /\.dump$/) {
  const files = readdirSync(directory).filter((f) => pattern.test(f))
    .map((f) => join(directory, f));

  if (files.length === 0) throw new Error(directory);

  return files.reduce((a, b) =>
    statSync(a).mtimeMs > statSync(b).mtimeMs ? a : b);
}

async function sendBackup(source, target, backupPath, targetPath) {
  const transfer = await client.api("POST", "/api/transfers/manual", {
    sourceDevice: source,
    targetDevice: target,
    targetPath,
    sourceItem: [{
      path: backupPath,
      isDir: false,
      fileSize: statSync(backupPath).size,
    }],
    sendAllFolder: false,
    checkIntegrity: true,
    transferOptions: { "target-action": "numbering" },
  });

  return transfer.monitorId;
}
string LatestBackup(string directory, string pattern = "*.dump")
{
    var files = Directory.GetFiles(directory, pattern);
    if (files.Length == 0) throw new FileNotFoundException(directory);

    return files.OrderByDescending(f => new FileInfo(f).LastWriteTimeUtc).First();
}

async Task<string> SendBackupAsync(string source, string target,
    string backupPath, string targetPath)
{
    JsonObject transfer = await client.ApiObjAsync("POST", "/api/transfers/manual",
        new JsonObject
        {
            ["sourceDevice"] = source,
            ["targetDevice"] = target,
            ["targetPath"] = targetPath,
            ["sourceItem"] = new JsonArray
            {
                new JsonObject
                {
                    ["path"] = backupPath,
                    ["isDir"] = false,
                    ["fileSize"] = new FileInfo(backupPath).Length,
                },
            },
            ["sendAllFolder"] = false,
            ["checkIntegrity"] = true,
            ["transferOptions"] = new JsonObject { ["target-action"] = "numbering" },
        });

    return J.Str(transfer, "monitorId");
}

백업 파일은 회차를 남겨야 하므로 도착 정책을 numbering으로 둡니다. overwrite를 쓰면 이전 백업이 사라져 복구 시점을 선택할 수 없습니다.

파일 크기를 함께 넘기면 서버가 항목별 크기를 다시 조회하지 않아 빨라집니다.

손상된 백업은 복구를 시도할 때 처음 드러나므로, checkIntegrity로 전송 단계에서 무결성을 확인합니다.

파일 유형별 분기#

덤프와 증분, 아카이브를 서로 다른 위치로 보내기

하나의 전송은 대상 하나를 다룹니다. 유형별로 대상이 다르면 전송을 나눠 만듭니다.

ROUTES = {
    "dump":    ("device-dc-01", "/backup/dump"),
    "daily":   ("device-cloud-01", "/backup/daily"),
    "archive": ("device-archive-01", "/backup/archive"),
}


def classify(filename):
    name = os.path.basename(filename).lower()

    if name.endswith(".dump"):
        return "dump"

    if "archive" in name or name.endswith(".tar.gz"):
        return "archive"

    return "daily"


def dispatch(source, files):
    transfers = {}

    for path in files:
        target, target_path = ROUTES[classify(path)]
        transfers[path] = send_backup(source, target, path, target_path)

    return transfers
static final Map<String, String[]> ROUTES = Map.of(
        "dump", new String[]{"device-dc-01", "/backup/dump"},
        "daily", new String[]{"device-cloud-01", "/backup/daily"},
        "archive", new String[]{"device-archive-01", "/backup/archive"});

String classify(String filename) {
    String name = Path.of(filename).getFileName().toString().toLowerCase();

    if (name.endsWith(".dump")) return "dump";
    if (name.contains("archive") || name.endsWith(".tar.gz")) return "archive";
    return "daily";
}

Map<String, String> dispatch(String source, List<String> files) throws IOException {
    Map<String, String> transfers = new LinkedHashMap<>();
    for (String path : files) {
        String[] route = ROUTES.get(classify(path));
        transfers.put(path, sendBackup(source, route[0], path, route[1]));
    }
    return transfers;
}
const ROUTES = {
  dump: ["device-dc-01", "/backup/dump"],
  daily: ["device-cloud-01", "/backup/daily"],
  archive: ["device-archive-01", "/backup/archive"],
};

function classify(filename) {
  const name = filename.split("/").pop().toLowerCase();

  if (name.endsWith(".dump")) return "dump";
  if (name.includes("archive") || name.endsWith(".tar.gz")) return "archive";
  return "daily";
}

async function dispatch(source, files) {
  const transfers = {};
  for (const path of files) {
    const [target, targetPath] = ROUTES[classify(path)];
    transfers[path] = await sendBackup(source, target, path, targetPath);
  }
  return transfers;
}
static readonly Dictionary<string, (string Device, string Path)> Routes = new()
{
    ["dump"] = ("device-dc-01", "/backup/dump"),
    ["daily"] = ("device-cloud-01", "/backup/daily"),
    ["archive"] = ("device-archive-01", "/backup/archive"),
};

string Classify(string filename)
{
    string name = Path.GetFileName(filename).ToLowerInvariant();

    if (name.EndsWith(".dump")) return "dump";
    if (name.Contains("archive") || name.EndsWith(".tar.gz")) return "archive";
    return "daily";
}

async Task<Dictionary<string, string>> DispatchAsync(string source, IEnumerable<string> files)
{
    var transfers = new Dictionary<string, string>();
    foreach (string path in files)
    {
        var (target, targetPath) = Routes[Classify(path)];
        transfers[path] = await SendBackupAsync(source, target, path, targetPath);
    }
    return transfers;
}

하나의 백업 파일을 여러 원격 환경에 함께 보내야 한다면 대상마다 전송을 만들고, 반환된 monitorId를 대상별로 보관해 나중에 각 전송을 조회합니다.

copies = {
    target: send_backup("device-db-01", target, backup_path, path)
    for target, path in [("device-dc-01", "/backup/dump"),
                         ("device-cloud-01", "/backup/mirror")]
}
Map<String, String> copies = new LinkedHashMap<>();
for (String[] dest : List.of(
        new String[]{"device-dc-01", "/backup/dump"},
        new String[]{"device-cloud-01", "/backup/mirror"})) {
    copies.put(dest[0], sendBackup("device-db-01", dest[0], backupPath, dest[1]));
}
const copies = {};
for (const [target, path] of [
  ["device-dc-01", "/backup/dump"],
  ["device-cloud-01", "/backup/mirror"],
]) {
  copies[target] = await sendBackup("device-db-01", target, backupPath, path);
}
var copies = new Dictionary<string, string>();
foreach (var (target, path) in new[]
{
    ("device-dc-01", "/backup/dump"),
    ("device-cloud-01", "/backup/mirror"),
})
{
    copies[target] = await SendBackupAsync("device-db-01", target, backupPath, path);
}

일정 자동화#

정해진 시각에 반복 실행되도록 등록하기

백업 스크립트를 고칠 수 없는 환경이라면 일정 자동화로 구성합니다.

def build_schedule_automation(name, source, source_path, target, target_path,
                              schedule):
    return {
        "name": name,
        "flowName": name,
        "transferType": "normal",
        "timezone": "Asia/Seoul",
        "step": 1,
        "isUpcoming": False,
        "details": [
            {
                "senderId": source,
                "receiverId": target,
                "sourceItem": [
                    {
                        "hash": encode_path(source, source_path),
                        "filePath": source_path,
                        "isDir": True,
                    }
                ],
                "targetPath": encode_path(target, target_path),
                "step": 1,
                "transferOptions": {
                    "noSchedule": False,
                    "target-action": "numbering",
                    "send-fileoption": {},
                },
            }
        ],
        "schedules": [schedule],
    }


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

MONTHLY = {
    "type": "month",
    "startDateType": "now",
    "day": "1",
    "hour": "04",
    "minute": "00",
    "ampm": "am",
    "startDate": now_iso(),
    "timezone": "Asia/Seoul",
}

api("POST", "/api/automations", build_schedule_automation(
    "daily backup", "device-db-01", "/backup",
    "device-dc-01", "/backup/daily", DAILY_3AM))
Map<String, Object> buildScheduleAutomation(String name, String source, String sourcePath,
                                            String target, String targetPath,
                                            Map<String, Object> schedule) {
    Map<String, Object> detail = Json.newObj(
            "senderId", source, "receiverId", target,
            "sourceItem", List.of(Json.newObj(
                    "hash", InnorixClient.encodePath(source, sourcePath),
                    "filePath", sourcePath, "isDir", true)),
            "targetPath", InnorixClient.encodePath(target, targetPath),
            "step", 1,
            "transferOptions", Json.newObj(
                    "noSchedule", false, "target-action", "numbering",
                    "send-fileoption", Json.newObj()));

    return Json.newObj(
            "name", name, "flowName", name,
            "transferType", "normal", "timezone", "Asia/Seoul",
            "step", 1, "isUpcoming", false,
            "details", List.of(detail),
            "schedules", List.of(schedule));
}
function buildScheduleAutomation(name, source, sourcePath, target, targetPath, schedule) {
  return {
    name,
    flowName: name,
    transferType: "normal",
    timezone: "Asia/Seoul",
    step: 1,
    isUpcoming: false,
    details: [{
      senderId: source,
      receiverId: target,
      sourceItem: [{
        hash: encodePath(source, sourcePath),
        filePath: sourcePath,
        isDir: true,
      }],
      targetPath: encodePath(target, targetPath),
      step: 1,
      transferOptions: {
        noSchedule: false,
        "target-action": "numbering",
        "send-fileoption": {},
      },
    }],
    schedules: [schedule],
  };
}
JsonObject BuildScheduleAutomation(string name, string source, string sourcePath,
    string target, string targetPath, JsonObject schedule)
{
    var detail = new JsonObject
    {
        ["senderId"] = source,
        ["receiverId"] = target,
        ["sourceItem"] = new JsonArray
        {
            new JsonObject
            {
                ["hash"] = InnorixClient.EncodePath(source, sourcePath),
                ["filePath"] = sourcePath,
                ["isDir"] = true,
            },
        },
        ["targetPath"] = InnorixClient.EncodePath(target, targetPath),
        ["step"] = 1,
        ["transferOptions"] = new JsonObject
        {
            ["noSchedule"] = false,
            ["target-action"] = "numbering",
            ["send-fileoption"] = new JsonObject(),
        },
    };

    return new JsonObject
    {
        ["name"] = name,
        ["flowName"] = name,
        ["transferType"] = "normal",
        ["timezone"] = "Asia/Seoul",
        ["step"] = 1,
        ["isUpcoming"] = false,
        ["details"] = new JsonArray { detail },
        ["schedules"] = new JsonArray { schedule },
    };
}

자동화 요청에서 반드시 지켜야 하는 항목이 네 개 있습니다.

항목 지정 방식
isUpcoming 반드시 false. 서버 기본값 true는 요청에 담긴 일정을 무시하고 5분짜리 일회성 일정으로 대체합니다. triggerAutomation이 붙은 단계는 서버가 false로 강제하므로, 트리거가 없는 첫 단계에만 직접 지정하면 됩니다
step 최상위와 details 양쪽에 넣습니다. 흐름 안에서의 홉 위치입니다
sourceItem hash(경로 토큰)와 filePath(평문 경로)를 함께 넣습니다
syncType transferOptions 안에 넣습니다. 1은 단방향, 2는 양방향입니다

네 항목 모두 누락해도 등록은 성공하고 실행 시점에 동작이 달라집니다. 반복 일정을 등록했는데 한 번만 실행되고 끝났다면 isUpcoming부터 확인합니다.

중복 등록 방지#

같은 백업 작업이 두 번 만들어지지 않게 하기

같은 이름의 자동화가 있어도 새로 만들어집니다. 배치가 재시도되면 같은 백업이 두 번 전송되고 도착 정책이 numbering이라 파일도 두 벌 쌓입니다.

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

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

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

        if len(items) < 100:
            return None

    return None
Map<String, Object> findAutomation(String name) {
    // the name we send is stored as flowName in the response
    // automationName is a server generated id like T4037-8500-1815, not the name we set.
    for (int page = 1; page <= 5; page++) {
        Map<String, Object> result = client.apiObj("GET", "/api/automations", null,
                Json.newObj("page", page, "size", 100, "search", name));

        List<Map<String, Object>> items = new ArrayList<>();
        for (Object flow : Json.arrOf(result, "automations")) {
            for (Object item : Json.arrOf(Json.asObj(flow), "automations")) {
                items.add(Json.asObj(item));
            }
        }

        for (Map<String, Object> item : items) {
            if (name.equals(Json.str(item, "flowName"))) return item;
        }

        if (items.size() < 100) return null;
    }

    return null;
}
async function findAutomation(name) {
  // the name we send is stored as flowName in the response
  // automationName is a server generated id like T4037-8500-1815, not the name we set.
  for (let page = 1; page <= 5; page += 1) {
    const result = (await client.api("GET", "/api/automations", null,
      { page, size: 100, search: name })) || {};

    const items = (result.automations || [])
      .flatMap((flow) => flow.automations || []);

    for (const item of items) {
      if (item.flowName === name) return item;
    }

    if (items.length < 100) return null;
  }

  return null;
}
async Task<JsonObject> FindAutomationAsync(string name)
{
    // the name we send is stored as flowName in the response
    // automationName is a server generated id like T4037-8500-1815, not the name we set.
    for (int page = 1; page <= 5; page++)
    {
        JsonObject result = await client.ApiObjAsync("GET", "/api/automations", null,
            new Dictionary<string, object> { ["page"] = page, ["size"] = 100, ["search"] = name });

        var items = J.ArrOf(result, "automations")
            .SelectMany(flow => J.ArrOf(J.AsObj(flow), "automations"))
            .Select(J.AsObj).ToList();

        foreach (JsonObject item in items)
        {
            if (J.Str(item, "flowName") == name) return item;
        }

        if (items.Count < 100) return null;
    }

    return null;
}

자동화 목록은 흐름 그룹으로 중첩되어 반환되므로 안쪽 배열까지 순회해야 합니다. 서버 검색은 부분 일치이므로, 받은 결과에서 이름이 정확히 일치하는 항목만 골라냅니다.

무결성 확인#

원격에 저장된 백업이 원본과 같은지 확인하기

def verify(monitor_id, timeout=1800, interval=10):
    api("POST", f"/api/transfers/{monitor_id}/verification", {})

    deadline = time.time() + timeout

    while time.time() < deadline:
        result = api("GET", f"/api/transfers/{monitor_id}/verification") or {}

        if result.get("verified"):
            return result

        time.sleep(interval)

    raise TimeoutError(monitor_id)
public Map<String, Object> verify(String monitorId, int timeoutSeconds, int intervalSeconds) {
    api("POST", "/api/transfers/" + monitorId + "/verification", Json.newObj());

    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;

    while (System.currentTimeMillis() < deadline) {
        Map<String, Object> result =
                apiObj("GET", "/api/transfers/" + monitorId + "/verification");

        if (Json.bool(result, "verified", false)) return result;

        sleep(intervalSeconds * 1000L);
    }

    throw new ApiError(0, "verification timeout: " + monitorId);
}
export async function verify(monitorId, { timeout = 1800, interval = 10 } = {}) {
  await api("POST", `/api/transfers/${monitorId}/verification`, {});

  const deadline = Date.now() + timeout * 1000;

  while (Date.now() < deadline) {
    const result = (await api("GET", `/api/transfers/${monitorId}/verification`)) || {};

    if (result.verified) return result;

    await sleep(interval * 1000);
  }

  throw new Error(`verification timeout: ${monitorId}`);
}
/// <summary>Start integrity verification, then poll until the result is ready.</summary>
public async Task<JsonObject> VerifyAsync(string monitorId, int timeoutSeconds = 1800,
    int intervalSeconds = 10)
{
    await ApiAsync("POST", "/api/transfers/" + monitorId + "/verification",
        new JsonObject()).ConfigureAwait(false);

    long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;

    while (Environment.TickCount64 < deadline)
    {
        JsonObject result = await ApiObjAsync(
            "GET", "/api/transfers/" + monitorId + "/verification").ConfigureAwait(false);

        if (J.Bool(result, "verified", false)) return result;

        await Task.Delay(intervalSeconds * 1000).ConfigureAwait(false);
    }

    throw new ApiError(0, "verification timeout: " + monitorId);
}
result = verify(monitor_id)

if not result["checksumMatched"]:
    alert(f"backup integrity failed: {result.get('mismatchedFiles')}")
Map<String, Object> result = client.verify(monitorId, 1800, 10);

if (!Json.bool(result, "checksumMatched", false)) {
    alert("backup integrity failed: " + Json.get(result, "mismatchedFiles"));
}
const result = await verify(monitorId);

if (!result.checksumMatched) {
  alert(`backup integrity failed: ${result.mismatchedFiles}`);
}
JsonObject result = await client.VerifyAsync(monitorId);

if (!J.Bool(result, "checksumMatched", false))
{
    Alert(
quot;backup integrity failed: {J.Get(result, "mismatchedFiles")}"
); }
응답 항목 내용
checksumAlgorithm 사용된 체크섬 알고리즘
sourceFileCount , targetFileCount 원본과 원격의 파일 수
checksumMatched 체크섬 일치 여부
mismatchedCount 불일치 건수

파일 수가 다르면 전송이 덜 된 것이고, 수는 같은데 불일치가 있으면 내용이 깨진 것입니다. 백업에서는 후자가 더 위험하므로 검증 실패를 즉시 알립니다.

결과 확인과 재전송#

전송 결과를 확인하고 실패한 파일 재전송하기

def wait(monitor_id, timeout=3600, interval=3):
    deadline = time.time() + timeout

    while time.time() < deadline:
        detail = api("GET", f"/api/transfers/{monitor_id}")

        if is_terminal(detail):
            return detail

        time.sleep(interval)

    raise TimeoutError(monitor_id)


def failed_files(monitor_id):
    result = api("GET", f"/api/transfers/{monitor_id}/files", params={
        "state": "any", "size": 500,
    }) or {}

    return [r for r in (result.get("children") or [])
            if r.get("status") in NOT_SUCCEEDED]


def retry_failed(monitor_id):
    rows = failed_files(monitor_id)

    if not rows:
        return 0

    api("POST", f"/api/transfers/{monitor_id}/retry", {
        "filesRetry": [
            {"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
            for r in rows
        ]
    })

    return len(rows)
public Map<String, Object> wait(String monitorId, int timeoutSeconds, int intervalSeconds) {
    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;

    while (System.currentTimeMillis() < deadline) {
        Map<String, Object> detail = apiObj("GET", "/api/transfers/" + monitorId);

        if (isTerminal(detail)) return detail;

        sleep(intervalSeconds * 1000L);
    }

    throw new ApiError(0, "timeout waiting for " + monitorId);
}

public List<Map<String, Object>> failedFiles(String monitorId) {
    List<Map<String, Object>> failed = new ArrayList<>();

    for (Map<String, Object> row : transferFiles(monitorId)) {
        Integer status = Json.intOrNull(row, "status");
        if (status != null && NOT_SUCCEEDED.contains(status)) failed.add(row);
    }

    return failed;
}

public int retryFailed(String monitorId) {
    List<Map<String, Object>> rows = failedFiles(monitorId);
    if (rows.isEmpty()) return 0;

    List<Object> filesRetry = new ArrayList<>();

    for (Map<String, Object> row : rows) {
        String path = Json.str(row, "sourceFilePath");
        if (path == null) continue;
        filesRetry.add(Json.newObj("filePath", path, "isDir", Json.bool(row, "isFolder", false)));
    }

    api("POST", "/api/transfers/" + monitorId + "/retry", Json.newObj("filesRetry", filesRetry));

    return rows.size();
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function wait(monitorId, { timeout = 3600, interval = 3 } = {}) {
  const deadline = Date.now() + timeout * 1000;

  while (Date.now() < deadline) {
    const detail = await api("GET", `/api/transfers/${monitorId}`);

    if (isTerminal(detail)) return detail;

    await sleep(interval * 1000);
  }

  throw new Error(`timeout waiting for ${monitorId}`);
}

export async function failedFiles(monitorId) {
  const result = (await api("GET", `/api/transfers/${monitorId}/files`, null, {
    state: "any", size: 500,
  })) || {};

  return (result.children || []).filter((row) => NOT_SUCCEEDED.has(row.status));
}

export async function retryFailed(monitorId) {
  const rows = await failedFiles(monitorId);

  if (rows.length === 0) return 0;

  await api("POST", `/api/transfers/${monitorId}/retry`, {
    filesRetry: rows
      .filter((row) => row.sourceFilePath)
      .map((row) => ({ filePath: row.sourceFilePath, isDir: Boolean(row.isFolder) })),
  });

  return rows.length;
}
public async Task<JsonObject> WaitAsync(string monitorId, int timeoutSeconds = 3600,
    int intervalSeconds = 3)
{
    long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;

    while (Environment.TickCount64 < deadline)
    {
        JsonObject detail = await ApiObjAsync("GET", "/api/transfers/" + monitorId)
            .ConfigureAwait(false);

        if (IsTerminal(detail)) return detail;

        await Task.Delay(intervalSeconds * 1000).ConfigureAwait(false);
    }

    throw new ApiError(0, "timeout waiting for " + monitorId);
}

public async Task<List<JsonObject>> FailedFilesAsync(string monitorId)
{
    List<JsonObject> rows = await TransferFilesAsync(monitorId).ConfigureAwait(false);

    return rows.Where(row =>
    {
        int? status = J.IntOrNull(row, "status");
        return status != null && NotSucceeded.Contains(status.Value);
    }).ToList();
}

/// <summary>Only callable once the transfer has reached a terminal state.</summary>
public async Task<int> RetryFailedAsync(string monitorId)
{
    List<JsonObject> rows = await FailedFilesAsync(monitorId).ConfigureAwait(false);
    if (rows.Count == 0) return 0;

    var filesRetry = new JsonArray();

    foreach (JsonObject row in rows)
    {
        string path = J.Str(row, "sourceFilePath");
        if (path == null) continue;

        filesRetry.Add(new JsonObject
        {
            ["filePath"] = path,
            ["isDir"] = J.Bool(row, "isFolder", false),
        });
    }

    await ApiAsync("POST", "/api/transfers/" + monitorId + "/retry",
        new JsonObject { ["filesRetry"] = filesRetry }).ConfigureAwait(false);

    return rows.Count;
}
detail = wait(monitor_id, timeout=7200)

if detail["status"] != STATUS_COMPLETE:
    alert(f"backup transfer failed - retried {retry_failed(monitor_id)} files")
Map<String, Object> detail = client.await(monitorId, 7200, null);

if (Json.intOr(detail, "status", -1) != InnorixClient.STATUS_COMPLETE) {
    alert("backup transfer failed - retried " + client.retryFailed(monitorId) + " files");
}
const detail = await wait(monitorId, { timeout: 7200 });

if (detail.status !== STATUS_COMPLETE) {
  alert(`backup transfer failed - retried ${await retryFailed(monitorId)} files`);
}
JsonObject detail = await client.WaitAsync(monitorId, 7200);

if (J.IntOrNull(detail, "status") != InnorixClient.StatusComplete)
{
    Alert(
quot;backup transfer failed - retried {await client.RetryFailedAsync(monitorId)} files"
); }

매일 실행되는 작업의 직전 회차가 정상 종료됐는지 점검하려면 기간을 지정해 최근 이력을 조회합니다.

from datetime import datetime, timedelta, timezone


def paginate(path, params=None, limit=200, max_pages=50):
    query = dict(params or {})
    query["limit"] = limit
    cursor = None

    for _ in range(max_pages):
        if cursor:
            query["cursor"] = cursor

        result = api("GET", path, params=query) or {}

        for record in result.get("data") or []:
            yield record

        pagination = result.get("pagination") or {}

        if not pagination.get("hasMore"):
            return

        cursor = pagination.get("nextCursor")

        if not cursor:
            return


end = datetime.now(timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%SZ"

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

failures = [r for r in rows if r.get("status") in NOT_SUCCEEDED]

if failures:
    alert(f"{len(failures)} backup transfers failed yesterday")
Instant end = Instant.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
        .withZone(ZoneOffset.UTC);

List<Map<String, Object>> rows = client.paginate("/api/transfer-history",
        Json.newObj("startDate", fmt.format(end.minus(1, ChronoUnit.DAYS)),
                    "endDate", fmt.format(end)), 200, 50);

List<Map<String, Object>> failures = new ArrayList<>();
for (Map<String, Object> row : rows) {
    Integer status = Json.intOrNull(row, "status");
    if (status != null && InnorixClient.NOT_SUCCEEDED.contains(status)) failures.add(row);
}

if (!failures.isEmpty()) {
    alert(failures.size() + " backup transfers failed yesterday");
}
const end = new Date();
const fmt = (d) => d.toISOString().replace(/\.\d{3}Z$/, "Z");

const rows = [];
for await (const row of paginate("/api/transfer-history", {
  startDate: fmt(new Date(end.getTime() - 86400000)),
  endDate: fmt(end),
})) rows.push(row);

const failures = rows.filter((r) => NOT_SUCCEEDED.has(r.status));

if (failures.length) {
  alert(`${failures.length} backup transfers failed yesterday`);
}
DateTime end = DateTime.UtcNow;
const string Fmt = "yyyy-MM-dd'T'HH:mm:ss'Z'";

List<JsonObject> rows = await client.PaginateAsync("/api/transfer-history",
    new Dictionary<string, object>
    {
        ["startDate"] = end.AddDays(-1).ToString(Fmt),
        ["endDate"] = end.ToString(Fmt),
    });

var failures = rows.Where(r =>
{
    int? status = J.IntOrNull(r, "status");
    return status != null && InnorixClient.NotSucceeded.Contains(status.Value);
}).ToList();

if (failures.Count > 0)
{
    Alert(
quot;{failures.Count} backup transfers failed yesterday"
); }

감사용으로 전체 이력을 파일로 받아야 한다면 CSV 내보내기를 씁니다.

params = {
    "periodDays": 30,
    "page": 1,
    "size": 10000,
    "filter": "[]",              # the server parses this as a JSON string, so send an empty array
    "sort": "startDate:desc",
}
Map<String, Object> params = Json.newObj(
        "periodDays", 30,
        "page", 1,
        "size", 10000,
        "filter", "[]",              // the server parses this as a JSON string, so send an empty array
        "sort", "startDate:desc");
const params = {
  periodDays: 30,
  page: 1,
  size: 10000,
  filter: "[]",              // the server parses this as a JSON string, so send an empty array
  sort: "startDate:desc",
};
var parameters = new Dictionary<string, object>
{
    ["periodDays"] = 30,
    ["page"] = 1,
    ["size"] = 10000,
    ["filter"] = "[]",              // the server parses this as a JSON string, so send an empty array
    ["sort"] = "startDate:desc",
};

조건이 없어도 filter에 빈 배열 문자열("[]")을 넣어 보냅니다. 서버가 이 값을 JSON으로 파싱하기 때문입니다. sort에 넣을 수 있는 필드는 status, sourceDeviceName, targetDeviceName, totalSize, sourceFileCount, startDate, endDate, automationName, formattedTransferTime, savedTime입니다.

확인 항목 확인 내용
원본 전송한 덤프와 백업 파일
대상 원격 데이터센터 또는 클라우드 경로
검증 파일 수와 체크섬 일치 여부
상태 전송 상태와 성공 여부
이력 최근 회차의 실행 결과
재전송 실패한 파일과 처리 결과
이전Azure Blob에서 사내 서버로 파일 전달하기다음지점·공장·엣지 장비의 파일을 중앙으로 수집하기

이 페이지에서

  • 시작하기
  • 기본 개념
  • 자동화 흐름
  • 운영 효과
  • IT 엔지니어
  • 원본 연결
  • 전송 구성
  • 자동 전송
  • 결과 관리
  • 개발자
  • 연동 준비
  • 백업 완료 후 전송
  • 파일 유형별 분기
  • 일정 자동화
  • 중복 등록 방지
  • 무결성 확인
  • 결과 확인과 재전송