1#!/usr/local/bin/python3.8 2 3 4__license__ = 'GPL v3' 5__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' 6""" 7Provides a command-line interface to ebook devices. 8 9For usage information run the script. 10""" 11 12import sys, time, os 13from optparse import OptionParser 14 15from calibre import __version__, __appname__, human_readable, fsync, prints 16from calibre.devices.errors import ArgumentError, DeviceError, DeviceLocked 17from calibre.customize.ui import device_plugins 18from calibre.devices.scanner import DeviceScanner 19from calibre.utils.config import device_prefs 20from polyglot.io import PolyglotStringIO 21 22MINIMUM_COL_WIDTH = 12 # : Minimum width of columns in ls output 23 24 25class FileFormatter: 26 27 def __init__(self, file): 28 self.is_dir = file.is_dir 29 self.is_readonly = file.is_readonly 30 self.size = file.size 31 self.ctime = file.ctime 32 self.wtime = file.wtime 33 self.name = file.name 34 self.path = file.path 35 36 @property 37 def mode_string(self): 38 """ The mode string for this file. There are only two modes read-only and read-write """ 39 mode, x = "-", "-" 40 if self.is_dir: 41 mode, x = "d", "x" 42 if self.is_readonly: 43 mode += "r-"+x+"r-"+x+"r-"+x 44 else: 45 mode += "rw"+x+"rw"+x+"rw"+x 46 return mode 47 48 @property 49 def isdir_name(self): 50 '''Return self.name + '/' if self is a directory''' 51 name = self.name 52 if self.is_dir: 53 name += '/' 54 return name 55 56 @property 57 def name_in_color(self): 58 """ The name in ANSI text. Directories are blue, ebooks are green """ 59 cname = self.name 60 blue, green, normal = "", "", "" 61 if self.term: 62 blue, green, normal = self.term.BLUE, self.term.GREEN, self.term.NORMAL 63 if self.is_dir: 64 cname = blue + self.name + normal 65 else: 66 ext = self.name[self.name.rfind("."):] 67 if ext in (".pdf", ".rtf", ".lrf", ".lrx", ".txt"): 68 cname = green + self.name + normal 69 return cname 70 71 @property 72 def human_readable_size(self): 73 """ File size in human readable form """ 74 return human_readable(self.size) 75 76 @property 77 def modification_time(self): 78 """ Last modified time in the Linux ls -l format """ 79 return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.wtime)) 80 81 @property 82 def creation_time(self): 83 """ Last modified time in the Linux ls -l format """ 84 return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.ctime)) 85 86 87def info(dev): 88 info = dev.get_device_information() 89 print("Device name: ", info[0]) 90 print("Device version: ", info[1]) 91 print("Software version:", info[2]) 92 print("Mime type: ", info[3]) 93 94 95def ls(dev, path, recurse=False, human_readable_size=False, ll=False, cols=0): 96 def col_split(l, cols): # split list l into columns 97 rows = len(l) // cols 98 if len(l) % cols: 99 rows += 1 100 m = [] 101 for i in range(rows): 102 m.append(l[i::rows]) 103 return m 104 105 def row_widths(table): # Calculate widths for each column in the row-wise table 106 tcols = len(table[0]) 107 rowwidths = [0 for i in range(tcols)] 108 for row in table: 109 c = 0 110 for item in row: 111 rowwidths[c] = len(item) if len(item) > rowwidths[c] else rowwidths[c] 112 c += 1 113 return rowwidths 114 115 output = PolyglotStringIO() 116 if path.endswith("/") and len(path) > 1: 117 path = path[:-1] 118 dirs = dev.list(path, recurse) 119 for dir in dirs: 120 if recurse: 121 prints(dir[0] + ":", file=output) 122 lsoutput, lscoloutput = [], [] 123 files = dir[1] 124 maxlen = 0 125 if ll: # Calculate column width for size column 126 for file in files: 127 size = len(str(file.size)) 128 if human_readable_size: 129 file = FileFormatter(file) 130 size = len(file.human_readable_size) 131 if size > maxlen: 132 maxlen = size 133 for file in files: 134 file = FileFormatter(file) 135 name = file.name if ll else file.isdir_name 136 lsoutput.append(name) 137 lscoloutput.append(name) 138 if ll: 139 size = str(file.size) 140 if human_readable_size: 141 size = file.human_readable_size 142 prints(file.mode_string, ("%"+str(maxlen)+"s")%size, file.modification_time, name, file=output) 143 if not ll and len(lsoutput) > 0: 144 trytable = [] 145 for colwidth in range(MINIMUM_COL_WIDTH, cols): 146 trycols = int(cols//colwidth) 147 trytable = col_split(lsoutput, trycols) 148 works = True 149 for row in trytable: 150 row_break = False 151 for item in row: 152 if len(item) > colwidth - 1: 153 works, row_break = False, True 154 break 155 if row_break: 156 break 157 if works: 158 break 159 rowwidths = row_widths(trytable) 160 trytablecol = col_split(lscoloutput, len(trytable[0])) 161 for r in range(len(trytable)): 162 for c in range(len(trytable[r])): 163 padding = rowwidths[c] - len(trytable[r][c]) 164 prints(trytablecol[r][c], "".ljust(padding), end=' ', file=output) 165 prints(file=output) 166 prints(file=output) 167 listing = output.getvalue().rstrip() + "\n" 168 output.close() 169 return listing 170 171 172def shutdown_plugins(): 173 for d in device_plugins(): 174 try: 175 d.shutdown() 176 except: 177 pass 178 179 180def main(): 181 from calibre.utils.terminal import geometry 182 cols = geometry()[0] 183 184 parser = OptionParser(usage="usage: %prog [options] command args\n\ncommand "+ 185 "is one of: info, books, df, ls, cp, mkdir, touch, cat, rm, eject, test_file\n\n"+ 186 "For help on a particular command: %prog command", version=__appname__+" version: " + __version__) 187 parser.add_option("--log-packets", help="print out packet stream to stdout. "+ 188 "The numbers in the left column are byte offsets that allow the packet size to be read off easily.", 189 dest="log_packets", action="store_true", default=False) 190 parser.remove_option("-h") 191 parser.disable_interspersed_args() # Allow unrecognized options 192 options, args = parser.parse_args() 193 194 if len(args) < 1: 195 parser.print_help() 196 return 1 197 198 command = args[0] 199 args = args[1:] 200 dev = None 201 scanner = DeviceScanner() 202 scanner.scan() 203 connected_devices = [] 204 205 for d in device_plugins(): 206 try: 207 d.startup() 208 except: 209 print('Startup failed for device plugin: %s'%d) 210 if d.MANAGES_DEVICE_PRESENCE: 211 cd = d.detect_managed_devices(scanner.devices) 212 if cd is not None: 213 connected_devices.append((cd, d)) 214 dev = d 215 break 216 continue 217 ok, det = scanner.is_device_connected(d) 218 if ok: 219 dev = d 220 dev.reset(log_packets=options.log_packets, detected_device=det) 221 connected_devices.append((det, dev)) 222 223 if dev is None: 224 print('Unable to find a connected ebook reader.', file=sys.stderr) 225 shutdown_plugins() 226 return 1 227 228 for det, d in connected_devices: 229 try: 230 d.open(det, None) 231 except: 232 continue 233 else: 234 dev = d 235 d.specialize_global_preferences(device_prefs) 236 break 237 238 try: 239 if command == "df": 240 total = dev.total_space(end_session=False) 241 free = dev.free_space() 242 where = ("Memory", "Card A", "Card B") 243 print("Filesystem\tSize \tUsed \tAvail \tUse%") 244 for i in range(3): 245 print("%-10s\t%s\t%s\t%s\t%s"%(where[i], human_readable(total[i]), human_readable(total[i]-free[i]), human_readable(free[i]), 246 str(0 if total[i]==0 else int(100*(total[i]-free[i])/(total[i]*1.)))+"%")) 247 elif command == 'eject': 248 dev.eject() 249 elif command == "books": 250 print("Books in main memory:") 251 for book in dev.books(): 252 print(book) 253 print("\nBooks on storage carda:") 254 for book in dev.books(oncard='carda'): 255 print(book) 256 print("\nBooks on storage cardb:") 257 for book in dev.books(oncard='cardb'): 258 print(book) 259 elif command == "mkdir": 260 parser = OptionParser(usage="usage: %prog mkdir [options] path\nCreate a folder on the device\n\npath must begin with / or card:/") 261 if len(args) != 1: 262 parser.print_help() 263 sys.exit(1) 264 dev.mkdir(args[0]) 265 elif command == "ls": 266 parser = OptionParser(usage="usage: %prog ls [options] path\nList files on the device\n\npath must begin with / or card:/") 267 parser.add_option( 268 "-l", help="In addition to the name of each file, print the file type, permissions, and timestamp (the modification time, in the local timezone). Times are local.", # noqa 269 dest="ll", action="store_true", default=False) 270 parser.add_option("-R", help="Recursively list subfolders encountered. /dev and /proc are omitted", 271 dest="recurse", action="store_true", default=False) 272 parser.remove_option("-h") 273 parser.add_option("-h", "--human-readable", help="show sizes in human readable format", dest="hrs", action="store_true", default=False) 274 options, args = parser.parse_args(args) 275 if len(args) != 1: 276 parser.print_help() 277 return 1 278 print(ls(dev, args[0], recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols), end=' ') 279 elif command == "info": 280 info(dev) 281 elif command == "cp": 282 usage="usage: %prog cp [options] source destination\nCopy files to/from the device\n\n"+\ 283 "One of source or destination must be a path on the device. \n\nDevice paths have the form\n"+\ 284 "dev:mountpoint/my/path\n"+\ 285 "where mountpoint is one of / or carda: or cardb:/\n\n"+\ 286 "source must point to a file for which you have read permissions\n"+\ 287 "destination must point to a file or folder for which you have write permissions" 288 parser = OptionParser(usage=usage) 289 parser.add_option('-f', '--force', dest='force', action='store_true', default=False, 290 help='Overwrite the destination file if it exists already.') 291 options, args = parser.parse_args(args) 292 if len(args) != 2: 293 parser.print_help() 294 return 1 295 if args[0].startswith("dev:"): 296 outfile = args[1] 297 path = args[0][4:] 298 if path.endswith("/"): 299 path = path[:-1] 300 if os.path.isdir(outfile): 301 outfile = os.path.join(outfile, path[path.rfind("/")+1:]) 302 try: 303 outfile = lopen(outfile, "wb") 304 except OSError as e: 305 print(e, file=sys.stderr) 306 parser.print_help() 307 return 1 308 dev.get_file(path, outfile) 309 fsync(outfile) 310 outfile.close() 311 elif args[1].startswith("dev:"): 312 try: 313 infile = lopen(args[0], "rb") 314 except OSError as e: 315 print(e, file=sys.stderr) 316 parser.print_help() 317 return 1 318 dev.put_file(infile, args[1][4:], replace_file=options.force) 319 infile.close() 320 else: 321 parser.print_help() 322 return 1 323 elif command == "cat": 324 outfile = sys.stdout 325 parser = OptionParser( 326 usage="usage: %prog cat path\nShow file on the device\n\npath should point to a file on the device and must begin with /,a:/ or b:/") 327 options, args = parser.parse_args(args) 328 if len(args) != 1: 329 parser.print_help() 330 return 1 331 if args[0].endswith("/"): 332 path = args[0][:-1] 333 else: 334 path = args[0] 335 outfile = sys.stdout 336 dev.get_file(path, outfile) 337 elif command == "rm": 338 parser = OptionParser(usage="usage: %prog rm path\nDelete files from the device\n\npath should point to a file or empty folder on the device "+ 339 "and must begin with / or card:/\n\n"+ 340 "rm will DELETE the file. Be very CAREFUL") 341 options, args = parser.parse_args(args) 342 if len(args) != 1: 343 parser.print_help() 344 return 1 345 dev.rm(args[0]) 346 elif command == "touch": 347 parser = OptionParser(usage="usage: %prog touch path\nCreate an empty file on the device\n\npath should point to a file on the device and must begin with /,a:/ or b:/\n\n"+ # noqa 348 "Unfortunately, I can't figure out how to update file times on the device, so if path already exists, touch does nothing") 349 options, args = parser.parse_args(args) 350 if len(args) != 1: 351 parser.print_help() 352 return 1 353 dev.touch(args[0]) 354 elif command == 'test_file': 355 parser = OptionParser(usage=("usage: %prog test_file path\n" 356 'Open device, copy file specified by path to device and ' 357 'then eject device.')) 358 options, args = parser.parse_args(args) 359 if len(args) != 1: 360 parser.print_help() 361 return 1 362 path = args[0] 363 from calibre.ebooks.metadata.meta import get_metadata 364 mi = get_metadata(lopen(path, 'rb'), path.rpartition('.')[-1].lower()) 365 print(dev.upload_books([args[0]], [os.path.basename(args[0])], 366 end_session=False, metadata=[mi])) 367 dev.eject() 368 else: 369 parser.print_help() 370 if getattr(dev, 'handle', False): 371 dev.close() 372 return 1 373 except DeviceLocked: 374 print("The device is locked. Use the --unlock option", file=sys.stderr) 375 except (ArgumentError, DeviceError) as e: 376 print(e, file=sys.stderr) 377 return 1 378 finally: 379 shutdown_plugins() 380 381 return 0 382 383 384if __name__ == '__main__': 385 main() 386