# This brings the website up-to-date from the website svn # and sizer svn. # The website's svn path. The current directory, as it happens. site_svn = "" # The profiler's svn path. sizer_svn = "../releases/0.1.1" # The FTP server and path ftp_server = "ftp.servage.net" ftp_path = "/pysizer" import os import sys import time import subprocess import ftplib import getpass import cPickle # Process options enabled = True all = False while len(sys.argv) > 1: if sys.argv[1] == "-n": enabled = False elif sys.argv[1] == "-a": all = True else: print "Unknown option", sys.argv[1] sys.argv.pop(1) username = raw_input("Username: ") password = getpass.getpass("Password: ") print # Run a command and return a file for its output. def run(cmd): proc = subprocess.Popen(cmd.split(), stdout = subprocess.PIPE) return proc.stdout # This is a file or directory that should be on both FTP and SVN. class Node(object): def __init__(self, path, svn_root, ftp_root, name = ""): # path is a tuple of filename components. # svn_root is a string to be rooted to the path to get an # svn working copy. # ftp_root is the same, to get a location on the ftp server. self.path = path self.svn_root = svn_root self.ftp_root = ftp_root self.children = {} self.size = 0 self.mtime = 0 self.enabled = enabled self.name_ = name def svn(self): return os.path.join(self.svn_root, *self.path) def ftp(self): return "/".join((self.ftp_root,) + self.path) def file(self): return run("svn cat " + self.svn()) def md5(self): proc = subprocess.Popen(["md5sum"], stdin = self.file(), stdout = subprocess.PIPE) return proc.stdout.read() def name(self): if self.path == (): return self.name_ else: return self.path[len(self.path)-1] def populate(self): # Recursively add all files we can find. myfiles = files(self.svn(), recursive = False) for f in myfiles: if f == () or f[len(f)-1] in self.children: continue path = self.path + f if os.path.isdir(Node(path, self.svn_root, self.ftp_root).svn()): node = Directory(path, self.svn_root, self.ftp_root) else: node = File(path, self. svn_root, self.ftp_root) node.populate() self.children[node.name()] = node class File(Node): def __init__(self, path, svn_root, ftp_root): super(File, self).__init__(path, svn_root, ftp_root) try: self.size = os.path.getsize(self.svn()) self.mtime = os.path.getmtime(self.svn()) except OSError: pass def update(self, ftp): # Given an FTP server in the parent directory, update this file. print "Uploading", self.ftp() if self.enabled: ftp.storbinary("STOR " + self.name(), self.file()) def remove(self, ftp): print "Removing file", self.ftp() if self.enabled: ftp.delete(self.name()) class Directory(Node): def update(self, ftp): print "Making directory", self.ftp() if self.enabled: ftp.mkd(self.name()) def remove(self, ftp): print "Removing directory", self.ftp() if self.enabled: ftp.rmd(self.name()) # Turn an rooted path into a path-tuple, given the root. def getsuffix(path, root): rest = [] while path != root: (path, this) = os.path.split(path) rest.append(this) rest.reverse() return tuple(rest) # Get a list of files under a path, without querying the server. def files(path, recursive = True): if recursive: r = "" else: r = "-N " file_info = run("svn status -v " + r + path).read() files = [] for line in file_info.split("\n"): s = line.split() # Modified files if len(s) == 5: file_path = s[4] # Unmodified files elif len(s) == 4: file_path = s[3] else: continue if file_path == ".": file_path = "" files.append(getsuffix(file_path, path)) return files # Here are the existing releases release_dir = os.path.join(sizer_svn, "dist") # ...and the documentation. doc_dir = os.path.join(sizer_svn, "doc") # ...and the rest of the website. www_dir = site_svn # Make the directory structure that we want. root = Directory((), www_dir, ftp_path) release = Directory((), release_dir, ftp_path + "/dist", "dist") doc = Directory((), doc_dir, ftp_path + "/doc", "doc") readme = File(("README",), sizer_svn, ftp_path) install = File(("INSTALL",), sizer_svn, ftp_path) root.children["dist"] = release root.children["doc"] = doc root.children["README"] = readme root.children["INSTALL"] = install www = root release.populate() doc.populate() www.populate() print "Connecting to FTP..." ftp = ftplib.FTP(ftp_server) ftp.login(username, password) ftp.cwd(ftp_path) # Get the pickled md5sums and FTP mtimes. try: print "Retrieving MD5 data..." pickled = [] ftp.retrbinary("RETR pickled-files", pickled.extend) pickled = "".join(pickled) md5times = cPickle.loads(pickled) except: print "No MD5 data, uploading all files" md5times = {} # Walk through the FTP server, deleting and adding things as we go. def walk_server(node): print "Looking at", node.ftp() ftp.cwd(node.ftp()) # Get a directory listing. lines = [] ftp.retrlines("LIST", lines.append) # Assume the listing has the format perms...name # (because that's what my FTP server uses :-P) found = set() for line in lines: sp = line.split() perms = sp[0] name = sp[-1:][0] if name == "." or name == "..": continue found.add(name) isdir = perms[0] == "d" # Make dummy file and directory nodes, in case we need them dummy_file = File(node.path + (name,), node.svn_root, node.ftp_root) dummy_dir = Directory(node.path + (name,), node.svn_root, node.ftp_root) if isdir: # Recurse into the subdirectory, possibly to delete subfiles. # We might need to create a dummy node. if name in node.children: walk_server(node.children[name]) else: walk_server(dummy_dir) ftp.cwd(node.ftp()) # Is the file here locally? if name in node.children: update = True child = node.children[name] if not isdir: (status, response) = ftp.sendcmd( "MDTM " + child.name()).split() if int(status) < 200 or int(status) >= 300: raise "Bad status from MDTM: " + str(status) remote = (response, ftp.size(child.name())) md5 = child.md5() # Check the mtime, size and md5sum, unless being forced if not all: try: (local, old) = md5times[child.ftp()] if (local, old) == (md5, remote): update = False except KeyError: # Update it, since we don't know about it pass # Upload the file if we need to if update: child.update(ftp) else: print "Ignoring", child.ftp() # Update the thing in the MD5 hash (status, response) = ftp.sendcmd( "MDTM " + child.name()).split() if int(status) < 200 or int(status) >= 300: raise "Bad status from MDTM: " + str(status) remote = (response, ftp.size(child.name())) md5times[child.ftp()] = (md5, remote) else: # Oh, we'll have to delete it then. if isdir: dummy_dir.remove(ftp) else: dummy_file.remove(ftp) # Update all files not on the server. for child in node.children.itervalues(): if child.name() not in found: child.update(ftp) (status, response) = ftp.sendcmd("MDTM " + child.ftp()).split() if int(status) < 200 or int(status) >= 300: raise "Bad status from MDTM: " + str(status) remote = (response, ftp.size(child.name())) md5times[child.ftp()] = (child.md5(), remote) if isinstance(child, Directory): walk_server(child) walk_server(root) print print "Updating MD5 data..." ftp.cwd(ftp_path) f = os.tmpfile() cPickle.dump(md5times, f) f.seek(0) ftp.storbinary("STOR pickled-files", f) f.close() print "Done."