Use exceptions for error handling
[rarslave2.git] / rarslave.py
1 #!/usr/bin/env python
2 # vim: set ts=4 sts=4 sw=4 textwidth=92:
3
4 """
5 The main program of the rarslave project.
6
7 This handles all of the commandline, configuration file, and option
8 work. It gets the environment set up for a run using the RarslaveDetector
9 class.
10 """
11
12 __author__    = "Ira W. Snyder (devel@irasnyder.com)"
13 __copyright__ = "Copyright (c) 2006,2007 Ira W. Snyder (devel@irasnyder.com)"
14 __license__   = "GNU GPL v2 (or, at your option, any later version)"
15
16 #    rarslave.py -- a usenet autorepair and autoextract utility
17 #
18 #    Copyright (C) 2006,2007  Ira W. Snyder (devel@irasnyder.com)
19 #
20 #    This program is free software; you can redistribute it and/or modify
21 #    it under the terms of the GNU General Public License as published by
22 #    the Free Software Foundation; either version 2 of the License, or
23 #    (at your option) any later version.
24 #
25 #    This program is distributed in the hope that it will be useful,
26 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
27 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28 #    GNU General Public License for more details.
29 #
30 #    You should have received a copy of the GNU General Public License
31 #    along with this program; if not, write to the Free Software
32 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
33
34 VERSION="2.0.0"
35 PROGRAM="rarslave"
36
37 import os, sys, optparse, logging
38 import rsutil
39 import RarslaveDetector
40
41 # Global options from the rsutil.globals class
42 options = rsutil.globals.options
43 config = rsutil.globals.config
44
45 # A tiny class to hold logging output until we're finished
46 class DelayedLogger (object):
47
48         """A small class to hold logging output until the program is finished running.
49            It emulates sys.stdout in the needed ways for the logging module."""
50
51         def __init__ (self, output=sys.stdout.write):
52                 self.__messages = []
53                 self.__output = output
54
55         def write (self, msg):
56                 self.__messages.append (msg)
57
58         def flush (self):
59                 pass
60
61         def size (self):
62                 """Returns the number of messages queued for printing"""
63                 return len (self.__messages)
64
65         def close (self):
66                 """Print all messages, clear the queue"""
67                 for m in self.__messages:
68                         self.__output (m)
69
70                 self.__messages = []
71
72 # A tiny class used to find unique PAR2 sets
73 class CompareSet (object):
74
75         """A small class used to find unique PAR2 sets"""
76
77         def __init__ (self, dir, p2file):
78                 self.dir = dir
79                 self.p2file = p2file
80
81                 self.basename = rsutil.common.get_basename (self.p2file)
82                 self.name_matches = rsutil.common.find_name_matches (self.dir, self.basename)
83
84         def __eq__ (self, rhs):
85                 return (self.dir == rhs.dir) \
86                                 and (self.basename == rhs.basename) \
87                                 and rsutil.common.list_eq (self.name_matches, rhs.name_matches)
88
89
90 def find_all_par2_files (dir):
91         """Finds all par2 files in the given directory.
92
93            dir -- the directory in which to search for PAR2 files
94
95            NOTE: does not return absolute paths"""
96
97         if not os.path.isdir (os.path.abspath (dir)):
98                 raise ValueError # bad directory given
99
100         dir = os.path.abspath (dir)
101         files = os.listdir (dir)
102
103         return rsutil.common.find_par2_files (files)
104
105 def generate_all_parsets (dir):
106         """Generate all parsets in the given directory
107
108            dir -- the directory in which to search"""
109
110         assert os.path.isdir (dir) # Directory MUST be valid
111
112         parsets = []
113         p2files = find_all_par2_files (dir)
114
115         for f in p2files:
116                 p = CompareSet (dir, f)
117                 if p not in parsets:
118                         parsets.append (p)
119
120         return [(p.dir, p.p2file) for p in parsets]
121
122 def check_required_progs():
123         """Check if the required programs are installed"""
124
125         needed = []
126
127         try:
128                 rsutil.common.run_command(['par2repair', '--help'])
129         except OSError:
130                 needed.append('par2repair')
131         except RuntimeError:
132                 pass
133
134         try:
135                 rsutil.common.run_command(['unrar', '--help'])
136         except OSError:
137                 needed.append('unrar')
138         except RuntimeError:
139                 pass
140
141         try:
142                 rsutil.common.run_command(['unzip', '--help'])
143         except OSError:
144                 needed.append('unzip')
145         except RuntimeError:
146                 pass
147
148         if needed:
149                 for n in needed:
150                         print 'Needed program "%s" not found in $PATH' % (n, )
151
152                 sys.exit(1)
153
154 def run_options (options):
155         """Process all of the commandline options, doing thing such as printing the
156            version number, etc."""
157
158         # Fix directories
159         options.work_dir = rsutil.common.full_abspath (options.work_dir)
160
161         # Make sure that the directory is valid
162         if not os.path.isdir (options.work_dir):
163                 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
164                 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
165                 sys.stderr.write ('configuration file to override the working directory permanently.\n')
166                 sys.exit (1)
167
168         if options.extract_dir != None:
169                 options.extract_dir = rsutil.common.full_abspath (options.extract_dir)
170
171         if options.version:
172                 print PROGRAM + ' - ' + VERSION
173                 print
174                 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
175                 print
176                 print 'This program comes with ABSOLUTELY NO WARRANTY.'
177                 print 'This is free software, and you are welcome to redistribute it'
178                 print 'under certain conditions. See the file COPYING for details.'
179                 sys.exit (0)
180
181         if options.check_progs:
182                 check_required_progs ()
183                 sys.exit (0)
184
185         if options.write_def_config:
186                 config.write_config (default=True)
187                 sys.exit (0)
188
189         if options.write_config:
190                 config.write_config ()
191                 sys.exit (0)
192
193 def find_loglevel (options):
194         """Find the log level that should be printed by the logging class"""
195
196         loglevel = options.verbose - options.quiet
197
198         if loglevel > 1:
199                 loglevel = 1
200
201         if loglevel < -3:
202                 loglevel = -3
203
204         LEVELS = {      1 : logging.DEBUG,
205                                 0 : logging.INFO,
206                                 -1: logging.WARNING,
207                                 -2: logging.ERROR,
208                                 -3: logging.CRITICAL
209         }
210
211         return LEVELS [loglevel]
212
213 def main ():
214
215         # Setup the logger
216         logger = DelayedLogger ()
217         logging.basicConfig (stream=logger, level=logging.WARNING, \
218                         format='%(levelname)-8s %(message)s')
219
220         # Build the OptionParser
221         parser = optparse.OptionParser()
222         parser.add_option('-n', '--not-recursive', action='store_false', dest='recursive',
223                                                 default=rsutil.common.config_get_value('options', 'recursive'),
224                                                 help="Don't run recursively")
225
226         parser.add_option('-d', '--work-dir', dest='work_dir', type='string',
227                                                 default=rsutil.common.config_get_value('directories', 'working_directory'),
228                                                 help="Start running at DIR", metavar='DIR')
229
230         parser.add_option('-e', '--extract-dir', dest='extract_dir', type='string',
231                                                 default=rsutil.common.config_get_value('directories', 'extract_directory'),
232                                                 help="Extract to DIR", metavar='DIR')
233
234         parser.add_option('-p', '--check-required-programs',
235                                                 action='store_true', dest='check_progs',
236                                                 default=False,
237                                                 help="Check for required programs")
238
239         parser.add_option('-f', '--write-default-config',
240                                                 action='store_true', dest='write_def_config',
241                                                 default=False, help="Write out a new default config")
242
243         parser.add_option('-c', '--write-new-config',
244                                                 action='store_true', dest='write_config',
245                                                 default=False, help="Write out the current config")
246
247         parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
248                                                 default=rsutil.common.config_get_value('options', 'interactive'),
249                                                 help="Confirm before removing files")
250
251         parser.add_option('-q', '--quiet', dest='quiet', action='count',
252                                                 default=0, help="Output fatal messages only")
253
254         parser.add_option('-v', '--verbose', dest='verbose', action='count',
255                                                 default=0, help="Output extra information")
256
257         parser.add_option('-V', '--version', dest='version', action='store_true',
258                                                 default=False, help="Output version information")
259
260         parser.version = VERSION
261
262         # Parse the given options
263         global options
264         (rsutil.globals.options, args) = parser.parse_args()
265         options = rsutil.globals.options
266
267         # Run any special actions that are needed on these options
268         run_options (options)
269
270         # Find the loglevel using the options given
271         logging.getLogger().setLevel (find_loglevel (options))
272
273         # Run recursively
274         if options.recursive:
275                 for (dir, subdirs, files) in os.walk (options.work_dir):
276                         parsets = generate_all_parsets (dir)
277                         for (p2dir, p2file) in parsets:
278                                 detector = RarslaveDetector.RarslaveDetector (p2dir, p2file)
279                                 detector.runMatchingTypes ()
280
281         # Non-recursive
282         else:
283                 parsets = generate_all_parsets (options.work_dir)
284                 for (p2dir, p2file) in parsets:
285                         detector = RarslaveDetector.RarslaveDetector (p2dir, p2file)
286                         detector.runMatchingTypes ()
287
288         # Print the results
289         if logger.size () > 0:
290                 print '\nLog\n' + '=' * 80
291                 logger.close ()
292
293         # Done!
294         return 0
295
296 if __name__ == '__main__':
297         main ()
298