1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3# 4# (c) Copyright 2003-2015 HP Development Company, L.P. 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19# 20# Author: Don Welch 21# 22 23__version__ = '3.3' 24__mod__ = 'hp-unload' 25__title__ = 'Photo Card Access Utility' 26__doc__ = "Access inserted photo cards on supported HPLIP printers. This provides an alternative for older devices that do not support USB mass storage or for access to photo cards over a network." 27 28# Std Lib 29import sys 30import os 31import os.path 32import getopt 33import re 34import cmd 35import time 36import fnmatch 37import string 38import operator 39 40try: 41 import readline 42except ImportError: 43 pass 44 45# Local 46from base.g import * 47from base import device, utils, tui, module 48from prnt import cups 49 50# Console class (from ASPN Python Cookbook) 51# Author: James Thiele 52# Date: 27 April 2004 53# Version: 1.0 54# Location: http://www.eskimo.com/~jet/python/examples/cmd/ 55# Copyright (c) 2004, James Thiele 56 57class Console(cmd.Cmd): 58 59 def __init__(self, pc): 60 cmd.Cmd.__init__(self) 61 self.intro = "Type 'help' for a list of commands. Type 'exit' to quit." 62 self.pc = pc 63 disk_info = self.pc.info() 64 pc.write_protect = disk_info[8] 65 if pc.write_protect: 66 log.warning("Photo card is write protected.") 67 self.prompt = log.bold("pcard: %s > " % self.pc.pwd()) 68 69 # Command definitions 70 def do_hist(self, args): 71 """Print a list of commands that have been entered""" 72 print(self._hist) 73 74 def do_exit(self, args): 75 """Exits from the console""" 76 return -1 77 78 def do_quit(self, args): 79 """Exits from the console""" 80 return -1 81 82 # Command definitions to support Cmd object functionality 83 def do_EOF(self, args): 84 """Exit on system end of file character""" 85 return self.do_exit(args) 86 87 def do_help(self, args): 88 """Get help on commands 89 'help' or '?' with no arguments prints a list of commands for which help is available 90 'help <command>' or '? <command>' gives help on <command> 91 """ 92 # The only reason to define this method is for the help text in the doc string 93 cmd.Cmd.do_help(self, args) 94 95 # Override methods in Cmd object 96 def preloop(self): 97 """Initialization before prompting user for commands. 98 Despite the claims in the Cmd documentaion, Cmd.preloop() is not a stub. 99 """ 100 cmd.Cmd.preloop(self) # sets up command completion 101 self._hist = [] # No history yet 102 self._locals = {} # Initialize execution namespace for user 103 self._globals = {} 104 105 def postloop(self): 106 """Take care of any unfinished business. 107 Despite the claims in the Cmd documentaion, Cmd.postloop() is not a stub. 108 """ 109 cmd.Cmd.postloop(self) # Clean up command completion 110 print("Exiting...") 111 112 def precmd(self, line): 113 """ This method is called after the line has been input but before 114 it has been interpreted. If you want to modifdy the input line 115 before execution (for example, variable substitution) do it here. 116 """ 117 self._hist += [line.strip()] 118 return line 119 120 def postcmd(self, stop, line): 121 """If you want to stop the console, return something that evaluates to true. 122 If you want to do some post command processing, do it here. 123 """ 124 return stop 125 126 def emptyline(self): 127 """Do nothing on empty input line""" 128 pass 129 130 def default(self, line): 131 print(log.bold("ERROR: Unrecognized command. Use 'help' to list commands.")) 132 133 def do_ldir(self, args): 134 """ List local directory contents.""" 135 os.system('ls -l') 136 137 def do_lls(self, args): 138 """ List local directory contents.""" 139 os.system('ls -l') 140 141 def do_dir(self, args): 142 """Synonym for the ls command.""" 143 return self.do_ls(args) 144 145 def do_ls(self, args): 146 """List photo card directory contents.""" 147 args = args.strip().lower() 148 files = self.pc.ls(True, args) 149 150 total_size = 0 151 formatter = utils.TextFormatter( 152 ( 153 {'width': 14, 'margin' : 2}, 154 {'width': 12, 'margin' : 2, 'alignment' : utils.TextFormatter.RIGHT}, 155 {'width': 30, 'margin' : 2}, 156 ) 157 ) 158 159 print() 160 print(log.bold(formatter.compose(("Name", "Size", "Type")))) 161 162 num_files = 0 163 for d in self.pc.current_directories(): 164 if d[0] in ('.', '..'): 165 print(formatter.compose((d[0], "", "directory"))) 166 else: 167 print(formatter.compose((d[0] + "/", "", "directory"))) 168 169 for f in self.pc.current_files(): 170 print(formatter.compose((f[0], utils.format_bytes(f[2]), self.pc.classify_file(f[0])))) 171 num_files += 1 172 total_size += f[2] 173 174 print(log.bold("% d files, %s" % (num_files, utils.format_bytes(total_size, True)))) 175 176 177 def do_df(self, args): 178 """Display free space on photo card. 179 Options: 180 -h\tDisplay in human readable format 181 """ 182 freespace = self.pc.df() 183 184 if args.strip().lower() == '-h': 185 fs = utils.format_bytes(freespace) 186 else: 187 fs = utils.commafy(freespace) 188 189 print("Freespace = %s Bytes" % fs) 190 191 192 def do_cp(self, args, remove_after_copy=False): 193 """Copy files from photo card to current local directory. 194 Usage: 195 \tcp FILENAME(S)|GLOB PATTERN(S) 196 Example: 197 \tCopy all JPEG and GIF files and a file named thumbs.db from photo card to local directory: 198 \tcp *.jpg *.gif thumbs.db 199 """ 200 args = args.strip().lower() 201 202 matched_files = self.pc.match_files(args) 203 204 if len(matched_files) == 0: 205 print("ERROR: File(s) not found.") 206 else: 207 total, delta = self.pc.cp_multiple(matched_files, remove_after_copy, self.cp_status_callback, self.rm_status_callback) 208 209 print(log.bold("\n%s transfered in %d sec (%d KB/sec)" % (utils.format_bytes(total), delta, (total/1024)/(delta)))) 210 211 def do_unload(self, args): 212 """Unload all image files from photocard to current local directory. 213 Note: 214 \tSubdirectories on photo card are not preserved 215 Options: 216 -x\tDon't remove files after copy 217 -p\tPrint unload list but do not copy or remove files""" 218 args = args.lower().strip().split() 219 dont_remove = False 220 if '-x' in args: 221 if self.pc.write_protect: 222 log.error("Photo card is write protected. -x not allowed.") 223 return 224 else: 225 dont_remove = True 226 227 228 unload_list = self.pc.get_unload_list() 229 print() 230 231 if len(unload_list) > 0: 232 if '-p' in args: 233 234 max_len = 0 235 for u in unload_list: 236 max_len = max(max_len, len(u[0])) 237 238 formatter = utils.TextFormatter( 239 ( 240 {'width': max_len+2, 'margin' : 2}, 241 {'width': 12, 'margin' : 2, 'alignment' : utils.TextFormatter.RIGHT}, 242 {'width': 12, 'margin' : 2}, 243 ) 244 ) 245 246 print() 247 print(log.bold(formatter.compose(("Name", "Size", "Type")))) 248 249 total = 0 250 for u in unload_list: 251 print(formatter.compose(('%s' % u[0], utils.format_bytes(u[1]), '%s/%s' % (u[2], u[3])))) 252 total += u[1] 253 254 255 print(log.bold("Found %d files to unload, %s" % (len(unload_list), utils.format_bytes(total, True)))) 256 else: 257 print(log.bold("Unloading %d files..." % len(unload_list))) 258 total, delta, was_cancelled = self.pc.unload(unload_list, self.cp_status_callback, self.rm_status_callback, dont_remove) 259 print(log.bold("\n%s unloaded in %d sec (%d KB/sec)" % (utils.format_bytes(total), delta, (total/1024)/delta))) 260 261 else: 262 print("No image, audio, or video files found.") 263 264 265 def cp_status_callback(self, src, trg, size): 266 if size == 1: 267 print() 268 print(log.bold("Copying %s..." % src)) 269 else: 270 print("\nCopied %s to %s (%s)..." % (src, trg, utils.format_bytes(size))) 271 272 def rm_status_callback(self, src): 273 print("Removing %s..." % src) 274 275 276 277 def do_rm(self, args): 278 """Remove files from photo card.""" 279 if self.pc.write_protect: 280 log.error("Photo card is write protected. rm not allowed.") 281 return 282 283 args = args.strip().lower() 284 285 matched_files = self.pc.match_files(args) 286 287 if len(matched_files) == 0: 288 print("ERROR: File(s) not found.") 289 else: 290 for f in matched_files: 291 self.pc.rm(f, False) 292 293 self.pc.ls() 294 295 def do_mv(self, args): 296 """Move files off photocard""" 297 if self.pc.write_protect: 298 log.error("Photo card is write protected. mv not allowed.") 299 return 300 self.do_cp(args, True) 301 302 def do_lpwd(self, args): 303 """Print name of local current/working directory.""" 304 print(os.getcwd()) 305 306 def do_lcd(self, args): 307 """Change current local working directory.""" 308 try: 309 os.chdir(args.strip()) 310 except OSError: 311 print(log.bold("ERROR: Directory not found.")) 312 print(os.getcwd()) 313 314 def do_pwd(self, args): 315 """Print name of photo card current/working directory 316 Usage: 317 \t>pwd""" 318 print(self.pc.pwd()) 319 320 def do_cd(self, args): 321 """Change current working directory on photo card. 322 Note: 323 \tYou may only specify one directory level at a time. 324 Usage: 325 \tcd <directory> 326 """ 327 args = args.lower().strip() 328 329 if args == '..': 330 if self.pc.pwd() != '/': 331 self.pc.cdup() 332 333 elif args == '.': 334 pass 335 336 elif args == '/': 337 self.pc.cd('/') 338 339 else: 340 matched_dirs = self.pc.match_dirs(args) 341 342 if len(matched_dirs) == 0: 343 print("Directory not found") 344 345 elif len(matched_dirs) > 1: 346 print("Pattern matches more than one directory") 347 348 else: 349 self.pc.cd(matched_dirs[0]) 350 351 self.prompt = log.bold("pcard: %s > " % self.pc.pwd()) 352 353 def do_cdup(self, args): 354 """Change to parent directory.""" 355 self.do_cd('..') 356 357 #def complete_cd( self, text, line, begidx, endidx ): 358 # print text, line, begidx, endidx 359 # #return "XXX" 360 361 def do_cache(self, args): 362 """Display current cache entries, or turn cache on/off. 363 Usage: 364 \tDisplay: cache 365 \tTurn on: cache on 366 \tTurn off: cache off 367 """ 368 args = args.strip().lower() 369 370 if args == 'on': 371 self.pc.cache_control(True) 372 373 elif args == 'off': 374 self.pc.cache_control(False) 375 376 else: 377 if self.pc.cache_state(): 378 cache_info = self.pc.cache_info() 379 380 t = list(cache_info.keys()) 381 t.sort() 382 print() 383 for s in t: 384 print("sector %d (%d hits)" % (s, cache_info[s])) 385 386 print(log.bold("Total cache usage: %s (%s maximum)" % (utils.format_bytes(len(t)*512), utils.format_bytes(photocard.MAX_CACHE * 512)))) 387 print(log.bold("Total cache sectors: %s of %s" % (utils.commafy(len(t)), utils.commafy(photocard.MAX_CACHE)))) 388 else: 389 print("Cache is off.") 390 391 def do_sector(self, args): 392 """Display sector data. 393 Usage: 394 \tsector <sector num> 395 """ 396 args = args.strip().lower() 397 cached = False 398 try: 399 sector = int(args) 400 except ValueError: 401 print("Sector must be specified as a number") 402 return 403 404 if self.pc.cache_check(sector) > 0: 405 print("Cached sector") 406 407 print(repr(self.pc.sector(sector))) 408 409 410 def do_tree(self, args): 411 """Display photo card directory tree.""" 412 tree = self.pc.tree() 413 print() 414 self.print_tree(tree) 415 416 def print_tree(self, tree, level=0): 417 for d in tree: 418 if type(tree[d]) == type({}): 419 print(''.join([' '*level*4, d, '/'])) 420 self.print_tree(tree[d], level+1) 421 422 423 def do_reset(self, args): 424 """Reset the cache.""" 425 self.pc.cache_reset() 426 427 428 def do_card(self, args): 429 """Print info about photocard.""" 430 print() 431 print("Device URI = %s" % self.pc.device.device_uri) 432 print("Model = %s" % self.pc.device.model_ui) 433 print("Working dir = %s" % self.pc.pwd()) 434 disk_info = self.pc.info() 435 print("OEM ID = %s" % disk_info[0]) 436 print("Bytes/sector = %d" % disk_info[1]) 437 print("Sectors/cluster = %d" % disk_info[2]) 438 print("Reserved sectors = %d" % disk_info[3]) 439 print("Root entries = %d" % disk_info[4]) 440 print("Sectors/FAT = %d" % disk_info[5]) 441 print("Volume label = %s" % disk_info[6]) 442 print("System ID = %s" % disk_info[7]) 443 print("Write protected = %d" % disk_info[8]) 444 print("Cached sectors = %s" % utils.commafy(len(self.pc.cache_info()))) 445 446 447 def do_display(self, args): 448 """Display an image with ImageMagick. 449 Usage: 450 \tdisplay <filename>""" 451 args = args.strip().lower() 452 matched_files = self.pc.match_files(args) 453 454 if len(matched_files) == 1: 455 456 typ = self.pc.classify_file(args).split('/')[0] 457 458 if typ == 'image': 459 fd, temp_name = utils.make_temp_file() 460 self.pc.cp(args, temp_name) 461 os.system('display %s' % temp_name) 462 os.remove(temp_name) 463 464 else: 465 print("File is not an image.") 466 467 elif len(matched_files) == 0: 468 print("File not found.") 469 470 else: 471 print("Only one file at a time may be specified for display.") 472 473 def do_show(self, args): 474 """Synonym for the display command.""" 475 self.do_display(args) 476 477 def do_thumbnail(self, args): 478 """Display an embedded thumbnail image with ImageMagick. 479 Note: 480 \tOnly works with JPEG/JFIF images with embedded JPEG/TIFF thumbnails 481 Usage: 482 \tthumbnail <filename>""" 483 args = args.strip().lower() 484 matched_files = self.pc.match_files(args) 485 486 if len(matched_files) == 1: 487 typ, subtyp = self.pc.classify_file(args).split('/') 488 489 if typ == 'image' and subtyp in ('jpeg', 'tiff'): 490 exif_info = self.pc.get_exif(args) 491 492 dir_name, file_name=os.path.split(args) 493 photo_name, photo_ext=os.path.splitext(args) 494 495 if 'JPEGThumbnail' in exif_info: 496 temp_file_fd, temp_file_name = utils.make_temp_file() 497 open(temp_file_name, 'wb').write(exif_info['JPEGThumbnail']) 498 os.system('display %s' % temp_file_name) 499 os.remove(temp_file_name) 500 501 elif 'TIFFThumbnail' in exif_info: 502 temp_file_fd, temp_file_name = utils.make_temp_file() 503 open(temp_file_name, 'wb').write(exif_info['TIFFThumbnail']) 504 os.system('display %s' % temp_file_name) 505 os.remove(temp_file_name) 506 507 else: 508 print("No thumbnail found.") 509 510 else: 511 print("Incorrect file type for thumbnail.") 512 513 elif len(matched_files) == 0: 514 print("File not found.") 515 else: 516 print("Only one file at a time may be specified for thumbnail display.") 517 518 def do_thumb(self, args): 519 """Synonym for the thumbnail command.""" 520 self.do_thumbnail(args) 521 522 def do_exif(self, args): 523 """Display EXIF info for file. 524 Usage: 525 \texif <filename>""" 526 args = args.strip().lower() 527 matched_files = self.pc.match_files(args) 528 529 if len(matched_files) == 1: 530 typ, subtyp = self.pc.classify_file(args).split('/') 531 #print "'%s' '%s'" % (typ, subtyp) 532 533 if typ == 'image' and subtyp in ('jpeg', 'tiff'): 534 exif_info = self.pc.get_exif(args) 535 536 formatter = utils.TextFormatter( 537 ( 538 {'width': 40, 'margin' : 2}, 539 {'width': 40, 'margin' : 2}, 540 ) 541 ) 542 543 print() 544 print(log.bold(formatter.compose(("Tag", "Value")))) 545 546 ee = list(exif_info.keys()) 547 ee.sort() 548 for e in ee: 549 if e not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename'): 550 #if e != 'EXIF MakerNote': 551 print(formatter.compose((e, '%s' % exif_info[e]))) 552 #else: 553 # print formatter.compose( ( e, ''.join( [ chr(x) for x in exif_info[e].values if chr(x) in string.printable ] ) ) ) 554 else: 555 print("Incorrect file type for thumbnail.") 556 557 elif len(matched_files) == 0: 558 print("File not found.") 559 else: 560 print("Only one file at a time may be specified for thumbnail display.") 561 562 def do_info(self, args): 563 """Synonym for the exif command.""" 564 self.do_exif(args) 565 566 def do_about(self, args): 567 utils.log_title(__title__, __version__) 568 569 570def status_callback(src, trg, size): 571 if size == 1: 572 print() 573 print(log.bold("Copying %s..." % src)) 574 else: 575 print("\nCopied %s to %s (%s)..." % (src, trg, utils.format_bytes(size))) 576 577 578 579mod = module.Module(__mod__, __title__, __version__, __doc__, None, 580 (GUI_MODE, INTERACTIVE_MODE, NON_INTERACTIVE_MODE), 581 (UI_TOOLKIT_QT3,), False, False, True) 582 583mod.setUsage(module.USAGE_FLAG_DEVICE_ARGS, 584 extra_options=[("Output directory:", "-o<dir> or --output=<dir> (Defaults to current directory)(Only used for non-GUI modes)", "option", False)], 585 see_also_list=['hp-toolbox']) 586 587opts, device_uri, printer_name, mode, ui_toolkit, loc = \ 588 mod.parseStdOpts('o', ['output=']) 589 590from pcard import photocard 591 592output_dir = os.getcwd() 593 594for o, a in opts: 595 if o in ('-o', '--output'): 596 output_dir = a 597 598if mode == GUI_MODE: 599 if not utils.canEnterGUIMode(): 600 mode = INTERACTIVE_MODE 601 602if mode == GUI_MODE: 603 if ui_toolkit == 'qt4': 604 log.error("%s does not support Qt4. Please use Qt3 or run in -i or -n modes.") 605 sys.exit(1) 606 607if mode in (INTERACTIVE_MODE, NON_INTERACTIVE_MODE): 608 try: 609 device_uri = mod.getDeviceUri(device_uri, printer_name, 610 filter={'pcard-type' : (operator.eq, 1)}) 611 612 if not device_uri: 613 sys.exit(1) 614 log.info("Using device : %s\n" % device_uri) 615 try: 616 pc = photocard.PhotoCard( None, device_uri, printer_name ) 617 except Error as e: 618 log.error("Unable to start photocard session: %s" % e.msg) 619 sys.exit(1) 620 621 pc.set_callback(update_spinner) 622 623 try: 624 pc.mount() 625 except Error: 626 log.error("Unable to mount photo card on device. Check that device is powered on and photo card is correctly inserted.") 627 pc.umount() 628 # TODO: 629 #pc.device.sendEvent(EVENT_PCARD_UNABLE_TO_MOUNT, typ='error') 630 sys.exit(1) 631 632 log.info(log.bold("\nPhotocard on device %s mounted" % pc.device.device_uri)) 633 log.info(log.bold("DO NOT REMOVE PHOTO CARD UNTIL YOU EXIT THIS PROGRAM")) 634 635 output_dir = os.path.realpath(os.path.normpath(os.path.expanduser(output_dir))) 636 637 try: 638 os.chdir(output_dir) 639 except OSError: 640 print(log.bold("ERROR: Output directory %s not found." % output_dir)) 641 sys.exit(1) 642 643 644 if mode == INTERACTIVE_MODE: # INTERACTIVE_MODE 645 console = Console(pc) 646 try: 647 try: 648 console . cmdloop() 649 except KeyboardInterrupt: 650 log.error("Aborted.") 651 except Exception as e: 652 log.error("An error occured: %s" % e) 653 finally: 654 pc.umount() 655 656 # TODO: 657 #pc.device.sendEvent(EVENT_END_PCARD_JOB) 658 659 660 else: # NON_INTERACTIVE_MODE 661 print("Output directory is %s" % os.getcwd()) 662 try: 663 unload_list = pc.get_unload_list() 664 print() 665 666 if len(unload_list) > 0: 667 668 max_len = 0 669 for u in unload_list: 670 max_len = max(max_len, len(u[0])) 671 672 formatter = utils.TextFormatter( 673 ( 674 {'width': max_len+2, 'margin' : 2}, 675 {'width': 12, 'margin' : 2, 'alignment' : utils.TextFormatter.RIGHT}, 676 {'width': 12, 'margin' : 2}, 677 ) 678 ) 679 680 print() 681 print(log.bold(formatter.compose(("Name", "Size", "Type")))) 682 683 total = 0 684 for u in unload_list: 685 print(formatter.compose(('%s' % u[0], utils.format_bytes(u[1]), '%s/%s' % (u[2], u[3])))) 686 total += u[1] 687 688 689 print(log.bold("Found %d files to unload, %s\n" % (len(unload_list), utils.format_bytes(total, True)))) 690 print(log.bold("Unloading files...\n")) 691 total, delta, was_cancelled = pc.unload(unload_list, status_callback, None, True) 692 print(log.bold("\n%s unloaded in %d sec (%d KB/sec)" % (utils.format_bytes(total), delta, (total/1024)/delta))) 693 694 695 finally: 696 pc.umount() 697 698 except KeyboardInterrupt: 699 log.error("User exit") 700 701 702else: # GUI_MODE (qt3 only) 703 try: 704 from qt import * 705 from ui import unloadform 706 except ImportError: 707 log.error("Unable to load Qt3 support. Is it installed?") 708 sys.exit(1) 709 710 app = QApplication(sys.argv) 711 QObject.connect(app, SIGNAL("lastWindowClosed()"), app, SLOT("quit()")) 712 713 if loc is None: 714 loc = user_conf.get('ui', 'loc', 'system') 715 if loc.lower() == 'system': 716 loc = str(QTextCodec.locale()) 717 log.debug("Using system locale: %s" % loc) 718 719 if loc.lower() != 'c': 720 e = 'utf8' 721 try: 722 l, x = loc.split('.') 723 loc = '.'.join([l, e]) 724 except ValueError: 725 l = loc 726 loc = '.'.join([loc, e]) 727 728 log.debug("Trying to load .qm file for %s locale." % loc) 729 trans = QTranslator(None) 730 731 qm_file = 'hplip_%s.qm' % l 732 log.debug("Name of .qm file: %s" % qm_file) 733 loaded = trans.load(qm_file, prop.localization_dir) 734 735 if loaded: 736 app.installTranslator(trans) 737 else: 738 loc = 'c' 739 740 if loc == 'c': 741 log.debug("Using default 'C' locale") 742 else: 743 log.debug("Using locale: %s" % loc) 744 QLocale.setDefault(QLocale(loc)) 745 prop.locale = loc 746 try: 747 locale.setlocale(locale.LC_ALL, locale.normalize(loc)) 748 except locale.Error: 749 pass 750 751 try: 752 w = unloadform.UnloadForm(['cups'], device_uri, printer_name) 753 except Error: 754 log.error("Unable to connect to HPLIP I/O. Please (re)start HPLIP and try again.") 755 sys.exit(1) 756 757 app.setMainWidget(w) 758 w.show() 759 760 app.exec_loop() 761 762log.info("") 763log.info("Done.") 764