You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

311 lines
13 KiB
Python

# Adafruit MicroPython Tool - File Operations
# Author: Tony DiCola
# Copyright (c) 2016 Adafruit Industries
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import ast
import textwrap
from ampy.pyboard import PyboardError
BUFFER_SIZE = 32 # Amount of data to read or write to the serial port at a time.
# This is kept small because small chips and USB to serial
# bridges usually have very small buffers.
class DirectoryExistsError(Exception):
pass
class Files(object):
"""Class to interact with a MicroPython board files over a serial connection.
Provides functions for listing, uploading, and downloading files from the
board's filesystem.
"""
def __init__(self, pyboard):
"""Initialize the MicroPython board files class using the provided pyboard
instance. In most cases you should create a Pyboard instance (from
pyboard.py) which connects to a board over a serial connection and pass
it in, but you can pass in other objects for testing, etc.
"""
self._pyboard = pyboard
def get(self, filename):
"""Retrieve the contents of the specified file and return its contents
as a byte string.
"""
# Open the file and read it a few bytes at a time and print out the
# raw bytes. Be careful not to overload the UART buffer so only write
# a few bytes at a time, and don't use print since it adds newlines and
# expects string data.
command = """
import sys
with open('{0}', 'rb') as infile:
while True:
result = infile.read({1})
if result == b'':
break
len = sys.stdout.write(result)
""".format(
filename, BUFFER_SIZE
)
self._pyboard.enter_raw_repl()
try:
out = self._pyboard.exec_(textwrap.dedent(command))
except PyboardError as ex:
# Check if this is an OSError #2, i.e. file doesn't exist and
# rethrow it as something more descriptive.
if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
raise RuntimeError("No such file: {0}".format(filename))
else:
raise ex
self._pyboard.exit_raw_repl()
return out
def ls(self, directory="/", long_format=True, recursive=False):
"""List the contents of the specified directory (or root if none is
specified). Returns a list of strings with the names of files in the
specified directory. If long_format is True then a list of 2-tuples
with the name and size (in bytes) of the item is returned. Note that
it appears the size of directories is not supported by MicroPython and
will always return 0 (i.e. no recursive size computation).
"""
# Disabling for now, see https://github.com/adafruit/ampy/issues/55.
# # Make sure directory ends in a slash.
# if not directory.endswith("/"):
# directory += "/"
# Make sure directory starts with slash, for consistency.
if not directory.startswith("/"):
directory = "/" + directory
command = """\
try:
import os
except ImportError:
import uos as os\n"""
if recursive:
command += """\
def listdir(directory):
result = set()
def _listdir(dir_or_file):
try:
# if its a directory, then it should provide some children.
children = os.listdir(dir_or_file)
except OSError:
# probably a file. run stat() to confirm.
os.stat(dir_or_file)
result.add(dir_or_file)
else:
# probably a directory, add to result if empty.
if children:
# queue the children to be dealt with in next iteration.
for child in children:
# create the full path.
if dir_or_file == '/':
next = dir_or_file + child
else:
next = dir_or_file + '/' + child
_listdir(next)
else:
result.add(dir_or_file)
_listdir(directory)
return sorted(result)\n"""
else:
command += """\
def listdir(directory):
if directory == '/':
return sorted([directory + f for f in os.listdir(directory)])
else:
return sorted([directory + '/' + f for f in os.listdir(directory)])\n"""
# Execute os.listdir() command on the board.
if long_format:
command += """
r = []
for f in listdir('{0}'):
size = os.stat(f)[6]
r.append('{{0}} - {{1}} bytes'.format(f, size))
print(r)
""".format(
directory
)
else:
command += """
print(listdir('{0}'))
""".format(
directory
)
self._pyboard.enter_raw_repl()
try:
out = self._pyboard.exec_(textwrap.dedent(command))
except PyboardError as ex:
# Check if this is an OSError #2, i.e. directory doesn't exist and
# rethrow it as something more descriptive.
if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
raise RuntimeError("No such directory: {0}".format(directory))
else:
raise ex
self._pyboard.exit_raw_repl()
# Parse the result list and return it.
return ast.literal_eval(out.decode("utf-8"))
def mkdir(self, directory, exists_okay=False):
"""Create the specified directory. Note this cannot create a recursive
hierarchy of directories, instead each one should be created separately.
"""
# Execute os.mkdir command on the board.
command = """
try:
import os
except ImportError:
import uos as os
os.mkdir('{0}')
""".format(
directory
)
self._pyboard.enter_raw_repl()
try:
out = self._pyboard.exec_(textwrap.dedent(command))
except PyboardError as ex:
# Check if this is an OSError #17, i.e. directory already exists.
if ex.args[2].decode("utf-8").find("OSError: [Errno 17] EEXIST") != -1:
if not exists_okay:
raise DirectoryExistsError(
"Directory already exists: {0}".format(directory)
)
else:
raise ex
self._pyboard.exit_raw_repl()
def put(self, filename, data):
"""Create or update the specified file with the provided data.
"""
# Open the file for writing on the board and write chunks of data.
self._pyboard.enter_raw_repl()
self._pyboard.exec_("f = open('{0}', 'wb')".format(filename))
size = len(data)
# Loop through and write a buffer size chunk of data at a time.
for i in range(0, size, BUFFER_SIZE):
chunk_size = min(BUFFER_SIZE, size - i)
chunk = repr(data[i : i + chunk_size])
# Make sure to send explicit byte strings (handles python 2 compatibility).
if not chunk.startswith("b"):
chunk = "b" + chunk
self._pyboard.exec_("f.write({0})".format(chunk))
self._pyboard.exec_("f.close()")
self._pyboard.exit_raw_repl()
def rm(self, filename):
"""Remove the specified file or directory."""
command = """
try:
import os
except ImportError:
import uos as os
os.remove('{0}')
""".format(
filename
)
self._pyboard.enter_raw_repl()
try:
out = self._pyboard.exec_(textwrap.dedent(command))
except PyboardError as ex:
message = ex.args[2].decode("utf-8")
# Check if this is an OSError #2, i.e. file/directory doesn't exist
# and rethrow it as something more descriptive.
if message.find("OSError: [Errno 2] ENOENT") != -1:
raise RuntimeError("No such file/directory: {0}".format(filename))
# Check for OSError #13, the directory isn't empty.
if message.find("OSError: [Errno 13] EACCES") != -1:
raise RuntimeError("Directory is not empty: {0}".format(filename))
else:
raise ex
self._pyboard.exit_raw_repl()
def rmdir(self, directory, missing_okay=False):
"""Forcefully remove the specified directory and all its children."""
# Build a script to walk an entire directory structure and delete every
# file and subfolder. This is tricky because MicroPython has no os.walk
# or similar function to walk folders, so this code does it manually
# with recursion and changing directories. For each directory it lists
# the files and deletes everything it can, i.e. all the files. Then
# it lists the files again and assumes they are directories (since they
# couldn't be deleted in the first pass) and recursively clears those
# subdirectories. Finally when finished clearing all the children the
# parent directory is deleted.
command = """
try:
import os
except ImportError:
import uos as os
def rmdir(directory):
os.chdir(directory)
for f in os.listdir():
try:
os.remove(f)
except OSError:
pass
for f in os.listdir():
rmdir(f)
os.chdir('..')
os.rmdir(directory)
rmdir('{0}')
""".format(
directory
)
self._pyboard.enter_raw_repl()
try:
out = self._pyboard.exec_(textwrap.dedent(command))
except PyboardError as ex:
message = ex.args[2].decode("utf-8")
# Check if this is an OSError #2, i.e. directory doesn't exist
# and rethrow it as something more descriptive.
if message.find("OSError: [Errno 2] ENOENT") != -1:
if not missing_okay:
raise RuntimeError("No such directory: {0}".format(directory))
else:
raise ex
self._pyboard.exit_raw_repl()
def run(self, filename, wait_output=True):
"""Run the provided script and return its output. If wait_output is True
(default) then wait for the script to finish and then print its output,
otherwise just run the script and don't wait for any output.
"""
self._pyboard.enter_raw_repl()
out = None
if wait_output:
# Run the file and wait for output to return.
out = self._pyboard.execfile(filename)
else:
# Read the file and run it using lower level pyboard functions that
# won't wait for it to finish or return output.
with open(filename, "rb") as infile:
self._pyboard.exec_raw_no_follow(infile.read())
self._pyboard.exit_raw_repl()
return out