From b5e718d6f4d37ca31c12eef603c6199c4b89a046 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 28 Aug 2015 02:43:22 +0200 Subject: Initial commit --- .gitignore | 3 ++ bin/git-tftpd | 17 +++++++ src/git_tftpd/__init__.py | 0 src/git_tftpd/backend.py | 30 ++++++++++++ src/git_tftpd/git.py | 86 +++++++++++++++++++++++++++++++++ src/git_tftpd/protocol.py | 31 ++++++++++++ src/git_tftpd/writer.py | 66 +++++++++++++++++++++++++ src/twisted/plugins/git_tftpd_plugin.py | 32 ++++++++++++ 8 files changed, 265 insertions(+) create mode 100644 .gitignore create mode 100755 bin/git-tftpd create mode 100644 src/git_tftpd/__init__.py create mode 100644 src/git_tftpd/backend.py create mode 100644 src/git_tftpd/git.py create mode 100644 src/git_tftpd/protocol.py create mode 100644 src/git_tftpd/writer.py create mode 100644 src/twisted/plugins/git_tftpd_plugin.py 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 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 ' + 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() -- cgit v1.2.3-1-g7c22