materials-commons-cli 2.2.0__tar.gz → 2.2.1__tar.gz
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.
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/PKG-INFO +1 -1
- materials_commons_cli-2.2.1/materials_commons/cli/__init__.py +1 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/__init__.py +2 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/command_handlers.py +369 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/downloader/file_download_manager.py +143 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/downloader/file_downloader.py +287 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/local_rest_server.py +100 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/projects.py +88 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/uploader/__init__.py +0 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/uploader/api.py +178 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/uploader/file_transfer_manager.py +212 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/uploader/file_uploader.py +431 -0
- materials_commons_cli-2.2.1/materials_commons/cli/desktop/websocket_server.py +242 -0
- materials_commons_cli-2.2.1/materials_commons/cli/subcommands/__init__.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons_cli.egg-info/PKG-INFO +1 -1
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons_cli.egg-info/SOURCES.txt +12 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/setup.py +1 -1
- materials_commons_cli-2.2.0/materials_commons/cli/__init__.py +0 -1
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/LICENSE +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/README.md +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/LICENSE.txt +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/cloned_project.py +0 -0
- {materials_commons_cli-2.2.0/materials_commons/cli/subcommands → materials_commons_cli-2.2.1/materials_commons/cli/desktop/downloader}/__init__.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/exceptions.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/file_functions.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/functions.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/globus.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/list_objects.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/parser.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/print_formatter.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/sqltable.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/clone.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/config.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/dataset.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/down.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/expt.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/fetch.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/globus.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/init.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/ls.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/mkdir.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/mv.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/proj.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/remote.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/rm.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/server.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/up.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/subcommands/versions.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/tmp_functions.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/tree_functions.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/treedb.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons/cli/user_config.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons_cli.egg-info/dependency_links.txt +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons_cli.egg-info/entry_points.txt +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons_cli.egg-info/not-zip-safe +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons_cli.egg-info/requires.txt +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/materials_commons_cli.egg-info/top_level.txt +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/setup.cfg +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_api.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_functions.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_mc_dataset.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_mc_down.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_mc_mkdir.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_mc_mv.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_mc_proj.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_mc_rm.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_mc_up.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_treecompare_no_cache.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_treecompare_with_cache.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_treedb.py +0 -0
- {materials_commons_cli-2.2.0 → materials_commons_cli-2.2.1}/test/test_user_config.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.2.1'
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Dict, Any, Callable, Awaitable
|
|
3
|
+
from materials_commons.cli import desktop
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
CommandHandler = Callable[[Any, Dict[str, Any]], Awaitable[None]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_handlers() -> Dict[str, CommandHandler]:
|
|
16
|
+
return {
|
|
17
|
+
"sync": handle_sync,
|
|
18
|
+
"refresh_cache": handle_refresh_cache,
|
|
19
|
+
"SHUTDOWN": handle_shutdown,
|
|
20
|
+
"LIST_DIRECTORY": handle_list_directory,
|
|
21
|
+
"LIST_PROJECTS": handle_list_projects,
|
|
22
|
+
|
|
23
|
+
# Upload commands
|
|
24
|
+
"UPLOAD_FILE": handle_upload_file,
|
|
25
|
+
"UPLOAD_DIRECTORY": handle_upload_directory,
|
|
26
|
+
"CANCEL_UPLOAD": handle_cancel_upload,
|
|
27
|
+
"UPLOAD_PAUSE": handle_pause_upload,
|
|
28
|
+
"UPLOAD_RESUME": handle_resume_upload,
|
|
29
|
+
|
|
30
|
+
# Download commands
|
|
31
|
+
"DOWNLOAD_FILE": handle_download_file,
|
|
32
|
+
"PAUSE_DOWNLOAD": handle_pause_download,
|
|
33
|
+
"RESUME_DOWNLOAD": handle_resume_download,
|
|
34
|
+
"CANCEL_DOWNLOAD": handle_cancel_download,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async def handle_sync(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
38
|
+
print(f"[handler] sync -> {cmd}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def handle_refresh_cache(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
42
|
+
print(f"[handler] refresh_cache -> {cmd}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def handle_shutdown(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
46
|
+
print(f"[handler] shutdown -> {cmd}")
|
|
47
|
+
os.kill(os.getpid(), signal.SIGINT)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def handle_list_directory(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
51
|
+
print(f"[handler] list_directory -> {cmd}")
|
|
52
|
+
payload = cmd.get("payload") or {}
|
|
53
|
+
request_id = payload.get("request_id")
|
|
54
|
+
dir_path = payload.get("directory_path")
|
|
55
|
+
response_payload = {"files": [], "request_id": request_id}
|
|
56
|
+
if dir_path:
|
|
57
|
+
files = []
|
|
58
|
+
for entry in Path(dir_path).iterdir():
|
|
59
|
+
try:
|
|
60
|
+
stat = entry.stat()
|
|
61
|
+
files.append({
|
|
62
|
+
"name": entry.name,
|
|
63
|
+
"path": entry.as_posix(),
|
|
64
|
+
"type": "directory" if entry.is_dir() else "file",
|
|
65
|
+
"size": stat.st_size,
|
|
66
|
+
"mtime": datetime.fromtimestamp(stat.st_mtime).isoformat() + "Z",
|
|
67
|
+
"ctime": datetime.fromtimestamp(stat.st_ctime).isoformat() + "Z"
|
|
68
|
+
})
|
|
69
|
+
except (OSError, PermissionError) as e:
|
|
70
|
+
logger.warning(f"Could not stat {entry}: {e}")
|
|
71
|
+
response_payload["files"] = files
|
|
72
|
+
await queue.put({"command": "LIST_DIRECTORY", "payload": response_payload})
|
|
73
|
+
|
|
74
|
+
async def handle_list_projects(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
75
|
+
print(f"[handler] list_projects -> {cmd}")
|
|
76
|
+
payload = cmd.get("payload") or {}
|
|
77
|
+
projects = desktop.list_local_projects()
|
|
78
|
+
response_payload = {
|
|
79
|
+
"projects": projects,
|
|
80
|
+
"request_id": payload.get("request_id"),
|
|
81
|
+
}
|
|
82
|
+
print(f"[handler] list_projects returning -> {response_payload}")
|
|
83
|
+
await queue.put({"command": "LIST_PROJECTS", "payload": response_payload})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def handle_upload_file(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Handler for UPLOAD_FILE command from the server. It will receive a request that
|
|
89
|
+
contains the project_id and project_path. It will optionally receive the file_path
|
|
90
|
+
if the file being requested is not in the project. Optionally, the server can send
|
|
91
|
+
a chunk_size to use for uploading the file.
|
|
92
|
+
|
|
93
|
+
Expected payload:
|
|
94
|
+
{
|
|
95
|
+
"project_path": "/path/to/file/in/project", # This assumes / for project root, client will resolve to file_path
|
|
96
|
+
"file_path": "path/to/file/in/project/file.csv", # Optional
|
|
97
|
+
"project_id": 1047,
|
|
98
|
+
"chunk_size": 1048576 # Optional
|
|
99
|
+
}
|
|
100
|
+
"""
|
|
101
|
+
# print(f"[handler] upload_file_request -> {cmd}")
|
|
102
|
+
payload = cmd.get("payload") or {}
|
|
103
|
+
project_path = payload.get("project_path")
|
|
104
|
+
file_path = payload.get("file_path")
|
|
105
|
+
project_id = payload.get("project_id")
|
|
106
|
+
chunk_size = payload.get("chunk_size", 1024 * 1024)
|
|
107
|
+
|
|
108
|
+
# We must have a project_id
|
|
109
|
+
if not project_id:
|
|
110
|
+
logger.error("Missing required field project_id in UPLOAD_FILE command")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# We also must have a project_path
|
|
114
|
+
if not project_path:
|
|
115
|
+
logger.error("Missing required field project_path in UPLOAD_FILE command")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# The server may send us just a project_path. If that happens, then we need to resolve it to a file_path
|
|
119
|
+
# based on the local project's path. The project_path the server sends us is the path on the MC server.
|
|
120
|
+
# All MC server projects start with /, so we need to resolve to the local client path to the project.
|
|
121
|
+
# To do this, we get the local path for the project, then join project_path and the local project's path
|
|
122
|
+
# to get the full local path. Note that project_path may start with a /, so we strip it off before joining.
|
|
123
|
+
if not file_path:
|
|
124
|
+
p = desktop.get_local_project_by_id(project_id)
|
|
125
|
+
if not p:
|
|
126
|
+
logger.error(f"Project {project_id} not found in local projects")
|
|
127
|
+
return
|
|
128
|
+
file_path = os.path.join(p["project_dir_path"], project_path.lstrip("/"))
|
|
129
|
+
|
|
130
|
+
# print(f"Resolved file_path: {file_path}")
|
|
131
|
+
|
|
132
|
+
# This will be set by the listener
|
|
133
|
+
file_manager = cmd.get("_file_manager")
|
|
134
|
+
if not file_manager:
|
|
135
|
+
logger.error("File transfer manager not available")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
logger.info(f"Received upload request for: {file_path}")
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
transfer_id = await file_manager.upload_file(
|
|
142
|
+
file_path=file_path,
|
|
143
|
+
project_id=project_id,
|
|
144
|
+
project_path=project_path,
|
|
145
|
+
chunk_size=chunk_size,
|
|
146
|
+
progress_callback=lambda sent, total: logger.debug(f"{file_path}: {sent}/{total}")
|
|
147
|
+
)
|
|
148
|
+
logger.info(f"Queued upload: {file_path} (transfer_id: {transfer_id})")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Failed to queue upload: {e}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def handle_upload_directory(queue: asyncio.Queue, cmd: Dict[str, Any]):
|
|
154
|
+
"""
|
|
155
|
+
Handler for UPLOAD_DIRECTORY command from the server.
|
|
156
|
+
|
|
157
|
+
Expected payload:
|
|
158
|
+
{
|
|
159
|
+
"project_path": "/path/to/file/in/project", # This assumes / for project root, client will resolve to file_path
|
|
160
|
+
"directory_path": "/local/path/to/data",
|
|
161
|
+
"project_id": 1047,
|
|
162
|
+
"recursive": true,
|
|
163
|
+
"chunk_size": 1048576 // optional
|
|
164
|
+
}
|
|
165
|
+
"""
|
|
166
|
+
payload = cmd.get("payload") or {}
|
|
167
|
+
mc_project_path = payload.get("mc_project_path")
|
|
168
|
+
local_directory_path = payload.get("local_directory_path")
|
|
169
|
+
project_id = payload.get("project_id")
|
|
170
|
+
recursive = payload.get("recursive", True)
|
|
171
|
+
chunk_size = payload.get("chunk_size", 1024 * 1024)
|
|
172
|
+
|
|
173
|
+
if not project_id:
|
|
174
|
+
logger.error("Missing required field project_id in UPLOAD_DIRECTORY command")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
if not mc_project_path:
|
|
178
|
+
logger.error("Missing required field mc_project_path in UPLOAD_DIRECTORY command")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
p = desktop.get_local_project_by_id(project_id)
|
|
182
|
+
if not p:
|
|
183
|
+
logger.error(f"Project {project_id} not found in local projects")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
if not local_directory_path:
|
|
187
|
+
local_directory_path = os.path.join(p["project_dir_path"], mc_project_path.lstrip("/"))
|
|
188
|
+
|
|
189
|
+
file_manager = cmd.get("_file_manager")
|
|
190
|
+
if not file_manager:
|
|
191
|
+
logger.error("File transfer manager not available")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# print(f"Calling upload_directory with recursive={recursive}")
|
|
195
|
+
# print(f" dest_dir = {mc_project_path}")
|
|
196
|
+
# print(f" dir_path = {local_directory_path}")
|
|
197
|
+
# print(f" local_project_root = {p['project_dir_path']}")
|
|
198
|
+
try:
|
|
199
|
+
await file_manager.upload_directory(
|
|
200
|
+
dest_dir=mc_project_path,
|
|
201
|
+
dir_path=local_directory_path,
|
|
202
|
+
project_id=project_id,
|
|
203
|
+
recursive=recursive,
|
|
204
|
+
chunk_size=chunk_size,
|
|
205
|
+
)
|
|
206
|
+
logger.info(f"Queued upload of directory: {local_directory_path}")
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Failed to queue upload of directory: {e}")
|
|
209
|
+
# try:
|
|
210
|
+
# transfer_ids = await file_manager.upload_directory(
|
|
211
|
+
# dir_path=directory_path,
|
|
212
|
+
# project_id=project_id,
|
|
213
|
+
# directory_id=directory_id,
|
|
214
|
+
# recursive=recursive,
|
|
215
|
+
# progress_callback=lambda fname, sent, total: logger.debug(f"{fname}: {sent}/{total}")
|
|
216
|
+
# )
|
|
217
|
+
# logger.info(f"Queued {len(transfer_ids)} files from {directory_path}")
|
|
218
|
+
# except Exception as e:
|
|
219
|
+
# logger.error(f"Failed to queue directory upload: {e}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def handle_cancel_upload(queue: asyncio.Queue, cmd: Dict[str, Any]):
|
|
223
|
+
"""
|
|
224
|
+
Handler for CANCEL_UPLOAD command from the server.
|
|
225
|
+
|
|
226
|
+
Expected payload:
|
|
227
|
+
{
|
|
228
|
+
"transfer_id": "uuid-here"
|
|
229
|
+
}
|
|
230
|
+
"""
|
|
231
|
+
payload = cmd.get("payload", {})
|
|
232
|
+
transfer_id = payload.get("transfer_id")
|
|
233
|
+
|
|
234
|
+
if not transfer_id:
|
|
235
|
+
logger.error("Missing transfer_id in CANCEL_UPLOAD command")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
file_manager = cmd.get("_file_manager")
|
|
239
|
+
if not file_manager:
|
|
240
|
+
logger.error("File transfer manager not available")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
logger.info(f"Received cancel request for transfer: {transfer_id}")
|
|
244
|
+
file_manager.cancel_upload(transfer_id)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def handle_pause_upload(queue: asyncio.Queue, cmd: Dict[str, Any]):
|
|
248
|
+
"""
|
|
249
|
+
Handler for PAUSE_UPLOAD command from the server.
|
|
250
|
+
|
|
251
|
+
Expected payload:
|
|
252
|
+
{
|
|
253
|
+
"transfer_id": "uuid-here"
|
|
254
|
+
}
|
|
255
|
+
"""
|
|
256
|
+
payload = cmd.get("payload", {})
|
|
257
|
+
transfer_id = payload.get("transfer_id")
|
|
258
|
+
|
|
259
|
+
if not transfer_id:
|
|
260
|
+
logger.error("Missing transfer_id in PAUSE_UPLOAD command")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
file_manager = cmd.get("_file_manager")
|
|
264
|
+
if not file_manager:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
logger.info(f"Received pause request for transfer: {transfer_id}")
|
|
268
|
+
file_manager.pause_upload(transfer_id)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
async def handle_resume_upload(queue: asyncio.Queue, cmd: Dict[str, Any]):
|
|
272
|
+
"""
|
|
273
|
+
Handler for RESUME_UPLOAD command from the server.
|
|
274
|
+
|
|
275
|
+
Expected payload:
|
|
276
|
+
{
|
|
277
|
+
"transfer_id": "uuid-here"
|
|
278
|
+
}
|
|
279
|
+
"""
|
|
280
|
+
payload = cmd.get("payload", {})
|
|
281
|
+
transfer_id = payload.get("transfer_id")
|
|
282
|
+
|
|
283
|
+
if not transfer_id:
|
|
284
|
+
logger.error("Missing transfer_id in RESUME_UPLOAD command")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
file_manager = cmd.get("_file_manager")
|
|
288
|
+
if not file_manager:
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
logger.info(f"Received resume request for transfer: {transfer_id}")
|
|
292
|
+
file_manager.resume_upload(transfer_id)
|
|
293
|
+
|
|
294
|
+
async def handle_download_file(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
295
|
+
"""
|
|
296
|
+
Handler for DOWNLOAD_FILE command from the server.
|
|
297
|
+
|
|
298
|
+
Expected payload:
|
|
299
|
+
{
|
|
300
|
+
"project_path": "/path/to/download/file/to/in/project", # Optional
|
|
301
|
+
"file_path": "/full/path/to/download/file/to, # Optional, but one of project_path or file_path must be specified
|
|
302
|
+
"project_id": 1047, # materials commons project id
|
|
303
|
+
"file_id": 159681, # materials commons file id
|
|
304
|
+
"size": 54323 # Size in bytes
|
|
305
|
+
"checksum": "md5-checksum-here" # md5 checksum of file
|
|
306
|
+
}
|
|
307
|
+
"""
|
|
308
|
+
# print(f"[handler] download_file -> {cmd}")
|
|
309
|
+
payload = cmd.get("payload") or {}
|
|
310
|
+
project_path = payload.get("project_path")
|
|
311
|
+
file_path = payload.get("file_path")
|
|
312
|
+
project_id = payload.get("project_id")
|
|
313
|
+
file_id = payload.get("file_id")
|
|
314
|
+
size = payload.get("size")
|
|
315
|
+
checksum = payload.get("checksum")
|
|
316
|
+
|
|
317
|
+
# We must have a project_id
|
|
318
|
+
if not project_id:
|
|
319
|
+
logger.error("Missing required field project_id in DOWNLOAD_FILE command")
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
# We also must have a file_id
|
|
323
|
+
if not file_id:
|
|
324
|
+
logger.error("Missing required field file_id in DOWNLOAD_FILE command")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# we must have a file_path or project_path
|
|
328
|
+
if not file_path and not project_path:
|
|
329
|
+
logger.error("Missing required field file_path or project_path in DOWNLOAD_FILE command")
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
# We must have a size
|
|
333
|
+
if not size:
|
|
334
|
+
logger.error("Missing required field size in DOWNLOAD_FILE command")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# We must have a checksum
|
|
338
|
+
if not checksum:
|
|
339
|
+
logger.error("Missing required field checksum in DOWNLOAD_FILE command")
|
|
340
|
+
|
|
341
|
+
if project_path:
|
|
342
|
+
# If we have a project path, then we need to translate it to a
|
|
343
|
+
# local file path
|
|
344
|
+
p = desktop.get_local_project_by_id(project_id)
|
|
345
|
+
if not p:
|
|
346
|
+
logger.error(f"Project {project_id} not found in local projects")
|
|
347
|
+
return
|
|
348
|
+
file_path = os.path.join(p["project_dir_path"], project_path.lstrip("/"))
|
|
349
|
+
|
|
350
|
+
# This will be set by the listener in websocket_server
|
|
351
|
+
file_manager = cmd.get("_file_manager")
|
|
352
|
+
if not file_manager:
|
|
353
|
+
logger.error("File transfer manager not available")
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
# print(f"I would download to {file_path} with size {size} and checksum {checksum}, file_id {file_id}")
|
|
357
|
+
await file_manager.download_file(project_id, file_id, file_path, size, checksum)
|
|
358
|
+
logger.info(f"Queued download of file: {file_path}")
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.error(f"Failed to queue download of file: {e}")
|
|
361
|
+
|
|
362
|
+
async def handle_pause_download(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
363
|
+
print(f"[handler] pause_download -> {cmd}")
|
|
364
|
+
|
|
365
|
+
async def handle_resume_download(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
366
|
+
print(f"[handler] resume_download -> {cmd}")
|
|
367
|
+
|
|
368
|
+
async def handle_cancel_download(queue: asyncio.Queue, cmd: Dict[str, Any]) -> None:
|
|
369
|
+
print(f"[handler] cancel_download -> {cmd}")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, Any, Optional, Callable
|
|
5
|
+
|
|
6
|
+
from materials_commons.cli.desktop.downloader.file_downloader import FileDownloader, logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileDownloadManager:
|
|
10
|
+
"""Manages multiple concurrent file downloads"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, send_queue: asyncio.Queue, client_id: str, mcurl: str, apitoken: str, max_concurrent: int = 3):
|
|
13
|
+
self.send_queue = send_queue
|
|
14
|
+
self.client_id = client_id
|
|
15
|
+
self.mcurl = mcurl
|
|
16
|
+
self.apitoken = apitoken
|
|
17
|
+
self.max_concurrent = max_concurrent
|
|
18
|
+
|
|
19
|
+
# Active downloads indexed by transfer_id
|
|
20
|
+
self.active_downloads: Dict[str, FileDownloader] = {}
|
|
21
|
+
self.download_queue: asyncio.Queue = asyncio.Queue()
|
|
22
|
+
self.results: Dict[str, bool] = {}
|
|
23
|
+
self._workers_running = False
|
|
24
|
+
|
|
25
|
+
async def start_workers(self):
|
|
26
|
+
"""Start background workers to process download queue"""
|
|
27
|
+
self._workers_running = True
|
|
28
|
+
workers = [
|
|
29
|
+
asyncio.create_task(self._download_worker(i))
|
|
30
|
+
for i in range(self.max_concurrent)
|
|
31
|
+
]
|
|
32
|
+
return workers
|
|
33
|
+
|
|
34
|
+
async def _download_worker(self, worker_id: int):
|
|
35
|
+
"""Worker that processes downloads from queue"""
|
|
36
|
+
logger.info(f"Download worker {worker_id} started")
|
|
37
|
+
|
|
38
|
+
while self._workers_running:
|
|
39
|
+
try:
|
|
40
|
+
# Get download from queue with timeout
|
|
41
|
+
downloader = await asyncio.wait_for(
|
|
42
|
+
self.download_queue.get(),
|
|
43
|
+
timeout=1.0
|
|
44
|
+
)
|
|
45
|
+
except asyncio.TimeoutError:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
logger.info(f"Worker {worker_id} starting download: {downloader.file_path.name}")
|
|
50
|
+
self.active_downloads[downloader.transfer_id] = downloader
|
|
51
|
+
|
|
52
|
+
success = await downloader.download()
|
|
53
|
+
self.results[downloader.transfer_id] = success
|
|
54
|
+
|
|
55
|
+
del self.active_downloads[downloader.transfer_id]
|
|
56
|
+
|
|
57
|
+
if success:
|
|
58
|
+
logger.info(f"Worker {worker_id} completed: {downloader.file_path.name}")
|
|
59
|
+
else:
|
|
60
|
+
logger.error(f"Worker {worker_id} failed: {downloader.file_path.name}")
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Worker {worker_id} error: {e}")
|
|
64
|
+
self.results[downloader.transfer_id] = False
|
|
65
|
+
if downloader.transfer_id in self.active_downloads:
|
|
66
|
+
del self.active_downloads[downloader.transfer_id]
|
|
67
|
+
|
|
68
|
+
finally:
|
|
69
|
+
self.download_queue.task_done()
|
|
70
|
+
|
|
71
|
+
async def download_file(
|
|
72
|
+
self,
|
|
73
|
+
project_id: int,
|
|
74
|
+
file_id: int,
|
|
75
|
+
file_path: str,
|
|
76
|
+
expected_size: Optional[int] = None,
|
|
77
|
+
expected_checksum: Optional[str] = None,
|
|
78
|
+
chunk_size: int = 1024 * 1024,
|
|
79
|
+
progress_callback: Optional[Callable[[int, int], None]] = None
|
|
80
|
+
) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Queue a file for download. Returns transfer_id.
|
|
83
|
+
"""
|
|
84
|
+
downloader = FileDownloader(
|
|
85
|
+
send_queue=self.send_queue,
|
|
86
|
+
file_id=file_id,
|
|
87
|
+
file_path=file_path,
|
|
88
|
+
base_url=self.mcurl,
|
|
89
|
+
apitoken=self.apitoken,
|
|
90
|
+
project_id=project_id,
|
|
91
|
+
client_id=self.client_id,
|
|
92
|
+
expected_size=expected_size,
|
|
93
|
+
expected_checksum=expected_checksum,
|
|
94
|
+
chunk_size=chunk_size,
|
|
95
|
+
progress_callback=progress_callback
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
await self.download_queue.put(downloader)
|
|
99
|
+
logger.info(f"Queued download: {file_path} (transfer_id: {downloader.transfer_id})")
|
|
100
|
+
|
|
101
|
+
return downloader.transfer_id
|
|
102
|
+
|
|
103
|
+
def pause_download(self, transfer_id: str):
|
|
104
|
+
"""Pause an active download"""
|
|
105
|
+
if transfer_id in self.active_downloads:
|
|
106
|
+
self.active_downloads[transfer_id].pause()
|
|
107
|
+
logger.info(f"Paused download: {transfer_id}")
|
|
108
|
+
|
|
109
|
+
def resume_download(self, transfer_id: str):
|
|
110
|
+
"""Resume a paused download"""
|
|
111
|
+
if transfer_id in self.active_downloads:
|
|
112
|
+
self.active_downloads[transfer_id].resume_pause()
|
|
113
|
+
logger.info(f"Resumed download: {transfer_id}")
|
|
114
|
+
|
|
115
|
+
def cancel_download(self, transfer_id: str):
|
|
116
|
+
"""Cancel an active download"""
|
|
117
|
+
if transfer_id in self.active_downloads:
|
|
118
|
+
self.active_downloads[transfer_id].cancel()
|
|
119
|
+
logger.info(f"Cancelled download: {transfer_id}")
|
|
120
|
+
|
|
121
|
+
async def wait_all(self):
|
|
122
|
+
"""Wait for all queued downloads to complete"""
|
|
123
|
+
await self.download_queue.join()
|
|
124
|
+
|
|
125
|
+
def get_active_downloads(self) -> list[Dict[str, Any]]:
|
|
126
|
+
"""Get list of currently active downloads"""
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
"transfer_id": downloader.transfer_id,
|
|
130
|
+
"file_name": downloader.file_path.name,
|
|
131
|
+
"bytes_received": downloader.bytes_received,
|
|
132
|
+
"file_size": downloader.expected_size,
|
|
133
|
+
"progress_pct": (
|
|
134
|
+
(downloader.bytes_received / downloader.expected_size) * 100
|
|
135
|
+
if downloader.expected_size else 0
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
for downloader in self.active_downloads.values()
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
def stop_workers(self):
|
|
142
|
+
"""Stop all worker tasks"""
|
|
143
|
+
self._workers_running = False
|