]> rtime.felk.cvut.cz Git - git-ftp-sync.git/blobdiff - git-ftp-sync
Added support for sftp (based on paramiko)
[git-ftp-sync.git] / git-ftp-sync
index d3d3e26625bfb2b50fad037dc29e0eb117250782..af32cbd6b14073b77e7a313692d5be5592994675 100755 (executable)
@@ -26,6 +26,7 @@ from subprocess import Popen, PIPE, call
 import re
 import os
 from os.path import dirname, normpath
+from urlparse import urlparse
 
 try:
     defpass = os.environ["PASSWORD"]
@@ -34,26 +35,28 @@ 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("-U", "--url", dest="url", default="",
+                  help="Destination URL (available protocols: sftp, ftp, file). Depricates -H, -u and -d.")
 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("-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("-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):
+    def __init__(self, type, path, oldrev, newrev, options, destroot=None):
         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
+        if destroot:                 # something -> prefix/something
+            path = destroot+"/"+path
         self.dest_path = normpath(path)
         self.oldrev = oldrev
         self.newrev = newrev
@@ -73,9 +76,21 @@ class Syncer:
             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))
+            if (normpath(dirname(change.dest_path)) != normpath(dest_root)):
+                retcode = 1
+                if (change.oldrev):         # If there is previous revision, check for it
+                    retcode = call("git ls-tree %s %s|grep -q ." % (change.oldrev, dirname(change.repo_path)), shell=True)
+                if (retcode != 0):
+                    dirs = normpath(dirname(change.dest_path)).split("/")
+                    #print "YYYYYYYYYYYYY",dirname(change.dest_path)
+                    destdir = ""
+                    for i in dirs:
+                        if (i==""): i="/"
+                        destdir = os.path.join(destdir, i);
+                        #print "XXXXXXXXXXXXX",destdir
+                        #self.mkd(dirname(change.dest_path))
+                        self.mkd(destdir)
+                
             # 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),
@@ -144,6 +159,65 @@ class FTPSync(Syncer):
         except ftplib.error_perm, detail:
             perror("FTP warning: %s %s" % (detail, path))
 
+class SFTPSync(Syncer):
+    def __init__(self, url):
+        import paramiko
+        # get host key, if we know one
+       hostkeytype = None
+       hostkey = None
+       try:
+           host_keys = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
+       except IOError:
+           try:
+               # try ~/ssh/ too, because windows can't have a folder named ~/.ssh/
+               host_keys = paramiko.util.load_host_keys(os.path.expanduser('~/ssh/known_hosts'))
+           except IOError:
+               print '*** Unable to open host keys file'
+               host_keys = {}
+       
+       if host_keys.has_key(url.hostname):
+           hostkeytype = host_keys[url.hostname].keys()[0]
+           hostkey = host_keys[url.hostname][hostkeytype]
+           print 'Using host key of type %s' % hostkeytype
+       
+       
+       # now, connect and use paramiko Transport to negotiate SSH2 across the connection
+        port = 22
+        if url.port: port = url.port
+        self.t = paramiko.Transport((url.hostname, port))
+        password = url.password
+        if not password: password = options.password
+        self.t.connect(username=url.username, password=password, hostkey=hostkey)
+        self.sftp = paramiko.SFTPClient.from_transport(self.t)
+
+    def close(self):
+        self.sftp.close()
+        self.t.close()
+               
+    def _mkd(self, path):
+        try:
+            self.sftp.mkdir(path)
+        except IOError, detail:
+            print "sftp warning:", detail
+            
+    def _storbinary(self, file, path):
+        remote = self.sftp.open(path, 'w')
+        s = file.read(10000)
+        while s:
+           remote.write(s)
+            s = file.read(10000)
+        remote.close()
+        
+
+    def _rmd(self, path):
+        """ Delete everything reachable from the directory named in 'self.dest_path',
+        assuming there are no symbolic links."""
+        self.sftp.rmdir(path)
+        # FIXME: this should be recursive deletion
+
+    def _delete(self, path):
+        self.sftp.remove(path)
+       
 
 class LocalSync(Syncer):
     def _mkd(self, path):
@@ -189,8 +263,10 @@ def selftest():
     mkdir www
 
     # Commit something to www directory in repo without the activated hook
-    echo 'Bye' > www/first.html
-    git add www/first.html
+    mkdir www/dir
+    mkdir www/dir/dir2
+    echo 'Bye' > www/dir/dir2/first.html
+    git add www/dir/dir2/first.html
     commit_push 'Added first.html'
     
     # Activate the hook and commit a non-mirrored file
@@ -202,7 +278,7 @@ def selftest():
 
     # Check that the first commit appears in www even if the hook was
     # activated later (only for local sync)
-    grep -q PASSWORD $REPO/hooks/post-receive || test -f ../www/first.html
+    grep -q PASSWORD $REPO/hooks/post-receive || test -f ../www/dir/dir2/first.html
     
     cd www
     echo 'Hello' > index.html
@@ -226,7 +302,7 @@ def selftest():
     cd ..
     git rm www/index2.html
     commit_push 'Removed index2'
