#!/usr/bin/python
# -*- coding: UTF-8 -*-

###############################################################################
#                                                                             #
# Bazaar Browser - A simple web application for browsing Bazaar repository    #
#                                                                             #
# Copyright (C) 2011  Fabien LOISON <http://www.flogisoft.com/>               #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU Affero General Public License as published by #
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.                         #
#                                                                             #
# You should have received a copy of the GNU Affero General Public License    #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################


"""Bazaar Browser - A simple web application for browsing Bazaar repository"""

__appname__ = "bzrbrowser"
__version__ = "0.1"
__author__ = "Fabien LOISON <http://www.flogisoft.com/>"
__licence__ = "AGPLv3+"


#Config
PROJECTS_PATH = "/var/repository"
CACHE_PATH = "/tmp"
PROJECTS_PUSH_URL = "sftp://USER@host/repository/"
README_FILES = ["README", "readme", "Readme", "ReadMe", "README.TXT",
    "README.txt", "Readme.txt", "Readme.TXT", "ReadMe.txt"]


from os.path import isfile, isdir, join
from os import listdir
from datetime import datetime
from re import sub

from bzrlib.branch import Branch
from bzrlib.export import tar_exporter, zip_exporter
from bzrlib.errors import NoSuchRevision
from flask import Flask, render_template, abort, request, redirect, Response, Markup


app = Flask(__name__)


def isbranch(path):
    """Check if the given path is a Bazaar branch"""
    if isdir(join(path, ".bzr")):
        return True
    else:
        return False


def isproject(path):
    """Check if the given path is a project"""
    trunk = join(path, "trunk")
    if isbranch(trunk):
        return True
    else:
        return False


def brz_infos_from_rev_id(branch, rev_id):
    """Get revision informations from a Bazaar rev_id"""
    revtree = branch.repository.get_revision(rev_id)
    return {
            'revno': branch.revision_id_to_revno(rev_id),
            'rev_id': rev_id,
            'message': revtree.message,
            'committer': revtree.committer,
            'timestamp': datetime.fromtimestamp(revtree.timestamp),
            'parent_ids': revtree.parent_ids,
            }


@app.route("/")
@app.route("/<path:path>")
def request_handler(path=""):
    """Handdle every non-action (+xxx) request"""
    fullpath = join(PROJECTS_PATH, path)
    #Force the trailing / for folders
    if isdir(fullpath) and len(path) > 0 and path[-1] != "/":
        return redirect("%s/" % path)
    #Avoid unauthorized path
    if not isfile(fullpath) and ".bzr" in path.split("/") or "+" in path:
        return abort(404)
    #If the path is a directory
    if isdir(fullpath):
        if isbranch(fullpath):
            return branch_infos(path)
        elif isproject(fullpath):
            return project_infos(path)
        else:
            return folder_infos(path)
    #If the path is a file
    elif isfile(fullpath):
        return file_request(fullpath)
    #Not found... New branch?
    else:
        return branch_new(path)


@app.route("/<path:path>/+export/rev<int:revno>.<string:archive_format>")
def export(path, revno, archive_format):
    """Export a revision in tgz or zip"""
    fullpath = join(PROJECTS_PATH, path)
    if isbranch(fullpath):
        formats = {
                'tar.gz': {
                    'do': tar_exporter.tgz_exporter,
                    'mime': "application/x-compressed-tar",
                    },
                'zip': {
                    'do': zip_exporter.zip_exporter,
                    'mime': "application/zip",
                    },
                }
        #Check if the format is supported
        if archive_format not in formats:
            return abort(404)
        #Get the rev_id from the revno
        branch = Branch.open(fullpath)
        try:
            rev_id = branch.dotted_revno_to_revision_id([revno])
        except NoSuchRevision:
            return abort(404)
        #Folder name and File path
        foldername = path.replace("/", "_")
        filepath = "%s_rev%i.%s" % (foldername, revno, archive_format)
        filepath = join(CACHE_PATH, filepath)
        #Export the revtree in tar.gz if the file does not already exists
        if not isfile(filepath):
            revtree = branch.repository.revision_tree(rev_id)
            revtree.lock_read()
            formats[archive_format]['do'](revtree, filepath, foldername, "/")
            revtree.unlock()
        #Return the archive if it was successfully created
        if isfile(filepath):
            return Response(
                    open(filepath, "rb").read(),
                    mimetype=formats[archive_format]['mime'],
                    )
        else:
            return abort(500)
    else:
        return abort(404)


