#!/usr/bin/env python """ ftpsync.py - Synchronised FTP Transfer Author: Sean B. Palmer, inamidst.com """ import sys, os, re, ftplib from optparse import OptionParser def error(msg): print >> sys.stderr, 'Error:', msg sys.exit(1) class FTPConf(dict): r_field = re.compile(r'(?s)([^\n:]+): (.*?)(?=\n[^ \t]|\Z)') def __init__(self, filename=None): if filename is not None: self.filename = filename elif os.environ.has_key('FTPCONF'): self.filename = os.environ.get('FTPCONF') else: self.filename = os.path.expanduser('~/.ftpconf') self.parse() def parse(self): if not os.path.exists(self.filename): error("Config file doesn't exist: %s" % self.filename) mode = os.stat(self.filename).st_mode if (mode & 070) or (mode & 07): msg = "Permissions too loose for: %s (%s)" error(msg % (self.filename, oct(mode))) item = [] f = open(self.filename) for line in f: line = line.rstrip('\r\n') if line: item.append(line) else: meta = dict(FTPConf.r_field.findall('\n'.join(item))) if meta.has_key('name'): accountname = meta['name'] del meta['name'] self[accountname] = meta else: raise ValueError('Must include an account name') item = [] f.close() class FTP(object): def __init__(self, account): args = (account['host'], account['username'], account['password']) self.ftp = ftplib.FTP(*args) print 'CONNECT %s@%s' % (args[1], args[0]) self.account = account def chpath(self, path): remotedir = self.account['remotedir'] localdir = self.account['localdir'] remotepath = remotedir + path[len(localdir):] self.ftp.cwd(remotepath) print 'CWD %s' % remotepath def store(self, name, filename): f = open(filename, 'rb') result = self.ftp.storbinary('STOR %s' % name, f) f.close() print 'STOR %s: %s' % (name, result) def chmod(self, name, newmode): result = self.ftp.sendcmd('SITE CHMOD %s %s' % (newmode, name)) print 'CHMOD %s %s: %s' % (newmode, name, result) def delete(self, name): if raw_input('Delete %s? [y/n]: ' % name).startswith('y'): result = self.ftp.delete(name) print 'DELETE %s: %s' % (name, result) else: print "Didn't delete %s" % name def stats(filename): stat = os.stat(filename) mode = oct(stat.st_mode)[-3:] return mode, stat.st_ctime, stat.st_mtime, stat.st_size def update(datafile, newmode, newctime, newmtime, newsize): fp = open(datafile, 'w') print >> fp, newmode, newctime, newmtime, newsize fp.close() def ftpsync(account, path): # zlib.adler32(...)? # ctime, mtime, mode, size, adlercrc # mode, ctime, mtime, size if not os.path.isdir(path): error("Not a directory: %s" % path) datadir = os.path.join(path, '.ftpsync') if not os.path.isdir(datadir): if os.path.exists(datadir): error("Something's already here that ain't a directory: %s" % datadir) os.mkdir(datadir) ftp = FTP(account) ftp.chpath(path) for name in sorted(os.listdir(path)): filename = os.path.join(path, name) if os.path.isfile(filename): newmode, newctime, newmtime, newsize = stats(filename) datafile = os.path.join(datadir, name) if os.path.exists(datafile): data = open(datafile).read() dataargs = data.rstrip('\r\n').split(' ') for i in (1, 2, 3): dataargs[i] = int(dataargs[i]) if tuple(dataargs) != (newmode, newctime, newmtime, newsize): ftp.store(name, filename) ftp.chmod(name, newmode) update(datafile, newmode, newctime, newmtime, newsize) else: ftp.store(name, filename) ftp.chmod(name, newmode) update(datafile, newmode, newctime, newmtime, newsize) for name in sorted(os.listdir(datadir)): filename = os.path.join(path, name) if not os.path.exists(filename): ftp.delete(name) os.remove(os.path.join(datadir, name)) import socket try: ftp.ftp.quit() except socket.error: pass print 'QUIT' def resolve(ftpconf, path): # path must be an absolute path result = None for name in ftpconf.iterkeys(): localdir = os.path.abspath(ftpconf[name]['localdir']) if path.startswith(localdir): if (result is None) or (len(localdir) < result[1]): result = (name, localdir) # @@ raise exception? if result: return ftpconf[result[0]] else: return None def main(argv=None): ftpconf = FTPConf() # parser = OptionParser(usage='%prog [options] *') # parser.add_option("-a", "--account", dest="account", default=False, # help="use a particular account", metavar='N') # options, args = parser.parse_args(argv) parser = OptionParser(usage='%prog ') options, args = parser.parse_args(argv) # if args: # account = ftpconf[options.account or 'default'] # ftpsync(account, names) # else: parser.print_help() if len(args) == 1: path = os.path.abspath(args[0]) account = resolve(ftpconf, path) ftpsync(account, path) else: parser.print_help() if __name__=="__main__": main()