NAS · File Server Cloud Migration

IT EngineersDevelopers

Getting Started

Core 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 transfers existing files to designated cloud storage while continuously applying changes that occur during the migration period, keeping file states aligned until the final cutover.

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

Connecting initial file migration with changed-file synchronization lets you migrate data from the existing file environment to the cloud and keep file states aligned through the cutover.

Migration Flow

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

File migration transfers the initial data to the cloud first, then continuously applies files created or modified during the migration period.

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

This flow lets you manage both the initial files and subsequent changes while progressing through the cloud cutover.

Migration Benefits

Carry the existing file workflow into the cloud environment

By configuring initial migration and change synchronization together, you can keep the file state connected between the source environment and the 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 status by environmentReview based on migration and synchronization results
Downstream WorkUse the existing storage locationConnect workflows using the cloud storage location

IT Engineers

Source Environment

Connect migration paths from NAS and file servers

First, connect the NAS, SMB, NFS, and file servers that store the files to be migrated, then specify the file paths to migrate to the cloud.

Configure access scope so files can be read and changes can be detected in each source environment.

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

Connecting multiple source environments lets you configure migration tasks by storage location within a single flow.

Cloud Connection

Configure cloud storage and destination paths

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

Specify the destination bucket or container and storage path, then connect it to the source file path.

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

Configuring the source paths 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 the migration environment is configured, transfer existing files and large datasets to cloud storage.

During execution, you can review the processed file count, total size, progress, and task status.

text
Original Files
      │
      ▼
Initial Transfer
      │
      ▼
Cloud Storage

The initial migration result becomes the baseline for subsequent changed-file synchronization and the final cutover.

Change Synchronization

Continuously apply files changed during the migration period

Files may continue to be created or modified in the existing file environment after the initial migration.

Use New Items and Modified Items to identify changes and configure those files to synchronize to cloud storage.

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

Continuously applying changed files keeps the post-migration file state aligned in the cloud environment.

Cutover Verification

Verify the results of initial migration and change synchronization

Before the final cutover, review the initial file migration result together with the synchronization status of files changed afterward.

Use file count and size, recent execution results, and synchronization status to assess cutover readiness between the source and cloud environments.

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 business operations to the cloud environment.

Final Cutover

Switch file operations to use the cloud environment as the reference

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

Connect the storage locations used by business systems, applications, and follow-up file tasks to the cloud so the file flow can continue.

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

After migration, connect downstream tasks and automation workflows using the files stored in the cloud as the basis.

Operational Response

Review migration and synchronization tasks and rerun them when needed

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

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

Review ItemDetailsFollow-up
Source EnvironmentNAS · SMB · NFS · file server connections and pathsCheck connections and paths
CloudStorage location and access statusCheck destination settings
Initial MigrationLarge-file processing resultRerun the required task
Change SynchronizationStatus of added and modified filesRerun synchronization
Execution RecordTask status and detailed processing resultReview settings and rerun

Developers

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

Integration Preparation

Prepare shared request code and path conventions

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

Determine the transfer status using the values below. There are five terminal states, and the successful state is Complete (2).

Status ValueMeaningTerminal
2CompleteYes
4ErrorYes
5CancelledYes
9Partially CompleteYes
99FailedYes
1 · 6 · 12 · 13Started · Transferring · Synchronizing · ReceivingNo

Determine Migration Scope

Check the number and total size of files to migrate in advance

You need to know the scale before starting 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")

Search requires the system to actually scan its disk. If not stopped, the scan continues even after the review is finished.

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 overwrite as the destination policy. It is common to rerun transfers during migration, and numbering would accumulate copies.

Folders with very large file counts can take a long time to transfer. If interrupted, resume the transfer instead of retransmitting 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")

Change Synchronization

Continue applying files changed during the migration period

Files continue to change at the source even when the initial migration takes several days. Transfer 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"},
})

This option is disabled by default. The agent on each system 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 and the cloud state diverges from the source.

If the script cannot keep running continuously, register it as an 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 requirements that must always be followed when creating an automation request.

ItemHow to Configure
isUpcomingMust be false. The server default of true ignores the schedule in the request and replaces it with a one-time five-minute schedule. A stage with triggerAutomation forces the value to false, so you only need to set it directly on the first stage without a trigger
stepSet it in both the top level and details. It indicates the hop position within the workflow
sourceItemInclude both hash (the path token) and filePath (the plain-text path)
syncTypeSet it inside transferOptions. 1 is one-way and 2 is two-way

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

Aggregate Progress

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

Results for each synchronization run can be reviewed through 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')}")

Execution history places the latest run at the beginning of the array. If the history is empty, the automation was registered but never executed, so check isUpcoming and the start condition.

Cutover Verification

Verify that files at the source and in the cloud match

To determine the cutover point, verify that the files in the two environments match. Compare source and destination 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 affected folder until the lists match. To compare size as well as names, include size in the list and compare it.

Cutover and Cleanup

Stop synchronization and make the cloud the reference environment

When the cutover is complete, stop the synchronization automation. If the source continues to be synchronized, changes from the cloud can be overwritten.

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

Delete the automation after the environment has remained stable for a defined period. Leaving it stopped can allow it to be resumed accidentally.

for automation_id in syncs.values():
    api("DELETE", f"/api/automations/{automation_id}")
Review ItemDetails
Migration ScopeFile count and total size
Initial MigrationTransfer status by folder
Change SynchronizationExecution result by run
VerificationWhether source and destination file lists match
CutoverSynchronization stopped and environment cleaned up