replx-agent 2.0.0__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.
Potentially problematic release.
This version of replx-agent might be problematic. Click here for more details.
- replx_agent/__init__.py +3 -0
- replx_agent/__main__.py +5 -0
- replx_agent/commands.py +131 -0
- replx_agent/helpers/__init__.py +3 -0
- replx_agent/helpers/scanner.py +308 -0
- replx_agent/pkg/__init__.py +11 -0
- replx_agent/pkg/compiler.py +153 -0
- replx_agent/pkg/registry.py +387 -0
- replx_agent/pkg/service.py +690 -0
- replx_agent/pkg/store.py +83 -0
- replx_agent/protocol/__init__.py +11 -0
- replx_agent/protocol/repl.py +1029 -0
- replx_agent/protocol/storage.py +1018 -0
- replx_agent/rpc/__init__.py +3 -0
- replx_agent/rpc/protocol.py +220 -0
- replx_agent/server/__init__.py +3 -0
- replx_agent/server/__main__.py +4 -0
- replx_agent/server/command_dispatcher.py +49 -0
- replx_agent/server/connection_manager.py +654 -0
- replx_agent/server/core.py +1221 -0
- replx_agent/server/handlers/__init__.py +24 -0
- replx_agent/server/handlers/exec.py +553 -0
- replx_agent/server/handlers/filesystem.py +383 -0
- replx_agent/server/handlers/firmware.py +118 -0
- replx_agent/server/handlers/i2c.py +23 -0
- replx_agent/server/handlers/package.py +182 -0
- replx_agent/server/handlers/repl.py +188 -0
- replx_agent/server/handlers/session.py +205 -0
- replx_agent/server/handlers/spi.py +23 -0
- replx_agent/server/handlers/transfer.py +600 -0
- replx_agent/server/handlers/uart.py +32 -0
- replx_agent/server/session_manager.py +391 -0
- replx_agent/server/tcp.py +207 -0
- replx_agent/terminal.py +566 -0
- replx_agent/transport/__init__.py +32 -0
- replx_agent/transport/serial.py +220 -0
- replx_agent/utils/__init__.py +95 -0
- replx_agent/utils/constants.py +78 -0
- replx_agent/utils/device_info.py +183 -0
- replx_agent/utils/exceptions.py +31 -0
- replx_agent-2.0.0.dist-info/METADATA +74 -0
- replx_agent-2.0.0.dist-info/RECORD +46 -0
- replx_agent-2.0.0.dist-info/WHEEL +5 -0
- replx_agent-2.0.0.dist-info/entry_points.txt +2 -0
- replx_agent-2.0.0.dist-info/licenses/LICENSE +21 -0
- replx_agent-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import ast
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import textwrap
|
|
6
|
+
import posixpath
|
|
7
|
+
import tempfile
|
|
8
|
+
import zlib
|
|
9
|
+
from typing import Tuple, List
|
|
10
|
+
|
|
11
|
+
from replx_agent.utils.exceptions import ProtocolError, TransportError, FileSystemError
|
|
12
|
+
from replx_agent.utils.constants import PUTDIR_EXCLUDED_DIRS, PUTDIR_EXCLUDED_EXTENSIONS
|
|
13
|
+
from replx_agent.utils.device_info import normalize_core, get_core_profile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_DEFLATE_WBITS = 12
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _python_string_literal(value: str) -> str:
|
|
20
|
+
return repr(value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DeviceStorage:
|
|
24
|
+
|
|
25
|
+
def __init__(self, repl_protocol, core: str = "RP2350", device: str = "", device_root_fs: str = "/"):
|
|
26
|
+
self.repl = repl_protocol
|
|
27
|
+
self.core = normalize_core(core)
|
|
28
|
+
self.device = device
|
|
29
|
+
self.device_root_fs = device_root_fs
|
|
30
|
+
|
|
31
|
+
_profile = get_core_profile(self.core)
|
|
32
|
+
self._DEVICE_CHUNK_SIZES = _profile['chunk_size']
|
|
33
|
+
self._PUT_BATCH_BYTES = _profile['put_batch_bytes']
|
|
34
|
+
self._GET_CHUNK_SIZE = _profile['get_chunk_size']
|
|
35
|
+
|
|
36
|
+
if self.core == "EFR32MG":
|
|
37
|
+
self._ls_detailed_func = self._ls_detailed_xbee
|
|
38
|
+
self._ls_recursive_func = self._ls_recursive_xbee
|
|
39
|
+
else:
|
|
40
|
+
self._ls_detailed_func = self._ls_detailed_standard
|
|
41
|
+
self._ls_recursive_func = self._ls_recursive_standard
|
|
42
|
+
|
|
43
|
+
def ls(self, path: str = "/") -> List[str]:
|
|
44
|
+
path_literal = _python_string_literal(path)
|
|
45
|
+
command = f"""
|
|
46
|
+
import os
|
|
47
|
+
try:
|
|
48
|
+
print(os.listdir({path_literal}))
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print('__ERROR__:' + str(e))
|
|
51
|
+
"""
|
|
52
|
+
result = self.repl.exec(command).decode('utf-8').strip()
|
|
53
|
+
if result.startswith("__ERROR__:"):
|
|
54
|
+
raise FileSystemError(f"ls failed: {result[len('__ERROR__:'):].strip()}")
|
|
55
|
+
try:
|
|
56
|
+
items = ast.literal_eval(result)
|
|
57
|
+
except (ValueError, SyntaxError) as e:
|
|
58
|
+
raise FileSystemError(f"Invalid ls response: {e}") from e
|
|
59
|
+
if not isinstance(items, list):
|
|
60
|
+
raise FileSystemError("Invalid ls response: expected a list")
|
|
61
|
+
return items
|
|
62
|
+
|
|
63
|
+
def ls_detailed(self, path: str = "/") -> List[list]:
|
|
64
|
+
return self._ls_detailed_func(path)
|
|
65
|
+
|
|
66
|
+
def ls_recursive(self, path: str = "/") -> List[list]:
|
|
67
|
+
return self._ls_recursive_func(path)
|
|
68
|
+
|
|
69
|
+
def _ls_detailed_standard(self, dir: str = "/") -> List[list]:
|
|
70
|
+
dir = self._normalize_remote_path(dir)
|
|
71
|
+
if not dir.startswith("/"):
|
|
72
|
+
dir = "/" + dir
|
|
73
|
+
if dir != '/' and dir.endswith('/'):
|
|
74
|
+
dir = dir.rstrip('/')
|
|
75
|
+
dir_literal = _python_string_literal(dir)
|
|
76
|
+
|
|
77
|
+
command = f"""
|
|
78
|
+
import os
|
|
79
|
+
import json
|
|
80
|
+
def get_detailed_listing(path):
|
|
81
|
+
try:
|
|
82
|
+
items = []
|
|
83
|
+
for item in os.listdir(path):
|
|
84
|
+
full_path = path + ('/' + item if path != '/' else item)
|
|
85
|
+
try:
|
|
86
|
+
stat_info = os.stat(full_path)
|
|
87
|
+
is_dir = stat_info[0] & 0x4000 != 0
|
|
88
|
+
size = 0 if is_dir else stat_info[6]
|
|
89
|
+
items.append([item, size, is_dir])
|
|
90
|
+
except Exception:
|
|
91
|
+
try:
|
|
92
|
+
os.listdir(full_path)
|
|
93
|
+
items.append([item, 0, True])
|
|
94
|
+
except Exception:
|
|
95
|
+
items.append([item, 0, False])
|
|
96
|
+
return sorted(items, key=lambda x: (not x[2], x[0].lower()))
|
|
97
|
+
except Exception as e:
|
|
98
|
+
return {{'__error__': str(e)}}
|
|
99
|
+
print(json.dumps(get_detailed_listing({dir_literal})))
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
out = self.repl.exec(command)
|
|
103
|
+
result_str = out.decode("utf-8", errors='replace').strip()
|
|
104
|
+
result = json.loads(result_str)
|
|
105
|
+
if isinstance(result, dict) and "__error__" in result:
|
|
106
|
+
raise FileSystemError(f"ls failed: {result['__error__']}")
|
|
107
|
+
return result
|
|
108
|
+
except TransportError:
|
|
109
|
+
raise
|
|
110
|
+
except json.JSONDecodeError as e:
|
|
111
|
+
raise FileSystemError(f"Invalid directory listing response: {e}") from e
|
|
112
|
+
|
|
113
|
+
def _ls_detailed_xbee(self, dir: str = "/") -> List[list]:
|
|
114
|
+
dir = self._normalize_remote_path(dir)
|
|
115
|
+
if not dir.startswith("/"):
|
|
116
|
+
dir = "/" + dir
|
|
117
|
+
dir_literal = _python_string_literal(dir)
|
|
118
|
+
|
|
119
|
+
command = f"""
|
|
120
|
+
import os
|
|
121
|
+
import json
|
|
122
|
+
def xbee_is_dir(path):
|
|
123
|
+
try:
|
|
124
|
+
os.listdir(path)
|
|
125
|
+
return True
|
|
126
|
+
except OSError:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def xbee_get_file_size(path):
|
|
130
|
+
size = 0
|
|
131
|
+
with open(path, 'rb') as f:
|
|
132
|
+
while True:
|
|
133
|
+
chunk = f.read(256)
|
|
134
|
+
if not chunk:
|
|
135
|
+
break
|
|
136
|
+
size += len(chunk)
|
|
137
|
+
return size
|
|
138
|
+
|
|
139
|
+
def get_detailed_listing(path):
|
|
140
|
+
try:
|
|
141
|
+
items = []
|
|
142
|
+
for item in os.listdir(path):
|
|
143
|
+
full_path = path + ('/' + item if path != '/' else item)
|
|
144
|
+
is_dir = xbee_is_dir(full_path)
|
|
145
|
+
if is_dir:
|
|
146
|
+
size = 0
|
|
147
|
+
else:
|
|
148
|
+
size = xbee_get_file_size(full_path)
|
|
149
|
+
items.append([item, size, is_dir])
|
|
150
|
+
return sorted(items, key=lambda x: (not x[2], x[0].lower()))
|
|
151
|
+
except Exception as e:
|
|
152
|
+
return {{'__error__': str(e)}}
|
|
153
|
+
print(json.dumps(get_detailed_listing({dir_literal})))
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
out = self.repl.exec(command)
|
|
157
|
+
result = json.loads(out.decode("utf-8").strip())
|
|
158
|
+
if isinstance(result, dict) and "__error__" in result:
|
|
159
|
+
raise FileSystemError(f"ls failed: {result['__error__']}")
|
|
160
|
+
return result
|
|
161
|
+
except TransportError:
|
|
162
|
+
raise
|
|
163
|
+
except json.JSONDecodeError as e:
|
|
164
|
+
raise FileSystemError(f"Invalid directory listing response: {e}") from e
|
|
165
|
+
|
|
166
|
+
def _ls_recursive_standard(self, dir: str = "/") -> List[list]:
|
|
167
|
+
dir = self._normalize_remote_path(dir)
|
|
168
|
+
if not dir.startswith("/"):
|
|
169
|
+
dir = "/" + dir
|
|
170
|
+
safe_dir = _python_string_literal(dir)
|
|
171
|
+
|
|
172
|
+
command = f"""
|
|
173
|
+
import os
|
|
174
|
+
import json
|
|
175
|
+
|
|
176
|
+
def get_recursive_listing(path, base_path=None):
|
|
177
|
+
if base_path is None:
|
|
178
|
+
base_path = path
|
|
179
|
+
items = []
|
|
180
|
+
try:
|
|
181
|
+
entries = os.listdir(path)
|
|
182
|
+
for item in entries:
|
|
183
|
+
full_path = path + ('/' + item if path != '/' else item)
|
|
184
|
+
|
|
185
|
+
# Calculate relative path from base
|
|
186
|
+
if base_path == '/':
|
|
187
|
+
rel_path = full_path
|
|
188
|
+
else:
|
|
189
|
+
rel_path = full_path[len(base_path):].lstrip('/')
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
stat_info = os.stat(full_path)
|
|
193
|
+
is_dir = stat_info[0] & 0x4000 != 0
|
|
194
|
+
size = 0 if is_dir else stat_info[6]
|
|
195
|
+
except Exception:
|
|
196
|
+
try:
|
|
197
|
+
os.listdir(full_path)
|
|
198
|
+
is_dir = True
|
|
199
|
+
size = 0
|
|
200
|
+
except Exception:
|
|
201
|
+
is_dir = False
|
|
202
|
+
size = 0
|
|
203
|
+
|
|
204
|
+
if is_dir:
|
|
205
|
+
items.append([rel_path, 0, True])
|
|
206
|
+
child = get_recursive_listing(full_path, base_path)
|
|
207
|
+
if isinstance(child, dict) and '__error__' in child:
|
|
208
|
+
return child
|
|
209
|
+
items.extend(child)
|
|
210
|
+
else:
|
|
211
|
+
items.append([rel_path, size, False])
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
return {{'__error__': str(e)}}
|
|
215
|
+
|
|
216
|
+
return sorted(items, key=lambda x: x[0].lower())
|
|
217
|
+
|
|
218
|
+
print(json.dumps(get_recursive_listing({safe_dir})))
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
out = self.repl.exec(command)
|
|
222
|
+
result = json.loads(out.decode("utf-8").strip())
|
|
223
|
+
if isinstance(result, dict) and "__error__" in result:
|
|
224
|
+
raise FileSystemError(f"ls_recursive failed: {result['__error__']}")
|
|
225
|
+
return result
|
|
226
|
+
except TransportError:
|
|
227
|
+
raise
|
|
228
|
+
except json.JSONDecodeError:
|
|
229
|
+
return self._ls_recursive_fallback(dir)
|
|
230
|
+
|
|
231
|
+
def _ls_recursive_xbee(self, dir: str = "/") -> List[list]:
|
|
232
|
+
dir = self._normalize_remote_path(dir)
|
|
233
|
+
if not dir.startswith("/"):
|
|
234
|
+
dir = "/" + dir
|
|
235
|
+
safe_dir = _python_string_literal(dir)
|
|
236
|
+
|
|
237
|
+
command = f"""
|
|
238
|
+
import os
|
|
239
|
+
import json
|
|
240
|
+
|
|
241
|
+
def xbee_is_dir(path):
|
|
242
|
+
try:
|
|
243
|
+
os.listdir(path)
|
|
244
|
+
return True
|
|
245
|
+
except OSError:
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
def xbee_get_file_size(path):
|
|
249
|
+
size = 0
|
|
250
|
+
with open(path, 'rb') as f:
|
|
251
|
+
while True:
|
|
252
|
+
chunk = f.read(256)
|
|
253
|
+
if not chunk:
|
|
254
|
+
break
|
|
255
|
+
size += len(chunk)
|
|
256
|
+
return size
|
|
257
|
+
|
|
258
|
+
def get_recursive_listing(path, base_path=None):
|
|
259
|
+
if base_path is None:
|
|
260
|
+
base_path = path
|
|
261
|
+
items = []
|
|
262
|
+
try:
|
|
263
|
+
entries = os.listdir(path)
|
|
264
|
+
for item in entries:
|
|
265
|
+
full_path = path + ('/' + item if path != '/' else item)
|
|
266
|
+
|
|
267
|
+
# Calculate relative path from base
|
|
268
|
+
if base_path == '/':
|
|
269
|
+
rel_path = full_path
|
|
270
|
+
else:
|
|
271
|
+
rel_path = full_path[len(base_path):].lstrip('/')
|
|
272
|
+
|
|
273
|
+
is_dir = xbee_is_dir(full_path)
|
|
274
|
+
|
|
275
|
+
if is_dir:
|
|
276
|
+
items.append([rel_path, 0, True])
|
|
277
|
+
child = get_recursive_listing(full_path, base_path)
|
|
278
|
+
if isinstance(child, dict) and '__error__' in child:
|
|
279
|
+
return child
|
|
280
|
+
items.extend(child)
|
|
281
|
+
else:
|
|
282
|
+
size = xbee_get_file_size(full_path)
|
|
283
|
+
items.append([rel_path, size, is_dir])
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
return {{'__error__': str(e)}}
|
|
287
|
+
|
|
288
|
+
return sorted(items, key=lambda x: x[0].lower())
|
|
289
|
+
|
|
290
|
+
print(json.dumps(get_recursive_listing({safe_dir})))
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
out = self.repl.exec(command)
|
|
294
|
+
result = json.loads(out.decode("utf-8").strip())
|
|
295
|
+
if isinstance(result, dict) and "__error__" in result:
|
|
296
|
+
raise FileSystemError(f"ls_recursive failed: {result['__error__']}")
|
|
297
|
+
return result
|
|
298
|
+
except TransportError:
|
|
299
|
+
raise
|
|
300
|
+
except json.JSONDecodeError:
|
|
301
|
+
return self._ls_recursive_fallback(dir)
|
|
302
|
+
|
|
303
|
+
def _ls_recursive_fallback(self, dir: str = "/") -> List[list]:
|
|
304
|
+
result = []
|
|
305
|
+
|
|
306
|
+
def walk_dir(current_dir, base_dir):
|
|
307
|
+
items = self.ls_detailed(current_dir)
|
|
308
|
+
for name, size, is_dir in items:
|
|
309
|
+
full_path = posixpath.join(current_dir, name)
|
|
310
|
+
if base_dir == '/':
|
|
311
|
+
rel_path = full_path
|
|
312
|
+
else:
|
|
313
|
+
rel_path = full_path[len(base_dir):].lstrip('/')
|
|
314
|
+
|
|
315
|
+
if is_dir:
|
|
316
|
+
result.append([rel_path, 0, True])
|
|
317
|
+
walk_dir(full_path, base_dir)
|
|
318
|
+
else:
|
|
319
|
+
result.append([rel_path, size, False])
|
|
320
|
+
|
|
321
|
+
walk_dir(dir, dir)
|
|
322
|
+
|
|
323
|
+
return sorted(result, key=lambda x: x[0].lower())
|
|
324
|
+
|
|
325
|
+
def is_dir(self, path: str) -> bool:
|
|
326
|
+
path = self._normalize_remote_path(path)
|
|
327
|
+
path_literal = _python_string_literal(path)
|
|
328
|
+
|
|
329
|
+
if self.core == "EFR32MG":
|
|
330
|
+
command = f"""
|
|
331
|
+
import os
|
|
332
|
+
try:
|
|
333
|
+
os.listdir({path_literal})
|
|
334
|
+
result = True
|
|
335
|
+
except OSError:
|
|
336
|
+
result = False
|
|
337
|
+
print(result)
|
|
338
|
+
"""
|
|
339
|
+
else:
|
|
340
|
+
command = f"""
|
|
341
|
+
import os
|
|
342
|
+
try:
|
|
343
|
+
result = os.stat({path_literal})[0] & 0x4000 != 0
|
|
344
|
+
except Exception:
|
|
345
|
+
try:
|
|
346
|
+
os.listdir({path_literal})
|
|
347
|
+
result = True
|
|
348
|
+
except OSError:
|
|
349
|
+
result = False
|
|
350
|
+
print(result)
|
|
351
|
+
"""
|
|
352
|
+
out = self.repl.exec(command)
|
|
353
|
+
try:
|
|
354
|
+
result = ast.literal_eval(out.decode("utf-8").strip())
|
|
355
|
+
except (ValueError, SyntaxError) as e:
|
|
356
|
+
raise FileSystemError(f"Invalid is_dir response: {e}") from e
|
|
357
|
+
if not isinstance(result, bool):
|
|
358
|
+
raise FileSystemError("Invalid is_dir response: expected a boolean")
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
def state(self, path: str) -> int:
|
|
362
|
+
path = self._normalize_remote_path(path)
|
|
363
|
+
path_literal = _python_string_literal(path)
|
|
364
|
+
|
|
365
|
+
if self.core == "EFR32MG":
|
|
366
|
+
command = f"""
|
|
367
|
+
try:
|
|
368
|
+
with open({path_literal}, 'rb') as f:
|
|
369
|
+
size = 0
|
|
370
|
+
while True:
|
|
371
|
+
chunk = f.read(256)
|
|
372
|
+
if not chunk:
|
|
373
|
+
break
|
|
374
|
+
size += len(chunk)
|
|
375
|
+
print(size)
|
|
376
|
+
except OSError as e:
|
|
377
|
+
print('__ERROR__:' + str(e))
|
|
378
|
+
"""
|
|
379
|
+
else:
|
|
380
|
+
command = f"""
|
|
381
|
+
import os
|
|
382
|
+
try:
|
|
383
|
+
st = os.stat({path_literal})
|
|
384
|
+
print(st[6])
|
|
385
|
+
except Exception:
|
|
386
|
+
print(0)
|
|
387
|
+
"""
|
|
388
|
+
out = self.repl.exec(command)
|
|
389
|
+
result = out.decode('utf-8').strip()
|
|
390
|
+
if result.startswith('__ERROR__:'):
|
|
391
|
+
raise FileSystemError(f"File-size query failed: {result[10:].strip()}")
|
|
392
|
+
try:
|
|
393
|
+
return int(result)
|
|
394
|
+
except ValueError as e:
|
|
395
|
+
raise FileSystemError(f"Invalid file-size response: {result!r}") from e
|
|
396
|
+
|
|
397
|
+
def _file_exists(self, path: str) -> bool:
|
|
398
|
+
path_literal = _python_string_literal(path)
|
|
399
|
+
|
|
400
|
+
if self.core == "EFR32MG":
|
|
401
|
+
command = f"""
|
|
402
|
+
try:
|
|
403
|
+
f = open({path_literal}, 'rb')
|
|
404
|
+
f.close()
|
|
405
|
+
print('1')
|
|
406
|
+
except Exception:
|
|
407
|
+
print('0')
|
|
408
|
+
"""
|
|
409
|
+
else:
|
|
410
|
+
command = f"""
|
|
411
|
+
import os
|
|
412
|
+
try:
|
|
413
|
+
os.stat({path_literal})
|
|
414
|
+
print('1')
|
|
415
|
+
except Exception:
|
|
416
|
+
print('0')
|
|
417
|
+
"""
|
|
418
|
+
result = self.repl.exec(command).decode('utf-8').strip()
|
|
419
|
+
if result == '1':
|
|
420
|
+
return True
|
|
421
|
+
if result == '0':
|
|
422
|
+
return False
|
|
423
|
+
raise FileSystemError(f"Invalid file-exists response: {result!r}")
|
|
424
|
+
|
|
425
|
+
def mkdir(self, dir: str) -> bool:
|
|
426
|
+
dir = dir.replace('\\', '/')
|
|
427
|
+
if not dir:
|
|
428
|
+
return False
|
|
429
|
+
dir_literal = _python_string_literal(dir)
|
|
430
|
+
command = f"""
|
|
431
|
+
import os
|
|
432
|
+
def mkdir(path):
|
|
433
|
+
current = '/' if path.startswith('/') else ''
|
|
434
|
+
for part in path.split('/'):
|
|
435
|
+
if not part:
|
|
436
|
+
continue
|
|
437
|
+
if current == '/':
|
|
438
|
+
current += part
|
|
439
|
+
elif current:
|
|
440
|
+
current += '/' + part
|
|
441
|
+
else:
|
|
442
|
+
current = part
|
|
443
|
+
try:
|
|
444
|
+
os.mkdir(current)
|
|
445
|
+
except OSError:
|
|
446
|
+
try:
|
|
447
|
+
os.listdir(current)
|
|
448
|
+
except OSError:
|
|
449
|
+
return False
|
|
450
|
+
return True
|
|
451
|
+
print(mkdir({dir_literal}))
|
|
452
|
+
"""
|
|
453
|
+
out = self.repl.exec(command)
|
|
454
|
+
try:
|
|
455
|
+
result = ast.literal_eval(out.decode("utf-8").strip())
|
|
456
|
+
except (ValueError, SyntaxError) as e:
|
|
457
|
+
raise FileSystemError(f"Invalid mkdir response: {e}") from e
|
|
458
|
+
if not isinstance(result, bool):
|
|
459
|
+
raise FileSystemError("Invalid mkdir response: expected a boolean")
|
|
460
|
+
return result
|
|
461
|
+
|
|
462
|
+
def rm(self, filename: str):
|
|
463
|
+
filename_literal = _python_string_literal(filename)
|
|
464
|
+
command = f"""
|
|
465
|
+
import os
|
|
466
|
+
os.remove({filename_literal})
|
|
467
|
+
"""
|
|
468
|
+
self.repl.exec(command)
|
|
469
|
+
|
|
470
|
+
def touch(self, filename: str, core: str = None):
|
|
471
|
+
core = core or self.core
|
|
472
|
+
filename_literal = _python_string_literal(filename)
|
|
473
|
+
if core == "EFR32MG":
|
|
474
|
+
command = f"""
|
|
475
|
+
import os
|
|
476
|
+
try:
|
|
477
|
+
# Try to open file to check if it exists
|
|
478
|
+
f = open({filename_literal}, 'rb')
|
|
479
|
+
f.close()
|
|
480
|
+
except Exception:
|
|
481
|
+
# File doesn't exist, create it
|
|
482
|
+
f = open({filename_literal}, 'wb')
|
|
483
|
+
f.close()
|
|
484
|
+
"""
|
|
485
|
+
else:
|
|
486
|
+
command = f"f = open({filename_literal}, 'a'); f.close()"
|
|
487
|
+
self.repl.exec(command)
|
|
488
|
+
|
|
489
|
+
def rmdir(self, dir: str):
|
|
490
|
+
dir_literal = _python_string_literal(dir)
|
|
491
|
+
if self.core == "EFR32MG":
|
|
492
|
+
command = f"""
|
|
493
|
+
import os
|
|
494
|
+
def rmdir(p):
|
|
495
|
+
for f in os.listdir(p):
|
|
496
|
+
fp = p + '/' + f
|
|
497
|
+
try:
|
|
498
|
+
os.remove(fp)
|
|
499
|
+
except OSError:
|
|
500
|
+
rmdir(fp)
|
|
501
|
+
os.rmdir(p)
|
|
502
|
+
rmdir({dir_literal})
|
|
503
|
+
"""
|
|
504
|
+
else:
|
|
505
|
+
command = f"""
|
|
506
|
+
import os
|
|
507
|
+
def rmdir(p):
|
|
508
|
+
for name in os.listdir(p):
|
|
509
|
+
fp = p + '/' + name if p != '/' else '/' + name
|
|
510
|
+
try:
|
|
511
|
+
if os.stat(fp)[0] & 0x4000:
|
|
512
|
+
rmdir(fp)
|
|
513
|
+
else:
|
|
514
|
+
os.remove(fp)
|
|
515
|
+
except OSError:
|
|
516
|
+
try:
|
|
517
|
+
rmdir(fp)
|
|
518
|
+
except Exception:
|
|
519
|
+
pass
|
|
520
|
+
os.rmdir(p)
|
|
521
|
+
rmdir({dir_literal})
|
|
522
|
+
"""
|
|
523
|
+
self.repl.exec(command)
|
|
524
|
+
|
|
525
|
+
def cp(self, source: str, dest: str):
|
|
526
|
+
source_literal = _python_string_literal(source)
|
|
527
|
+
dest_literal = _python_string_literal(dest)
|
|
528
|
+
command = f"""
|
|
529
|
+
with open({source_literal}, 'rb') as src:
|
|
530
|
+
with open({dest_literal}, 'wb') as dst:
|
|
531
|
+
while True:
|
|
532
|
+
chunk = src.read(512)
|
|
533
|
+
if not chunk:
|
|
534
|
+
break
|
|
535
|
+
dst.write(chunk)
|
|
536
|
+
"""
|
|
537
|
+
self.repl.exec(command)
|
|
538
|
+
|
|
539
|
+
def mv(self, source: str, dest: str):
|
|
540
|
+
source_literal = _python_string_literal(source)
|
|
541
|
+
dest_literal = _python_string_literal(dest)
|
|
542
|
+
if self.core == "EFR32MG":
|
|
543
|
+
command = f"""
|
|
544
|
+
import os
|
|
545
|
+
try:
|
|
546
|
+
os.listdir({source_literal})
|
|
547
|
+
is_dir = True
|
|
548
|
+
except Exception:
|
|
549
|
+
is_dir = False
|
|
550
|
+
|
|
551
|
+
if is_dir:
|
|
552
|
+
# For directories: create dest, copy contents recursively, remove source
|
|
553
|
+
def copy_dir(src, dst):
|
|
554
|
+
try:
|
|
555
|
+
os.mkdir(dst)
|
|
556
|
+
except Exception:
|
|
557
|
+
pass
|
|
558
|
+
for f in os.listdir(src):
|
|
559
|
+
sf = src + '/' + f
|
|
560
|
+
df = dst + '/' + f
|
|
561
|
+
try:
|
|
562
|
+
os.listdir(sf)
|
|
563
|
+
copy_dir(sf, df)
|
|
564
|
+
except Exception:
|
|
565
|
+
with open(sf, 'rb') as s:
|
|
566
|
+
with open(df, 'wb') as d:
|
|
567
|
+
while True:
|
|
568
|
+
c = s.read(512)
|
|
569
|
+
if not c:
|
|
570
|
+
break
|
|
571
|
+
d.write(c)
|
|
572
|
+
def rm_dir(p):
|
|
573
|
+
for f in os.listdir(p):
|
|
574
|
+
fp = p + '/' + f
|
|
575
|
+
try:
|
|
576
|
+
os.remove(fp)
|
|
577
|
+
except OSError:
|
|
578
|
+
rm_dir(fp)
|
|
579
|
+
os.rmdir(p)
|
|
580
|
+
copy_dir({source_literal}, {dest_literal})
|
|
581
|
+
rm_dir({source_literal})
|
|
582
|
+
else:
|
|
583
|
+
# For files: simple copy and remove
|
|
584
|
+
with open({source_literal}, 'rb') as src:
|
|
585
|
+
with open({dest_literal}, 'wb') as dst:
|
|
586
|
+
while True:
|
|
587
|
+
chunk = src.read(512)
|
|
588
|
+
if not chunk:
|
|
589
|
+
break
|
|
590
|
+
dst.write(chunk)
|
|
591
|
+
os.remove({source_literal})
|
|
592
|
+
"""
|
|
593
|
+
else:
|
|
594
|
+
command = f"""
|
|
595
|
+
import os
|
|
596
|
+
os.rename({source_literal}, {dest_literal})
|
|
597
|
+
"""
|
|
598
|
+
self.repl.exec(command)
|
|
599
|
+
|
|
600
|
+
def mem(self) -> Tuple[int, int, int, float]:
|
|
601
|
+
command = """
|
|
602
|
+
import gc
|
|
603
|
+
gc.collect()
|
|
604
|
+
free = gc.mem_free()
|
|
605
|
+
alloc = gc.mem_alloc()
|
|
606
|
+
total = free + alloc
|
|
607
|
+
usage_pct = round(alloc / total * 100, 2)
|
|
608
|
+
print(free, alloc, total, usage_pct)
|
|
609
|
+
"""
|
|
610
|
+
out = self.repl.exec(command)
|
|
611
|
+
free_str, alloc_str, total_str, usage_pct_str = out.decode("utf-8").strip().split()
|
|
612
|
+
return int(free_str), int(alloc_str), int(total_str), float(usage_pct_str)
|
|
613
|
+
|
|
614
|
+
def df(self) -> Tuple[int, int, int, float]:
|
|
615
|
+
command = """
|
|
616
|
+
import os
|
|
617
|
+
def get_fs_info(path='/'):
|
|
618
|
+
stats = os.statvfs(path)
|
|
619
|
+
block_size = stats[0]
|
|
620
|
+
total_blocks = stats[2]
|
|
621
|
+
free_blocks = stats[3]
|
|
622
|
+
total = block_size * total_blocks
|
|
623
|
+
free = block_size * free_blocks
|
|
624
|
+
used = total - free
|
|
625
|
+
usage_pct = round(used / total * 100, 2) if total > 0 else 0.0
|
|
626
|
+
return total, used, free, usage_pct
|
|
627
|
+
print(get_fs_info())
|
|
628
|
+
"""
|
|
629
|
+
out = self.repl.exec(command)
|
|
630
|
+
return ast.literal_eval(out.decode("utf-8").strip())
|
|
631
|
+
|
|
632
|
+
def format(self) -> bool:
|
|
633
|
+
normalized_core = normalize_core(self.core)
|
|
634
|
+
|
|
635
|
+
if normalized_core in ("ESP32S3", "ESP32C6", "ESP32C5", "ESP32P4"):
|
|
636
|
+
command = """
|
|
637
|
+
import os
|
|
638
|
+
from flashbdev import bdev
|
|
639
|
+
os.umount('/')
|
|
640
|
+
os.VfsLfs2.mkfs(bdev)
|
|
641
|
+
os.mount(bdev, '/')
|
|
642
|
+
"""
|
|
643
|
+
elif normalized_core == "EFR32MG":
|
|
644
|
+
command = """
|
|
645
|
+
import os
|
|
646
|
+
os.format()
|
|
647
|
+
"""
|
|
648
|
+
elif normalized_core == "RP2350":
|
|
649
|
+
command = """
|
|
650
|
+
import os, rp2
|
|
651
|
+
try:
|
|
652
|
+
os.umount('/')
|
|
653
|
+
except Exception:
|
|
654
|
+
pass
|
|
655
|
+
bdev = rp2.Flash()
|
|
656
|
+
os.VfsLfs2.mkfs(bdev, progsize=256)
|
|
657
|
+
fs = os.VfsLfs2(bdev, progsize=256)
|
|
658
|
+
os.mount(fs, '/')
|
|
659
|
+
"""
|
|
660
|
+
elif normalized_core == "MIMXRT1062DVJ6A":
|
|
661
|
+
command = """
|
|
662
|
+
import os, mimxrt
|
|
663
|
+
try:
|
|
664
|
+
os.umount('/flash')
|
|
665
|
+
except Exception:
|
|
666
|
+
pass
|
|
667
|
+
bdev = mimxrt.Flash()
|
|
668
|
+
os.VfsLfs2.mkfs(bdev)
|
|
669
|
+
fs = os.VfsLfs2(bdev)
|
|
670
|
+
os.mount(fs, '/flash')
|
|
671
|
+
os.chdir('/flash')
|
|
672
|
+
"""
|
|
673
|
+
else:
|
|
674
|
+
return False
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
self.repl.exec(command)
|
|
678
|
+
except ProtocolError:
|
|
679
|
+
return False
|
|
680
|
+
return True
|
|
681
|
+
|
|
682
|
+
def get(self, remote: str, local: str = None) -> bytes:
|
|
683
|
+
import binascii as binascii_module
|
|
684
|
+
|
|
685
|
+
local_file = None
|
|
686
|
+
local_target = None
|
|
687
|
+
temp_path = None
|
|
688
|
+
content_parts = []
|
|
689
|
+
|
|
690
|
+
if local:
|
|
691
|
+
if os.path.isdir(local):
|
|
692
|
+
local = os.path.join(local, posixpath.basename(remote.rstrip('/')))
|
|
693
|
+
local_target = os.path.abspath(local)
|
|
694
|
+
|
|
695
|
+
bytes_read = 0
|
|
696
|
+
|
|
697
|
+
try:
|
|
698
|
+
file_size = self.state(remote)
|
|
699
|
+
|
|
700
|
+
if file_size == 0:
|
|
701
|
+
if not self._file_exists(remote):
|
|
702
|
+
raise ProtocolError(f"File not found: {remote}")
|
|
703
|
+
if local_target:
|
|
704
|
+
local_dir = os.path.dirname(local_target) or os.curdir
|
|
705
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
706
|
+
fd, temp_path = tempfile.mkstemp(
|
|
707
|
+
prefix=f".{os.path.basename(local_target)}.",
|
|
708
|
+
suffix=".tmp",
|
|
709
|
+
dir=local_dir,
|
|
710
|
+
)
|
|
711
|
+
os.close(fd)
|
|
712
|
+
os.replace(temp_path, local_target)
|
|
713
|
+
temp_path = None
|
|
714
|
+
return None
|
|
715
|
+
return b""
|
|
716
|
+
|
|
717
|
+
if local_target:
|
|
718
|
+
local_dir = os.path.dirname(local_target) or os.curdir
|
|
719
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
720
|
+
fd, temp_path = tempfile.mkstemp(
|
|
721
|
+
prefix=f".{os.path.basename(local_target)}.",
|
|
722
|
+
suffix=".tmp",
|
|
723
|
+
dir=local_dir,
|
|
724
|
+
)
|
|
725
|
+
local_file = os.fdopen(fd, "wb")
|
|
726
|
+
|
|
727
|
+
with self.repl.session():
|
|
728
|
+
init_command = f"""
|
|
729
|
+
import sys
|
|
730
|
+
import binascii
|
|
731
|
+
f = open({_python_string_literal(remote)}, 'rb')
|
|
732
|
+
"""
|
|
733
|
+
self.repl._exec(textwrap.dedent(init_command))
|
|
734
|
+
|
|
735
|
+
CHUNK_SIZE = self._GET_CHUNK_SIZE
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
while bytes_read < file_size:
|
|
739
|
+
remaining = min(CHUNK_SIZE, file_size - bytes_read)
|
|
740
|
+
|
|
741
|
+
read_cmd = f"""
|
|
742
|
+
chunk = f.read({remaining})
|
|
743
|
+
if chunk:
|
|
744
|
+
encoded = binascii.b2a_base64(chunk)
|
|
745
|
+
sys.stdout.write(encoded.decode('ascii'))
|
|
746
|
+
"""
|
|
747
|
+
encoded_data = self.repl._exec(textwrap.dedent(read_cmd))
|
|
748
|
+
|
|
749
|
+
if not encoded_data:
|
|
750
|
+
break
|
|
751
|
+
try:
|
|
752
|
+
cleaned_data = encoded_data.strip().replace(b'\r', b'').replace(b'\n', b'')
|
|
753
|
+
chunk_data = binascii_module.a2b_base64(cleaned_data)
|
|
754
|
+
except Exception as e:
|
|
755
|
+
raise ProtocolError(f"Failed to decode base64 data: {e}")
|
|
756
|
+
|
|
757
|
+
if not chunk_data:
|
|
758
|
+
break
|
|
759
|
+
if local_file is not None:
|
|
760
|
+
local_file.write(chunk_data)
|
|
761
|
+
else:
|
|
762
|
+
content_parts.append(chunk_data)
|
|
763
|
+
|
|
764
|
+
bytes_read += len(chunk_data)
|
|
765
|
+
finally:
|
|
766
|
+
self.repl._exec("f.close()")
|
|
767
|
+
|
|
768
|
+
if bytes_read != file_size:
|
|
769
|
+
raise ProtocolError(f"Download incomplete: got {bytes_read}/{file_size} bytes")
|
|
770
|
+
|
|
771
|
+
if local_file is not None:
|
|
772
|
+
local_file.flush()
|
|
773
|
+
os.fsync(local_file.fileno())
|
|
774
|
+
local_file.close()
|
|
775
|
+
local_file = None
|
|
776
|
+
os.replace(temp_path, local_target)
|
|
777
|
+
temp_path = None
|
|
778
|
+
|
|
779
|
+
except ProtocolError:
|
|
780
|
+
raise
|
|
781
|
+
except Exception as e:
|
|
782
|
+
raise ProtocolError(f"Download failed: {e}") from e
|
|
783
|
+
finally:
|
|
784
|
+
if local_file is not None:
|
|
785
|
+
local_file.close()
|
|
786
|
+
if temp_path:
|
|
787
|
+
try:
|
|
788
|
+
os.remove(temp_path)
|
|
789
|
+
except FileNotFoundError:
|
|
790
|
+
pass
|
|
791
|
+
|
|
792
|
+
if not local_target:
|
|
793
|
+
return b''.join(content_parts)
|
|
794
|
+
|
|
795
|
+
def put(self, local: str, remote: str, progress_callback=None):
|
|
796
|
+
sent = 0
|
|
797
|
+
needs_retry = False
|
|
798
|
+
file_opened = False
|
|
799
|
+
|
|
800
|
+
use_deflate = False
|
|
801
|
+
with self.repl.session():
|
|
802
|
+
try:
|
|
803
|
+
if self.core != "EFR32MG":
|
|
804
|
+
# Probe for the deflate module and define a helper that inflates
|
|
805
|
+
# raw-deflate (4 KiB window) chunks sent as base64.
|
|
806
|
+
out = self.repl._exec(
|
|
807
|
+
"import binascii\n"
|
|
808
|
+
"try:\n"
|
|
809
|
+
" import deflate,io\n"
|
|
810
|
+
f" def _w(d):f.write(deflate.DeflateIO(io.BytesIO(binascii.a2b_base64(d)),deflate.RAW,{_DEFLATE_WBITS}).read())\n"
|
|
811
|
+
" print('Z1')\n"
|
|
812
|
+
"except ImportError:\n"
|
|
813
|
+
" print('Z0')\n"
|
|
814
|
+
f"f = open({_python_string_literal(remote)}, 'wb')"
|
|
815
|
+
)
|
|
816
|
+
use_deflate = b'Z1' in out
|
|
817
|
+
else:
|
|
818
|
+
self.repl._exec(f"f = open({_python_string_literal(remote)}, 'wb')")
|
|
819
|
+
file_opened = True
|
|
820
|
+
except ProtocolError as e:
|
|
821
|
+
if "EEXIST" in str(e):
|
|
822
|
+
needs_retry = True
|
|
823
|
+
else:
|
|
824
|
+
raise
|
|
825
|
+
else:
|
|
826
|
+
try:
|
|
827
|
+
with open(local, "rb") as f:
|
|
828
|
+
total = os.fstat(f.fileno()).st_size
|
|
829
|
+
|
|
830
|
+
if progress_callback:
|
|
831
|
+
progress_callback(0, total)
|
|
832
|
+
|
|
833
|
+
batch_src_lines = []
|
|
834
|
+
batch_bytes = 0
|
|
835
|
+
DEVICE_CHUNK = self._DEVICE_CHUNK_SIZES
|
|
836
|
+
BATCH_LIMIT = max(1, int(self._PUT_BATCH_BYTES))
|
|
837
|
+
|
|
838
|
+
def _flush_batch():
|
|
839
|
+
nonlocal batch_src_lines, batch_bytes
|
|
840
|
+
if not batch_src_lines:
|
|
841
|
+
return
|
|
842
|
+
|
|
843
|
+
code = ";\n".join(batch_src_lines)
|
|
844
|
+
self.repl._exec(code)
|
|
845
|
+
batch_src_lines = []
|
|
846
|
+
batch_bytes = 0
|
|
847
|
+
|
|
848
|
+
if progress_callback:
|
|
849
|
+
progress_callback(sent, total)
|
|
850
|
+
|
|
851
|
+
while True:
|
|
852
|
+
chunk = f.read(DEVICE_CHUNK)
|
|
853
|
+
if not chunk:
|
|
854
|
+
_flush_batch()
|
|
855
|
+
break
|
|
856
|
+
|
|
857
|
+
line = f"f.write({repr(chunk)})"
|
|
858
|
+
if self.core != "EFR32MG":
|
|
859
|
+
encoded_line = f"f.write(binascii.a2b_base64({base64.b64encode(chunk)!r}))"
|
|
860
|
+
if len(encoded_line) < len(line):
|
|
861
|
+
line = encoded_line
|
|
862
|
+
if use_deflate:
|
|
863
|
+
z = zlib.compressobj(9, zlib.DEFLATED, -_DEFLATE_WBITS)
|
|
864
|
+
packed = z.compress(chunk) + z.flush()
|
|
865
|
+
deflate_line = f"_w({base64.b64encode(packed)!r})"
|
|
866
|
+
if len(deflate_line) < len(line):
|
|
867
|
+
line = deflate_line
|
|
868
|
+
batch_src_lines.append(line)
|
|
869
|
+
batch_bytes += len(line)
|
|
870
|
+
sent += len(chunk)
|
|
871
|
+
|
|
872
|
+
if batch_bytes >= BATCH_LIMIT:
|
|
873
|
+
_flush_batch()
|
|
874
|
+
|
|
875
|
+
self.repl._exec("f.close()")
|
|
876
|
+
file_opened = False
|
|
877
|
+
|
|
878
|
+
if progress_callback:
|
|
879
|
+
progress_callback(total, total)
|
|
880
|
+
|
|
881
|
+
except Exception:
|
|
882
|
+
if file_opened:
|
|
883
|
+
try:
|
|
884
|
+
self.repl._exec("try:\n f.close()\nexcept:\n pass")
|
|
885
|
+
except Exception:
|
|
886
|
+
pass
|
|
887
|
+
raise
|
|
888
|
+
|
|
889
|
+
if needs_retry:
|
|
890
|
+
self.rm(remote)
|
|
891
|
+
self.put(local, remote, progress_callback)
|
|
892
|
+
|
|
893
|
+
def getdir_batch(self, remote: str, local: str, progress_callback=None):
|
|
894
|
+
base_remote = remote.replace("\\", "/")
|
|
895
|
+
base_local = os.path.abspath(local)
|
|
896
|
+
os.makedirs(base_local, exist_ok=True)
|
|
897
|
+
|
|
898
|
+
try:
|
|
899
|
+
with self.repl.session():
|
|
900
|
+
try:
|
|
901
|
+
items = self.ls_recursive(base_remote)
|
|
902
|
+
except Exception as e:
|
|
903
|
+
raise ProtocolError(f"Failed to list remote directory: {e}") from e
|
|
904
|
+
return self._getdir_from_items(items, base_remote, base_local, progress_callback)
|
|
905
|
+
except ProtocolError:
|
|
906
|
+
raise
|
|
907
|
+
except Exception as e:
|
|
908
|
+
raise ProtocolError(f"Failed to download remote directory: {e}") from e
|
|
909
|
+
|
|
910
|
+
def _getdir_from_items(self, items, base_remote: str, base_local: str, progress_callback):
|
|
911
|
+
if not items:
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
file_specs = []
|
|
915
|
+
for item in items:
|
|
916
|
+
rel_path = item[0]
|
|
917
|
+
is_dir = item[2]
|
|
918
|
+
relative_path = rel_path.lstrip('/')
|
|
919
|
+
local_path = os.path.join(base_local, *relative_path.split('/'))
|
|
920
|
+
if is_dir:
|
|
921
|
+
os.makedirs(local_path, exist_ok=True)
|
|
922
|
+
continue
|
|
923
|
+
|
|
924
|
+
local_dir = os.path.dirname(local_path)
|
|
925
|
+
if local_dir and not os.path.exists(local_dir):
|
|
926
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
927
|
+
|
|
928
|
+
if rel_path.startswith('/'):
|
|
929
|
+
full_remote = rel_path
|
|
930
|
+
elif base_remote == '/':
|
|
931
|
+
full_remote = '/' + rel_path
|
|
932
|
+
else:
|
|
933
|
+
full_remote = base_remote + ('/' if not base_remote.endswith('/') else '') + rel_path
|
|
934
|
+
|
|
935
|
+
file_specs.append((full_remote, local_path))
|
|
936
|
+
|
|
937
|
+
if not file_specs:
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
total = len(file_specs)
|
|
941
|
+
for idx, (remote_file, local_file) in enumerate(file_specs):
|
|
942
|
+
if progress_callback:
|
|
943
|
+
progress_callback(idx, total, os.path.basename(remote_file))
|
|
944
|
+
|
|
945
|
+
try:
|
|
946
|
+
self.get(remote_file, local_file)
|
|
947
|
+
except Exception as e:
|
|
948
|
+
raise ProtocolError(f"Failed to download {remote_file}: {e}")
|
|
949
|
+
|
|
950
|
+
if progress_callback:
|
|
951
|
+
progress_callback(total, total, "Complete")
|
|
952
|
+
|
|
953
|
+
def putdir(self, local: str, remote: str):
|
|
954
|
+
import posixpath
|
|
955
|
+
|
|
956
|
+
base_local = os.path.abspath(local)
|
|
957
|
+
base_remote = remote.replace("\\", "/")
|
|
958
|
+
|
|
959
|
+
with self.repl.session():
|
|
960
|
+
for parent, child_dirs, child_files in os.walk(base_local, followlinks=False):
|
|
961
|
+
child_dirs[:] = [
|
|
962
|
+
name for name in child_dirs if name.lower() not in PUTDIR_EXCLUDED_DIRS
|
|
963
|
+
]
|
|
964
|
+
rel = os.path.relpath(parent, base_local).replace("\\", "/")
|
|
965
|
+
remote_parent = posixpath.normpath(
|
|
966
|
+
posixpath.join(base_remote, "" if rel == "." else rel)
|
|
967
|
+
)
|
|
968
|
+
if not self.mkdir(remote_parent):
|
|
969
|
+
raise FileSystemError(f"Could not create remote directory: {remote_parent}")
|
|
970
|
+
|
|
971
|
+
for filename in child_files:
|
|
972
|
+
if os.path.splitext(filename)[1].lower() in PUTDIR_EXCLUDED_EXTENSIONS:
|
|
973
|
+
continue
|
|
974
|
+
local_path = os.path.join(parent, filename)
|
|
975
|
+
remote_path = posixpath.join(remote_parent, filename)
|
|
976
|
+
self.put(local_path, remote_path)
|
|
977
|
+
|
|
978
|
+
def _normalize_remote_path(self, path: str) -> str:
|
|
979
|
+
root = posixpath.normpath(self.device_root_fs or '/')
|
|
980
|
+
path = (path or '/').replace('\\', '/')
|
|
981
|
+
|
|
982
|
+
if root == '/':
|
|
983
|
+
return posixpath.normpath('/' + path.lstrip('/'))
|
|
984
|
+
|
|
985
|
+
if path == root or path.startswith(root + '/'):
|
|
986
|
+
real_path = posixpath.normpath(path)
|
|
987
|
+
else:
|
|
988
|
+
real_path = posixpath.normpath(posixpath.join(root, path.lstrip('/')))
|
|
989
|
+
|
|
990
|
+
if real_path != root and not real_path.startswith(root + '/'):
|
|
991
|
+
raise FileSystemError("Remote path is outside the device filesystem root")
|
|
992
|
+
return real_path
|
|
993
|
+
|
|
994
|
+
def _print_progress_bar(self, current: int, total: int, bar_length: int = 40):
|
|
995
|
+
pct = 0 if total == 0 else min(1.0, current / total)
|
|
996
|
+
block = min(bar_length, int(round(bar_length * pct)))
|
|
997
|
+
bar = "#" * block + "-" * (bar_length - block)
|
|
998
|
+
percent = int(pct * 100)
|
|
999
|
+
print(f"\r[{bar}] {percent}% ({current}/{total})", end="", flush=True)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def create_storage(repl_protocol, core: str = "RP2350", device: str = "", device_root_fs: str = "/") -> DeviceStorage:
|
|
1003
|
+
return DeviceStorage(
|
|
1004
|
+
repl_protocol,
|
|
1005
|
+
core=core,
|
|
1006
|
+
device=device,
|
|
1007
|
+
device_root_fs=device_root_fs
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
SerialStorage = DeviceStorage
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
__all__ = [
|
|
1015
|
+
'DeviceStorage',
|
|
1016
|
+
'SerialStorage',
|
|
1017
|
+
'create_storage',
|
|
1018
|
+
]
|