#!/usr/bin/python
# -*- coding: utf-8 -*-
# WebDAV-to-Subversion proxy server
# (C) Martin v. Löwis 2008
# $Id: svnproxy.py 3349 2008-06-29 18:41:36Z Martin vonLoewis $
#
# This program wraps a subversion repository as a WebDAV
# server. Each DAV read operation reads the HEAD version
# of the repository (possibly with a 1s delay due to caching).
# Each write operation (PUT/MKCOL/DELETE) results in a
# separate subversion revision, with a manufactured commit message
# "Commit through svnproxy".
#
# The server depends on the DAV package, provided by
# PyWebDAV (http://pypi.python.org/pypi/PyWebDAV)
#
# COPY and MOVE are not supported yet.
# DAV level 2 operations (LOCK/UNLOCK/CHECKLOCK) are
# not even supported by PyWebDAV.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import BaseHTTPServer, cStringIO, optparse
from DAV.constants import COLLECTION, OBJECT
from DAV.iface import *
from DAV.WebDAVServer import DAVRequestHandler
from svn import core, ra, client, delta

commitmsg = "Commit through svnproxy"

class Proxy(dav_interface):
    def __init__(self, repo, uri):
        self.baseuri = uri
        if not repo.endswith("/"):
            repo += "/"
        self.repo = repo
        self.cache_time = 0
        self.info_cache = {}
        self.child_cache = {}
        self.cache_rev = 0
        cb = ra.callbacks_t()
        self.session = ra.open(repo, cb, None, None)

    def http2svn(self, uri):
        assert uri.startswith(self.baseuri)
        path = unicode(uri[len(self.baseuri):], "utf-8")
        return self.repo+path

    def svn2http(self, svn):
        assert svn.startswith(self.repo)
        path = svn[len(self.repo):].encode("utf-8")
        return self.baseuri+path

    def validate_cache(self):
        now = time.time()
        if now-self.cache_time > 1:
            rev = ra.get_latest_revnum(self.session)
            if rev > self.cache_rev:
                self.info_cache = {}
                self.child_cache = {}
                self.cache_rev = rev
            self.cache_time = now   

    def info(self, path):
        self.validate_cache()
        path2 = path
        if path.endswith("/"):
            path2 = path[:-1]
        try:
            return self.info_cache[path2]
        except KeyError:
            pass
        relpath = path2[len(self.repo):]
        dirent = ra.stat(self.session, relpath, self.cache_rev)
        self.info_cache[path2] = dirent
        return dirent

    def get_prop2(self, uri, ns, pname):
        print "get_prop2", uri, ns, pname
        return dav_interface.get_prop2(self, uri, ns, pname)

    #def get_prop(self, uri, ns, pname):
    #    print "get_prop", uri, ns, pname
    #    return dav_interface.get_prop(self, uri, ns, pname)
    
    def get_creationdate(self, uri):
        # XXX find out creation date
        return self.get_lastmodified(uri)

    def get_lastmodified(self, uri):
        self.validate_cache()
        info = self.info(self.http2svn(uri))
        if info is None:
            raise DAV_NotFound
        return info.time/1000000

    def _get_dav_resourcetype(self, uri):
        self.validate_cache()
        info = self.info(self.http2svn(uri))
        if info is None:
            raise DAV_NotFound
        kind = info.kind
        if kind == core.svn_node_file:
            return OBJECT
        elif kind == core.svn_node_dir:
            return COLLECTION
        elif kind == core.svn_node_none:
            raise DAV_NotFound
        else: # unknown
            raise RuntimeError, "unknown node kind"

    def _get_dav_getcontentlength(self,uri):
        self.validate_cache()
        info = self.info(self.http2svn(uri))
        if info is None:
            raise DAV_NotFound
        return info.size

    def get_childs(self, uri):
        self.validate_cache()
        try:
            return self.child_cache[uri]
        except KeyError:
            pass
        info = self.info(self.http2svn(uri))
        if info is None:
            raise DAV_NotFound
        if info.kind != core.svn_node_dir:
            return []
        svn = self.http2svn(uri)
        if svn.endswith("/"):
            svn = svn[:-1]
        children, rev, props = ra.get_dir(self. session,
                                          svn[len(self.repo):],
                                          self.cache_rev)
        result = []
        for name,dirent in children.items():
            self.info_cache[svn+"/"+name] = dirent
            result.append(self.svn2http(svn+"/"+name))
        self.child_cache[uri] = result
        return result
        
    def get_data(self, uri):
        # Get latest revision
        s = cStringIO.StringIO()
        path = self.http2svn(uri)[len(self.repo):]
        try:
            props = ra.get_file(self.session, path, -1, s)
        except core.SubversionException:
            # XXX check error code
            raise DAV_NotFound
        return s.getvalue()

    def put(self, uri, data, contenttype=None):
        rev = ra.get_latest_revnum(self.session)
        path = self.http2svn(uri)
        relpath = path[len(self.repo):]
        kind = ra.check_path(self.session, relpath, rev)
        if kind in (core.svn_node_dir, core.svn_node_unknown):
            raise DAV_Forbidden
        editor, ebaton = ra.get_commit_editor(self.session,
                                              commitmsg,
                                              None, None, False)
        root = delta.editor_invoke_open_root(editor, ebaton, rev)
        if kind == core.svn_node_none:
            f = delta.editor_invoke_add_file(editor, relpath,
                                             root, None, -1)
        else:
            f = delta.editor_invoke_open_file(editor, relpath,
                                              root, rev, None)
        # XXX record content type
        handler, baton = delta.editor_invoke_apply_textdelta(editor, f, None)
        delta.svn_txdelta_send_string(data,
                              handler, baton, None)
        delta.editor_invoke_close_file(editor, f, None, None)
        delta.editor_invoke_close_directory(editor, root)
        try:
            delta.editor_invoke_close_edit(editor, ebaton)
        except core.SubversionException, e:
            raise DAV_Forbidden(str(e))
        return None

    def mkcol(self, uri):
        rev = ra.get_latest_revnum(self.session)
        path = self.http2svn(uri)
        relpath = path[len(self.repo):]
        kind = ra.check_path(self.session, relpath, rev)
        if kind != core.svn_node_none:
            # not allowed, method requires non-existent collection
            raise DAV_Error, 405 
        parent = relpath.rsplit("/",1)[0]
        kind = ra.check_path(self.session, parent, rev)
        if kind != core.svn_node_dir:
            # conflict, parent directory does not exist
            raise DAV_Error, 409
        editor, ebaton = ra.get_commit_editor(self.session,
                                              commitmsg,
                                              None, None, False)
        root = delta.editor_invoke_open_root(editor, ebaton, rev)
        delta.editor_invoke_add_directory(editor, relpath, root, None, -1)
        delta.editor_invoke_close_directory(editor, root)
        try:
            delta.editor_invoke_close_edit(editor, ebaton)
        except core.SubversionException, e:
            raise DAV_Forbidden(str(e))
        return None        

    def delone(self, uri):
        rev = ra.get_latest_revnum(self.session)
        path = self.http2svn(uri)
        relpath = path[len(self.repo):]
        kind = ra.check_path(self.session, relpath, rev)
        if kind == core.svn_node_none:
            return {uri:404}
        editor, ebaton = ra.get_commit_editor(self.session,
                                              commitmsg,
                                              None, None, False)
        root = delta.editor_invoke_open_root(editor, ebaton, rev)
        delta.editor_invoke_delete_entry(editor, relpath, rev, root)
        delta.editor_invoke_close_directory(editor, root)
        try:
            delta.editor_invoke_close_edit(editor, ebaton)
        except core.SubversionException, e:
            raise DAV_Forbidden(str(e))
        return {}
    deltree = delone

    # moveone
    # movetree
    # copyone
    # copytree

    def exists(self, uri):
        self.validate_cache()
        return self.info(self.http2svn(uri)) is not None

    def is_collection(self, uri):
        self.validate_cache()
        info = self.info(self.http2svn(uri))
        if info is None:
            return False
        return info.kind == core.svn_node_dir

usage = "usage: %prog [options] <repo>\n" +\
        "e.g.  svnproxy svn+ssh://test@svn.hpi.uni-potsdam.de/svn"
     
parser = optparse.OptionParser(usage=usage)
parser.add_option("-p", "--port", action="store", type="int",
                  default=30303, help="Listen on port (default 30303)")
options, args = parser.parse_args()
if len(args) != 1:
    parser.error("Need exactly one repository argument")
repo = unicode(args[0])

uri = "http://localhost:%d/" % options.port
# No object created here
handler = DAVRequestHandler
# whereas this *is* an instance
handler.IFACE_CLASS = Proxy(repo, uri) 
handler.verbose = 1
handler.DO_AUTH = False
server = BaseHTTPServer.HTTPServer(('localhost', options.port), handler)
print "Listening on "+uri
server.serve_forever()

