Migrate Large Volumes of Files from NAS and File Servers to the Cloud

IT EngineersDevelopers

Getting Started

Basic Concepts

Migrate data from existing file environments to cloud storage

Enterprise files are distributed across various environments, including NAS, SMB shared folders, NFS storage, and file servers.

Cloud migration consists of transferring existing files to designated cloud storage and continuously applying changes that occur during the migration period to manage file state until the final cutover.

text
Existing File Environment
      │
      ▼
Initial File Migration
      │
      ▼
Changed Files Synchronization
      │
      ▼
Check Cutover Status
      │
      ▼
Cloud Cutover

By connecting the initial file migration with changed-file synchronization, you can migrate data from the existing file environment to the cloud and manage file state until the cutover.

Migration Flow

Continue from initial file migration through changed-file synchronization to final cutover

File migration consists of transferring the initial data to the cloud and continuously applying files created or modified during the migration period.

text
① Source Environment Connection
        │
        ▼
② Initial File Migration
        │
        ▼
③ Detect Changes
        │
        ▼
④ Changed Files Synchronization
        │
        ▼
⑤ Check Cutover Status
        │
        ▼
⑥ Cloud Cutover

This flow lets you manage the initial files and subsequent changes together while proceeding with the cloud cutover.

Cutover Benefits

Continue the existing file flow into the cloud environment

Configuring initial migration and change synchronization together lets you manage file state across the source environment and cloud throughout the cutover period.

CategoryExisting File EnvironmentCloud Cutover Flow
Initial DataManaged in the existing environmentMigrate large volumes of files to the cloud
Changed FilesCheck status by fileContinuously synchronize changes
Cutover PreparationCheck file state by environmentVerify based on migration and synchronization results
Subsequent WorkUse the existing storage locationConnect based on the cloud storage location

IT Engineers

Source Environment

Connect migration paths from NAS and file servers

First, connect the NAS, SMB, NFS, or file server where the files to be migrated are stored, and specify the file paths to migrate to the cloud.

Set the access scope so files can be read and changes can be checked in each source environment.

text
NAS ─────────┐
SMB ─────────┤
NFS ─────────┼──→ Migration Flow
File Server ─┘

By connecting multiple source environments, you can configure migration tasks for each file location in a single flow.

Cloud Connection

Configure cloud storage and target paths

After connecting the source environment, configure the cloud storage and target location where files will be stored.

Specify the target bucket or container and storage path, and connect them to the source file path.

text
Original Storage
        │
        ▼
 Migration Flow
        │
        ▼
Cloud Storage
   └─ Target Path

Configuring the source paths to migrate and cloud storage locations prepares the file flow for initial migration and change synchronization.

Initial Migration

Transfer existing large volumes of files to the cloud

After configuring the migration environment, transfer existing files and large datasets to cloud storage.

During execution, you can check the number of processed files, total capacity, progress, and task status.

text
Original Files
      │
      ▼
Initial Transfer
      │
      ▼
Cloud Storage

Use the initial migration results as the baseline for subsequent changed-file synchronization and final cutover.

Change Synchronization

Continuously apply files changed during the migration period

Even after the initial migration, new files may be created or existing files may be modified in the original file environment.

