summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander <alex@spline.inf.fu-berlin.de>2015-08-28 02:43:22 +0200
committerAlexander <alex@spline.inf.fu-berlin.de>2015-08-28 02:43:22 +0200
commitb5e718d6f4d37ca31c12eef603c6199c4b89a046 (patch)
tree3f8a96985aae9a998a9a65e9cc8bda3f12ae0fc0
downloadgit-tftpd-b5e718d6f4d37ca31c12eef603c6199c4b89a046.tar.gz
git-tftpd-b5e718d6f4d37ca31c12eef603c6199c4b89a046.tar.bz2
git-tftpd-b5e718d6f4d37ca31c12eef603c6199c4b89a046.zip
Initial commit
-rw-r--r--.gitignore3
-rwxr-xr-xbin/git-tftpd17
-rw-r--r--src/git_tftpd/__init__.py0
-rw-r--r--src/git_tftpd/backend.py30
-rw-r--r--src/git_tftpd/git.py86
-rw-r--r--src/git_tftpd/protocol.py31
-rw-r--r--src/git_tftpd/writer.py66
-rw-r--r--src/twisted/plugins/git_tftpd_plugin.py32
8 files changed, 265 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..93a3244
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+repo/
+*.pyc
+dropin.cache
diff --git a/bin/git-tftpd b/bin/git-tftpd
new file mode 100755
index 0000000..b78b6e9
--- /dev/null
+++ b/bin/git-tftpd
@@ -0,0 +1,17 @@
+#!/bin/bash
+dir="$(dirname "$0")"
+
+args=()
+while [[ $# > 0 && "$1" != "--" ]] ; do
+ args+=("$1")
+ shift
+done
+
+if [[ "$1" = "--" ]]; then
+ shift
+else
+ set -- "${args[@]}"
+ args=()
+fi
+
+PYTHONPATH="$dir/../src/" twistd "${args[@]}" git-tftpd "$@"
diff --git a/src/git_tftpd/__init__.py b/src/git_tftpd/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/git_tftpd/__init__.py
diff --git a/src/git_tftpd/backend.py b/src/git_tftpd/backend.py
new file mode 100644
index 0000000..6fae5bd
--- /dev/null
+++ b/src/git_tftpd/backend.py
@@ -0,0 +1,30 @@
+from .writer import GitWriter
+
+from tftp.backend import IBackend
+from tftp.errors import Unsupported, AccessViolation
+from tftp.util import deferred
+
+from twisted.python.context import get
+from twisted.python.filepath import FilePath, InsecurePath
+
+from zope import interface
+
+
+class GitBackend(object):
+ interface.implements(IBackend)
+
+ def __init__(self, base_path):
+ self.base_path = base_path
+ self.base = FilePath(base_path)
+
+ @deferred
+ def get_reader(self, file_name):
+ raise Unsupported("Reading not supported")
+
+ @deferred
+ def get_writer(self, file_name):
+ try:
+ target_path = self.base.descendant(file_name.split("/"))
+ except InsecurePath, e:
+ raise AccessViolation("Insecure path: %s" % e)
+ return GitWriter(target_path, self.base_path, get('remote'))
diff --git a/src/git_tftpd/git.py b/src/git_tftpd/git.py
new file mode 100644
index 0000000..695ccb5
--- /dev/null
+++ b/src/git_tftpd/git.py
@@ -0,0 +1,86 @@
+import os
+
+from dulwich.client import get_transport_and_path
+from dulwich.errors import UpdateRefsError, SendPackError
+from dulwich.refs import SYMREF
+from dulwich.repo import Repo
+
+from twisted.python import log
+
+
+LOCAL_BRANCH_PREFIX = 'refs/heads/'
+REMOTE_BRANCH_PREFIX = 'refs/remotes/'
+
+
+class GitRepo(object):
+ """ Simple high-level wrapper for dulwich. """
+
+ def __init__(self, repo):
+ self.path = repo
+ self.repo = Repo(repo)
+
+ def stage(self, filenames):
+ paths = [os.path.relpath(filename, self.path)
+ for filename in filenames]
+ log.msg('Staging: %s' % ', '.join(paths), system='git')
+ self.repo.stage(paths)
+
+ def has_changed(self):
+ head = self.repo.object_store[self.repo.head()]
+ index = self.repo.open_index()
+ if any(index.changes_from_tree(self.repo.object_store, head.tree)):
+ return True
+
+ log.msg('No changes', system='git')
+ return False
+
+ def commit(self, message, committer):
+ log.msg('Committing: %s' % message, system='git')
+ self.repo.do_commit(message, committer=committer)
+
+ def _get_git_remote_url(self, remote_name):
+ try:
+ config = self.repo.get_config()
+ return config.get(("remote", remote_name), "url")
+ except KeyError:
+ pass
+ return None
+
+ def _get_git_branch(self):
+ contents = self.repo.refs.read_ref('HEAD')
+ if not contents.startswith(SYMREF):
+ return None
+
+ refname = contents[len(SYMREF):]
+ if refname.startswith(LOCAL_BRANCH_PREFIX):
+ return refname[len(LOCAL_BRANCH_PREFIX):]
+
+ return refname
+
+ def push(self, remote):
+ remote_location = self._get_git_remote_url(remote)
+ if remote_location is not None:
+ branch = self._get_git_branch()
+ local_ref = '%s%s' % (LOCAL_BRANCH_PREFIX, branch)
+ log.msg('Pushing %s to %s' % (branch, remote_location),
+ system='git')
+
+ def update_refs(refs):
+ refs[local_ref] = self.repo.refs['HEAD']
+ return refs
+
+ client, path = get_transport_and_path(remote_location)
+ try:
+ new_refs = client.send_pack(
+ path, update_refs,
+ self.repo.object_store.generate_pack_contents)
+
+ # update remote refs
+ if local_ref in new_refs:
+ remote_ref = '%s%s/%s' % (REMOTE_BRANCH_PREFIX,
+ remote, branch)
+ self.repo.refs[remote_ref] = new_refs[local_ref]
+ except (UpdateRefsError, SendPackError) as e:
+ log.err(e, system='git')
+ else:
+ log.msg('Not pushing, origin remote not found', system='git')
diff --git a/src/git_tftpd/protocol.py b/src/git_tftpd/protocol.py
new file mode 100644
index 0000000..ce270c7
--- /dev/null
+++ b/src/git_tftpd/protocol.py
@@ -0,0 +1,31 @@
+from tftp.protocol import TFTP
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+
+class TFTPProtocol(TFTP):
+ """Simple hack to stop sending packets, if the connection was closed."""
+
+ def __init__(self, backend, _clock=None):
+ TFTP.__init__(self, backend, _clock)
+ self._real_session_sendData = None
+
+ def _session_sendData(self, session, bytes):
+ if session.transport.connected:
+ # Send data, if "connection" is established
+ self._real_session_sendData(bytes)
+ elif session.timeout_watchdog is not None and \
+ session.timeout_watchdog.active():
+ # Kill the timeout watchdog, so that we do not get:
+ # "Timed out after a successful transfer"
+ session.timeout_watchdog.cancel()
+
+ @inlineCallbacks
+ def _startSession(self, datagram, addr, mode):
+ session = yield TFTP._startSession(self, datagram, addr, mode)
+
+ # monkey patch the sendData method
+ self._real_session_sendData = session.session.sendData
+ session.session.sendData = \
+ lambda bytes: self._session_sendData(session.session, bytes)
+
+ returnValue(session)
diff --git a/src/git_tftpd/writer.py b/src/git_tftpd/writer.py
new file mode 100644
index 0000000..bee731c
--- /dev/null
+++ b/src/git_tftpd/writer.py
@@ -0,0 +1,66 @@
+import os
+import shutil
+import socket
+import tempfile
+
+from .git import GitRepo
+
+from tftp.backend import IWriter
+from twisted.python import log
+from zope import interface
+
+
+class GitWriter(object):
+ interface.implements(IWriter)
+
+ def __init__(self, file_path, repo, remote):
+ file_dir = file_path.parent()
+ if not file_dir.exists():
+ file_dir.makedirs()
+
+ self.file_path = file_path
+ self.temp_destination = tempfile.TemporaryFile()
+ self.repo = os.path.abspath(repo)
+ self.remote = remote
+ self.state = 'active'
+
+ def _get_hostname(self, remote):
+ try:
+ flags = socket.NI_DGRAM | socket.NI_NOFQDN | socket.NI_NAMEREQD
+ (host, _) = socket.getnameinfo(remote, flags)
+ except:
+ (host, _) = remote
+ return host
+
+ def _do_git_commit(self):
+ git_repo = GitRepo(self.repo)
+ git_repo.stage([self.file_path.path])
+
+ if not git_repo.has_changed():
+ return
+
+ msg = 'Automatic commit after tftp upload'
+ if self.remote is not None:
+ msg += ' from %s' % self._get_hostname(self.remote)
+ committer = 'Backup Server <backup@spline.inf.fu-berlin.de>'
+ git_repo.commit(msg, committer)
+ git_repo.push('origin')
+
+ def write(self, data):
+ self.temp_destination.write(data)
+
+ def finish(self):
+ if self.state not in ('finished', 'cancelled'):
+ self.destination_file = self.file_path.open('w')
+ self.temp_destination.seek(0)
+ shutil.copyfileobj(self.temp_destination, self.destination_file)
+ self.temp_destination.close()
+ self.destination_file.close()
+
+ self._do_git_commit()
+ self.state = 'finished'
+
+ def cancel(self):
+ if self.state not in ('finished', 'cancelled'):
+ self.temp_destination.close()
+ self.state = 'cancelled'
diff --git a/src/twisted/plugins/git_tftpd_plugin.py b/src/twisted/plugins/git_tftpd_plugin.py
new file mode 100644
index 0000000..1d2b53e
--- /dev/null
+++ b/src/twisted/plugins/git_tftpd_plugin.py
@@ -0,0 +1,32 @@
+from git_tftpd.backend import GitBackend
+from git_tftpd.protocol import TFTPProtocol
+
+from twisted.python import usage
+from twisted.plugin import IPlugin
+from twisted.application.service import IServiceMaker
+from twisted.application import internet
+
+from zope.interface import implements
+
+class Options(usage.Options):
+ optParameters = [
+ ['port', 'p', 50069, 'The port number to listen on.'],
+ ['repo', 'r', None, 'The path to the git repo. [required]'],
+ ]
+
+
+class MyServiceMaker(object):
+ implements(IServiceMaker, IPlugin)
+ tapname = 'git-tftpd'
+ description = 'A tftp server, that commits the files into a git repo.'
+ options = Options
+
+ def makeService(self, options):
+ if options['repo'] is None:
+ raise usage.UsageError, 'Missing required argument'
+
+ backend = GitBackend(options['repo'])
+ return internet.UDPServer(int(options["port"]), TFTPProtocol(backend))
+
+
+serviceMaker = MyServiceMaker()