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