#!/usr/bin/env python # * Copyright 2013 Alistair Buxton # * # * License: This program is free software; you can redistribute it and/or # * modify it under the terms of the GNU General Public License as published # * by the Free Software Foundation; either version 3 of the License, or (at # * your option) any later version. This program is distributed in the hope # * that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # * GNU General Public License for more details. import cmd import os import pipes import posixpath import fnmatch import urllib2 from datetime import datetime from gzip import GzipFile import logging import re logging.basicConfig(level=logging.DEBUG) from collections import defaultdict class FileInfo(object): def __init__(self, data): tmp = data.strip().split('@') self.date = datetime.fromtimestamp(int(tmp[0], 10)) self.path = tmp[1].split('/') self.name = tmp[2] self.size = int(tmp[3], 10) self.a = tmp[4] self.b = tmp[5] self.c = tmp[6] self.desc = tmp[7] def pretty_size(self): if self.size > 99000000: tmp = "%.0fM" % (self.size / 1000000.0) elif self.size > 999999: tmp = "%.1fM" % (self.size / 1000000.0) elif self.size > 99000: tmp = "%.0fK" % (self.size / 1000.0) elif self.size > 999: tmp = "%.1fK" % (self.size / 1000.0) else: tmp = "%dB" % (self.size) return tmp.rjust(5, ' ') def pretty_name(self, pad): return self.name.ljust(min(pad,len(self.name)+1), ' ').ljust(pad, '.') def info(self, pad=16): return ' '.join([ self.pretty_name(pad), self.pretty_size(), self.date.date().isoformat(), self.desc ]) class ADT(cmd.Cmd): intro = 'Welcome to Python Aminet Download Tool.\n' cached = os.path.expanduser("~")+'/.pyadt/' mirror = 'ftp://uk.aminet.net/pub/aminet/' def __init__(self): if not os.path.exists(ADT.cached): logging.debug('Make directory: ' + ADT.cached) os.makedirs(ADT.cached) self.db = defaultdict(lambda: defaultdict(lambda: defaultdict(FileInfo))) self.load_db() cmd.Cmd.__init__(self) self.cwd = '' self.update_prompt() ttyname = os.ttyname(0) if 'ttyUSB' in ttyname or 'ttyS' in ttyname: self.sz = True print 'Detected serial terminal. Will transfer downloaded files using zmodem.' else: self.sz = False def update_prompt(self): self.prompt = 'Aminet:' + self.cwd + '> ' def get_file(self, filename, force=False): if force or not os.path.exists(ADT.cached+filename): if not os.path.exists(ADT.cached+os.path.dirname(filename)): logging.debug('Make directory: ' + ADT.cached+os.path.dirname(filename)) os.makedirs(ADT.cached+os.path.dirname(filename)) logging.debug('Download file: ' + ADT.mirror+filename) response = urllib2.urlopen(ADT.mirror+filename) file(ADT.cached+filename, 'wb').write(response.read()) def load_db(self, force=False): self.get_file('info/adt/ADT_LOCAL.gz', force) local = GzipFile(ADT.cached+'info/adt/ADT_LOCAL.gz') next(local) for line in local: try: f = FileInfo(line) self.db[f.path[0]][f.path[1]][f.name] = f except ValueError: logging.warn('Bad File Info: ' + line) print ('Database loaded.') def path_exists(self, path): path = path.strip('/').split('/') n = len(path) if n == 1: return path[0] == '' or path[0] in self.db elif n == 2: return path[0] in self.db and path[1] in self.db[path[0]] else: return False def file_exists(self, path): path = path.strip('/').split('/') n = len(path) if n == 3: return path[0] in self.db and path[1] in self.db[path[0]] and path[2] in self.db[path[0]][path[1]] else: return False def cd(self, old, new): if new == '': return old elif new[0] == '/': tmp = posixpath.normpath(new) else: tmp = posixpath.normpath('/'+old+'/'+new) tmp = tmp.strip('/') logging.debug('Old: ' + old + ' New: ' + new + ' Result: ' + tmp) return tmp # Commands def do_exit(self, s): 'Exit Python Aminet Download Tool.' return True do_quit = do_exit do_EOF = do_exit def emptyline(self): pass def do_update(self, arg): 'Update Aminet index database.' self.load_db(force=True) def do_ls(self, arg): 'List file/directories in current working directory.' path = [p.lower() for p in self.cd(self.cwd, arg).split('/')] n = len(path) if n == 1: if path[0] == '': for k in sorted(self.db.keys()): relpath = posixpath.relpath('/'+k, '/'+self.cwd) if relpath == '.': relpath = '../'+k elif relpath == '..': relpath = '../../'+k print relpath else: for k in sorted(self.db.keys()): if fnmatch.fnmatch(k, path[0]): for kk in self.db[k].keys(): relpath = posixpath.relpath('/'+k+'/'+kk, '/'+self.cwd) if relpath == '.': relpath = '../'+kk print relpath elif n == 2: maxpad = 1 for k in self.db.keys(): if fnmatch.fnmatch(k, path[0]): for kk in self.db[k].keys(): if fnmatch.fnmatch(kk, path[1]): relpath = posixpath.relpath('/'+k+'/'+kk, '/'+self.cwd) if relpath == '.': relpath = '' else: relpath += '/' for v in self.db[k][kk].values(): if len(v.name)+len(relpath) > maxpad: maxpad = len(v.name)+len(relpath) for k in sorted(self.db.keys()): if fnmatch.fnmatch(k, path[0]): for kk in sorted(self.db[k].keys()): if fnmatch.fnmatch(kk, path[1]): relpath = posixpath.relpath('/'+k+'/'+kk, '/'+self.cwd) if relpath == '.': relpath = '' else: relpath += '/' for v in sorted(self.db[k][kk].values(), key=lambda f: f.name): print relpath + v.info(maxpad-len(relpath)) elif n == 3: maxpad = 1 for k in self.db.keys(): if fnmatch.fnmatch(k, path[0]): for kk in self.db[k].keys(): if fnmatch.fnmatch(kk, path[1]): relpath = posixpath.relpath('/'+k+'/'+kk, '/'+self.cwd) if relpath == '.': relpath = '' else: relpath += '/' for v in self.db[k][kk].values(): if fnmatch.fnmatch(v.name.lower(), path[2]): if len(v.name)+len(relpath) > maxpad: maxpad = len(v.name)+len(relpath) for k in sorted(self.db.keys()): if fnmatch.fnmatch(k, path[0]): for kk in sorted(self.db[k].keys()): if fnmatch.fnmatch(kk, path[1]): relpath = posixpath.relpath('/'+k+'/'+kk, '/'+self.cwd) if relpath == '.': relpath = '' else: relpath += '/' for v in sorted(self.db[k][kk].values(), key=lambda f: f.name): if fnmatch.fnmatch(v.name.lower(), path[2]): print relpath + v.info(maxpad-len(relpath)) def do_cd(self, arg): 'Change working directory.' if arg == '': return newcwd = self.cd(self.cwd, arg) if self.path_exists(newcwd): self.cwd = newcwd self.update_prompt() else: print ('No such directory.') def do_get(self, arg): 'Download named file.' if arg == '': return filepath = self.cd(self.cwd, arg) if self.file_exists(filepath): self.get_file(filepath) if self.sz: os.system('sz '+pipes.quote(ADT.cached+filepath)) else: print ('No such file.') def do_search(self, arg): 'Search using case insensitive regexp' pattern = re.compile(arg, re.IGNORECASE) for k in self.db.keys(): for kk in self.db[k].keys(): for v in self.db[k][kk].values(): if pattern.search(v.name) or pattern.search(v.desc): path = k+'/'+kk+'/' print path + v.info(28 - len(path)) if __name__=='__main__': ADT().cmdloop()