Skip to content

Commit db59e55

Browse files
jimmodpgeorge
authored andcommitted
tools/mpremote: Make filesystem commands use transport API.
This introduces a Python filesystem API on `Transport` that is implemented entirely with eval/exec provided by the underlying transport subclass. Updates existing mpremote filesystem commands (and `edit) to use this API. Also re-implements recursive `cp` to allow arbitrary source / destination. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <[email protected]> Signed-off-by: Damien George <[email protected]>
1 parent 1091021 commit db59e55

File tree

5 files changed

+379
-305
lines changed

5 files changed

+379
-305
lines changed

tools/mpremote/mpremote/commands.py

Lines changed: 229 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
import serial.tools.list_ports
66

7-
from .transport import TransportError
8-
from .transport_serial import SerialTransport, stdout_write_bytes
7+
from .transport import TransportError, stdout_write_bytes
8+
from .transport_serial import SerialTransport
99

1010

1111
class CommandError(Exception):
@@ -106,61 +106,238 @@ def show_progress_bar(size, total_size, op="copying"):
106106
)
107107

108108

109+
def _remote_path_join(a, *b):
110+
if not a:
111+
a = "./"
112+
result = a.rstrip("/")
113+
for x in b:
114+
result += "/" + x.strip("/")
115+
return result
116+
117+
118+
def _remote_path_dirname(a):
119+
a = a.rsplit("/", 1)
120+
if len(a) == 1:
121+
return ""
122+
else:
123+
return a[0]
124+
125+
126+
def _remote_path_basename(a):
127+
return a.rsplit("/", 1)[-1]
128+
129+
130+
def do_filesystem_cp(state, src, dest, multiple):
131+
if dest.startswith(":"):
132+
dest_exists = state.transport.fs_exists(dest[1:])
133+
dest_isdir = dest_exists and state.transport.fs_isdir(dest[1:])
134+
else:
135+
dest_exists = os.path.exists(dest)
136+
dest_isdir = dest_exists and os.path.isdir(dest)
137+
138+
if multiple:
139+
if not dest_exists:
140+
raise CommandError("cp: destination does not exist")
141+
if not dest_isdir:
142+
raise CommandError("cp: destination is not a directory")
143+
144+
# Download the contents of source.
145+
try:
146+
if src.startswith(":"):
147+
data = state.transport.fs_readfile(src[1:], progress_callback=show_progress_bar)
148+
filename = _remote_path_basename(src[1:])
149+
else:
150+
with open(src, "rb") as f:
151+
data = f.read()
152+
filename = os.path.basename(src)
153+
except IsADirectoryError:
154+
raise CommandError("cp: -r not specified; omitting directory")
155+
156+
# Write back to dest.
157+
if dest.startswith(":"):
158+
# If the destination path is just the directory, then add the source filename.
159+
if dest_isdir:
160+
dest = ":" + _remote_path_join(dest[1:], filename)
161+
162+
# Write to remote.
163+
state.transport.fs_writefile(dest[1:], data, progress_callback=show_progress_bar)
164+
else:
165+
# If the destination path is just the directory, then add the source filename.
166+
if dest_isdir:
167+
dest = os.path.join(dest, filename)
168+
169+
# Write to local file.
170+
with open(dest, "wb") as f:
171+
f.write(data)
172+
173+
174+
def do_filesystem_recursive_cp(state, src, dest, multiple):
175+
# Ignore trailing / on both src and dest. (Unix cp ignores them too)
176+
src = src.rstrip("/" + os.path.sep + (os.path.altsep if os.path.altsep else ""))
177+
dest = dest.rstrip("/" + os.path.sep + (os.path.altsep if os.path.altsep else ""))
178+
179+
# If the destination directory exists, then we copy into it. Otherwise we
180+
# use the destination as the target.
181+
if dest.startswith(":"):
182+
dest_exists = state.transport.fs_exists(dest[1:])
183+
else:
184+
dest_exists = os.path.exists(dest)
185+
186+
# Recursively find all files to copy from a directory.
187+
# `dirs` will be a list of dest split paths.
188+
# `files` will be a list of `(dest split path, src joined path)`.
189+
dirs = []
190+
files = []
191+
192+
# For example, if src=/tmp/foo, with /tmp/foo/x.py and /tmp/foo/a/b/c.py,
193+
# and if the destination directory exists, then we will have:
194+
# dirs = [['foo'], ['foo', 'a'], ['foo', 'a', 'b']]
195+
# files = [(['foo', 'x.py'], '/tmp/foo/x.py'), (['foo', 'a', 'b', 'c.py'], '/tmp/foo/a/b/c.py')]
196+
# If the destination doesn't exist, then we will have:
197+
# dirs = [['a'], ['a', 'b']]
198+
# files = [(['x.py'], '/tmp/foo/x.py'), (['a', 'b', 'c.py'], '/tmp/foo/a/b/c.py')]
199+
200+
def _list_recursive(base, src_path, dest_path, src_join_fun, src_isdir_fun, src_listdir_fun):
201+
src_path_joined = src_join_fun(base, *src_path)
202+
if src_isdir_fun(src_path_joined):
203+
if dest_path:
204+
dirs.append(dest_path)
205+
for entry in src_listdir_fun(src_path_joined):
206+
_list_recursive(
207+
base,
208+
src_path + [entry],
209+
dest_path + [entry],
210+
src_join_fun,
211+
src_isdir_fun,
212+
src_listdir_fun,
213+
)
214+
else:
215+
files.append(
216+
(
217+
dest_path,
218+
src_path_joined,
219+
)
220+
)
221+
222+
if src.startswith(":"):
223+
src_dirname = [_remote_path_basename(src[1:])]
224+
dest_dirname = src_dirname if dest_exists else []
225+
_list_recursive(
226+
_remote_path_dirname(src[1:]),
227+
src_dirname,
228+
dest_dirname,
229+
src_join_fun=_remote_path_join,
230+
src_isdir_fun=state.transport.fs_isdir,
231+
src_listdir_fun=lambda p: [x.name for x in state.transport.fs_listdir(p)],
232+
)
233+
else:
234+
src_dirname = [os.path.basename(src)]
235+
dest_dirname = src_dirname if dest_exists else []
236+
_list_recursive(
237+
os.path.dirname(src),
238+
src_dirname,
239+
dest_dirname,
240+
src_join_fun=os.path.join,
241+
src_isdir_fun=os.path.isdir,
242+
src_listdir_fun=os.listdir,
243+
)
244+
245+
# If no directories were encountered then we must have just had a file.
246+
if not dirs:
247+
return do_filesystem_cp(state, src, dest, multiple)
248+
249+
def _mkdir(a, *b):
250+
try:
251+
if a.startswith(":"):
252+
state.transport.fs_mkdir(_remote_path_join(a[1:], *b))
253+
else:
254+
os.mkdir(os.path.join(a, *b))
255+
except FileExistsError:
256+
pass
257+
258+
# Create the destination if necessary.
259+
if not dest_exists:
260+
_mkdir(dest)
261+
262+
# Create all sub-directories relative to the destination.
263+
for d in dirs:
264+
_mkdir(dest, *d)
265+
266+
# Copy all files, in sorted order to help it be deterministic.
267+
files.sort()
268+
for dest_path_split, src_path_joined in files:
269+
if src.startswith(":"):
270+
src_path_joined = ":" + src_path_joined
271+
272+
if dest.startswith(":"):
273+
dest_path_joined = ":" + _remote_path_join(dest[1:], *dest_path_split)
274+
else:
275+
dest_path_joined = os.path.join(dest, *dest_path_split)
276+
277+
do_filesystem_cp(state, src_path_joined, dest_path_joined, multiple=False)
278+
279+
109280
def do_filesystem(state, args):
110281
state.ensure_raw_repl()
111282
state.did_action()
112283

113-
def _list_recursive(files, path):
114-
if os.path.isdir(path):
115-
for entry in os.listdir(path):
116-
_list_recursive(files, "/".join((path, entry)))
117-
else:
118-
files.append(os.path.split(path))
119-
120284
command = args.command[0]
121285
paths = args.path
122286

123287
if command == "cat":
124-
# Don't be verbose by default when using cat, so output can be
125-
# redirected to something.
288+
# Don't do verbose output for `cat` unless explicitly requested.
126289
verbose = args.verbose is True
127290
else:
128291
verbose = args.verbose is not False
129292

130-
if command == "cp" and args.recursive:
131-
if paths[-1] != ":":
132-
raise CommandError("'cp -r' destination must be ':'")
133-
paths.pop()
134-
src_files = []
135-
for path in paths:
136-
if path.startswith(":"):
137-
raise CommandError("'cp -r' source files must be local")
138-
_list_recursive(src_files, path)
139-
known_dirs = {""}
140-
state.transport.exec("import os")
141-
for dir, file in src_files:
142-
dir_parts = dir.split("/")
143-
for i in range(len(dir_parts)):
144-
d = "/".join(dir_parts[: i + 1])
145-
if d not in known_dirs:
146-
state.transport.exec(
147-
"try:\n os.mkdir('%s')\nexcept OSError as e:\n print(e)" % d
148-
)
149-
known_dirs.add(d)
150-
state.transport.filesystem_command(
151-
["cp", "/".join((dir, file)), ":" + dir + "/"],
152-
progress_callback=show_progress_bar,
153-
verbose=verbose,
154-
)
293+
if command == "cp":
294+
# Note: cp requires the user to specify local/remote explicitly via
295+
# leading ':'.
296+
297+
# The last argument must be the destination.
298+
if len(paths) <= 1:
299+
raise CommandError("cp: missing destination path")
300+
cp_dest = paths[-1]
301+
paths = paths[:-1]
155302
else:
156-
if args.recursive:
157-
raise CommandError("'-r' only supported for 'cp'")
158-
try:
159-
state.transport.filesystem_command(
160-
[command] + paths, progress_callback=show_progress_bar, verbose=verbose
161-
)
162-
except OSError as er:
163-
raise CommandError(er)
303+
# All other commands implicitly use remote paths. Strip the
304+
# leading ':' if the user included them.
305+
paths = [path[1:] if path.startswith(":") else path for path in paths]
306+
307+
# ls implicitly lists the cwd.
308+
if command == "ls" and not paths:
309+
paths = [""]
310+
311+
# Handle each path sequentially.
312+
for path in paths:
313+
if verbose:
314+
if command == "cp":
315+
print("{} {} {}".format(command, path, cp_dest))
316+
else:
317+
print("{} :{}".format(command, path))
318+
319+
if command == "cat":
320+
state.transport.fs_printfile(path)
321+
elif command == "ls":
322+
for result in state.transport.fs_listdir(path):
323+
print(
324+
"{:12} {}{}".format(
325+
result.st_size, result.name, "/" if result.st_mode & 0x4000 else ""
326+
)
327+
)
328+
elif command == "mkdir":
329+
state.transport.fs_mkdir(path)
330+
elif command == "rm":
331+
state.transport.fs_rmfile(path)
332+
elif command == "rmdir":
333+
state.transport.fs_rmdir(path)
334+
elif command == "touch":
335+
state.transport.fs_touchfile(path)
336+
elif command == "cp":
337+
if args.recursive:
338+
do_filesystem_recursive_cp(state, path, cp_dest, len(paths) > 1)
339+
else:
340+
do_filesystem_cp(state, path, cp_dest, len(paths) > 1)
164341

165342

166343
def do_edit(state, args):
@@ -174,11 +351,15 @@ def do_edit(state, args):
174351
dest_fd, dest = tempfile.mkstemp(suffix=os.path.basename(src))
175352
try:
176353
print("edit :%s" % (src,))
177-
os.close(dest_fd)
178-
state.transport.fs_touch(src)
179-
state.transport.fs_get(src, dest, progress_callback=show_progress_bar)
354+
state.transport.fs_touchfile(src)
355+
data = state.transport.fs_readfile(src, progress_callback=show_progress_bar)
356+
with open(dest_fd, "wb") as f:
357+
f.write(data)
180358
if os.system('%s "%s"' % (os.getenv("EDITOR"), dest)) == 0:
181-
state.transport.fs_put(dest, src, progress_callback=show_progress_bar)
359+
with open(dest, "rb") as f:
360+
state.transport.fs_writefile(
361+
src, f.read(), progress_callback=show_progress_bar
362+
)
182363
finally:
183364
os.unlink(dest)
184365

tools/mpremote/mpremote/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def argparse_filesystem():
190190
"enable verbose output (defaults to True for all commands except cat)",
191191
)
192192
cmd_parser.add_argument(
193-
"command", nargs=1, help="filesystem command (e.g. cat, cp, ls, rm, touch)"
193+
"command", nargs=1, help="filesystem command (e.g. cat, cp, ls, rm, rmdir, touch)"
194194
)
195195
cmd_parser.add_argument("path", nargs="+", help="local and remote paths")
196196
return cmd_parser

tools/mpremote/mpremote/mip.py

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@
1212

1313

1414
_PACKAGE_INDEX = "https://micropython.org/pi/v2"
15-
_CHUNK_SIZE = 128
1615

1716

1817
# This implements os.makedirs(os.dirname(path))
1918
def _ensure_path_exists(transport, path):
20-
import os
21-
2219
split = path.split("/")
2320

2421
# Handle paths starting with "/".
@@ -34,22 +31,6 @@ def _ensure_path_exists(transport, path):
3431
prefix += "/"
3532

3633

37-
# Copy from src (stream) to dest (function-taking-bytes)
38-
def _chunk(src, dest, length=None, op="downloading"):
39-
buf = memoryview(bytearray(_CHUNK_SIZE))
40-
total = 0
41-
if length:
42-
show_progress_bar(0, length, op)
43-
while True:
44-
n = src.readinto(buf)
45-
if n == 0:
46-
break
47-
dest(buf if n == _CHUNK_SIZE else buf[:n])
48-
total += n
49-
if length:
50-
show_progress_bar(total, length, op)
51-
52-
5334
def _rewrite_url(url, branch=None):
5435
if not branch:
5536
branch = "HEAD"
@@ -83,15 +64,10 @@ def _rewrite_url(url, branch=None):
8364
def _download_file(transport, url, dest):
8465
try:
8566
with urllib.request.urlopen(url) as src:
86-
fd, path = tempfile.mkstemp()
87-
try:
88-
print("Installing:", dest)
89-
with os.fdopen(fd, "wb") as f:
90-
_chunk(src, f.write, src.length)
91-
_ensure_path_exists(transport, dest)
92-
transport.fs_put(path, dest, progress_callback=show_progress_bar)
93-
finally:
94-
os.unlink(path)
67+
data = src.read()
68+
print("Installing:", dest)
69+
_ensure_path_exists(transport, dest)
70+
transport.fs_writefile(dest, data, progress_callback=show_progress_bar)
9571
except urllib.error.HTTPError as e:
9672
if e.status == 404:
9773
raise CommandError(f"File not found: {url}")

0 commit comments

Comments
 (0)