def branch_infos(path):
    """Returns a page with informations about a branch"""
    fullpath = join(PROJECTS_PATH, path)
    branch = Branch.open(fullpath)
    rev_id = branch.last_revision()
    log = []
    for i in range(10):
        log.append(brz_infos_from_rev_id(branch, rev_id))
        if log[-1]['parent_ids']:
            rev_id = log[-1]['parent_ids'][0]
        else:
            break
    return render_template(
            "branch_view.html",
            title="/%s" % path,
            path="/%s" % path,
            log=log,
            branch_readonly_url=request.url,
            branch_readwrite_url="%s%s" % (PROJECTS_PUSH_URL, path),
            )


def branch_new(path):
    """Returns a page with the informations for creating a new branch"""
    return render_template(
            "newbranch_view.html",
            title="/%s" % path,
            path="/%s" % path,
            branch_readwrite_url="%s%s" % (PROJECTS_PUSH_URL, path),
            ), 404


def folder_infos(path):
    """Returns a page with a folder explorer"""
    fullpath = join(PROJECTS_PATH, path)
    folders = []
    branches = []
    for folder in listdir(fullpath):
        if isbranch(join(fullpath, folder)):
            branches.append(folder)
        elif isdir(fullpath):
            folders.append(folder)
    folders.sort()
    branches.sort()
    return render_template(
            "folder_view.html",
            title="/%s" % path,
            path="/%s" % path,
            folders=folders,
            branches=branches,
            )


def project_infos(path):
    """Returns a page with the project informations"""
    fullpath = join(PROJECTS_PATH, path)
    folders = []
    branches = []
    for folder in listdir(fullpath):
        if isbranch(join(fullpath, folder)):
            branches.append(folder)
        elif isdir(fullpath):
            folders.append(folder)
    folders.sort()
    branches.sort()
    #Bazaar infos (from the trunk)
    branch = Branch.open(join(fullpath, "trunk"))
    rev_id = branch.last_revision()
    tree = branch.basis_tree()
    #get the trunk last commit
    lastcommit = brz_infos_from_rev_id(branch, rev_id)
    #Search for a readme file
    readme = "No README file found in the trunk branch..."
    tree.lock_read()
    for readme_file in README_FILES:
        if tree.has_filename(readme_file):
            readme = tree.get_file_by_path("/README").read()
            break
    tree.unlock()
    return render_template(
            "project_view.html",
            title="/%s" % path,
            path="/%s" % path,
            folders=folders,
            branches=branches,
            readme=unicode(readme, "UTF-8"),
            lastcommit=lastcommit,
            branch_readonly_url="%strunk/" % request.url,
            branch_readwrite_url="%s%strunk/" % (PROJECTS_PUSH_URL, path),
            )


def file_request(fullpath):
    """Permit Bazaar to download a branch"""
    if ".bzr" in fullpath.split("/"):
        return open(fullpath, "rb").read()
    else:
        return abort(403)


@app.template_filter("dtformat")
def datetime_format(dt):
    """Makes human readable dates"""
    return dt.strftime("%A %d %B %Y, %H:%M:%S")


@app.template_filter("hideemail")
def hide_email(text):
    """Hide emails for avoiding spam"""
    return sub(
            r"<(.+)@(..?.?).*?(.?)\.([a-zA-Z0-9]{2,8}>)",
            r"<\1 AT \2[...]\3 DOT \4",
            text,
            )


@app.template_filter("rmemail")
def remove_email(text):
    """Hide emails for avoiding spam"""
    return sub(
            r"\s*<(.+)@(..?.?).*?(.?)\.([a-zA-Z0-9]{2,8}>)", r"", text)


@app.template_filter("parsereadme")
def parse_readme(text):
    """Hide emails for avoiding spam"""
    #Hide emails
    text = hide_email(text)
    #Esxape html
    text = text.replace("&", "&amp;")
    text = text.replace(">", "&gt;")
    text = text.replace("<", "&lt;")
    #Links
    text = sub(
            r"([a-z0-9]+://)(.+?)(&lt;|&gt;|\(|\)|\[|\]|\[|\}|\s|$|'|\")",
            r'<a href="\1\2">\1\2</a>\3',
            text,
            )
    text = sub(
            r"([^/])(www\.)(.+?)(&lt;|&gt;|\(|\)|\[|\]|\[|\}|\s|$|'|\")",
            r'\1<a href="http://\2\3">\2\3</a>\4',
            text,
            )
    text = sub(
            r"([^/])(ftp\.)(.+?)(&lt;|&gt;|\(|\)|\[|\]|\[|\}|\s|$|'|\")",
            r'\1<a href="ftp://\2\3">\2\3</a>\4',
            text,
            )
    return Markup(text)


if __name__ == "__main__":
    app.run(debug=True)


