#!/usr/bin/python
# snapshot.cgi - simple python CGI script to generate git snapshots
# (C) 2005, Sham Chukoury <eleusis@xmms.org>
# (C) 2005, XMMS2 development team (http://xmms2.xmms.org)
# This program is licensed under the GPL v2

import sys
import os
import cgi
import time
import bz2

git_bin_dir = "/usr/local/bin"
git_base_dir = "/var/cache/git"
snapshot_dir = "/var/www/git/snapshots"
snapshots_url = "http://git.xmms.se/"

def redirect(url):
        print "Location: %s\n" % url

def send_text(msg):
        print "Content-Type: text/plain\n"
        print msg

class Snapshot:
        def __init__(self, git_dir, treename, commitID):
                self.git_dir = git_dir
                self.treename = treename
                self.dir = os.path.join(snapshot_dir, treename)

                if (commitID == "HEAD"):
                        self.commitID = file("%s/HEAD" % git_dir).read().rstrip()
                else:
                        self.commitID = commitID

                # read commit data
                os.putenv("GIT_OBJECT_DIRECTORY", "%s/objects" % git_dir)
                f = os.popen("%s commit %s" % (
                os.path.join(git_bin_dir, "git-cat-file"), self.commitID))
                self.tree = f.readline().rstrip().split(" ")[-1]
                f.readline() # parent
                f.readline() # author
                commit = f.readline().rstrip()

                # parse committer field, generate snapshot name
                committime = int(commit.split(" ")[-2])
                self.name = "%s-snapshot-%s" % (treename, time.strftime("%Y%m%d%H%M",time.localtime(committime)))
                self.filepath = "%s/%s.tar.bz2" % (self.dir, self.name)

        # check whether snapshot dir exists, or create it
        def check_dir(self):
                if not os.access("%s/" % self.dir, os.F_OK):
                        os.mkdir("%s/" % self.dir)

        # check whether snapshot file exists, or build it
        def build(self):
                self.check_dir()
                if not os.access(self.filepath, os.F_OK):
                        # todo: trap possible errors here
                        os.system("%s %s %s | bzip2 > %s" % (
                        os.path.join(git_bin_dir, "git-tar-tree"),
                        self.tree, self.name, self.filepath))
                        file("%s/.htaccess" % self.dir,"a").write('AddDescription "%s git snapshot (%s)" %s.tar.bz2\n' % (self.treename, self.commitID, self.name)) 

        # open snapshot file
        def get_file(self):
                try:
                        retFile = file(self.filepath, "r"), self.name + ".tar.bz2"
                except IOError:
                        retFile = None, None
                return retFile

        def send_bheaders(self, filename = None, size = None, lastmod = None):
                if filename is None:
                        filename = self.name + ".tar.bz2"
                sizestr = ""
                if size is not None:
                        sizestr = "; size=%i" % size
                print "Content-Type: application/x-bzip2"
                print "Content-Encoding: x-bzip2"
                print "Content-Disposition: inline; filename=%s%s" % (
                filename, sizestr)
                print "Accept-Ranges: none"
                if size is not None:
                        print "Content-Length: %i" % size
                if lastmod is not None:
                        print "Last-Modified: %s" % (
                        time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(lastmod)))
                print ""

        # send pre-made tarball (self.build, or otherwise)
        def send_binary(self):
                bfile, filename = self.get_file()
                if bfile is None:
                        send_text("Sorry, could not provide snapshot for tree %s, commit %s" % (self.treename, commitID))
                else:
                        self.send_bheaders(size=os.stat(self.filepath)[6],
                        lastmod = os.stat(self.filepath)[8])
                        for line in bfile:
                                sys.stdout.write(line)
                        bfile.close()

        # make snapshot tarball and send, on the fly
        def on_the_fly(self):
                def cache_chunk_send(chunk, cfile):
                        cfile.write(chunk)
                        sys.stdout.write(chunk)

                # try to get file from disk if it exists, first
                if os.access(self.filepath, os.F_OK):
                        self.send_binary()
                else:
                        self.check_dir()
                        cachefile = file(self.filepath, "w")
                        tar = os.popen("%s %s %s" % (
                        os.path.join(git_bin_dir, "git-tar-tree"),
                        self.tree, self.name))

                        kompressor = bz2.BZ2Compressor()
                        self.send_bheaders()
                        for line in tar:
                                cache_chunk_send(kompressor.compress(line),
                                cachefile)
                        cache_chunk_send(kompressor.flush(), cachefile)
                        cachefile.close()
                        tar.close()

if not os.access(snapshot_dir, os.F_OK):
        try:
                os.mkdir(snapshot_dir)
        except OSError:
                pass

def valid_hash(hash):
        retVal = True

        if hash != "HEAD":
                if len(hash) != 40:
                        retVal = False
                for char in hash:
                        if char not in "0123456789abcdef":
                                retVal = False
        return retVal

fs = cgi.FieldStorage()
if (fs.has_key("tree") and fs.has_key("commit")):
        tree = fs["tree"].value
        commit = fs["commit"].value

        # validate tree name
        if not os.access(os.path.join(git_base_dir, tree), os.F_OK):
                send_text("No such tree: %s" % tree)
                sys.exit()

        # validate commit hash
        if not valid_hash(commit):
                send_text("Invalid hash: %s" % commit)
                sys.exit()

        snap = Snapshot(os.path.join(git_base_dir, tree), tree, commit)
        snap.build()
        snap.send_binary()
        #snap.on_the_fly()

else:
        # user requested url directly, without a commit hash
        # redirect to snapshots dir
        redirect(snapshots_url)