Use New ItemsandModified Items` to identify changes and configure those files to be synchronized to cloud storage.

text
Original Environment
        │
   New / Modified
        │
        ▼
   Change Detection
        │
        ▼
      Sync
        │
        ▼
  Cloud Storage

By continuously applying changed files, you can continue managing file state in the cloud after the initial migration.

Cutover Verification

Verify initial migration and change-synchronization results

Before the final cutover, check the initial file migration results together with the synchronization status of subsequently changed files.

You can verify cutover readiness between the source and cloud environments based on file count and capacity, recent execution results, and synchronization status.

text
Initial Transfer ──┐
                   ├──→ Migration Status
Change Sync ───────┘
                         │
                    ┌────┴────┐
                    ▼         ▼
                 Ready    Check Items

Reviewing the initial migration and change-synchronization results together lets you determine when to switch operations to the cloud environment.

Final Cutover

Switch file operations to the cloud environment

After cutover verification is complete, set cloud storage as the baseline environment for subsequent file operations.

Connect the storage locations of business systems, applications, and subsequent file tasks to the cloud to continue the file flow.

text
Before
NAS / File Server
        │
        ▼
 Migration Flow
        │
        ▼
Cloud Storage
        │
        ▼
After
App / Service / Workflow

After migration, you can connect subsequent work and automation tasks based on files stored in the cloud.

Operations Response

Review and rerun migration and synchronization tasks

For tasks requiring review during migration, use Runs and detailed execution records to check source connectivity, target paths, file-processing results, and synchronization status.

text
Migration Run
      │
      ▼
Status Check
      │
 ┌────┴─────┐
 ▼          ▼
Complete  Check Items
             │
             ▼
       View Details
             │
             ▼
       Update Settings
             │
             ▼
           Retry
             │
             ▼
       Result Confirm

Check ItemDetailsFollow-up Action
Source EnvironmentNAS, SMB, NFS, and File Server Connections and PathsCheck connection and path
CloudStorage location and access statusCheck target settings
Initial MigrationLarge-file processing resultsRerun the required task
Change SynchronizationStatus of added and modified filesRerun the synchronization task
Execution HistoryTask status and detailed processing resultsCheck settings and rerun

Developers

Move an existing shared folder to the cloud and continuously apply changes during the migration period

Integration Setup

Prepare common API-call code and path notation

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)
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())

Transfer status is determined by the values below. There are five terminal states, and the successful value is Complete (2).

Status ValueMeaningTerminal
2CompleteYes
4ErrorYes
5CanceledYes
9Partially CompleteYes
99FailedYes
1 , 6 , 12 , 13Starting, Transferring, Synchronizing, ReceivingNo

Determine the Migration Scope

Check the number and capacity of files to migrate in advance

You need to know the scale before starting the migration so you can plan the schedule.

def scan(device_id, path, max_pages=200):
    page = api("POST", f"/api/devices/{device_id}/files/search",
               {"path": path, "pageSize": 1000})

    total = size = 0
    search_id = page.get("searchId")

    try:
        for _ in range(max_pages):
            for item in page.get("items") or []:
                if item["type"] == "file":
                    total += 1
                    size += item.get("size") or 0

            if not page.get("hasMore"):
                break

            page = api("GET", f"/api/devices/{device_id}/files/search",
                       params={"cursor": page["nextCursor"]})
    finally:
        # stop the device scan once the count is done
        if search_id:
            api("POST", f"/api/devices/{device_id}/files/search/cancel",
                {"uuid": search_id})

    return total, size


count, size = scan("device-nas-01", "/share/team")
print(f"{count} files / {size / (1024 ** 3):.1f}GB")

A search actually scans the device's disk. If it is not stopped, scanning continues even after the check is complete.

Initial Migration

Send existing large volumes of files to the cloud

Sending everything at once increases the scope that must be restarted after a failure. Split the transfer by top-level folder.

def target_for(folder, base):
    return folder.replace("/share", base, 1)


def migrate(source, target, folder, base):
    transfer = api("POST", "/api/transfers/manual", {
        "sourceDevice": source,
        "targetDevice": target,
        "targetPath": target_for(folder, base),
        "sourcePaths": [folder],
        "sendAllFolder": True,
        "transferOptions": {"target-action": "overwrite"},
    })

    return transfer["monitorId"]


FOLDERS = ["/share/team/design", "/share/team/docs", "/share/team/archive"]

transfers = {
    folder: migrate("device-nas-01", "device-cloud-01", folder, "/migrated")
    for folder in FOLDERS
}

Use the overwrite destination policy. Reruns are common during migration, and numbering causes copies to accumulate.

Folders containing very large numbers of files can take a long time to transfer. If interrupted, the transfer can resume, so there is no need to retransmit from the beginning.

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)
for folder, monitor_id in transfers.items():
    detail = wait(monitor_id, timeout=86400)

    print(f"{folder:28} {detail.get('statusName')}"
          f" {detail.get('fileCount')} files")

    if detail["status"] != STATUS_COMPLETE:
        print(f"  retried {retry_failed(monitor_id)} files")

Changes Synchronization

Continuously apply files changed during the migration period

Files continue to change at the source even while the initial migration takes several days. Send only the changes periodically.

transfer = api("POST", "/api/transfers/manual", {
    "sourceDevice": "device-nas-01",
    "targetDevice": "device-cloud-01",
    "targetPath": "/migrated/team/design",
    "sourcePaths": ["/share/team/design"],
    "sendAllFolder": True,
    "incremental": True,
    "transferOptions": {"target-action": "overwrite"},
})

The default is off. The device agent calculates changes and sends only files added or modified since the last transfer.

Always use overwrite for incremental transfers. With numbering, modified files accumulate under new names, causing the cloud state to differ from the source.

If you cannot keep the script running, register it as hourly automation.

def build_sync(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": "overwrite",
                    "send-fileoption": {},
                },
            }
        ],
        "schedules": [schedule],
    }


HOURLY = {
    "type": "hour",
    "startDateType": "now",
    "startDate": now_iso(),
    "timezone": "Asia/Seoul",
}

syncs = {
    folder: api("POST", "/api/automations", build_sync(
        f"sync {folder}", "device-nas-01", folder,
        "device-cloud-01", target_for(folder, "/migrated"),
        HOURLY))["automationId"]
    for folder in FOLDERS
}

There are four items that must be observed in automation requests.

ItemConfiguration
isUpcomingMust be false. The server default true ignores the schedule in the request and replaces it with a five-minute one-time schedule. Steps with triggerAutomation are forced to false by the server, so specify it directly only on the first step without a trigger
stepInclude it at both the top level and in details. It is the hop position within the flow
sourceItemInclude both hash (path token) and filePath (plain-text path)
syncTypePut it inside transferOptions. 1 is one-way and 2 is two-way

Registration succeeds even if all four items are omitted, but behavior changes at execution time. If a recurring schedule was registered but ran only once, check isUpcoming first.

Progress Statistics

Query progress by transfer and synchronization run

def progress(transfers):
    for folder, monitor_id in transfers.items():
        detail = api("GET", f"/api/transfers/{monitor_id}")

        print(f"{folder:28} {detail.get('statusName'):12}"
              f" {detail.get('percent', 0):>5}%"
              f" {detail.get('fileCount', 0)} files")

Review each run's synchronization-automation results in the execution history.

for folder, automation_id in syncs.items():
    runs = api("GET", f"/api/automations/{automation_id}/executions") or []
    latest = runs[0] if runs else {}

    print(f"{folder:28} runs {len(runs):>3}"
          f" last {latest.get('startTime')} {latest.get('status')}")

The latest execution appears first in the array. If the history is empty, the automation was registered but not executed, so check isUpcoming and the start condition.

Cutover Verification

Verify that source and cloud files match

To determine the cutover point, verify that files in the two environments match. Compare the source and target file lists by folder to find missing files.

def list_names(device_id, path):
    names, page = set(), 1

    while True:
        result = api("GET", f"/api/devices/{device_id}/files", params={
            "path": path, "page": page, "size": 200, "type": "file",
        }) or {}

        names.update(item["name"] for item in result.get("items") or [])

        if page >= (result.get("lastPage") or 1):
            return names

        page += 1


missing = list_names("device-nas-01", "/share/team/design") \
        - list_names("device-cloud-01", "/migrated/team/design")

print(f"{len(missing)} missing")

If files are missing, retransmit the corresponding folder to align the lists. To compare size as well as names, include size in the list.

Cutover and Cleanup

Stop synchronization and make the cloud the baseline environment

After the cutover, stop the synchronization automation. If the source continues to be applied, changes on the cloud side may be overwritten.

for automation_id in syncs.values():
    api("POST", f"/api/automations/{automation_id}/pause", {"pause": True})

Delete it after confirming there are no problems for a period of time. Leaving it paused may allow it to be accidentally resumed later.

for automation_id in syncs.values():
    api("DELETE", f"/api/automations/{automation_id}")
Check ItemDetails
Migration ScopeFile count and total capacity
Initial MigrationTransfer status by folder
Change SynchronizationExecution results by run
VerificationWhether source and target file lists match
CutoverStop and clean up synchronization