From 0d8f2fb7fbf076f8913bd05963d26a9e432ddf5f Mon Sep 17 00:00:00 2001 From: Michal Sojka Date: Thu, 6 Mar 2008 10:11:00 +0000 Subject: [PATCH] Added git-ftp-sync darcs-hash:20080306101108-f2ef6-e6346a2efdb298cc39c50237f1b7d45068511d11.gz --- git-ftp-sync | 289 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100755 git-ftp-sync diff --git a/git-ftp-sync b/git-ftp-sync new file mode 100755 index 0000000..5d40c9d --- /dev/null +++ b/git-ftp-sync @@ -0,0 +1,289 @@ +#!/usr/bin/env python + +# Script for mirroring (a part of) git repository to a FTP site. Only +# changed files are copied to FTP. Works with bare repositories. +# +# Author: Michal Sojka +# License: GPL + +# Typical usage in hooks/post-receive (or pre-receive if you want push +# command to fail in case of error during mirroring): +# export PASSWORD=secret +# /usr/local/bin/git-ftp-sync -H ftp.example.com -u username -r www +# or +# /usr/local/bin/git-ftp-sync -d /var/www/projectweb -r www + +# The first comamnd line mirrors the content og www directory in +# repository to FTP site at ftp.example.com as user "username" with +# password "secret". + + +import sys +from ftplib import FTP +import ftplib +from optparse import OptionParser +from subprocess import Popen, PIPE, call +import re +import os +from os.path import dirname, normpath + +try: + defpass = os.environ["PASSWORD"] + del os.environ["PASSWORD"] +except KeyError: + defpass = "" + +opt_parser = OptionParser(usage="usage: %prog [options] repository revision") +opt_parser.add_option("-H", "--host", dest="host", + help="FTP server address. If not specified update files localy without FTP.") +opt_parser.add_option("-u", "--user", dest="user", + help="FTP login name") +opt_parser.add_option("-p", "--pass", dest="password", default=defpass, + help="FTP password (defaults to environment variable PASSWORD)") +opt_parser.add_option("-r", "--repodir", dest="repodir", default="", + help="Synchronize only this directory (and its subdirectories) from within a repository") +opt_parser.add_option("-d", "--dir", dest="dir", default="", + help="An existing directory (on FTP site or localhost), where to store synchronized files.") + +class RepoChange: + """Represents one line in git diff --name-status""" + def __init__(self, type, path, oldrev, newrev, options): + self.type = type.strip() # one of ADMRCUX + self.repo_path = path + if path.startswith(options.repodir): # www/something -> something + path = path[len(options.repodir):] + if options.dir: # something -> prefix/something + path = options.dir+"/"+path + self.dest_path = normpath(path) + self.oldrev = oldrev + self.newrev = newrev + + def isDir(self): + return self.repo_path[-1] == "/" + + def inDir(self, dir): + """Dir: empty string or / closed directory name(s), without starting . or /""" + return self.repo_path.startswith(dir) + +class Syncer: + """Abstract class performing synchronization""" + def sync(self, change, dest_root): + if change.type[0] == 'A' and change.isDir(): + # Never happens in git + self.mkd(change.dest_path) + elif change.type[0] in ["A", "C", "M"]: + # Check whether the target directory exists + retcode = call("git ls-tree %s %s|grep -q ." % (change.oldrev, dirname(change.repo_path)), shell=True) + if (retcode != 0 and normpath(dirname(change.dest_path)) != normpath(dest_root)): + self.mkd(dirname(change.dest_path)) + # Upload the file + print "%s: UPLOAD"%self.__class__.__name__, change.dest_path + pipe = Popen("git cat-file blob %s:%s" % (change.newrev, change.repo_path), + stdout=PIPE, shell=True) + self._storbinary(pipe.stdout, change.dest_path) + elif change.type[0] == "D": + if change.isDir(): + # Never happens in git + self.rmd(change.dest_path) + else: + print "%s: DEL "%self.__class__.__name__, change.dest_path + self._delete(change.dest_path) + else: + print >> sys.stderr, "Unknown change:", change.type, change.dest_path + sys.exit(1) + + def mkd(self, path): + print "%s: MKD "%self.__class__.__name__, path + self._mkd(path) + + def rmd(self, path): + print "%s: RMD "%self.__class__.__name__, path + self._rmd(path); + + def delete_empty_directories(self, changes, revision, dest_root): + dirs = {} + for change in changes: + if change.type[0] in ["D", "R"]: + dirs[dirname(change.repo_path)] = dirname(change.dest_path) + for d in dirs.keys(): + retcode = call("git ls-tree %s %s|grep -q ." % (revision, d), shell=True) + if (retcode != 0 and normpath(dirs[d]) != normpath(dest_root)): + self.rmd(dirs[d]) + + def close(self): + pass + +class FTPSync(Syncer): + def __init__(self, ftp): + self.ftp = ftp + + def close(self): + self.ftp.close() + + def _mkd(self, path): + try: + self.ftp.mkd(path) + except ftplib.error_perm, detail: + print >> sys.stderr, "FTP warning:", detail, path + + def _storbinary(self, string, path): + #print >> sys.stderr, self.dest_path + self.ftp.storbinary("STOR %s"%path, string) + + def _rmd(self, path): + try: + # FIXME: this should be recursive deletion + self.ftp.rmd(path) + except ftplib.error_perm, detail: + print >> sys.stderr, "FTP warning:", detail + + def _delete(self, path): + try: + #print >> sys.stderr, path + self.ftp.delete(path) + except ftplib.error_perm, detail: + print >> sys.stderr, "FTP warning:", detail, self.dest_path + + +class LocalSync(Syncer): + def _mkd(self, path): + try: + os.mkdir(path) + except OSError, detail: + print "warning:", detail + + def _storbinary(self, file, path): + f = open(path, 'wb') + s = file.read(10000) + while s: + f.write(s) + s = file.read(10000) + f.close() + + def _rmd(self, path): + """ Delete everything reachable from the directory named in 'self.dest_path', + assuming there are no symbolic links.""" + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(path) + + def _delete(self, path): + os.remove(path) + +def selftest(): + commands = """ + set -x -e + commit_push() { git commit -q -m "$1"; git push $REPO master; } + mkdir repo.git + cd repo.git + git --bare init + echo -e "%s" > hooks/pre-receive + chmod +x hooks/pre-receive + REPO=$PWD + cd .. + mkdir www + mkdir working + cd working + git init + echo 'abc' > non-mirrored-file + git add non-mirrored-file + commit_push 'Added non-mirrored-file' + mkdir www + cd www + echo 'Hello' > index.html + git add index.html + commit_push 'Added index.html' + mkdir subdir + echo 'asdf' > subdir/subfile.html + git add subdir/subfile.html + commit_push 'Added subdir' + + git mv subdir subdir2 + commit_push 'Renamed directory' + + git mv subdir2/subfile.html subdir2/renamed.html + commit_push 'Renamed file' + + git rm -r subdir2 + commit_push 'Removed subdir2' + git mv index.html index2.html + commit_push 'Renamed file index->index2' + cd .. + git rm www/index2.html + commit_push 'Removed index2' + cd $REPO/.. + rm -rf working repo.git www + """ + selfpath = sys.argv[0] + + print "Checking local sync" + hook = "#!/bin/sh\n%s -d %s/www/ -r www" % (selfpath, os.getcwd()) + ret = os.system(commands % hook) + if ret: return + + print "\n\nChecking FTP sync (server: localhost, user: test, password: env. variable PASSWORD)" + password = defpass + hook = "#!/bin/sh\nexport PASSWORD=%(password)s\n%(selfpath)s -r www -H localhost -u test" % locals() + ret = os.system(commands % hook) + if ret: return + + print + print "Selftest succeeded!" + +def build_change_list(changes, oldrev, newrev): + # Read changes + gitdiff = Popen("/usr/bin/git diff --name-status %s %s"%(oldrev, newrev), + stdout=PIPE, stderr=PIPE, close_fds=True, shell=True) + change_re = re.compile("(\S+)\s+(.*)$"); + for line in gitdiff.stdout: + #print line, + m = change_re.match(line) + if (m): + change = RepoChange(m.group(1), m.group(2), oldrev, newrev, options) + if change.inDir(options.repodir): + changes.append(change) + + + +# Parse command line options +(options, args) = opt_parser.parse_args() + +if len(args) > 0 and args[0] == "selftest": + selftest() + sys.exit(0) + +options.repodir = normpath(options.repodir).lstrip(".") +if options.repodir: options.repodir+="/" + +changes = list() + +# Read the changes +for line in sys.stdin: + (oldrev, newrev, refname) = line.split() + if refname == "refs/heads/master": + build_change_list(changes, oldrev, newrev) + +if not changes: + sys.exit(0) + +# Apply changes +try: + if options.host: + syncer = FTPSync(FTP(options.host, options.user, options.password)) + else: + syncer = LocalSync() + + for change in changes: + syncer.sync(change, options.dir) + syncer.delete_empty_directories(changes, newrev, options.dir) + + syncer.close() + +except ftplib.all_errors, detail: + print >> sys.stderr, "FTP error: ", detail + sys.exit(1) + + -- 2.39.2