Pause, Resume & Cancel a Transfer

Control a running transfer and retry failed files.

Once a transfer is running you can pause, resume, or cancel it, and retry only the files that failed. Because these actions are asynchronous, the example waits for the transfer to be running before pausing, and for the pause to settle before resuming.

Steps

  1. Create a transfer and wait until its status is transferring (6).
  2. Pause it with PATCH /api/transfer/pause/{monitorId}.
  3. After the pause settles (status 3), resume with .../resume/{monitorId}.
  4. If some files failed (status 9), call POST /api/transfer/retry-failed-files.

Configuration

Set your credentials and IDs via environment variables or a .env file (INNORIX_BASE_URL, INNORIX_WORKSPACE_ID, INNORIX_EMAIL, INNORIX_PASSWORD, and the INNORIX_SOURCE_ID / INNORIX_TARGET_ID / path variables the example uses).

API

MethodEndpointDescription
POST/api/transfer/manualTransferCreate a transfer to control
GET/api/transfer/{monitorId}/detail-unifiedPoll transfer status
PATCH/api/transfer/pause/{monitorId}Pause a running transfer
PATCH/api/transfer/resume/{monitorId}Resume a paused transfer
PATCH/api/transfer/cancel/{monitorId}Cancel a transfer
POST/api/transfer/retry-failed-filesRetry only the failed files
python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
""" 05 - Transfer control & retry ============================ Pause, resume, and cancel an in-flight transfer, and retry only the files that failed. Control actions are PATCH requests with no body. Timing note: pause/resume are asynchronous. Send pause only after the transfer is actually running (status transferring=6), and resume only after the pause has settled (status transferPause=3). Sending resume too early returns: 400 "Transfer is already PAUSE_OR_CANCEL cannot excute command" so we poll for the right state and retry resume a few times. This file is self-contained: it embeds a minimal client so it can be read and run on its own. Endpoints: PATCH /api/transfer/pause/{monitorId} PATCH /api/transfer/resume/{monitorId} PATCH /api/transfer/cancel/{monitorId} POST /api/transfer/retry-failed-files Run: python 05_transfer_control.py """ from __future__ import annotations import os import time import json import logging import requests try: from dotenv import load_dotenv load_dotenv() except ImportError: pass # --------------------------------------------------------------------------- # # Configuration # --------------------------------------------------------------------------- # BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com") WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID", "<WORKSPACE_ID>") EMAIL = os.getenv("INNORIX_EMAIL", "<YOUR_EMAIL>") PASSWORD = os.getenv("INNORIX_PASSWORD", "<YOUR_PASSWORD>") SOURCE_ID = os.getenv("INNORIX_SOURCE_ID", "<SOURCE_DEVICE_ID>") TARGET_ID = os.getenv("INNORIX_TARGET_ID", "<TARGET_DEVICE_ID>") TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "C:/Users/innorix/Downloads") SOURCE_PATHS = [p.strip() for p in os.getenv( "INNORIX_SOURCE_PATHS", "C:/Users/innorix/Downloads/image").split(",") if p.strip()] logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S") log = logging.getLogger("innorix") TRANSFER_STATUS = { -1: "transferQueue", 0: "WaitingTransfer", 1: "StartTransfer", 2: "transferComplete", 3: "transferPause", 4: "transferError", 5: "transferCancel", 6: "transferring", 7: "transferSkip", 8: "transferRetry", 9: "transferPartialComplete", 10: "transferRetryChecking", 11: "virusScanning", 12: "syncing", 13: "transferInComming", 14: "transferActivity", 99: "transferFail", } TERMINAL_STATES = {2, 4, 5, 9, 99} TRANSFERRING = 6 RESUMABLE_AFTER_PAUSE = {3} def status_name(code): if code is None: return "unknown" return TRANSFER_STATUS.get(code, f"unknown({code})") # --------------------------------------------------------------------------- # # Minimal client # --------------------------------------------------------------------------- # class ApiError(RuntimeError): """Raised when the API returns a non-2xx response.""" class InnorixClient: def __init__(self, base_url: str, workspace_id: str, timeout: float = 15.0): self.base_url = base_url.rstrip("/") self.workspace_id = workspace_id self.timeout = timeout self.session = requests.Session() def _request(self, method: str, path: str, **kwargs): kwargs.setdefault("timeout", self.timeout) res = self.session.request(method, f"{self.base_url}{path}", **kwargs) try: res.raise_for_status() except requests.HTTPError as exc: raise ApiError(f"{method} {path} -> {res.status_code}: {res.text[:500]}") from exc if not res.content: return None try: return res.json() except ValueError: return res.text def login(self, email: str, password: str) -> str: data = self._request("POST", "/api/auth/login", json={"email": email, "password": password}, timeout=10) token = data["data"]["user"]["accessToken"] self.session.headers.update({ "Authorization": f"Bearer {token}", "x-workspace-id": self.workspace_id, "Content-Type": "application/json", }) log.info("logged in") return token def create_transfer(self, source_id, target_id, target_path, source_file_paths) -> str: body = { "sourceId": source_id, "targetId": target_id, "targetPath": target_path, "sourceItem": [{"filePath": p} for p in source_file_paths], } data = self._request("POST", "/api/transfer/manualTransfer", json=body) monitor_id = data["data"]["monitorId"] log.info("transfer created monitorId=%s", monitor_id) return monitor_id def get_detail(self, monitor_id: str) -> dict: data = self._request("GET", f"/api/transfer/{monitor_id}/detail-unified", params={"workSpaceId": self.workspace_id}, timeout=10) if isinstance(data, dict) and "status" not in data and isinstance(data.get("data"), dict): return data["data"] return data def get_status(self, monitor_id: str): raw = self.get_detail(monitor_id).get("status") return int(raw) if raw is not None else None def wait_for_completion(self, monitor_id: str, interval: float = 3.0) -> int: while True: status = self.get_status(monitor_id) log.info("status=%s(%s)", status, status_name(status)) if status in TERMINAL_STATES: return status time.sleep(interval) def control(self, monitor_id: str, action: str): if action not in {"pause", "resume", "cancel"}: raise ValueError(f"unsupported action: {action}") result = self._request("PATCH", f"/api/transfer/{action}/{monitor_id}", timeout=10) log.info("transfer %s requested monitorId=%s", action, monitor_id) print(f"[control:{action}] response:") print(json.dumps(result, indent=2, ensure_ascii=False)) return result def retry_failed(self, monitor_id: str, files: list): return self._request("POST", "/api/transfer/retry-failed-files", json={"monitorId": monitor_id, "filesRetry": files}) # --------------------------------------------------------------------------- # # Helpers # --------------------------------------------------------------------------- # def wait_until(client, monitor_id, target_states, timeout=30.0, interval=1.5): """Poll until status is in target_states (or a terminal state) or timeout.""" deadline = time.monotonic() + timeout while time.monotonic() < deadline: status = client.get_status(monitor_id) if status in target_states or status in TERMINAL_STATES: return status time.sleep(interval) return client.get_status(monitor_id) def resume_with_retry(client, monitor_id, attempts=5, delay=1.5): """Resume, retrying while the server still reports the pause isn't settled.""" for i in range(attempts): try: client.control(monitor_id, "resume") return True except ApiError as exc: # 400 while state is still settling if i == attempts - 1: raise print(f"resume not ready yet ({exc}); retrying...") time.sleep(delay) return False # --------------------------------------------------------------------------- # # Run # --------------------------------------------------------------------------- # def main() -> None: client = InnorixClient(BASE_URL, WORKSPACE_ID) client.login(EMAIL, PASSWORD) monitor_id = client.create_transfer( source_id=SOURCE_ID, target_id=TARGET_ID, target_path=TARGET_PATH, source_file_paths=SOURCE_PATHS, ) print("monitorId:", monitor_id) # Wait until the transfer is actually running before pausing. running = wait_until(client, monitor_id, {TRANSFERRING}) print("status before pause:", status_name(running)) if running in TERMINAL_STATES: print("transfer finished before it could be paused (too small/fast)") return client.control(monitor_id, "pause") # Wait until the pause has settled before resuming. settled = wait_until(client, monitor_id, RESUMABLE_AFTER_PAUSE) print("status after pause:", status_name(settled)) if settled in TERMINAL_STATES: print("transfer already finished; nothing to resume") else: resume_with_retry(client, monitor_id) # To cancel instead of resuming: # client.control(monitor_id, "cancel") final = client.wait_for_completion(monitor_id, interval=3.0) print(f"final status: {final} ({status_name(final)})") # If the transfer partially failed (status 9), retry just the failed files. if final == 9: client.retry_failed(monitor_id, [ {"filePath": "C:/data/export/file.txt", "isFolder": False}, ]) if __name__ == "__main__": main()