-    git rm www/first.html
+    git rm www/dir/dir2/first.html
     commit_push 'Removed first.html'
     cd $REPO/..
     rm -rf working repo.git www    
@@ -244,6 +320,12 @@ def selftest():
     ret = os.system(commands % hook)
     if ret: return
 
+    print "\n\nChecking SFTP sync (server: localhost, user: test, password: env. variable PASSWORD)"
+    password = defpass
+    hook = """#!/bin/sh\nexport PASSWORD=%(password)s\n%(selfpath)s -r www --url sftp://test@localhost""" % locals()
+    ret = os.system(commands % hook)
+    if ret: return
+
     print
     print "Selftest succeeded!"
 
@@ -252,22 +334,21 @@ def perror(str):
 
 
 def update_ref(hash):
-    remote = "ftp"
-    branch = "master"
-    if (not os.path.exists("refs/remotes/"+remote)):
-        os.makedirs("refs/remotes/"+remote)
-    file("refs/remotes/"+remote+"/"+branch, "w").write(newrev+"\n")
+    cmd = "git update-ref refs/remotes/ftp/master %s"%(hash)
+    #print "Runnging", cmd
+    os.system(cmd)
 
-def add_to_change_list(changes, git_command):
+def add_to_change_list(changes, git_command, oldrev, newrev, destroot):
     # Read changes
+    ##print "Running: ", git_command
     gitdiff = Popen(git_command,
                     stdout=PIPE, stderr=PIPE, close_fds=True, shell=True)
     change_re = re.compile("(\S+)\s+(.*)$");
     for line in gitdiff.stdout:
-        #print line,
+        ##print line,
         m = change_re.match(line)
         if (m):
-            change = RepoChange(m.group(1), m.group(2), oldrev, newrev, options)
+            change = RepoChange(m.group(1), m.group(2), oldrev, newrev, options, destroot)
             if change.inDir(options.repodir):
                 changes.append(change)
 
@@ -281,31 +362,50 @@ if len(args) > 0 and args[0] == "selftest":
 options.repodir = normpath(options.repodir).lstrip(".")
 if options.repodir: options.repodir+="/"
 
+if options.host:
+    url = urlparse("ftp://"+options.user+"@"+options.host+"/"+options.dir)
+elif options.dir and not options.url:
+    url = urlparse("file:///"+options.dir)
+else:
+    url = urlparse(options.url)
+
 changes = list()
 
 # Read the changes
-for line in sys.stdin:
-    (oldrev, newrev, refname) = line.split()
-    if refname == "refs/heads/master":
-        try:
-            oldrev=file("refs/remotes/ftp/master").readline().strip();
-            git_command = "/usr/bin/git diff --name-status %s %s"%(oldrev, newrev)
-        except IOError:
-            # We are run for the first time, so (A)dd all files in the repo.
-            git_command = r"git ls-tree -r --name-only %s | sed -e 's/\(.*\)/A \1/'"%(newrev);
-
-        add_to_change_list(changes, git_command)
-
+if 'GIT_DIR' in os.environ:
+    # Invocation from hook
+    for line in sys.stdin:
+        (oldrev, newrev, refname) = line.split()
+        if refname == "refs/heads/master":
+            try:
+                oldrev=os.popen('git show-ref --hash refs/remotes/ftp/master').read().strip()
+                if not oldrev: raise IOError, "No ref" # Simulate failure if the branch doesn't exist
+                git_command = "git diff --name-status %s %s"%(oldrev, newrev)
+            except IOError:
+                # We are run for the first time, so (A)dd all files in the repo.
+                oldrev = None
+                git_command = r"git ls-tree -r --name-only %s | sed -e 's/\(.*\)/A \1/'"%(newrev);
+
+        add_to_change_list(changes, git_command, oldrev, newrev, url.path)
+else:
+    # Manual invocation
+    newrev = "HEAD"
+    oldrev = None;
+    git_command = r"git ls-tree -r --name-only HEAD | sed -e 's/\(.*\)/A \1/'";
+    add_to_change_list(changes, git_command, oldrev, newrev)
+        
 if not changes:
     perror("No changes to sync")
     sys.exit(0)
 
 # Apply changes
 try:
-    if options.host:
+    if url.scheme == "ftp":
         syncer = FTPSync(FTP(options.host, options.user, options.password))
-    else:
+    elif url.scheme == "file":
         syncer = LocalSync()
+    elif url.scheme == "sftp":
+        syncer = SFTPSync(url)
 
     for change in changes:
         syncer.sync(change, options.dir)
@@ -313,10 +413,12 @@ try:
 
     syncer.close()
 
-except ftplib.all_errors, detail:
-    perror("FTP synchronization error: %s" % detail);
-    perror("I will try it next time again");
-    sys.exit(1)
+# except ftplib.all_errors, detail:
+#     perror("FTP synchronization error: %s" % detail);
+#     perror("I will try it next time again");
+#     sys.exit(1)
+except:
+    raise
 
 # If succeessfull, update remote ref
 update_ref(newrev)