Add Copyright / License information + Documentation
[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="rarslave2"
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         shell_not_found = 32512
126         needed = []
127
128         if rsutil.common.run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
129                 needed.append ('par2repair')
130
131         if rsutil.common.run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
132                 needed.append ('unrar')
133
134         if rsutil.common.run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
135                 needed.append ('unzip')
136
137         if needed:
138                 for n in needed:
139                         print 'Needed program "%s" not found in $PATH' % (n, )
140
141                 sys.exit(1)
142
143 def run_options (options):
144         """Process all of the commandline options, doing thing such as printing the
145            version number, etc."""
146
147         # Fix directories
148         options.work_dir = rsutil.common.full_abspath (options.work_dir)
149
150         # Make sure that the directory is valid
151         if not os.path.isdir (options.work_dir):
152                 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
153                 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
154                 sys.stderr.write ('configuration file to override the working directory permanently.\n')
155                 sys.exit (1)
156
157         if options.extract_dir != None:
158                 options.extract_dir = rsutil.common.full_abspath (options.extract_dir)
159
160         if options.version:
161                 print PROGRAM + ' - ' + VERSION
162                 print
163                 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
164                 print
165                 print 'This program comes with ABSOLUTELY NO WARRANTY.'
166                 print 'This is free software, and you are welcome to redistribute it'
167                 print 'under certain conditions. See the file COPYING for details.'
168                 sys.exit (0)
169
170         if options.check_progs:
171                 check_required_progs ()
172
173         if options.write_def_config:
174                 config.write_config (default=True)
175                 sys.exit (0)
176
177         if options.write_config:
178                 config.write_config ()
179                 sys.exit (0)
180
181 def find_loglevel (options):
182         """Find the log level that should be printed by the logging class"""
183
184         loglevel = options.verbose - options.quiet
185
186         if loglevel > 1:
187                 loglevel = 1
188
189         if loglevel < -3:
190                 loglevel = -3
191
192         LEVELS = {      1 : logging.DEBUG,
193                                 0 : logging.INFO,
194                                 -1: logging.WARNING,
195                                 -2: logging.ERROR,
196                                 -3: logging.CRITICAL
197         }
198
199         return LEVELS [loglevel]
200
201 def main ():
202
203         # Setup the logger
204         logger = DelayedLogger ()
205         logging.basicConfig (stream=logger, level=logging.WARNING, \
206                         format='%(levelname)-8s %(message)s')
207
208         # Build the OptionParser
209         parser = optparse.OptionParser()
210         parser.add_option('-n', '--not-recursive', action='store_false', dest='recursive',
211                                                 default=rsutil.common.config_get_value('options', 'recursive'),
212                                                 help="Don't run recursively")
213
214         parser.add_option('-d', '--work-dir', dest='work_dir', type='string',
215                                                 default=rsutil.common.config_get_value('directories', 'working_directory'),
216                                                 help="Start running at DIR", metavar='DIR')
217
218         parser.add_option('-e', '--extract-dir', dest='extract_dir', type='string',
219                                                 default=rsutil.common.config_get_value('directories', 'extract_directory'),
220                                                 help="Extract to DIR", metavar='DIR')
221
222         parser.add_option('-p', '--check-required-programs',
223                                                 action='store_true', dest='check_progs',
224                                                 default=False,
225                                                 help="Check for required programs")
226
227         parser.add_option('-f', '--write-default-config',
228                                                 action='store_true', dest='write_def_config',
229                                                 default=False, help="Write out a new default config")
230
231         parser.add_option('-c', '--write-new-config',
232                                                 action='store_true', dest='write_config',
233                                                 default=False, help="Write out the current config")
234
235         parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
236                                                 default=rsutil.common.config_get_value('options', 'interactive'),
237                                                 help="Confirm before removing files")
238
239         parser.add_option('-q', '--quiet', dest='quiet', action='count',
240                                                 default=0, help="Output fatal messages only")
241
242         parser.add_option('-v', '--verbose', dest='verbose', action='count',
243                                                 default=0, help="Output extra information")
244
245         parser.add_option('-V', '--version', dest='version', action='store_true',
246                                                 default=False, help="Output version information")
247
248         parser.version = VERSION
249
250         # Parse the given options
251         global options
252         (rsutil.globals.options, args) = parser.parse_args()
253         options = rsutil.globals.options
254
255         # Run any special actions that are needed on these options
256         run_options (options)
257
258         # Find the loglevel using the options given
259         logging.getLogger().setLevel (find_loglevel (options))
260
261         # Run recursively
262         if options.recursive:
263                 for (dir, subdirs, files) in os.walk (options.work_dir):
264                         parsets = generate_all_parsets (dir)
265                         for (p2dir, p2file) in parsets:
266                                 detector = RarslaveDetector.RarslaveDetector (p2dir, p2file)
267                                 ret = detector.runMatchingTypes ()
268
269         # Non-recursive
270         else:
271                 parsets = generate_all_parsets (options.work_dir)
272                 for (p2dir, p2file) in parsets:
273                         detector = RarslaveDetector.RarslaveDetector (p2dir, p2file)
274                         ret = detector.runMatchingTypes ()
275
276         # Print the results
277         if logger.size () > 0:
278                 print '\nLog\n' + '=' * 80
279                 logger.close ()
280
281         # Done!
282         return 0
283
284 if __name__ == '__main__':
285         main ()
286