Getting Started#
Core Concept#
Importing and exporting approved files through defined paths and procedures
In separated network environments such as air-gapped and external networks, manage file transfer directions, destination paths, and processing criteria according to your operational requirements.
File import and export can be configured so that users request files for transfer, which then pass through defined approval and validation procedures before being transferred to the designated environment.
For example, files intended for use in an external environment can be imported into an air-gapped network, or result files generated during internal operations can be exported to an external environment after going through the required approval process.

Transfer Flow#
Connect the process from file request through approval and validation to final import or export
File transfers in network-segmented environments follow a defined procedure from file selection through delivery to the target environment.
File Request
↓
Transfer Target Confirmation
↓
Apply Approval Criteria
↓
File Validation
↓
Import or Export
↓
Record Transfer ResultApply the required approval and validation criteria based on the transfer direction, and review the processed results in the execution history.
This allows you to configure file flows between internal and external environments according to operational requirements and manage the status and results of each transfer together.

Operational Benefits#
Manage file movement, validation, and processing history as a single workflow
In network-segmented environments, you need an operational workflow that shows which files were processed and in which direction, alongside the transfer process itself.
By configuring import and export operations, you can manage file requests, approval results, validation information, and transfer status as a single execution flow.
| Management Item | File Import/Export Flow |
|---|---|
| Transfer Request | Manage requests according to the user and business requirements |
| Transfer Direction | Manage import/export paths between internal and external networks |
| Approval | Apply the approval workflow according to defined criteria |
| File Validation | Link inspection results to the transfer operation |
| Execution History | Review everything from the request to the final processing result |
This extends separate file import and export management into an operational workflow that runs from file request through validation and transfer result review.
IT Engineer#
Environment Setup#
Connect transfer paths between internal and external networks
First, connect the network environments and transfer devices used to import or export files.
Specify the file paths and destination locations for each environment, then configure import and export flows according to the direction in which files are processed.
External Environment
Import
Import
Approval · Validation
┌──────────┐
│ Approval · Validation │
Internal Environment
│
▼
Internal EnvironmentExport operations move files generated in the internal environment to the external environment according to the defined approval and validation criteria.
Internal Environment
Export
Export
Approval · Validation
┌──────────┐
│ Approval · Validation │
External Environment
│
▼
External Environment
Approval and Validation Settings#
Configure processing procedures based on files, users, and business requirements
For file import and export operations, define which files are processed and under what criteria.
Configure approval workflows based on file type, user, business purpose, and transfer direction, and connect file validation steps when required.
For example, you can require files with specific extensions to go through a separate approval process, or configure the next step to run only after approval from a designated business owner.
Transfer Request
│
Check Approval Criteria
│
▼
Approval Processing
│
▼
File Validation
│
▼
Execute Transfer
Import and Export Flow#
Deliver files to the designated environment based on approval and validation results
After configuring the transfer environments and processing criteria, configure the workflow so approved files are imported or exported through the designated paths.
For both import and export operations, you can specify the required target environment and storage location, while managing the file processing results in a single execution record.
Import Request → Approval → Validation → Internal Network Storage
Export Request → Approval → Validation → External Network StorageAlthough the two flows are distinguished by transfer direction and destination path, approval, validation, and execution results can be managed under the same operational standards.

Review History#
Manage everything from the request to the final transfer result through execution records
Once an import or export operation runs, review the request information, approval result, validation status, and file processing result in the execution history.
Each operation can include the following information.
| Review Item | Details |
|---|---|
| Request Information | Requesting user and files |
| Transfer Direction | Import or export |
| Approval Result | Approval status |
| Validation Result | File inspection and processing result |
| Transfer Status | Current progress and completion result |
| Execution History | Processing time and complete history for each operation |
Request
↓
Approval
↓
Validation
↓
Transfer
↓
CompletedThis lets you trace, at the execution level, which procedures a specific file went through and which environment it was processed into.

