Refactored code using decorators and descriptors

time for ls -lR / reduced from 45s to 12s
Added write support (touch, rm, mkdir, cp host->device)
This commit is contained in:
Kovid Goyal 2006-11-09 01:33:51 +00:00
parent 7e1ca9880b
commit 6f1767a717
4 changed files with 478 additions and 236 deletions

View File

@ -20,6 +20,6 @@ the following rule in C{/etc/udev/rules.d/90-local.rules} ::
BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="plugdev" BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="plugdev"
You may have to adjust the GROUP and the location of the rules file to suit your distribution. You may have to adjust the GROUP and the location of the rules file to suit your distribution.
""" """
VERSION = "0.1.1" VERSION = "0.2"
__docformat__ = "epytext" __docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -44,7 +44,7 @@ Contains the logic for communication with the device (a SONY PRS-500).
The public interface of class L{PRS500Device} defines the methods for performing various tasks. The public interface of class L{PRS500Device} defines the methods for performing various tasks.
""" """
import usb, sys import usb, sys, os, time
from array import array from array import array
from prstypes import * from prstypes import *
@ -52,12 +52,13 @@ from errors import *
MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output
_packet_number = 0 #: Keep track of the packet number of packet tracing _packet_number = 0 #: Keep track of the packet number of packet tracing
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L] #: Protocol versions libprs500 has been tested with
def _log_packet(packet, header, stream=sys.stderr): def _log_packet(packet, header, stream=sys.stderr):
""" Log C{packet} to stream C{stream}. Header should be a small word describing the type of packet. """ """ Log C{packet} to stream C{stream}. Header should be a small word describing the type of packet. """
global _packet_number global _packet_number
_packet_number += 1 _packet_number += 1
print >>stream, header, "(Packet #", str(_packet_number) + ")\n" print >>stream, str(_packet_number), header, "Type:", packet.__class__.__name__
print >>stream, packet print >>stream, packet
print >>stream, "--" print >>stream, "--"
@ -76,7 +77,10 @@ class File(object):
def __repr__(self): def __repr__(self):
""" Return path to self """ """ Return path to self """
return self.path return "File:"+self.path
def __str__(self):
return self.name
class DeviceDescriptor: class DeviceDescriptor:
@ -124,6 +128,34 @@ class PRS500Device(object):
PRS500_BULK_IN_EP = 0x81 #: Endpoint for Bulk reads PRS500_BULK_IN_EP = 0x81 #: Endpoint for Bulk reads
PRS500_BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes PRS500_BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes
def safe(func):
"""
Decorator that wraps a call to C{func} to ensure that exceptions are handled correctly.
As a convenience, C{safe} automatically sends the a L{USBConnect} after calling func, unless func has
a keyword argument named C{end_session} set to C{False}.
An L{ArgumentError} will cause the L{USBConnect} command to be sent to the device, unless end_session is set to C{False}.
An L{usb.USBError} will cause the library to release control of the USB interface via a call to L{close}.
"""
def run_session(*args, **kwargs):
dev = args[0]
res = None
try:
res = func(*args, **kwargs)
except ArgumentError, e:
if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev._send_validated_command(USBConnect())
raise e
except usb.USBError, e:
dev.close()
raise e
if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev._send_validated_command(USBConnect())
return res
return run_session
def __init__(self, log_packets=False) : def __init__(self, log_packets=False) :
""" @param log_packets: If true the packet stream to/from the device is logged """ """ @param log_packets: If true the packet stream to/from the device is logged """
self.device_descriptor = DeviceDescriptor(PRS500Device.SONY_VENDOR_ID, self.device_descriptor = DeviceDescriptor(PRS500Device.SONY_VENDOR_ID,
@ -143,7 +175,9 @@ class PRS500Device(object):
def open(self) : def open(self) :
""" """
Claim an interface on the device for communication. Requires write privileges to the device file. Claim an interface on the device for communication. Requires write privileges to the device file.
Also initialize the device. See the source code for the sequenceof initialization commands.
@todo: Implement unlocking of the device
@todo: Check this on Mac OSX @todo: Check this on Mac OSX
""" """
self.device = self.device_descriptor.getDevice() self.device = self.device_descriptor.getDevice()
@ -152,11 +186,22 @@ class PRS500Device(object):
sys.exit(1) sys.exit(1)
self.handle = self.device.open() self.handle = self.device.open()
if sys.platform == 'darwin' : if sys.platform == 'darwin' :
# XXX : For some reason, Mac OS X doesn't set the # For some reason, Mac OS X doesn't set the
# configuration automatically like Linux does. # configuration automatically like Linux does.
self.handle.setConfiguration(1) # TODO: Check on Mac OSX self.handle.setConfiguration(1)
self.handle.claimInterface(self.device_descriptor.interface_id) self.handle.claimInterface(self.device_descriptor.interface_id)
self.handle.reset() self.handle.reset()
res = self._send_validated_command(GetUSBProtocolVersion())
if res.code != 0: raise ProtocolError("Unable to get USB Protocol version.")
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
if version not in KNOWN_USB_PROTOCOL_VERSIONS:
print >>sys.stderr, "WARNING: Usb protocol version " + hex(version) + " is unknown"
res = self._send_validated_command(SetBulkSize(size=0x028000))
if res.code != 0: raise ProtocolError("Unable to set bulk size.")
self._send_validated_command(UnlockDevice(key=0x312d))
if res.code != 0:
raise ProtocolError("Unlocking of device not implemented. Remove locking and retry.")
def close(self): def close(self):
""" Release device interface """ """ Release device interface """
@ -189,115 +234,113 @@ class PRS500Device(object):
PRS500Device._validate_response(res, type=command.type, number=cnumber) PRS500Device._validate_response(res, type=command.type, number=cnumber)
return res return res
def _bulk_read_packet(self, data_type=Answer, size=4096): def _bulk_write(self, data, packet_size=0x1000):
""" """
Read in a data packet via a Bulk Read. Send data to device via a bulk transfer.
@type data: Any listable type supporting __getslice__
@param packet_size: Size of packets to be sent to device. C{data} is broken up into packets to be sent to device.
"""
def bulk_write_packet(packet):
self.handle.bulkWrite(PRS500Device.PRS500_BULK_OUT_EP, packet)
if self._log_packets: _log_packet(Answer(packet), "Answer h->d")
@param data_type: an object of type type. The data packet is returned as an object of type C{data_type}. bytes_left = len(data)
@param size: the expected size of the data packet. if bytes_left + 16 <= packet_size:
packet_size = bytes_left +16
first_packet = Answer(bytes_left+16)
first_packet[16:] = data
first_packet.length = len(data)
else:
first_packet = Answer(packet_size)
first_packet[16:] = data[0:packet_size-16]
first_packet.length = packet_size-16
first_packet.number = 0x10005
bulk_write_packet(first_packet)
pos = first_packet.length
bytes_left -= first_packet.length
while bytes_left > 0:
endpos = pos + packet_size if pos + packet_size <= len(data) else len(data)
bulk_write_packet(data[pos:endpos])
bytes_left -= endpos - pos
pos = endpos
res = Response(self.handle.controlMsg(0xc0, 0x81, Response.SIZE, timeout=5000))
if self._log_packets: _log_packet(res, "Response")
if res.rnumber != 0x10005 or res.code != 0:
raise ProtocolError("Sending via Bulk Transfer failed with response:\n"+str(res))
if res.data_size != len(data):
raise ProtocolError("Unable to transfer all data to device. Response packet:\n"+str(res))
def _bulk_read(self, bytes, command_number=0x00, packet_size=4096, data_type=Answer):
""" """
Read in C{bytes} bytes via a bulk transfer in packets of size S{<=} C{packet_size}
@param data_type: an object of type type. The data packet is returned as an object of type C{data_type}.
@return: A list of packets read from the device. Each packet is of type data_type
"""
def bulk_read_packet(data_type=Answer, size=0x1000):
data = data_type(self.handle.bulkRead(PRS500Device.PRS500_BULK_IN_EP, size)) data = data_type(self.handle.bulkRead(PRS500Device.PRS500_BULK_IN_EP, size))
if self._log_packets: _log_packet(data, "Answer d->h") if self._log_packets: _log_packet(data, "Answer d->h")
return data return data
def _bulk_read(self, bytes, command_number=0x00, packet_size=4096, data_type=Answer):
""" Read in C{bytes} bytes via a bulk transfer in packets of size S{<=} C{packet_size} """
bytes_left = bytes bytes_left = bytes
packets = [] packets = []
while bytes_left > 0: while bytes_left > 0:
if packet_size > bytes_left: packet_size = bytes_left if packet_size > bytes_left: packet_size = bytes_left
packet = self._bulk_read_packet(data_type=data_type, size=packet_size) packet = bulk_read_packet(data_type=data_type, size=packet_size)
bytes_left -= len(packet) bytes_left -= len(packet)
packets.append(packet) packets.append(packet)
self._send_validated_command(AcknowledgeBulkRead(packets[0].number), cnumber=command_number) self._send_validated_command(AcknowledgeBulkRead(packets[0].number), cnumber=command_number)
return packets return packets
def _test_bulk_reads(self): @safe
""" Carries out a test of bulk reading as part of session initialization. """ def get_device_information(self, end_session=True):
self._send_validated_command( ShortCommand(number=0x00, type=0x01, command=0x00) )
self._bulk_read(24, command_number=0x00)
def _start_session(self):
""" """
Send the initialization sequence to the device. See the code for details. Ask device for device information. See L{DeviceInfoQuery}.
This method should be called before any real work is done. Though most things seem to work without it. @return: (device name, device version, software version on device, mime type)
""" """
self.handle.reset()
self._test_bulk_reads()
self._send_validated_command( ShortCommand(number=0x0107, command=0x028000, type=0x01) ) # TODO: Figure out the meaning of this command
self._test_bulk_reads()
self._send_validated_command( ShortCommand(number=0x0106, type=0x01, command=0x312d) ) # TODO: Figure out the meaning of this command
self._send_validated_command( ShortCommand(number=0x01, type=0x01, command=0x01) )
def _end_session(self):
""" Send the end session command to the device. Causes the device to change status from "Do not disconnect" to "USB Connected" """
self._send_validated_command( ShortCommand(number=0x01, type=0x01, command=0x00) )
def _run_session(self, *args):
"""
Wrapper that automatically calls L{_start_session} and L{_end_session}.
@param args: An array whose first element is the method to call and whose remaining arguments are passed to that mathos as an array.
"""
self._start_session()
res = None
try:
res = args[0](args[1:])
except ArgumentError, e:
self._end_session()
raise e
self._end_session()
return res
def _get_device_information(self, args):
""" Ask device for device information. See L{DeviceInfoQuery}. """
size = self._send_validated_command(DeviceInfoQuery()).data[2] + 16 size = self._send_validated_command(DeviceInfoQuery()).data[2] + 16
data = self._bulk_read(size, command_number=DeviceInfoQuery.NUMBER, data_type=DeviceInfo)[0] data = self._bulk_read(size, command_number=DeviceInfoQuery.NUMBER, data_type=DeviceInfo)[0]
return (data.device_name, data.device_version, data.software_version, data.mime_type) return (data.device_name, data.device_version, data.software_version, data.mime_type)
def get_device_information(self): @safe
""" Return (device name, device version, software version on device, mime type). See L{_get_device_information} """ def path_properties(self, path, end_session=True):
return self._run_session(self._get_device_information) """ Send command asking device for properties of C{path}. Return L{FileProperties}. """
def _get_path_properties(self, path):
""" Send command asking device for properties of C{path}. Return (L{Response}, L{Answer}). """
res = self._send_validated_command(PathQuery(path), response_type=ListResponse) res = self._send_validated_command(PathQuery(path), response_type=ListResponse)
data = self._bulk_read(0x28, data_type=FileProperties, command_number=PathQuery.NUMBER)[0] data = self._bulk_read(0x28, data_type=FileProperties, command_number=PathQuery.NUMBER)[0]
if path.endswith("/"): path = path[:-1] if path.endswith("/"): path = path[:-1]
if res.path_not_found : raise PathError(path + " does not exist on device") if res.path_not_found : raise PathError(path + " does not exist on device")
if res.is_invalid : raise PathError(path + " is not a valid path") if res.is_invalid : raise PathError(path + " is not a valid path")
if res.is_unmounted : raise PathError(path + " is not mounted") if res.is_unmounted : raise PathError(path + " is not mounted")
return (res, data) if res.code not in (0, PathResponseCodes.IS_FILE):
raise PathError(path + " has an unknown error. Code: " + hex(res.code))
return data
def get_file(self, path, outfile): @safe
def get_file(self, path, outfile, end_session=True):
""" """
Read the file at path on the device and write it to outfile. For the logic see L{_get_file}. Read the file at path on the device and write it to outfile. For the logic see L{_get_file}.
@param outfile: file object like C{sys.stdout} or the result of an C{open} call
"""
self._run_session(self._get_file, path, outfile)
def _get_file(self, args):
"""
Fetch a file from the device and write it to an output stream.
The data is fetched in chunks of size S{<=} 32K. Each chunk is make of packets of size S{<=} 4K. See L{FileOpen}, The data is fetched in chunks of size S{<=} 32K. Each chunk is make of packets of size S{<=} 4K. See L{FileOpen},
L{FileRead} and L{FileClose} for details on the command packets used. L{FileRead} and L{FileClose} for details on the command packets used.
@param args: C{path, outfile = arg[0], arg[1]} @param outfile: file object like C{sys.stdout} or the result of an C{open} call
""" """
path, outfile = args[0], args[1]
if path.endswith("/"): path = path[:-1] # We only copy files if path.endswith("/"): path = path[:-1] # We only copy files
res, data = self._get_path_properties(path) file = self.path_properties(path, end_session=False)
if data.is_dir: raise PathError("Cannot read as " + path + " is a directory") if file.is_dir: raise PathError("Cannot read as " + path + " is a directory")
bytes = data.file_size bytes = file.file_size
self._send_validated_command(FileOpen(path)) res = self._send_validated_command(FileOpen(path))
if res.code != 0:
raise PathError("Unable to open " + path + " for reading. Response code: " + hex(res.code))
id = self._bulk_read(20, data_type=IdAnswer, command_number=FileOpen.NUMBER)[0].id id = self._bulk_read(20, data_type=IdAnswer, command_number=FileOpen.NUMBER)[0].id
bytes_left, chunk_size, pos = bytes, 0x8000, 0 bytes_left, chunk_size, pos = bytes, 0x8000, 0
while bytes_left > 0: while bytes_left > 0:
if chunk_size > bytes_left: chunk_size = bytes_left if chunk_size > bytes_left: chunk_size = bytes_left
res = self._send_validated_command(FileRead(id, pos, chunk_size)) res = self._send_validated_command(FileIO(id, pos, chunk_size))
packets = self._bulk_read(chunk_size+16, command_number=FileRead.NUMBER, packet_size=4096) if res.code != 0:
self._send_validated_command(FileClose(id))
raise ProtocolError("Error while reading from " + path + ". Response code: " + hex(res.code))
packets = self._bulk_read(chunk_size+16, command_number=FileIO.RNUMBER, packet_size=4096)
try: try:
array('B', packets[0][16:]).tofile(outfile) # The first 16 bytes are meta information on the packet stream array('B', packets[0][16:]).tofile(outfile) # The first 16 bytes are meta information on the packet stream
for i in range(1, len(packets)): for i in range(1, len(packets)):
@ -308,70 +351,69 @@ class PRS500Device(object):
bytes_left -= chunk_size bytes_left -= chunk_size
pos += chunk_size pos += chunk_size
self._send_validated_command(FileClose(id)) self._send_validated_command(FileClose(id))
# Not going to check response code to see if close was successful as there's not much we can do if it wasnt
def _list(self, args):
@safe
def list(self, path, recurse=False, end_session=True):
""" """
Ask the device to list a path. See the code for details. See L{DirOpen}, Return a listing of path. See the code for details. See L{DirOpen},
L{DirRead} and L{DirClose} for details on the command packets used. L{DirRead} and L{DirClose} for details on the command packets used.
@param args: C{path=args[0]}
@return: A list of tuples. The first element of each tuple is a string, the path. The second is a L{FileProperties}.
If the path points to a file, the list will have length 1.
"""
path = args[0]
if not path.endswith("/"): path += "/" # Initially assume path is a directory
files = []
res, data = self._get_path_properties(path)
if res.is_file:
path = path[:-1]
res, data = self._get_path_properties(path)
files = [ (path, data) ]
else:
# Get query ID used to ask for next element in list
self._send_validated_command(DirOpen(path), response_type=ListResponse)
id = self._bulk_read(0x14, data_type=IdAnswer, command_number=DirOpen.NUMBER)[0].id
# Create command asking for next element in list
next = DirRead(id)
items = []
while True:
res = self._send_validated_command(next, response_type=ListResponse)
size = res.data[2] + 16
data = self._bulk_read(size, data_type=ListAnswer, command_number=DirRead.NUMBER)[0]
# path_not_found seems to happen if the usb server doesn't have the permissions to access the directory
if res.is_eol or res.path_not_found: break
items.append(data.name)
self._send_validated_command(DirClose(id))
for item in items:
ipath = path + item
res, data = self._get_path_properties(ipath)
files.append( (ipath, data) )
files.sort()
return files
def list(self, path, recurse=False):
"""
Return a listing of path.
See L{_list} for the communication logic.
@type path: string @type path: string
@param path: The path to list @param path: The path to list
@type recurse: boolean @type recurse: boolean
@param recurse: If true do a recursive listing @param recurse: If true do a recursive listing
@return: A list of tuples. The first element of each tuple is a path. The second element is a list of L{Files<File>}. @return: A list of tuples. The first element of each tuple is a path. The second element is a list of L{Files<File>}.
The path is the path we are listing, the C{Files} are the files/directories in that path. If it is a recursive The path is the path we are listing, the C{Files} are the files/directories in that path. If it is a recursive
list, then the first element will be (C{path}, children), the next will be (child, its children) and so on. list, then the first element will be (C{path}, children), the next will be (child, its children) and so on. If it
is not recursive the length of the outermost list will be 1.
""" """
files = self._run_session(self._list, path) def _list(path): # Do a non recursive listsing of path
files = [ File(file) for file in files ] if not path.endswith("/"): path += "/" # Initially assume path is a directory
files = []
candidate = self.path_properties(path, end_session=False)
if not candidate.is_dir:
path = path[:-1]
data = self.path_properties(path, end_session=False)
files = [ File((path, data)) ]
else:
# Get query ID used to ask for next element in list
res = self._send_validated_command(DirOpen(path))
if res.code != 0:
raise PathError("Unable to open directory " + path + " for reading. Response code: " + hex(res.code))
id = self._bulk_read(0x14, data_type=IdAnswer, command_number=DirOpen.NUMBER)[0].id
# Create command asking for next element in list
next = DirRead(id)
items = []
while True:
res = self._send_validated_command(next, response_type=ListResponse)
size = res.data_size + 16
data = self._bulk_read(size, data_type=ListAnswer, command_number=DirRead.NUMBER)[0]
# path_not_found seems to happen if the usb server doesn't have the permissions to access the directory
if res.is_eol or res.path_not_found: break
elif res.code != 0:
raise ProtocolError("Unknown error occured while reading contents of directory " + path + ". Response code: " + haex(res.code))
items.append(data.name)
self._send_validated_command(DirClose(id)) # Ignore res.code as we cant do anything if close fails
for item in items:
ipath = path + item
data = self.path_properties(ipath, end_session=False)
files.append( File( (ipath, data) ) )
files.sort()
return files
files = _list(path)
dirs = [(path, files)] dirs = [(path, files)]
for file in files: for file in files:
if recurse and file.is_dir and not file.path.startswith(("/dev","/proc")): if recurse and file.is_dir and not file.path.startswith(("/dev","/proc")):
dirs[len(dirs):] = self.list(file.path, recurse=True) dirs[len(dirs):] = self.list(file.path, recurse=True, end_session=False)
return dirs return dirs
def available_space(self): @safe
def available_space(self, end_session=True):
""" """
Get free space available on the mountpoints: Get free space available on the mountpoints:
1. /Data/ Device memory 1. /Data/ Device memory
@ -380,10 +422,6 @@ class PRS500Device(object):
@return: A list of tuples. Each tuple has form ("location", free space, total space) @return: A list of tuples. Each tuple has form ("location", free space, total space)
""" """
return self._run_session(self._available_space)
def _available_space(self, args):
""" L{available_space} """
data = [] data = []
for path in ("/Data/", "a:/", "b:/"): for path in ("/Data/", "a:/", "b:/"):
res = self._send_validated_command(FreeSpaceQuery(path),timeout=5000) # Timeout needs to be increased as it takes time to read card res = self._send_validated_command(FreeSpaceQuery(path),timeout=5000) # Timeout needs to be increased as it takes time to read card
@ -392,3 +430,113 @@ class PRS500Device(object):
data.append( (path, pkt.free_space, pkt.total) ) data.append( (path, pkt.free_space, pkt.total) )
return data return data
def _exists(self, path):
""" Return (True, FileProperties) if path exists or (False, None) otherwise """
dest = None
try:
dest = self.path_properties(path, end_session=False)
except PathError, e:
if "does not exist" in str(e): return (False, None)
else: raise e
return (True, dest)
@safe
def touch(self, path, end_session=True):
"""
Create a file at path
@todo: Update file modification time if it exists
"""
if path.endswith("/") and len(path) > 1: path = path[:-1]
exists, file = self._exists(path)
if exists and file.is_dir:
raise PathError("Cannot touch directories")
if not exists:
res = self._send_validated_command(FileCreate(path))
if res.code != 0:
raise PathError("Could not create file " + path + ". Response code: " + str(hex(res.code)))
## res = self._send_validated_command(SetFileInfo(path))
## if res.code != 0:
## raise ProtocolError("Unable to touch " + path + ". Response code: " + hex(res.code))
## file.wtime = int(time.time())
## self._bulk_write(file[16:])
@safe
def put_file(self, infile, path, end_session=True):
exists, dest = self._exists(path)
if exists:
if not dest.is_dir: raise PathError("Cannot write to " + path + " as it already exists")
if not path.endswith("/"): path += "/"
path += os.path.basename(infile.name)
exists, dest = self._exists(path)
if exists: raise PathError("Cannot write to " + path + " as it already exists")
res = self._send_validated_command(FileCreate(path))
if res.code != 0:
raise ProtocolError("There was an error creating device:"+path+". Response code: "+hex(res.code))
chunk_size = 0x8000
data_left = True
res = self._send_validated_command(FileOpen(path, mode=FileOpen.WRITE))
if res.code != 0:
raise ProtocolError("Unable to open " + path + " for writing. Response code: " + hex(res.code))
id = self._bulk_read(20, data_type=IdAnswer, command_number=FileOpen.NUMBER)[0].id
pos = 0
while data_left:
data = array('B')
try:
data.fromfile(infile, chunk_size)
except EOFError:
data_left = False
res = self._send_validated_command(FileIO(id, pos, len(data), mode=FileIO.WNUMBER))
if res.code != 0:
raise ProtocolError("Unable to write to " + path + ". Response code: " + hex(res.code))
self._bulk_write(data)
pos += len(data)
self._send_validated_command(FileClose(id)) # Ignore res.code as cant do anything if close fails
file = self.path_properties(path, end_session=False)
if file.file_size != pos:
raise ProtocolError("Copying to device failed. The file was truncated by " + str(data.file_size - pos) + " bytes")
@safe
def del_file(self, path, end_session=True):
data = self.path_properties(path, end_session=False)
if data.is_dir: raise PathError("Cannot delete directories")
res = self._send_validated_command(FileDelete(path), response_type=ListResponse)
if res.code != 0:
raise ProtocolError("Unable to delete " + path + " with response:\n" + str(res))
@safe
def mkdir(self, path, end_session=True):
if not path.endswith("/"): path += "/"
error_prefix = "Cannot create directory " + path
res = self._send_validated_command(DirCreate(path)).data[0]
if res == 0xffffffcc:
raise PathError(error_prefix + " as it already exists")
elif res == PathResponseCodes.NOT_FOUND:
raise PathError(error_prefix + " as " + path[0:path[:-1].rfind("/")] + " does not exist ")
elif res == PathResponseCodes.INVALID:
raise PathError(error_prefix + " as " + path + " is invalid")
elif res != 0:
raise PathError(error_prefix + ". Response code: " + hex(res))
@safe
def rm(self, path, end_session=True):
""" Delete path from device if it is a file or an empty directory """
dir = self.path_properties(path, end_session=False)
if not dir.is_dir:
self.del_file(path, end_session=False)
else:
if not path.endswith("/"): path += "/"
res = self._send_validated_command(DirDelete(path))
if res.code == PathResponseCodes.HAS_CHILDREN:
raise PathError("Cannot delete directory " + path + " as it is not empty")
if res.code != 0:
raise ProtocolError("Failed to delete directory " + path + ". Response code: " + hex(res.code))
#dev = PRS500Device(log_packets=False)
#dev.open()
#print dev.get_file("/etc/sysctl.conf", sys.stdout)
#dev.close()

View File

@ -44,6 +44,14 @@ DWORD = "<I" #: Unsigned integer little endian encoded in 4 bytes
DDWORD = "<Q" #: Unsigned long long little endian encoded in 8 bytes DDWORD = "<Q" #: Unsigned long long little endian encoded in 8 bytes
class PathResponseCodes(object):
""" Known response commands to path related commands """
NOT_FOUND = 0xffffffd7
INVALID = 0xfffffff9
IS_FILE = 0xffffffd2
HAS_CHILDREN = 0xffffffcc
class TransferBuffer(list): class TransferBuffer(list):
""" """
@ -333,6 +341,27 @@ class DirClose(ShortCommand):
""" @param id: The identifier returned as a result of a L{DirOpen} command """ """ @param id: The identifier returned as a result of a L{DirOpen} command """
ShortCommand.__init__(self, number=DirClose.NUMBER, type=0x01, command=id) ShortCommand.__init__(self, number=DirClose.NUMBER, type=0x01, command=id)
class USBConnect(ShortCommand):
""" Ask device to change status to 'USB connected' i.e., tell the device that the present sequence of commands is complete """
NUMBER=0x1 #: Command number
def __init__(self):
ShortCommand.__init__(self, number=USBConnect.NUMBER, type=0x01, command=0x00)
class GetUSBProtocolVersion(ShortCommand):
""" Get USB Protocol version used by device """
NUMBER=0x0 #: Command number
def __init__(self):
ShortCommand.__init__(self, number=GetUSBProtocolVersion.NUMBER, type=0x01, command=0x00)
class SetBulkSize(ShortCommand):
NUMBER = 0x107 #: Command number
def __init__(self, size=0x028000):
ShortCommand.__init__(self, number=SetBulkSize.NUMBER, type=0x01, command=size)
class UnlockDevice(ShortCommand):
NUMBER = 0x106 #: Command number
def __init__(self, key=0x312d):
ShortCommand.__init__(self, number=UnlockDevice.NUMBER, type=0x01, command=key)
class LongCommand(Command): class LongCommand(Command):
@ -390,6 +419,12 @@ class FreeSpaceQuery(PathCommand):
def __init__(self, path): def __init__(self, path):
PathCommand.__init__(self, path, FreeSpaceQuery.NUMBER) PathCommand.__init__(self, path, FreeSpaceQuery.NUMBER)
class DirCreate(PathCommand):
""" Create a directory """
NUMBER = 0x30
def __init__(self, path):
PathCommand.__init__(self, path, DirCreate.NUMBER)
class DirOpen(PathCommand): class DirOpen(PathCommand):
""" Open a directory for reading its contents """ """ Open a directory for reading its contents """
NUMBER = 0x33 #: Command number NUMBER = 0x33 #: Command number
@ -417,6 +452,24 @@ class FileClose(ShortCommand):
def __init__(self, id): def __init__(self, id):
ShortCommand.__init__(self, number=FileClose.NUMBER, type=0x01, command=id) ShortCommand.__init__(self, number=FileClose.NUMBER, type=0x01, command=id)
class FileCreate(PathCommand):
""" Create a file """
NUMBER=0x1a #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, FileCreate.NUMBER)
class FileDelete(PathCommand):
""" Delete a file """
NUMBER=0x1B
def __init__(self, path):
PathCommand.__init__(self, path, FileDelete.NUMBER)
class DirDelete(PathCommand):
""" Delete a directory """
NUMBER=0x31
def __init__(self, path):
PathCommand.__init__(self, path, DirDelete.NUMBER)
class FileOpen(PathCommand): class FileOpen(PathCommand):
""" File open command """ """ File open command """
NUMBER = 0x10 #: Command number NUMBER = 0x10 #: Command number
@ -442,13 +495,14 @@ class FileOpen(PathCommand):
return property(**locals()) return property(**locals())
class FileRead(Command): class FileIO(Command):
""" Command to read from an open file """ """ Command to read/write from an open file """
NUMBER = 0x16 #: Command number to read from a file RNUMBER = 0x16 #: Command number to read from a file
WNUMBER = 0x17 #: Command number to write to a file
id = field(start=16, fmt=DWORD) #: The file ID returned by a FileOpen command id = field(start=16, fmt=DWORD) #: The file ID returned by a FileOpen command
offset = field(start=20, fmt=DDWORD) #: offset in the file at which to read offset = field(start=20, fmt=DDWORD) #: offset in the file at which to read
size = field(start=28, fmt=DWORD) #: The number of bytes to reead from file. size = field(start=28, fmt=DWORD) #: The number of bytes to reead from file.
def __init__(self, id, offset, size): def __init__(self, id, offset, size, mode=0x16):
""" """
@param id: File identifier returned by a L{FileOpen} command @param id: File identifier returned by a L{FileOpen} command
@type id: C{unsigned int} @type id: C{unsigned int}
@ -456,9 +510,10 @@ class FileRead(Command):
@type offset: C{unsigned long long} @type offset: C{unsigned long long}
@param size: number of bytes to read @param size: number of bytes to read
@type size: C{unsigned int} @type size: C{unsigned int}
@param mode: Either L{FileIO.RNUMBER} or L{File.WNUMBER}
""" """
Command.__init__(self, 32) Command.__init__(self, 32)
self.number=FileRead.NUMBER self.number=mode
self.type = 0x01 self.type = 0x01
self.length = 16 self.length = 16
self.id = id self.id = id
@ -472,6 +527,11 @@ class PathQuery(PathCommand):
def __init__(self, path): def __init__(self, path):
PathCommand.__init__(self, path, PathQuery.NUMBER) PathCommand.__init__(self, path, PathQuery.NUMBER)
class SetFileInfo(PathCommand):
""" Set File information """
NUMBER = 0x19 #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, SetFileInfo.NUMBER)
class Response(Command): class Response(Command):
""" """
@ -482,6 +542,8 @@ class Response(Command):
SIZE = 32 #: Size of response packets in the SONY protocol SIZE = 32 #: Size of response packets in the SONY protocol
rnumber = field(start=16, fmt=DWORD) #: Response number, the command number of a command packet sent sometime before this packet was received rnumber = field(start=16, fmt=DWORD) #: Response number, the command number of a command packet sent sometime before this packet was received
code = field(start=20, fmt=DWORD) #: Used to indicate error conditions. A value of 0 means there was no error
data_size = field(start=28, fmt=DWORD) #: Used to indicate the size of the next bulk read
def __init__(self, packet): def __init__(self, packet):
""" C{len(packet) == Response.SIZE} """ """ C{len(packet) == Response.SIZE} """
@ -513,8 +575,6 @@ class ListResponse(Response):
IS_EOL = 0xfffffffa #: There are no more entries in the list IS_EOL = 0xfffffffa #: There are no more entries in the list
PATH_NOT_FOUND = 0xffffffd7 #: Queried path is not found PATH_NOT_FOUND = 0xffffffd7 #: Queried path is not found
code = field(start=20, fmt=DWORD) #: Used to indicate conditions like EOL/Error/IsFile etc.
@apply @apply
def is_file(): def is_file():
""" True iff queried path is a file """ """ True iff queried path is a file """
@ -553,11 +613,16 @@ class ListResponse(Response):
class Answer(TransferBuffer): class Answer(TransferBuffer):
""" Defines the structure of packets sent to host via a bulk transfer (i.e., bulk reads) """ """ Defines the structure of packets sent to host via a bulk transfer (i.e., bulk reads) """
number = field(start=0, fmt=DWORD) #: Answer identifier, should be sent in an acknowledgement packet number = field(start=0, fmt=DWORD) #: Answer identifier
length = field(start=12, fmt=DWORD) #: Length of data to follow
def __init__(self, packet): def __init__(self, packet):
""" @param packet: C{len(packet)} S{>=} C{16} """ """ @param packet: C{len(packet)} S{>=} C{16} """
if len(packet) < 16 : raise PacketError(str(self.__class__)[7:-2] + " packets must have a length of atleast 16 bytes") if "__len__" in dir(packet):
if len(packet) < 16 :
raise PacketError(str(self.__class__)[7:-2] + " packets must have a length of atleast 16 bytes")
elif packet < 16:
raise PacketError(str(self.__class__)[7:-2] + " packets must have a length of atleast 16 bytes")
TransferBuffer.__init__(self, packet) TransferBuffer.__init__(self, packet)
@ -565,38 +630,30 @@ class FileProperties(Answer):
""" Defines the structure of packets that contain size, date and permissions information about files/directories. """ """ Defines the structure of packets that contain size, date and permissions information about files/directories. """
file_size = field(start=16, fmt=DDWORD) file_size = field(start=16, fmt=DDWORD) #: Size in bytes of the file
ctime = field(start=28, fmt=DDWORD) #: Creation time file_type = field(start=24, fmt=DWORD) #: 1 == file, 2 == dir
wtime = field(start=16, fmt=DDWORD) #: Modification time ctime = field(start=28, fmt=DWORD) #: Creation time
wtime = field(start=32, fmt=DWORD) #: Modification time
permissions = field(start=36, fmt=DWORD) #: 0 = default permissions, 4 = read only
@apply @apply
def is_dir(): def is_dir():
doc =\ doc = """True if path points to a directory, False if it points to a file."""
"""
True if path points to a directory, False if it points to a file. C{unsigned int} stored in 4 bytes at byte 24.
Value of 1 == file and 2 == dir
"""
def fget(self): def fget(self):
return (self.unpack(start=24, fmt=DWORD)[0] == 2) return (self.file_type == 2)
def fset(self, val): def fset(self, val):
if val: val = 2 if val: val = 2
else: val = 1 else: val = 1
self.pack(val, start=24, fmt=DWORD) self.file_type = val
return property(**locals()) return property(**locals())
@apply @apply
def is_readonly(): def is_readonly():
doc =\ doc = """ Whether this file is readonly."""
"""
Whether this file is readonly. C{unsigned int} stored in 4 bytes at byte 36.
A value of 0 corresponds to read/write and 4 corresponds to read-only. The device doesn't send full permissions information.
"""
def fget(self): def fget(self):
return self.unpack(start=36, fmt=DWORD)[0] != 0 return self.unpack(start=36, fmt=DWORD)[0] != 0
@ -608,6 +665,10 @@ class FileProperties(Answer):
return property(**locals()) return property(**locals())
class USBProtocolVersion(Answer):
version = field(start=16, fmt=DDWORD)
class IdAnswer(Answer): class IdAnswer(Answer):
""" Defines the structure of packets that contain identifiers for queries. """ """ Defines the structure of packets that contain identifiers for queries. """
@ -637,8 +698,8 @@ class FreeSpaceAnswer(Answer):
total = field(start=24, fmt=DDWORD) total = field(start=24, fmt=DDWORD)
free_space = field(start=32, fmt=DDWORD) free_space = field(start=32, fmt=DDWORD)
class ListAnswer(Answer):
class ListAnswer(Answer):
""" Defines the structure of packets that contain items in a list. """ """ Defines the structure of packets that contain items in a list. """
name_length = field(start=20, fmt=DWORD) name_length = field(start=20, fmt=DWORD)
name = stringfield(name_length, start=24) name = stringfield(name_length, start=24)

View File

@ -73,14 +73,14 @@ class FileFormatter(object):
def modification_time(): def modification_time():
doc=""" Last modified time in the Linux ls -l format """ doc=""" Last modified time in the Linux ls -l format """
def fget(self): def fget(self):
return time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.wtime)) return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.wtime))
return property(**locals()) return property(**locals())
@apply @apply
def creation_time(): def creation_time():
doc=""" Last modified time in the Linux ls -l format """ doc=""" Last modified time in the Linux ls -l format """
def fget(self): def fget(self):
return time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.ctime)) return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.ctime))
return property(**locals()) return property(**locals())
def info(dev): def info(dev):
@ -165,9 +165,11 @@ def main():
term = TerminalController() term = TerminalController()
cols = term.COLS cols = term.COLS
parser = OptionParser(usage="usage: %prog [options] command args\n\ncommand is one of: info, df, ls, cp, cat or rm\n\n"+ parser = OptionParser(usage="usage: %prog [options] command args\n\ncommand is one of: info, df, ls, cp, mkdir, touch, cat or rm\n\n"+
"For help on a particular command: %prog command", version="libprs500 version: " + VERSION) "For help on a particular command: %prog command", version="libprs500 version: " + VERSION)
parser.add_option("--log-packets", help="print out packet stream to stdout", dest="log_packets", action="store_true", default=False) parser.add_option("--log-packets", help="print out packet stream to stdout. "+\
"The numbers in the left column are byte offsets that allow the packet size to be read off easily.",
dest="log_packets", action="store_true", default=False)
parser.remove_option("-h") parser.remove_option("-h")
parser.disable_interspersed_args() # Allow unrecognized options parser.disable_interspersed_args() # Allow unrecognized options
options, args = parser.parse_args() options, args = parser.parse_args()
@ -178,80 +180,111 @@ def main():
command = args[0] command = args[0]
args = args[1:] args = args[1:]
dev = PRS500Device(log_packets=options.log_packets) dev = PRS500Device(log_packets=options.log_packets)
if command == "df":
dev.open() dev.open()
try:
if command == "df":
data = dev.available_space() data = dev.available_space()
dev.close()
print "Filesystem\tSize \tUsed \tAvail \tUse%" print "Filesystem\tSize \tUsed \tAvail \tUse%"
for datum in data: for datum in data:
total, free, used, percent = human_readable(datum[2]), human_readable(datum[1]), human_readable(datum[2]-datum[1]), \ total, free, used, percent = human_readable(datum[2]), human_readable(datum[1]), human_readable(datum[2]-datum[1]), \
str(0 if datum[2]==0 else int(100*(datum[2]-datum[1])/(datum[2]*1.)))+"%" str(0 if datum[2]==0 else int(100*(datum[2]-datum[1])/(datum[2]*1.)))+"%"
print "%-10s\t%s\t%s\t%s\t%s"%(datum[0], total, used, free, percent) print "%-10s\t%s\t%s\t%s\t%s"%(datum[0], total, used, free, percent)
elif command == "mkdir":
parser = OptionParser(usage="usage: %prog mkdir [options] path\n\npath must begin with /,a:/ or b:/")
if len(args) != 1:
parser.print_help()
sys.exit(1)
dev.mkdir(args[0])
elif command == "ls": elif command == "ls":
parser = OptionParser(usage="usage: %prog ls [options] path\n\npath must begin with /,a:/ or b:/") parser = OptionParser(usage="usage: %prog ls [options] path\n\npath must begin with /,a:/ or b:/")
parser.add_option("--color", help="show ls output in color", dest="color", action="store_true", default=False) parser.add_option("--color", help="show ls output in color", dest="color", action="store_true", default=False)
parser.add_option("-l", help="In addition to the name of each file, print the file type, permissions, and timestamp (the modification time unless other times are selected)", dest="ll", action="store_true", default=False) parser.add_option("-l", help="In addition to the name of each file, print the file type, permissions, and timestamp (the modification time unless other times are selected). Times are local.", dest="ll", action="store_true", default=False)
parser.add_option("-R", help="Recursively list subdirectories encountered. /dev and /proc are omitted", dest="recurse", action="store_true", default=False) parser.add_option("-R", help="Recursively list subdirectories encountered. /dev and /proc are omitted", dest="recurse", action="store_true", default=False)
parser.remove_option("-h") parser.remove_option("-h")
parser.add_option("-h", "--human-readable", help="show sizes in human readable format", dest="hrs", action="store_true", default=False) parser.add_option("-h", "--human-readable", help="show sizes in human readable format", dest="hrs", action="store_true", default=False)
options, args = parser.parse_args(args) options, args = parser.parse_args(args)
if len(args) < 1: if len(args) != 1:
parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
dev.open() dev.open()
try:
print ls(dev, args[0], term, color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols), print ls(dev, args[0], term, color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols),
except ArgumentError, e:
print >> sys.stderr, e
sys.exit(1)
finally:
dev.close()
elif command == "info": elif command == "info":
dev.open()
try:
info(dev) info(dev)
finally: dev.close()
elif command == "cp": elif command == "cp":
parser = OptionParser(usage="usage: %prog cp [options] source destination\n\nsource is a path on the device and must begin with /,a:/ or b:/"+ usage="usage: %prog cp [options] source destination\n\n"+\
"\n\ndestination is a path on your computer and can point to either a file or a directory") "One of source or destination must be a path on the device. Device paths have the form:\n"+\
"device:mountpoint/my/path\n"+\
"where mountpoint is one of /, a: or b:\n"+\
"source must point to a file for which you have read permissions\n"+\
"destination must point to a file or directory for which you have write permissions"
parser = OptionParser(usage=usage)
options, args = parser.parse_args(args) options, args = parser.parse_args(args)
if len(args) < 2: if len(args) != 2:
parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
if args[0].endswith("/"): path = args[0][:-1] if args[0].startswith("device:"):
else: path = args[0]
outfile = args[1] outfile = args[1]
path = args[0][7:]
if path.endswith("/"): path = path[:-1]
if os.path.isdir(outfile): if os.path.isdir(outfile):
outfile = os.path.join(outfile, path[path.rfind("/")+1:]) outfile = os.path.join(outfile, path[path.rfind("/")+1:])
outfile = open(outfile, "w")
dev.open()
try: try:
dev.get_file(path, outfile) outfile = open(outfile, "w")
except ArgumentError, e: except IOError, e:
print >> sys.stderr, e print >> sys.stderr, e
finally: parser.print_help()
dev.close() sys.exit(1)
dev.get_file(path, outfile)
outfile.close() outfile.close()
elif args[1].startswith("device:"):
try:
infile = open(args[0], "r")
except IOError, e:
print >> sys.stderr, e
parser.print_help()
sys.exit(1)
dev.put_file(infile, args[1][7:])
infile.close()
else:
parser.print_help()
sys.exit(1)
elif command == "cat": elif command == "cat":
outfile = sys.stdout outfile = sys.stdout
parser = OptionParser(usage="usage: %prog cat path\n\npath should point to a file on the device and must begin with /,a:/ or b:/") parser = OptionParser(usage="usage: %prog cat path\n\npath should point to a file on the device and must begin with /,a:/ or b:/")
options, args = parser.parse_args(args) options, args = parser.parse_args(args)
if len(args) < 1: if len(args) != 1:
parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
if args[0].endswith("/"): path = args[0][:-1] if args[0].endswith("/"): path = args[0][:-1]
else: path = args[0] else: path = args[0]
outfile = sys.stdout outfile = sys.stdout
dev.open()
try:
dev.get_file(path, outfile) dev.get_file(path, outfile)
except ArgumentError, e: elif command == "rm":
print >>sys.stderr, e parser = OptionParser(usage="usage: %prog rm path\n\npath should point to a file or empty directory on the device "+\
finally: "and must begin with /,a:/ or b:/\n\n"+\
dev.close() "rm will DELETE the file. Be very CAREFUL")
else: options, args = parser.parse_args(args)
if len(args) != 1:
parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
dev.rm(args[0])
elif command == "touch":
parser = OptionParser(usage="usage: %prog touch path\n\npath should point to a file on the device and must begin with /,a:/ or b:/\n\n"+
"Unfortunately, I cant figure out how to update file times on the device, so if path already exists, touch does nothing" )
options, args = parser.parse_args(args)
if len(args) != 1:
parser.print_help()
sys.exit(1)
dev.touch(args[0])
else:
parser.print_help()
if dev.handle: dev.close()
sys.exit(1)
except ArgumentError, e:
print >>sys.stderr, e
sys.exit(1)
finally:
if dev.handle: dev.close()
if __name__ == "__main__": if __name__ == "__main__":
main() main()