binalyze-air-sdk 1.0.2__py3-none-any.whl → 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- binalyze_air/__init__.py +77 -77
- binalyze_air/apis/__init__.py +67 -27
- binalyze_air/apis/acquisitions.py +107 -0
- binalyze_air/apis/api_tokens.py +49 -0
- binalyze_air/apis/assets.py +161 -0
- binalyze_air/apis/audit_logs.py +26 -0
- binalyze_air/apis/{authentication.py → auth.py} +29 -27
- binalyze_air/apis/auto_asset_tags.py +79 -75
- binalyze_air/apis/backup.py +177 -0
- binalyze_air/apis/baseline.py +46 -0
- binalyze_air/apis/cases.py +225 -0
- binalyze_air/apis/cloud_forensics.py +116 -0
- binalyze_air/apis/event_subscription.py +96 -96
- binalyze_air/apis/evidence.py +249 -53
- binalyze_air/apis/interact.py +153 -36
- binalyze_air/apis/investigation_hub.py +234 -0
- binalyze_air/apis/license.py +104 -0
- binalyze_air/apis/logger.py +83 -0
- binalyze_air/apis/multipart_upload.py +201 -0
- binalyze_air/apis/notifications.py +115 -0
- binalyze_air/apis/organizations.py +267 -0
- binalyze_air/apis/params.py +44 -39
- binalyze_air/apis/policies.py +186 -0
- binalyze_air/apis/preset_filters.py +79 -0
- binalyze_air/apis/recent_activities.py +71 -0
- binalyze_air/apis/relay_server.py +104 -0
- binalyze_air/apis/settings.py +395 -27
- binalyze_air/apis/tasks.py +80 -0
- binalyze_air/apis/triage.py +197 -0
- binalyze_air/apis/user_management.py +183 -74
- binalyze_air/apis/webhook_executions.py +50 -0
- binalyze_air/apis/webhooks.py +322 -230
- binalyze_air/base.py +207 -133
- binalyze_air/client.py +217 -1337
- binalyze_air/commands/__init__.py +175 -145
- binalyze_air/commands/acquisitions.py +661 -387
- binalyze_air/commands/api_tokens.py +55 -0
- binalyze_air/commands/assets.py +324 -362
- binalyze_air/commands/{authentication.py → auth.py} +36 -36
- binalyze_air/commands/auto_asset_tags.py +230 -230
- binalyze_air/commands/backup.py +47 -0
- binalyze_air/commands/baseline.py +32 -396
- binalyze_air/commands/cases.py +609 -602
- binalyze_air/commands/cloud_forensics.py +88 -0
- binalyze_air/commands/event_subscription.py +101 -101
- binalyze_air/commands/evidences.py +918 -988
- binalyze_air/commands/interact.py +172 -58
- binalyze_air/commands/investigation_hub.py +315 -0
- binalyze_air/commands/license.py +183 -0
- binalyze_air/commands/logger.py +126 -0
- binalyze_air/commands/multipart_upload.py +363 -0
- binalyze_air/commands/notifications.py +45 -0
- binalyze_air/commands/organizations.py +200 -221
- binalyze_air/commands/policies.py +175 -203
- binalyze_air/commands/preset_filters.py +55 -0
- binalyze_air/commands/recent_activities.py +32 -0
- binalyze_air/commands/relay_server.py +144 -0
- binalyze_air/commands/settings.py +431 -29
- binalyze_air/commands/tasks.py +95 -56
- binalyze_air/commands/triage.py +224 -360
- binalyze_air/commands/user_management.py +351 -126
- binalyze_air/commands/webhook_executions.py +77 -0
- binalyze_air/config.py +244 -244
- binalyze_air/exceptions.py +49 -49
- binalyze_air/http_client.py +426 -305
- binalyze_air/models/__init__.py +287 -285
- binalyze_air/models/acquisitions.py +365 -250
- binalyze_air/models/api_tokens.py +73 -0
- binalyze_air/models/assets.py +438 -438
- binalyze_air/models/audit.py +247 -272
- binalyze_air/models/audit_logs.py +14 -0
- binalyze_air/models/{authentication.py → auth.py} +69 -69
- binalyze_air/models/auto_asset_tags.py +227 -116
- binalyze_air/models/backup.py +138 -0
- binalyze_air/models/baseline.py +231 -231
- binalyze_air/models/cases.py +275 -275
- binalyze_air/models/cloud_forensics.py +145 -0
- binalyze_air/models/event_subscription.py +170 -171
- binalyze_air/models/evidence.py +65 -65
- binalyze_air/models/evidences.py +367 -348
- binalyze_air/models/interact.py +266 -135
- binalyze_air/models/investigation_hub.py +265 -0
- binalyze_air/models/license.py +150 -0
- binalyze_air/models/logger.py +83 -0
- binalyze_air/models/multipart_upload.py +352 -0
- binalyze_air/models/notifications.py +138 -0
- binalyze_air/models/organizations.py +293 -293
- binalyze_air/models/params.py +153 -127
- binalyze_air/models/policies.py +260 -249
- binalyze_air/models/preset_filters.py +79 -0
- binalyze_air/models/recent_activities.py +70 -0
- binalyze_air/models/relay_server.py +121 -0
- binalyze_air/models/settings.py +538 -84
- binalyze_air/models/tasks.py +215 -149
- binalyze_air/models/triage.py +141 -142
- binalyze_air/models/user_management.py +200 -97
- binalyze_air/models/webhook_executions.py +33 -0
- binalyze_air/queries/__init__.py +121 -133
- binalyze_air/queries/acquisitions.py +155 -155
- binalyze_air/queries/api_tokens.py +46 -0
- binalyze_air/queries/assets.py +186 -105
- binalyze_air/queries/audit.py +400 -416
- binalyze_air/queries/{authentication.py → auth.py} +55 -55
- binalyze_air/queries/auto_asset_tags.py +59 -59
- binalyze_air/queries/backup.py +66 -0
- binalyze_air/queries/baseline.py +21 -185
- binalyze_air/queries/cases.py +292 -292
- binalyze_air/queries/cloud_forensics.py +137 -0
- binalyze_air/queries/event_subscription.py +54 -54
- binalyze_air/queries/evidence.py +139 -139
- binalyze_air/queries/evidences.py +279 -279
- binalyze_air/queries/interact.py +140 -28
- binalyze_air/queries/investigation_hub.py +329 -0
- binalyze_air/queries/license.py +85 -0
- binalyze_air/queries/logger.py +58 -0
- binalyze_air/queries/multipart_upload.py +180 -0
- binalyze_air/queries/notifications.py +71 -0
- binalyze_air/queries/organizations.py +222 -222
- binalyze_air/queries/params.py +154 -115
- binalyze_air/queries/policies.py +149 -149
- binalyze_air/queries/preset_filters.py +60 -0
- binalyze_air/queries/recent_activities.py +44 -0
- binalyze_air/queries/relay_server.py +42 -0
- binalyze_air/queries/settings.py +533 -20
- binalyze_air/queries/tasks.py +125 -81
- binalyze_air/queries/triage.py +230 -230
- binalyze_air/queries/user_management.py +193 -83
- binalyze_air/queries/webhook_executions.py +39 -0
- binalyze_air_sdk-1.0.3.dist-info/METADATA +752 -0
- binalyze_air_sdk-1.0.3.dist-info/RECORD +132 -0
- {binalyze_air_sdk-1.0.2.dist-info → binalyze_air_sdk-1.0.3.dist-info}/WHEEL +1 -1
- binalyze_air/apis/endpoints.py +0 -22
- binalyze_air/apis/evidences.py +0 -216
- binalyze_air/apis/users.py +0 -68
- binalyze_air/commands/users.py +0 -101
- binalyze_air/models/endpoints.py +0 -76
- binalyze_air/models/users.py +0 -82
- binalyze_air/queries/endpoints.py +0 -25
- binalyze_air/queries/users.py +0 -69
- binalyze_air_sdk-1.0.2.dist-info/METADATA +0 -706
- binalyze_air_sdk-1.0.2.dist-info/RECORD +0 -82
- {binalyze_air_sdk-1.0.2.dist-info → binalyze_air_sdk-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
"""License command classes for Binalyze AIR SDK."""
|
2
|
+
|
3
|
+
from typing import Optional, Dict, Any
|
4
|
+
from ..models.license import License
|
5
|
+
from ..queries.license import GetLicenseQuery, SetLicenseQuery
|
6
|
+
|
7
|
+
|
8
|
+
class LicenseCommand:
|
9
|
+
"""Base command class for license operations."""
|
10
|
+
|
11
|
+
def __init__(self, api_client):
|
12
|
+
"""Initialize license command.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
api_client: The License API client instance
|
16
|
+
"""
|
17
|
+
self._api_client = api_client
|
18
|
+
|
19
|
+
|
20
|
+
class GetLicenseCommand(LicenseCommand):
|
21
|
+
"""Command for retrieving current license information.
|
22
|
+
|
23
|
+
This command provides comprehensive license details including
|
24
|
+
expiration status, usage statistics, and validation.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def execute(self) -> License:
|
28
|
+
"""Execute get license command.
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
License object with current license information
|
32
|
+
|
33
|
+
Raises:
|
34
|
+
APIError: If the request fails
|
35
|
+
"""
|
36
|
+
query = GetLicenseQuery()
|
37
|
+
response = self._api_client._get_license_query(query)
|
38
|
+
|
39
|
+
if response.get('success') and response.get('result'):
|
40
|
+
return License.from_dict(response['result'])
|
41
|
+
else:
|
42
|
+
raise ValueError(
|
43
|
+
f"Failed to get license: {response.get('errors', [])}"
|
44
|
+
)
|
45
|
+
|
46
|
+
def get_license_status(self) -> Dict[str, Any]:
|
47
|
+
"""Get comprehensive license status information.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
Dictionary with license status, usage, and warnings
|
51
|
+
"""
|
52
|
+
license_info = self.execute()
|
53
|
+
usage_stats = license_info.get_usage_percentage()
|
54
|
+
|
55
|
+
status = {
|
56
|
+
'license_key': license_info.key,
|
57
|
+
'customer_name': license_info.customer_name,
|
58
|
+
'is_active': not license_info.is_expired(),
|
59
|
+
'is_lifetime': license_info.is_lifetime,
|
60
|
+
'expires_on': license_info.expires_on.isoformat(),
|
61
|
+
'remaining_days': license_info.remaining_days,
|
62
|
+
'device_usage': {
|
63
|
+
'current': license_info.device_count,
|
64
|
+
'maximum': license_info.max_device_count,
|
65
|
+
'percentage': usage_stats['device_usage_percent']
|
66
|
+
},
|
67
|
+
'client_usage': {
|
68
|
+
'current': license_info.client_count,
|
69
|
+
'maximum': license_info.max_client_count,
|
70
|
+
'percentage': usage_stats['client_usage_percent']
|
71
|
+
},
|
72
|
+
'warnings': []
|
73
|
+
}
|
74
|
+
|
75
|
+
# Add warnings for various conditions
|
76
|
+
if license_info.is_expired():
|
77
|
+
status['warnings'].append('License has expired')
|
78
|
+
elif license_info.is_near_expiry(30):
|
79
|
+
status['warnings'].append(
|
80
|
+
f'License expires in {license_info.remaining_days} days'
|
81
|
+
)
|
82
|
+
|
83
|
+
if usage_stats['device_usage_percent'] > 80:
|
84
|
+
status['warnings'].append(
|
85
|
+
f'Device usage at {usage_stats["device_usage_percent"]}%'
|
86
|
+
)
|
87
|
+
|
88
|
+
if usage_stats['client_usage_percent'] > 80:
|
89
|
+
status['warnings'].append(
|
90
|
+
f'Client usage at {usage_stats["client_usage_percent"]}%'
|
91
|
+
)
|
92
|
+
|
93
|
+
if license_info.is_locked_down:
|
94
|
+
status['warnings'].append('License is in locked down mode')
|
95
|
+
|
96
|
+
return status
|
97
|
+
|
98
|
+
|
99
|
+
class SetLicenseCommand(LicenseCommand):
|
100
|
+
"""Command for setting/updating license key.
|
101
|
+
|
102
|
+
This command allows updating the system license key
|
103
|
+
and validates the operation.
|
104
|
+
"""
|
105
|
+
|
106
|
+
def __init__(self, api_client, license_key: str):
|
107
|
+
"""Initialize set license command.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
api_client: The License API client instance
|
111
|
+
license_key: The new license key to set
|
112
|
+
"""
|
113
|
+
super().__init__(api_client)
|
114
|
+
self._license_key = license_key
|
115
|
+
|
116
|
+
def execute(self) -> bool:
|
117
|
+
"""Execute set license command.
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
True if license was set successfully
|
121
|
+
|
122
|
+
Raises:
|
123
|
+
APIError: If the request fails
|
124
|
+
ValueError: If license key is invalid
|
125
|
+
"""
|
126
|
+
if not self._license_key or not self._license_key.strip():
|
127
|
+
raise ValueError("License key cannot be empty")
|
128
|
+
|
129
|
+
query = SetLicenseQuery(self._license_key)
|
130
|
+
response = self._api_client._set_license_query(query)
|
131
|
+
|
132
|
+
if response.get('success'):
|
133
|
+
return True
|
134
|
+
else:
|
135
|
+
raise ValueError(
|
136
|
+
f"Failed to set license: {response.get('errors', [])}"
|
137
|
+
)
|
138
|
+
|
139
|
+
def set_and_verify(self) -> Dict[str, Any]:
|
140
|
+
"""Set license key and verify the operation.
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Dictionary with operation result and new license status
|
144
|
+
"""
|
145
|
+
# Set the license
|
146
|
+
success = self.execute()
|
147
|
+
|
148
|
+
if success:
|
149
|
+
# Verify by getting current license info
|
150
|
+
get_command = GetLicenseCommand(self._api_client)
|
151
|
+
try:
|
152
|
+
new_license = get_command.execute()
|
153
|
+
return {
|
154
|
+
'success': True,
|
155
|
+
'message': 'License updated successfully',
|
156
|
+
'license_info': {
|
157
|
+
'key': new_license.key,
|
158
|
+
'customer_name': new_license.customer_name,
|
159
|
+
'expires_on': new_license.expires_on.isoformat(),
|
160
|
+
'remaining_days': new_license.remaining_days,
|
161
|
+
'is_active': not new_license.is_expired()
|
162
|
+
}
|
163
|
+
}
|
164
|
+
except Exception as e:
|
165
|
+
return {
|
166
|
+
'success': True,
|
167
|
+
'message': 'License set but verification failed',
|
168
|
+
'warning': str(e)
|
169
|
+
}
|
170
|
+
|
171
|
+
return {
|
172
|
+
'success': False,
|
173
|
+
'message': 'Failed to set license'
|
174
|
+
}
|
175
|
+
|
176
|
+
@property
|
177
|
+
def license_key(self) -> str:
|
178
|
+
"""Get the license key to be set.
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
The license key
|
182
|
+
"""
|
183
|
+
return self._license_key
|
@@ -0,0 +1,126 @@
|
|
1
|
+
"""Logger command classes for Binalyze AIR SDK."""
|
2
|
+
|
3
|
+
from typing import Optional, Dict, Any
|
4
|
+
from pathlib import Path
|
5
|
+
from ..models.logger import LogDownloadResponse
|
6
|
+
from ..queries.logger import DownloadLogsQuery
|
7
|
+
|
8
|
+
|
9
|
+
class LoggerCommand:
|
10
|
+
"""Base command class for logger operations."""
|
11
|
+
|
12
|
+
def __init__(self, api_client):
|
13
|
+
"""Initialize logger command.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
api_client: The Logger API client instance
|
17
|
+
"""
|
18
|
+
self._api_client = api_client
|
19
|
+
|
20
|
+
|
21
|
+
class DownloadLogsCommand(LoggerCommand):
|
22
|
+
"""Command for downloading application logs as ZIP file.
|
23
|
+
|
24
|
+
This command provides comprehensive log download functionality
|
25
|
+
with options for latest logs only and automatic file saving.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, api_client, latest_log_file: bool = False):
|
29
|
+
"""Initialize download logs command.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
api_client: The Logger API client instance
|
33
|
+
latest_log_file: Whether to download only the latest log file
|
34
|
+
"""
|
35
|
+
super().__init__(api_client)
|
36
|
+
self._latest_log_file = latest_log_file
|
37
|
+
|
38
|
+
def execute(self) -> LogDownloadResponse:
|
39
|
+
"""Execute download logs command.
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
LogDownloadResponse with downloaded log data
|
43
|
+
|
44
|
+
Raises:
|
45
|
+
APIError: If the request fails
|
46
|
+
"""
|
47
|
+
query = DownloadLogsQuery(self._latest_log_file)
|
48
|
+
response_data, headers = self._api_client._download_logs_query(query)
|
49
|
+
|
50
|
+
# Create response object from binary data and headers
|
51
|
+
return LogDownloadResponse.from_response(response_data, headers)
|
52
|
+
|
53
|
+
def download_and_save(self, save_path: Optional[str] = None) -> Dict[str, Any]:
|
54
|
+
"""Download logs and save to file.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
save_path: Optional path where to save the logs.
|
58
|
+
If not provided, saves to current directory.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Dictionary with download result and file information
|
62
|
+
"""
|
63
|
+
try:
|
64
|
+
# Download the logs
|
65
|
+
log_response = self.execute()
|
66
|
+
|
67
|
+
# Determine save path
|
68
|
+
if save_path is None:
|
69
|
+
save_path = log_response.filename or 'air_logs.zip'
|
70
|
+
|
71
|
+
# Ensure directory exists
|
72
|
+
Path(save_path).parent.mkdir(parents=True, exist_ok=True)
|
73
|
+
|
74
|
+
# Save to file
|
75
|
+
success = log_response.save_to_file(save_path)
|
76
|
+
|
77
|
+
return {
|
78
|
+
'success': success,
|
79
|
+
'file_path': save_path,
|
80
|
+
'file_size': log_response.size,
|
81
|
+
'content_type': log_response.content_type,
|
82
|
+
'filename': log_response.filename,
|
83
|
+
'latest_only': self._latest_log_file
|
84
|
+
}
|
85
|
+
|
86
|
+
except Exception as e:
|
87
|
+
return {
|
88
|
+
'success': False,
|
89
|
+
'error': str(e),
|
90
|
+
'file_path': save_path,
|
91
|
+
'latest_only': self._latest_log_file
|
92
|
+
}
|
93
|
+
|
94
|
+
def get_log_info(self) -> Dict[str, Any]:
|
95
|
+
"""Get information about available logs without downloading.
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Dictionary with log information
|
99
|
+
"""
|
100
|
+
try:
|
101
|
+
# Download logs to get metadata
|
102
|
+
log_response = self.execute()
|
103
|
+
|
104
|
+
return {
|
105
|
+
'available': True,
|
106
|
+
'size': log_response.size,
|
107
|
+
'content_type': log_response.content_type,
|
108
|
+
'filename': log_response.filename,
|
109
|
+
'latest_only': self._latest_log_file
|
110
|
+
}
|
111
|
+
|
112
|
+
except Exception as e:
|
113
|
+
return {
|
114
|
+
'available': False,
|
115
|
+
'error': str(e),
|
116
|
+
'latest_only': self._latest_log_file
|
117
|
+
}
|
118
|
+
|
119
|
+
@property
|
120
|
+
def latest_log_file(self) -> bool:
|
121
|
+
"""Get the latest log file flag.
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
Whether downloading only the latest log file
|
125
|
+
"""
|
126
|
+
return self._latest_log_file
|
@@ -0,0 +1,363 @@
|
|
1
|
+
"""Multipart Upload command classes for Binalyze AIR SDK."""
|
2
|
+
|
3
|
+
from typing import Optional, Dict, Any, List, Callable
|
4
|
+
from pathlib import Path
|
5
|
+
import time
|
6
|
+
from ..models.multipart_upload import (
|
7
|
+
UploadInitializeResponse, UploadPartResponse, UploadStatusResponse,
|
8
|
+
UploadFinalizeResponse, MultipartUploadSession, FileChunker
|
9
|
+
)
|
10
|
+
from ..queries.multipart_upload import (
|
11
|
+
InitializeUploadQuery, UploadPartQuery, CheckUploadStatusQuery, FinalizeUploadQuery
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
class MultipartUploadCommand:
|
16
|
+
"""Base command class for multipart upload operations."""
|
17
|
+
|
18
|
+
def __init__(self, api_client):
|
19
|
+
"""Initialize multipart upload command.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
api_client: The Multipart Upload API client instance
|
23
|
+
"""
|
24
|
+
self._api_client = api_client
|
25
|
+
|
26
|
+
|
27
|
+
class InitializeUploadCommand(MultipartUploadCommand):
|
28
|
+
"""Command for initializing a multipart upload session."""
|
29
|
+
|
30
|
+
def execute(self) -> UploadInitializeResponse:
|
31
|
+
"""Execute upload initialization command.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
UploadInitializeResponse with file ID for the session
|
35
|
+
|
36
|
+
Raises:
|
37
|
+
APIError: If the request fails
|
38
|
+
"""
|
39
|
+
query = InitializeUploadQuery()
|
40
|
+
response_data = self._api_client._initialize_upload_query(query)
|
41
|
+
|
42
|
+
return UploadInitializeResponse.from_dict(response_data)
|
43
|
+
|
44
|
+
|
45
|
+
class UploadPartCommand(MultipartUploadCommand):
|
46
|
+
"""Command for uploading a file part in a multipart upload."""
|
47
|
+
|
48
|
+
def __init__(self, api_client, file_id: str, part_number: int, file_data: bytes, filename: Optional[str] = None):
|
49
|
+
"""Initialize upload part command.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
api_client: The Multipart Upload API client instance
|
53
|
+
file_id: Upload session identifier
|
54
|
+
part_number: Sequential part number (starting from 1)
|
55
|
+
file_data: Binary file data for this part
|
56
|
+
filename: Optional filename for the part
|
57
|
+
"""
|
58
|
+
super().__init__(api_client)
|
59
|
+
self._file_id = file_id
|
60
|
+
self._part_number = part_number
|
61
|
+
self._file_data = file_data
|
62
|
+
self._filename = filename
|
63
|
+
|
64
|
+
def execute(self) -> UploadPartResponse:
|
65
|
+
"""Execute upload part command.
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
UploadPartResponse indicating success/failure
|
69
|
+
|
70
|
+
Raises:
|
71
|
+
APIError: If the request fails
|
72
|
+
"""
|
73
|
+
query = UploadPartQuery(self._file_id, self._part_number, self._file_data, self._filename)
|
74
|
+
response_data = self._api_client._upload_part_query(query)
|
75
|
+
|
76
|
+
return UploadPartResponse.from_dict(response_data, self._part_number)
|
77
|
+
|
78
|
+
|
79
|
+
class CheckUploadStatusCommand(MultipartUploadCommand):
|
80
|
+
"""Command for checking upload status."""
|
81
|
+
|
82
|
+
def __init__(self, api_client, file_id: str):
|
83
|
+
"""Initialize upload status check command.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
api_client: The Multipart Upload API client instance
|
87
|
+
file_id: Upload session identifier to check
|
88
|
+
"""
|
89
|
+
super().__init__(api_client)
|
90
|
+
self._file_id = file_id
|
91
|
+
|
92
|
+
def execute(self) -> UploadStatusResponse:
|
93
|
+
"""Execute upload status check command.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
UploadStatusResponse with ready status
|
97
|
+
|
98
|
+
Raises:
|
99
|
+
APIError: If the request fails
|
100
|
+
"""
|
101
|
+
query = CheckUploadStatusQuery(self._file_id)
|
102
|
+
response_data = self._api_client._check_upload_status_query(query)
|
103
|
+
|
104
|
+
return UploadStatusResponse.from_dict(response_data, self._file_id)
|
105
|
+
|
106
|
+
|
107
|
+
class FinalizeUploadCommand(MultipartUploadCommand):
|
108
|
+
"""Command for finalizing a multipart upload."""
|
109
|
+
|
110
|
+
def __init__(self, api_client, file_id: str):
|
111
|
+
"""Initialize upload finalization command.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
api_client: The Multipart Upload API client instance
|
115
|
+
file_id: Upload session identifier to finalize
|
116
|
+
"""
|
117
|
+
super().__init__(api_client)
|
118
|
+
self._file_id = file_id
|
119
|
+
|
120
|
+
def execute(self) -> UploadFinalizeResponse:
|
121
|
+
"""Execute upload finalization command.
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
UploadFinalizeResponse indicating success/failure
|
125
|
+
|
126
|
+
Raises:
|
127
|
+
APIError: If the request fails
|
128
|
+
"""
|
129
|
+
query = FinalizeUploadQuery(self._file_id)
|
130
|
+
response_data = self._api_client._finalize_upload_query(query)
|
131
|
+
|
132
|
+
return UploadFinalizeResponse.from_dict(response_data, self._file_id)
|
133
|
+
|
134
|
+
|
135
|
+
class CompleteFileUploadCommand(MultipartUploadCommand):
|
136
|
+
"""Command for uploading a complete file using multipart upload.
|
137
|
+
|
138
|
+
This command orchestrates the entire upload process:
|
139
|
+
1. Initialize upload session
|
140
|
+
2. Upload all file parts
|
141
|
+
3. Check upload status
|
142
|
+
4. Finalize upload
|
143
|
+
"""
|
144
|
+
|
145
|
+
def __init__(
|
146
|
+
self,
|
147
|
+
api_client,
|
148
|
+
file_path: str,
|
149
|
+
chunk_size: int = 5 * 1024 * 1024,
|
150
|
+
progress_callback: Optional[Callable[[float, int, int], None]] = None
|
151
|
+
):
|
152
|
+
"""Initialize complete file upload command.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
api_client: The Multipart Upload API client instance
|
156
|
+
file_path: Path to the file to upload
|
157
|
+
chunk_size: Size of each chunk in bytes (default 5MB)
|
158
|
+
progress_callback: Optional callback for progress updates (percentage, current_part, total_parts)
|
159
|
+
"""
|
160
|
+
super().__init__(api_client)
|
161
|
+
self._file_path = file_path
|
162
|
+
self._chunk_size = chunk_size
|
163
|
+
self._progress_callback = progress_callback
|
164
|
+
self._session: Optional[MultipartUploadSession] = None
|
165
|
+
|
166
|
+
def execute(self) -> Dict[str, Any]:
|
167
|
+
"""Execute complete file upload.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
Dictionary with upload results and session information
|
171
|
+
|
172
|
+
Raises:
|
173
|
+
APIError: If any step of the upload fails
|
174
|
+
FileNotFoundError: If the file doesn't exist
|
175
|
+
"""
|
176
|
+
try:
|
177
|
+
# Validate file exists
|
178
|
+
if not Path(self._file_path).exists():
|
179
|
+
raise FileNotFoundError(f"File not found: {self._file_path}")
|
180
|
+
|
181
|
+
# Step 1: Initialize upload session
|
182
|
+
init_response = self._initialize_session()
|
183
|
+
if not init_response.success:
|
184
|
+
return {
|
185
|
+
'success': False,
|
186
|
+
'error': 'Failed to initialize upload session',
|
187
|
+
'file_path': self._file_path
|
188
|
+
}
|
189
|
+
|
190
|
+
# Step 2: Upload all parts
|
191
|
+
upload_results = self._upload_all_parts()
|
192
|
+
if not upload_results['success']:
|
193
|
+
return {
|
194
|
+
'success': False,
|
195
|
+
'error': upload_results['error'],
|
196
|
+
'file_path': self._file_path,
|
197
|
+
'file_id': self._session.file_id if self._session else None,
|
198
|
+
'uploaded_parts': len(self._session.uploaded_parts) if self._session else 0,
|
199
|
+
'total_parts': self._session.total_parts if self._session else 0
|
200
|
+
}
|
201
|
+
|
202
|
+
# Step 3: Check upload status
|
203
|
+
status_response = self._check_status()
|
204
|
+
if not status_response.success or not status_response.ready:
|
205
|
+
return {
|
206
|
+
'success': False,
|
207
|
+
'error': 'Upload not ready for finalization',
|
208
|
+
'file_path': self._file_path,
|
209
|
+
'file_id': self._session.file_id if self._session else None,
|
210
|
+
'ready': status_response.ready
|
211
|
+
}
|
212
|
+
|
213
|
+
# Step 4: Finalize upload
|
214
|
+
finalize_response = self._finalize_upload()
|
215
|
+
if not finalize_response.success:
|
216
|
+
return {
|
217
|
+
'success': False,
|
218
|
+
'error': finalize_response.error_message or 'Failed to finalize upload',
|
219
|
+
'file_path': self._file_path,
|
220
|
+
'file_id': self._session.file_id if self._session else None
|
221
|
+
}
|
222
|
+
|
223
|
+
# Success!
|
224
|
+
return {
|
225
|
+
'success': True,
|
226
|
+
'file_path': self._file_path,
|
227
|
+
'file_id': self._session.file_id if self._session else None,
|
228
|
+
'total_parts': self._session.total_parts if self._session else 0,
|
229
|
+
'total_size': self._session.total_size if self._session else 0,
|
230
|
+
'chunk_size': self._session.chunk_size if self._session else 0,
|
231
|
+
'upload_complete': True
|
232
|
+
}
|
233
|
+
|
234
|
+
except Exception as e:
|
235
|
+
return {
|
236
|
+
'success': False,
|
237
|
+
'error': str(e),
|
238
|
+
'file_path': self._file_path,
|
239
|
+
'file_id': self._session.file_id if self._session else None
|
240
|
+
}
|
241
|
+
|
242
|
+
def _initialize_session(self) -> UploadInitializeResponse:
|
243
|
+
"""Initialize the upload session."""
|
244
|
+
# Create session from file
|
245
|
+
self._session = FileChunker.create_session_from_file(self._file_path, self._chunk_size)
|
246
|
+
|
247
|
+
# Initialize upload
|
248
|
+
init_command = InitializeUploadCommand(self._api_client)
|
249
|
+
init_response = init_command.execute()
|
250
|
+
|
251
|
+
# Update session with file ID
|
252
|
+
if init_response.success:
|
253
|
+
self._session.file_id = init_response.file_id
|
254
|
+
|
255
|
+
return init_response
|
256
|
+
|
257
|
+
def _upload_all_parts(self) -> Dict[str, Any]:
|
258
|
+
"""Upload all file parts."""
|
259
|
+
if not self._session:
|
260
|
+
return {'success': False, 'error': 'Session not initialized'}
|
261
|
+
|
262
|
+
failed_parts = []
|
263
|
+
|
264
|
+
for part_number in range(1, self._session.total_parts + 1):
|
265
|
+
try:
|
266
|
+
# Read chunk data
|
267
|
+
chunk_data = FileChunker.read_chunk(
|
268
|
+
self._file_path,
|
269
|
+
part_number,
|
270
|
+
self._session.chunk_size
|
271
|
+
)
|
272
|
+
|
273
|
+
# Upload part
|
274
|
+
upload_command = UploadPartCommand(
|
275
|
+
self._api_client,
|
276
|
+
self._session.file_id,
|
277
|
+
part_number,
|
278
|
+
chunk_data,
|
279
|
+
Path(self._file_path).name
|
280
|
+
)
|
281
|
+
|
282
|
+
part_response = upload_command.execute()
|
283
|
+
|
284
|
+
if part_response.success:
|
285
|
+
self._session.add_uploaded_part(part_number)
|
286
|
+
|
287
|
+
# Call progress callback if provided
|
288
|
+
if self._progress_callback:
|
289
|
+
progress = self._session.get_progress_percentage()
|
290
|
+
self._progress_callback(progress, part_number, self._session.total_parts)
|
291
|
+
else:
|
292
|
+
failed_parts.append({
|
293
|
+
'part_number': part_number,
|
294
|
+
'error': part_response.error_message
|
295
|
+
})
|
296
|
+
|
297
|
+
except Exception as e:
|
298
|
+
failed_parts.append({
|
299
|
+
'part_number': part_number,
|
300
|
+
'error': str(e)
|
301
|
+
})
|
302
|
+
|
303
|
+
if failed_parts:
|
304
|
+
return {
|
305
|
+
'success': False,
|
306
|
+
'error': f"Failed to upload {len(failed_parts)} parts",
|
307
|
+
'failed_parts': failed_parts
|
308
|
+
}
|
309
|
+
|
310
|
+
return {'success': True}
|
311
|
+
|
312
|
+
def _check_status(self) -> UploadStatusResponse:
|
313
|
+
"""Check upload status."""
|
314
|
+
if not self._session:
|
315
|
+
raise ValueError("Session not initialized")
|
316
|
+
status_command = CheckUploadStatusCommand(self._api_client, self._session.file_id)
|
317
|
+
return status_command.execute()
|
318
|
+
|
319
|
+
def _finalize_upload(self) -> UploadFinalizeResponse:
|
320
|
+
"""Finalize the upload."""
|
321
|
+
if not self._session:
|
322
|
+
raise ValueError("Session not initialized")
|
323
|
+
finalize_command = FinalizeUploadCommand(self._api_client, self._session.file_id)
|
324
|
+
return finalize_command.execute()
|
325
|
+
|
326
|
+
def get_session_info(self) -> Optional[Dict[str, Any]]:
|
327
|
+
"""Get current session information.
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
Dictionary with session details or None if not initialized
|
331
|
+
"""
|
332
|
+
if not self._session:
|
333
|
+
return None
|
334
|
+
|
335
|
+
return {
|
336
|
+
'file_id': self._session.file_id,
|
337
|
+
'file_path': self._session.file_path,
|
338
|
+
'total_parts': self._session.total_parts,
|
339
|
+
'uploaded_parts': len(self._session.uploaded_parts),
|
340
|
+
'total_size': self._session.total_size,
|
341
|
+
'chunk_size': self._session.chunk_size,
|
342
|
+
'progress_percentage': self._session.get_progress_percentage(),
|
343
|
+
'is_complete': self._session.is_complete(),
|
344
|
+
'missing_parts': self._session.get_missing_parts()
|
345
|
+
}
|
346
|
+
|
347
|
+
|
348
|
+
# ---------------------------------------------------------------------------
|
349
|
+
# Abort Upload Command
|
350
|
+
# ---------------------------------------------------------------------------
|
351
|
+
|
352
|
+
|
353
|
+
class AbortUploadCommand(MultipartUploadCommand):
|
354
|
+
"""Command for aborting a multipart upload session (discard all parts)."""
|
355
|
+
|
356
|
+
def __init__(self, api_client, file_id: str):
|
357
|
+
super().__init__(api_client)
|
358
|
+
self._file_id = file_id
|
359
|
+
|
360
|
+
def execute(self) -> Dict[str, Any]: # returns plain dict. specific model not needed
|
361
|
+
"""Abort upload session by POSTing fileId to multipart-upload/abort."""
|
362
|
+
payload = {"fileId": self._file_id}
|
363
|
+
return self._api_client.http_client.post("multipart-upload/abort", json_data=payload)
|