Security Operations#
Manage file flows based on access scope and transfer policies
During operations, manage access scope by user and device, transferable files and paths, and approval and validation criteria together.
When the business environment or operating standards change, adjust the relevant policies and configure the updated criteria to apply to subsequent import and export operations.
| Management Area | Operating Standard |
|---|---|
| User | Scope for file requests and operation execution |
| Device | Transfer environments that can be connected |
| File | Processing targets and file types |
| Path | Import/export destination locations |
| Approval | Processing procedure by business operation |
| Validation | File inspection and result criteria |

Operational Response#
Review processing status and rerun required operations
For operations that require review, check the processing details based on the import/export direction, approval status, validation result, connection status, and destination path.
Review Execution History
↓
Review Processing Stage
↓
Check Approval, Validation, and Connection Status
↓
Adjust Environment or Policy
↓
Rerun Operation
↓
Confirm Final ResultAfter rerunning the operation, use the new execution record to confirm that the files were successfully delivered to the designated environment.

This configuration connects file request → approval → validation → import/export → execution history management in a network-segmented environment as a single workflow. You can manage the processing criteria and results for each step together while building an operational file transfer flow between internal and external environments.
Developer#
Connect import/export requests to approval, inspection, and transfer steps while retaining a complete history
Integration Setup#
Separate clients by network and prepare status values
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);
}Transfer status is determined using the values below. There are five terminal states, and the successful state is Complete (2).
| Status | Meaning | Terminal |
|---|---|---|
| 2 | Complete | Yes |
| 4 | Error | Yes |
| 5 | Cancelled | Yes |
| 9 | Partially Complete | Yes |
| 99 | Failed | Yes |
| 1 · 6 · 12 · 13 | Started · Transferring · Synchronizing · Receiving | No |
If internal and external networks use separate workspaces, specify the workspace identifier for each request. Keep the clients separate so code running on one side cannot access resources on the other.
def client_for(workspace_id):
def call(method, path, body=None, params=None):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {TOKEN}",
"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 response.status_code == 403:
raise PermissionError(f"{workspace_id}: {path}")
if not response.ok:
raise RuntimeError(payload.get("message"))
return payload.get("data")
return call
external = client_for(EXTERNAL_WORKSPACE) # external network
internal = client_for(INTERNAL_WORKSPACE) # internal networkInnorixClient clientFor(String workspaceId) {
// A separate client per workspace keeps the two networks isolated.
return new InnorixClient(session, workspaceId, false);
}
InnorixClient external = clientFor(EXTERNAL_WORKSPACE); // external network
InnorixClient internal = clientFor(INTERNAL_WORKSPACE); // internal networkfunction clientFor(workspaceId) {
// A separate client per workspace keeps the two networks isolated.
return new Client(session, workspaceId);
}
const external = clientFor(EXTERNAL_WORKSPACE); // external network
const internal = clientFor(INTERNAL_WORKSPACE); // internal networkClient ClientFor(string workspaceId)
{
// A separate client per workspace keeps the two networks isolated.
return new Client(session, workspaceId);
}
Client external = ClientFor(ExternalWorkspace); // external network
Client internal = ClientFor(InternalWorkspace); // internal networkSpecifying a workspace without access permission returns 403. Handling this as a separate exception lets you distinguish policy violations from general errors in your records.
Create a Request#
Turn transfer requests into business records and validate their paths
Import and export follow the same procedure; only the direction differs. Treating requests through a common structure allows approval and validation logic to be shared.
import uuid
def create_request(user_id, direction, files, purpose):
request_id = str(uuid.uuid4())
db.insert("transfer_requests", {
"id": request_id,
"userId": user_id,
"direction": direction, # "import" or "export"
"files": files,
"purpose": purpose,
"state": "pending",
})
return request_idString createRequest(String userId, String direction, List<String> files, String purpose) {
String requestId = UUID.randomUUID().toString();
db.insert("transfer_requests", Json.newObj(
"id", requestId,
"userId", userId,
"direction", direction, // "import" or "export"
"files", files,
"purpose", purpose,
"state", "pending"));
return requestId;
}import { randomUUID } from "node:crypto";
async function createRequest(userId, direction, files, purpose) {
const requestId = randomUUID();
await db.insert("transfer_requests", {
id: requestId,
userId,
direction, // "import" or "export"
files,
purpose,
state: "pending",
});
return requestId;
}async Task<string> CreateRequestAsync(string userId, string direction,
IEnumerable<string> files, string purpose)
{
string requestId = Guid.NewGuid().ToString();
await db.InsertAsync("transfer_requests", new
{
id = requestId,
userId,
direction, // "import" or "export"
files,
purpose,
state = "pending",
});
return requestId;
}Checking that the files actually exist when the request is created prevents failures during execution after approval.
def validate_request(call, source_id, target_id, files, target_path):
# sourceItems reads filePath, not path
result = call("POST", "/api/transfers/validate-path", {
"sourceId": source_id,
"targetId": target_id,
"sourceItems": [{"filePath": p} for p in files],
"targetPath": target_path,
}) or {}
if result.get("invalidSourcePaths"):
raise ValueError(f"missing source paths: {result['invalidSourcePaths']}")
if result.get("validTargetPath") is False:
raise ValueError(f"target path not found: {target_path}")
return resultMap<String, Object> validateRequest(InnorixClient call, String sourceId, String targetId,
List<String> files, String targetPath) {
List<Object> items = new ArrayList<>();
// sourceItems reads filePath, not path
for (String p : files) items.add(Json.newObj("filePath", p));
Map<String, Object> result = call.apiObj("POST", "/api/transfers/validate-path", Json.newObj(
"sourceId", sourceId,
"targetId", targetId,
"sourceItems", items,
"targetPath", targetPath));
if (!Json.arrOf(result, "invalidSourcePaths").isEmpty()) {
throw new IllegalArgumentException("missing source paths: "
+ Json.arrOf(result, "invalidSourcePaths"));
}
if (Boolean.FALSE.equals(Json.boolOrNull(result, "validTargetPath"))) {
throw new IllegalArgumentException("target path not found: " + targetPath);
}
return result;
}async function validateRequest(call, sourceId, targetId, files, targetPath) {
const result = (await call("POST", "/api/transfers/validate-path", {
sourceId,
targetId,
// sourceItems reads filePath, not path
sourceItems: files.map((p) => ({ filePath: p })),
targetPath,
})) || {};
if (result.invalidSourcePaths?.length) {
throw new Error(`missing source paths: ${result.invalidSourcePaths}`);
}
if (result.validTargetPath === false) {
throw new Error(`target path not found: ${targetPath}`);
}
return result;
}async Task<JsonObject> ValidateRequestAsync(Client call, string sourceId,
string targetId, IEnumerable<string> files, string targetPath)
{
var items = new JsonArray();
// sourceItems reads filePath, not path
foreach (string p in files) items.Add(new JsonObject { ["filePath"] = p });
JsonObject result = await call.ApiObjAsync("POST", "/api/transfers/validate-path",
new JsonObject
{
["sourceId"] = sourceId,
["targetId"] = targetId,
["sourceItems"] = items,
["targetPath"] = targetPath,
});
if (J.ArrOf(result, "invalidSourcePaths").Count > 0)
{
throw new ArgumentException(
quot;missing source paths: {J.ArrOf(result, "invalidSourcePaths")}");
}
if (J.BoolOrNull(result, "validTargetPath") == false)
{
throw new ArgumentException(quot;target path not found: {targetPath}");
}
return result;
}| Record Item | Details |
|---|---|
userId |
Requesting user |
direction |
Import or export |
files |
Files and paths to transfer |
purpose |
Business purpose |
state |
Current processing stage |
Approval Integration#
Use the approval system result as a condition for transfer execution
Once the approval result is received, proceed to the next step.
def on_approval(request_id, approved, approver):
request = db.get("transfer_requests", request_id)
db.update("transfer_requests", request_id, {
"state": "approved" if approved else "rejected",
"approver": approver,
})
if not approved:
return None
return send_for_scan(request)
def ensure_approved(request):
if request["state"] not in ("approved", "validated"):
raise PermissionError(f"request not approved: {request['id']}")String onApproval(String requestId, boolean approved, String approver) {
Map<String, Object> request = db.get("transfer_requests", requestId);
db.update("transfer_requests", requestId, Json.newObj(
"state", approved ? "approved" : "rejected",
"approver", approver));
if (!approved) return null;
return sendForScan(request);
}
void ensureApproved(Map<String, Object> request) {
String state = Json.str(request, "state");
if (!"approved".equals(state) && !"validated".equals(state)) {
throw new SecurityException("request not approved: " + Json.str(request, "id"));
}
}async function onApproval(requestId, approved, approver) {
const request = await db.get("transfer_requests", requestId);
await db.update("transfer_requests", requestId, {
state: approved ? "approved" : "rejected",
approver,
});
if (!approved) return null;
return sendForScan(request);
}
function ensureApproved(request) {
if (!["approved", "validated"].includes(request.state)) {
throw new Error(`request not approved: ${request.id}`);
}
}async Task<string> OnApprovalAsync(string requestId, bool approved, string approver)
{
var request = await db.GetAsync("transfer_requests", requestId);
await db.UpdateAsync("transfer_requests", requestId, new
{
state = approved ? "approved" : "rejected",
approver,
});
if (!approved) return null;
return await SendForScanAsync(request);
}
void EnsureApproved(JsonObject request)
{
string state = J.Str(request, "state");
if (state != "approved" && state != "validated")
{
throw new UnauthorizedAccessException(quot;request not approved: {J.Str(request, "id")}");
}
}To prevent unapproved requests from being executed, the function that creates the transfer must check the request status first.
Inspection Integration#
Send files to the inspection folder first and wait for the result
File inspection is performed by a separate system. First send the files to the inspection folder, then use the result to determine the next step.
def send_for_scan(request, scan_device, scan_path):
ensure_approved(request)
transfer = external("POST", "/api/transfers/manual", {
"sourceDevice": request["sourceDevice"],
"targetDevice": scan_device,
"targetPath": f"{scan_path}/{request['id']}",
"sourceItem": [{"path": p, "isDir": False} for p in request["files"]],
"sendAllFolder": False,
"checkIntegrity": True,
"transferOptions": {"target-action": "numbering"},
})
db.update("transfer_requests", request["id"], {
"scanMonitorId": transfer["monitorId"],
"state": "scanning",
})
return transfer["monitorId"]String sendForScan(Map<String, Object> request, String scanDevice, String scanPath) {
ensureApproved(request);
List<Object> items = new ArrayList<>();
for (Object p : Json.arrOf(request, "files")) {
items.add(Json.newObj("path", p, "isDir", false));
}
Map<String, Object> transfer = external.apiObj("POST", "/api/transfers/manual", Json.newObj(
"sourceDevice", Json.str(request, "sourceDevice"),
"targetDevice", scanDevice,
"targetPath", scanPath + "/" + Json.str(request, "id"),
"sourceItem", items,
"sendAllFolder", false,
"checkIntegrity", true,
"transferOptions", Json.newObj("target-action", "numbering")));
db.update("transfer_requests", Json.str(request, "id"), Json.newObj(
"scanMonitorId", Json.str(transfer, "monitorId"),
"state", "scanning"));
return Json.str(transfer, "monitorId");
}async function sendForScan(request, scanDevice, scanPath) {
ensureApproved(request);
const transfer = await external("POST", "/api/transfers/manual", {
sourceDevice: request.sourceDevice,
targetDevice: scanDevice,
targetPath: `${scanPath}/${request.id}`,
sourceItem: request.files.map((p) => ({ path: p, isDir: false })),
sendAllFolder: false,
checkIntegrity: true,
transferOptions: { "target-action": "numbering" },
});
await db.update("transfer_requests", request.id, {
scanMonitorId: transfer.monitorId,
state: "scanning",
});
return transfer.monitorId;
}async Task<string> SendForScanAsync(JsonObject request, string scanDevice, string scanPath)
{
EnsureApproved(request);
var items = new JsonArray();
foreach (JsonNode p in J.ArrOf(request, "files"))
{
items.Add(new JsonObject { ["path"] = p?.DeepClone(), ["isDir"] = false });
}
JsonObject transfer = await external.ApiObjAsync("POST", "/api/transfers/manual",
new JsonObject
{
["sourceDevice"] = J.Str(request, "sourceDevice"),
["targetDevice"] = scanDevice,
["targetPath"] = quot;{scanPath}/{J.Str(request, "id")}",
["sourceItem"] = items,
["sendAllFolder"] = false,
["checkIntegrity"] = true,
["transferOptions"] = new JsonObject { ["target-action"] = "numbering" },
});
await db.UpdateAsync("transfer_requests", J.Str(request, "id"), new
{
scanMonitorId = J.Str(transfer, "monitorId"),
state = "scanning",
});
return J.Str(transfer, "monitorId");
}The agent marks the transfer complete only after the file size stops changing and writing has finished. This prevents the inspection system from opening a file that is still being written and producing an incorrect result.
The integrity of the transfer itself is checked through the validation API.
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);
}Execute Import and Export#
Send only files that pass inspection to the target network
def execute(request, scan_result):
if not scan_result.get("passed"):
db.update("transfer_requests", request["id"], {"state": "blocked"})
return None
verification = verify(request["scanMonitorId"])
if not verification.get("checksumMatched"):
db.update("transfer_requests", request["id"], {"state": "corrupted"})
return None
transfer = internal("POST", "/api/transfers/manual", {
"sourceDevice": request["scanDevice"],
"targetDevice": request["targetDevice"],
"targetPath": request["targetPath"],
"sourcePaths": [f"{request['scanPath']}/{request['id']}"],
"sendAllFolder": True,
"checkIntegrity": True,
"transferOptions": {"target-action": "numbering"},
})
db.update("transfer_requests", request["id"], {
"monitorId": transfer["monitorId"],
"state": "transferring",
})
return transfer["monitorId"]String execute(Map<String, Object> request, Map<String, Object> scanResult) {
if (!Json.bool(scanResult, "passed", false)) {
db.update("transfer_requests", Json.str(request, "id"), Json.newObj("state", "blocked"));
return null;
}
Map<String, Object> verification = internal.verify(Json.str(request, "scanMonitorId"), 1800, 10);
if (!Json.bool(verification, "checksumMatched", false)) {
db.update("transfer_requests", Json.str(request, "id"), Json.newObj("state", "corrupted"));
return null;
}
Map<String, Object> transfer = internal.apiObj("POST", "/api/transfers/manual", Json.newObj(
"sourceDevice", Json.str(request, "scanDevice"),
"targetDevice", Json.str(request, "targetDevice"),
"targetPath", Json.str(request, "targetPath"),
"sourcePaths", List.of(Json.str(request, "scanPath") + "/" + Json.str(request, "id")),
"sendAllFolder", true,
"checkIntegrity", true,
"transferOptions", Json.newObj("target-action", "numbering")));
db.update("transfer_requests", Json.str(request, "id"), Json.newObj(
"monitorId", Json.str(transfer, "monitorId"),
"state", "transferring"));
return Json.str(transfer, "monitorId");
}async function execute(request, scanResult) {
if (!scanResult.passed) {
await db.update("transfer_requests", request.id, { state: "blocked" });
return null;
}
const verification = await verify(request.scanMonitorId);
if (!verification.checksumMatched) {
await db.update("transfer_requests", request.id, { state: "corrupted" });
return null;
}
const transfer = await internal("POST", "/api/transfers/manual", {
sourceDevice: request.scanDevice,
targetDevice: request.targetDevice,
targetPath: request.targetPath,
sourcePaths: [`${request.scanPath}/${request.id}`],
sendAllFolder: true,
checkIntegrity: true,
transferOptions: { "target-action": "numbering" },
});
await db.update("transfer_requests", request.id, {
monitorId: transfer.monitorId,
state: "transferring",
});
return transfer.monitorId;
}async Task<string> ExecuteAsync(JsonObject request, JsonObject scanResult)
{
if (!J.Bool(scanResult, "passed", false))
{
await db.UpdateAsync("transfer_requests", J.Str(request, "id"), new { state = "blocked" });
return null;
}
JsonObject verification = await internal.VerifyAsync(J.Str(request, "scanMonitorId"));
if (!J.Bool(verification, "checksumMatched", false))
{
await db.UpdateAsync("transfer_requests", J.Str(request, "id"), new { state = "corrupted" });
return null;
}
JsonObject transfer = await internal.ApiObjAsync("POST", "/api/transfers/manual",
new JsonObject
{
["sourceDevice"] = J.Str(request, "scanDevice"),
["targetDevice"] = J.Str(request, "targetDevice"),
["targetPath"] = J.Str(request, "targetPath"),
["sourcePaths"] = new JsonArray { quot;{J.Str(request, "scanPath")}/{J.Str(request, "id")}" },
["sendAllFolder"] = true,
["checkIntegrity"] = true,
["transferOptions"] = new JsonObject { ["target-action"] = "numbering" },
});
await db.UpdateAsync("transfer_requests", J.Str(request, "id"), new
{
monitorId = J.Str(transfer, "monitorId"),
state = "transferring",
});
return J.Str(transfer, "monitorId");
}Recording the state at each step makes it possible to determine later where the process stopped.
| State | Meaning |
|---|---|
pending |
Awaiting approval |
approved |
Approved |
scanning |
Inspection in progress |
blocked |
Inspection failed |
corrupted |
Integrity mismatch |
transferring |
Transferring to the target network |
done · failed |
Final result |
Confirm the Result#
Reflect the transfer result in the request status
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;
}def finalize(request_id):
request = db.get("transfer_requests", request_id)
detail = internal("GET", f"/api/transfers/{request['monitorId']}")
if not is_terminal(detail):
return None
succeeded = detail["status"] == STATUS_COMPLETE
db.update("transfer_requests", request_id, {
"state": "done" if succeeded else "failed",
"finalStatus": detail["status"],
})
return succeededBoolean finalize(String requestId) {
Map<String, Object> request = db.get("transfer_requests", requestId);
Map<String, Object> detail = internal.apiObj("GET",
"/api/transfers/" + Json.str(request, "monitorId"));
if (!InnorixClient.isTerminal(detail)) return null;
boolean succeeded = Json.intOr(detail, "status", -1) == InnorixClient.STATUS_COMPLETE;
db.update("transfer_requests", requestId, Json.newObj(
"state", succeeded ? "done" : "failed",
"finalStatus", Json.intOrNull(detail, "status")));
return succeeded;
}async function finalize(requestId) {
const request = await db.get("transfer_requests", requestId);
const detail = await internal("GET", `/api/transfers/${request.monitorId}`);
if (!isTerminal(detail)) return null;
const succeeded = detail.status === STATUS_COMPLETE;
await db.update("transfer_requests", requestId, {
state: succeeded ? "done" : "failed",
finalStatus: detail.status,
});
return succeeded;
}async Task<bool?> FinalizeAsync(string requestId)
{
var request = await db.GetAsync("transfer_requests", requestId);
JsonObject detail = await internal.ApiObjAsync("GET",
quot;/api/transfers/{J.Str(request, "monitorId")}");
if (!InnorixClient.IsTerminal(detail)) return null;
bool succeeded = J.Int(detail, "status", -1) == InnorixClient.StatusComplete;
await db.UpdateAsync("transfer_requests", requestId, new
{
state = succeeded ? "done" : "failed",
finalStatus = J.IntOrNull(detail, "status"),
});
return succeeded;
}Partially Complete (9) and Cancelled (5) are also terminal states. If every terminal state is treated as a success, requests that transferred only part of their files will be recorded as completed.
Retrieve History#
Review import/export records for audit purposes
Transfer history provides evidence of which files were processed and in which direction.
from datetime import datetime, timedelta, timezone
def paginate(call, 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 = call("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"
for row in paginate(internal, "/api/transfer-history", params={
"startDate": (end - timedelta(days=30)).strftime(fmt),
"endDate": end.strftime(fmt),
}):
print(row.get("startDate"), row.get("statusName"),
row.get("sourceDeviceName"), "->", row.get("targetDeviceName"))Instant end = Instant.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
.withZone(ZoneOffset.UTC);
List<Map<String, Object>> rows = internal.paginate("/api/transfer-history",
Json.newObj("startDate", fmt.format(end.minus(30, ChronoUnit.DAYS)),
"endDate", fmt.format(end)), 200, 50);
for (Map<String, Object> row : rows) {
System.out.println(Json.str(row, "startDate") + " "
+ Json.str(row, "statusName") + " "
+ Json.str(row, "sourceDeviceName") + " -> "
+ Json.str(row, "targetDeviceName"));
}const end = new Date();
const fmt = (d) => d.toISOString().replace(/\.\d{3}Z$/, "Z");
for await (const row of paginate(internal, "/api/transfer-history", {
startDate: fmt(new Date(end.getTime() - 30 * 86400000)),
endDate: fmt(end),
})) {
console.log(row.startDate, row.statusName,
row.sourceDeviceName, "->", row.targetDeviceName);
}DateTime end = DateTime.UtcNow;
const string Fmt = "yyyy-MM-dd'T'HH:mm:ss'Z'";
List<JsonObject> rows = await internal.PaginateAsync("/api/transfer-history",
new Dictionary<string, object>
{
["startDate"] = end.AddDays(-30).ToString(Fmt),
["endDate"] = end.ToString(Fmt),
});
foreach (JsonObject row in rows)
{
Console.WriteLine(quot;{J.Str(row, "startDate")} {J.Str(row, "statusName")} "
+ quot;{J.Str(row, "sourceDeviceName")} -> {J.Str(row, "targetDeviceName")}");
}If you need a file for an audit submission, use CSV export.
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",
};Recording the transfer monitorId together with the business request identifier lets you trace in both directions: from execution history to the request and from the request to the execution history.
Exception Handling#
Review interrupted requests step by step and reprocess them
A failed inspection and a failed transfer require different responses. The former indicates a problem with the file itself and cannot be fixed by retransmission; the latter can be recovered through retransmission.
def review(request_id):
request = db.get("transfer_requests", request_id)
state = request["state"]
if state == "rejected":
return "rejected - notify the requester with the reason"
if state == "blocked":
return "scan blocked - check the files and request again"
if state == "corrupted":
return "integrity mismatch - check the source"
if state == "transferring":
return f"retried {retry_failed(request['monitorId'])} failed files"
return f"current state: {state}"String review(String requestId) {
Map<String, Object> request = db.get("transfer_requests", requestId);
String state = Json.str(request, "state");
return switch (state) {
case "rejected" -> "rejected - notify the requester with the reason";
case "blocked" -> "scan blocked - check the files and request again";
case "corrupted" -> "integrity mismatch - check the source";
case "transferring" -> "retried "
+ internal.retryFailed(Json.str(request, "monitorId")) + " failed files";
default -> "current state: " + state;
};
}async function review(requestId) {
const request = await db.get("transfer_requests", requestId);
const { state } = request;
if (state === "rejected") return "rejected - notify the requester with the reason";
if (state === "blocked") return "scan blocked - check the files and request again";
if (state === "corrupted") return "integrity mismatch - check the source";
if (state === "transferring") {
return `retried ${await retryFailed(request.monitorId)} failed files`;
}
return `current state: ${state}`;
}async Task<string> ReviewAsync(string requestId)
{
var request = await db.GetAsync("transfer_requests", requestId);
string state = J.Str(request, "state");
return state switch
{
"rejected" => "rejected - notify the requester with the reason",
"blocked" => "scan blocked - check the files and request again",
"corrupted" => "integrity mismatch - check the source",
"transferring" => quot;retried {await internal.RetryFailedAsync(J.Str(request, "monitorId"))} failed files",
_ => quot;current state: {state}",
};
}| Category | Symptom | Response |
|---|---|---|
| Approval Rejected | rejected |
Notify the requester of the reason |
| Inspection Failed | blocked |
Check the files and submit a new request |
| Integrity Mismatch | corrupted |
Check the source state |
| Transfer Failed | failed |
Retransmit the failed files |
| Review Item | Details |
|---|---|
| Request | Approval status and target files |
| Inspection | Inspection-folder transfer and result |
| Integrity | File count and checksum match status |
| Execution | Transfer status to the target network |
| History | Transfer records for audit purposes |