#!/usr/bin/env python3
"""The WGSI application.

   Ways to host this app:
   1. run server.serve()
   2. set application = get_app() in a .wsgi
"""
import sys
import os
import traceback
import shutil
import mimetypes
import re
import zipfile
import time
import email.utils
import hashlib
import json
from base64 import b64decode
from urllib.parse import urlsplit, urlunsplit, urljoin, quote, unquote, parse_qs

# dependency
import bottle
from bottle import request, redirect, static_file, template
from bottle import HTTPResponse, HTTPError
import commonmark

# this package
from . import *
from . import util

# runtime variables
# initializated in get_app() just before app is started
runtime = {}

# main app instance
app = bottle.Bottle()

# patch a bug in Bottle 0.12.*
# https://github.com/bottlepy/bottle/issues/1129
def patch_bottle_0_12():
    from distutils.version import LooseVersion

    if LooseVersion(bottle.__version__) >= LooseVersion("0.13"):
        return

    from bottle import parse_range_header, parse_date, _file_iter_range, tob

    global static_file

    # implement static_file() in 0.13
    # removed "close" param of _file_iter_range as it's not implemented in 0.12 yet.
    def static_file(filename, root,
                    mimetype=True,
                    download=False,
                    charset='UTF-8',
                    etag=None):
        """ Open a file in a safe way and return an instance of :exc:`HTTPResponse`
            that can be sent back to the client.
            :param filename: Name or path of the file to send, relative to ``root``.
            :param root: Root path for file lookups. Should be an absolute directory
                path.
            :param mimetype: Provide the content-type header (default: guess from
                file extension)
            :param download: If True, ask the browser to open a `Save as...` dialog
                instead of opening the file with the associated program. You can
                specify a custom filename as a string. If not specified, the
                original filename is used (default: False).
            :param charset: The charset for files with a ``text/*`` mime-type.
                (default: UTF-8)
            :param etag: Provide a pre-computed ETag header. If set to ``False``,
                ETag handling is disabled. (default: auto-generate ETag header)
            While checking user input is always a good idea, this function provides
            additional protection against malicious ``filename`` parameters from
            breaking out of the ``root`` directory and leaking sensitive information
            to an attacker.
            Read-protected files or files outside of the ``root`` directory are
            answered with ``403 Access Denied``. Missing files result in a
            ``404 Not Found`` response. Conditional requests (``If-Modified-Since``,
            ``If-None-Match``) are answered with ``304 Not Modified`` whenever
            possible. ``HEAD`` and ``Range`` requests (used by download managers to
            check or continue partial downloads) are also handled automatically.
        """

        root = os.path.join(os.path.abspath(root), '')
        filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
        headers = dict()

        if not filename.startswith(root):
            return HTTPError(403, "Access denied.")
        if not os.path.exists(filename) or not os.path.isfile(filename):
            return HTTPError(404, "File does not exist.")
        if not os.access(filename, os.R_OK):
            return HTTPError(403, "You do not have permission to access this file.")

        if mimetype is True:
            if download and download is not True:
                mimetype, encoding = mimetypes.guess_type(download)
            else:
                mimetype, encoding = mimetypes.guess_type(filename)
            if encoding: headers['Content-Encoding'] = encoding

        if mimetype:
            if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\
            and charset and 'charset' not in mimetype:
                mimetype += '; charset=%s' % charset
            headers['Content-Type'] = mimetype

        if download:
            download = os.path.basename(filename if download is True else download)
            headers['Content-Disposition'] = 'attachment; filename="%s"' % download

        stats = os.stat(filename)
        headers['Content-Length'] = clen = stats.st_size
        headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime,
                                                          usegmt=True)
        headers['Date'] = email.utils.formatdate(time.time(), usegmt=True)

        getenv = request.environ.get

        if etag is None:
            etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime,
                                       clen, filename)
            etag = hashlib.sha1(tob(etag)).hexdigest()

        if etag:
            headers['ETag'] = etag
            check = getenv('HTTP_IF_NONE_MATCH')
            if check and check == etag:
                return HTTPResponse(status=304, **headers)

        if not (etag and check):
            ims = getenv('HTTP_IF_MODIFIED_SINCE')
            if ims:
                ims = parse_date(ims.split(";")[0].strip())
            if ims is not None and ims >= int(stats.st_mtime):
                return HTTPResponse(status=304, **headers)

        body = '' if request.method == 'HEAD' else open(filename, 'rb')

        headers["Accept-Ranges"] = "bytes"
        range_header = getenv('HTTP_RANGE')
        if range_header:
            ranges = list(parse_range_header(range_header, clen))
            if not ranges:
                return HTTPError(416, "Requested Range Not Satisfiable")
            offset, end = ranges[0]
            headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen)
            headers["Content-Length"] = str(end - offset)
            if body: body = _file_iter_range(body, offset, end - offset)
            return HTTPResponse(body, status=206, **headers)
        return HTTPResponse(body, **headers)

    bottle.static_file = static_file

patch_bottle_0_12()


def http_response(body='', status=None, headers=None, format=None, **more_headers):
    """Handles formatted response.

    ref: https://jsonapi.org
    """
    if format == 'json':
        more_headers['Content-type'] = 'application/json'

        body = {
            'success': True,
            'data': body,
            }

        body = json.dumps(body, ensure_ascii=False)

    return HTTPResponse(body, status, headers, **more_headers)


def http_error(
         status=500,
         body=None,
         exception=None,
         traceback=None,
         format=None, **more_headers):
    """Handles formatted error response.
    """
    if format == 'json':
        more_headers['Content-type'] = 'application/json'

        body = {
            'error': {
                'status': status,
                'message': body,
                },
            }
        body = json.dumps(body, ensure_ascii=False)
        return HTTPResponse(body, status, **more_headers)

    else:
        return HTTPError(status, body, exception, traceback, **more_headers)


def get_base():
    return (config['app']['base'] or request.environ.get('SCRIPT_NAME', '')).rstrip('/')


def get_pathname():
    """Revise request.pathname of Bottle

    When visiting http://example.com/app, request.pathname returns '/app/',
    while we want '/app'.

    http://example.com effectively means http://example.com/, so keep at least
    '/' as final fallback.
    """
    return (request.environ.get('SCRIPT_NAME', '') + request.environ.get('PATH_INFO', '')) or '/'


def get_archive_path(filepath, localpath):
    """Parse archive file path and the sub-archive path.

    - Priority:
      /path/to/fileordir.htz!
      /path/to/fileordir.htz
      /path/to/fileordir.htz!/path/to/fileordir.htz!
      /path/to/fileordir.htz!/path/to/fileordir.htz
      ...

    - Returns a tuple (archivefile, subarchivepath).
    """
    if not os.path.lexists(localpath):
        for m in re.finditer(r'![/\\]', filepath, flags=re.I):
            archivefile = os.path.join(runtime['root'], filepath[:m.start(0)].strip('/\\'))
            if os.path.isdir(archivefile + '!'):
                return (None, None)

            if zipfile.is_zipfile(archivefile):
                subarchivepath = filepath[m.end(0):].rstrip('/')
                return (archivefile, subarchivepath)

    return (None, None)


def is_local_access():
    """Determine if the client is in same device.
    """
    return request.urlparts.hostname in ('localhost', '127.0.0.1', request.remote_addr)


def handle_authorization(format=None):
    """Check if authorized or not.

    Return None if authorization passed, otherwise the header and body for authorization.

    Also modify runtime['permission'] for further access control.
    """
    if not len(config.auth):
        return

    auth_pass = False
    user, pw = request.auth or ('', '')

    # The higher level (e.g. Apache if this is run via Apache WSGI) may remove
    # the password and gets None. Revise to '' to prevent further error.
    if pw is None: pw = ''

    if user:
        for _, entry in config.auth.items():
            entry_user = entry.get('user', '')
            entry_pw = entry.get('pw', '')
            entry_pw_salt = entry.get('pw_salt', '')
            entry_pw_type = entry.get('pw_type', '')
            entry_permission = entry.get('permission', 'all')
            if (user == entry_user and
                    util.encrypt(pw, entry_pw_salt, entry_pw_type) == entry_pw):
                auth_pass = True
                runtime['permission'] = entry.get('permission', '')
                break

    if not auth_pass:
        headers = {
            'WWW-Authenticate': 'Basic realm="Authentication required.", charset="UTF-8"',
            'Content-type': 'text/html',
            }
        return http_error(401, "You are not authorized.", format=format, **headers)


def handle_directory_listing(localpath, format=None):
    """List contents in a directory.
    """
    # ensure directory has trailing '/'
    pathname = get_pathname()
    if not pathname.endswith('/'):
        parts = request.urlparts
        new_parts = (parts[0], parts[1], quote(pathname) + '/', parts[3], parts[4])
        new_url = urlunsplit(new_parts)
        return redirect(new_url)

    if not os.path.exists(localpath):
        return http_error(404, "Directory does not exist.", format=format)

    if not os.access(localpath, os.R_OK):
        return http_error(403, "You do not have permission to view this directory.", format=format)

    # headers
    headers = {}
    stats = os.stat(localpath)
    headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, usegmt=True)
    headers['Date'] = email.utils.formatdate(time.time(), usegmt=True)
    headers['Cache-Control'] = 'no-store'

    # output index
    subentries = util.listdir(localpath)

    if format:
        data = []
        for entry in subentries:
            data.append({
                    'name': entry.name,
                    'type': entry.type,
                    'size': entry.size,
                    'last_modified': entry.last_modified,
                    })
        return http_response(data, format=format, **headers)

    body = template('index.tpl',
            sitename=runtime['name'],
            is_local=is_local_access(),
            base=get_base(),
            path=request.path,
            subarchivepath=None,
            subentries=subentries,
            )

    return HTTPResponse(body, **headers)


def handle_zip_directory_listing(zip, archivefile, subarchivepath, format=None):
    """List contents in a directory.
    """
    # ensure directory has trailing '/'
    pathname = get_pathname()
    if not pathname.endswith('/'):
        parts = request.urlparts
        new_parts = (parts[0], parts[1], quote(pathname) + '/', parts[3], parts[4])
        new_url = urlunsplit(new_parts)
        return redirect(new_url)

    from bottle import parse_date, tob

    headers = {}

    # check 304
    stats = os.stat(archivefile)
    headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, usegmt=True)
    headers['Cache-Control'] = 'no-cache'

    getenv = request.environ.get

    etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime,
                               stats.st_size, archivefile)
    etag = hashlib.sha1(tob(etag)).hexdigest()

    headers['ETag'] = etag
    check = getenv('HTTP_IF_NONE_MATCH')
    if check and check == etag:
        return HTTPResponse(status=304, **headers)

    if not check:
        ims = getenv('HTTP_IF_MODIFIED_SINCE')
        if ims:
            ims = parse_date(ims.split(";")[0].strip())
        if ims is not None and ims >= int(stats.st_mtime):
            return HTTPResponse(status=304, **headers)

    subentries = util.zip_listdir(zip, subarchivepath)

    try:
        body = template('index.tpl',
                sitename=runtime['name'],
                is_local=is_local_access(),
                base=get_base(),
                path=request.path,
                subarchivepath=subarchivepath,
                subentries=subentries,
                )
        return HTTPResponse(body, **headers)
    except util.ZipDirNotFoundError:
        return http_error(404, "File does not exist.", format=format)


def handle_subarchive_path(
        archivefile,
        subarchivepath,
        mimetype,
        encoding,
        download=False,
        charset='UTF-8',
        etag=None,
        format=None):
    """Show content of a path in a zip file.
    """
    from bottle import parse_range_header, parse_date, _file_iter_range, tob

    if not os.access(archivefile, os.R_OK):
        return http_error(403, "You do not have permission to access this file.", format=format)

    try:
        zip = zipfile.ZipFile(archivefile)
    except:
        return http_error(500, "Unable to open the ZIP file.", format=format)

    try:
        # KeyError is raised if subarchivepath does not exist
        info = zip.getinfo(subarchivepath)
    except KeyError:
        # subarchivepath does not exist
        # possibility a missing directory entry?
        return handle_zip_directory_listing(zip, archivefile, subarchivepath)

    fh = zip.open(subarchivepath, 'r')

    headers = dict()

    if encoding: headers['Content-Encoding'] = encoding

    if mimetype is True:
        if download and download is not True:
            mimetype, encoding = mimetypes.guess_type(download)
        else:
            mimetype, encoding = mimetypes.guess_type(subarchivepath)
        if encoding: headers['Content-Encoding'] = encoding

    if mimetype:
        if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\
        and charset and 'charset' not in mimetype:
            mimetype += '; charset=%s' % charset
        headers['Content-Type'] = mimetype

    if download:
        download = os.path.basename(subarchivepath if download is True else download)
        headers['Content-Disposition'] = 'attachment; filename="%s"' % download

    headers['Content-Length'] = clen = info.file_size

    lm = info.date_time
    epoch = int(time.mktime((lm[0], lm[1], lm[2], lm[3], lm[4], lm[5], 0, 0, -1)))
    headers['Last-Modified'] = email.utils.formatdate(epoch, usegmt=True)

    headers['Date'] = email.utils.formatdate(time.time(), usegmt=True)

    getenv = request.environ.get

    if etag is None:
        etag = '%d:%d:%s' % (epoch, clen, subarchivepath)
        etag = hashlib.sha1(tob(etag)).hexdigest()

    if etag:
        headers['ETag'] = etag
        check = getenv('HTTP_IF_NONE_MATCH')
        if check and check == etag:
            return HTTPResponse(status=304, **headers)

    if not (etag and check):
        ims = getenv('HTTP_IF_MODIFIED_SINCE')
        if ims:
            ims = parse_date(ims.split(";")[0].strip())
        if ims is not None and ims >= int(epoch):
            return HTTPResponse(status=304, **headers)

    body = '' if request.method == 'HEAD' else fh

    headers["Accept-Ranges"] = "bytes"
    range_header = getenv('HTTP_RANGE')
    if range_header:
        ranges = list(parse_range_header(range_header, clen))
        if not ranges:
            return http_error(416, "Requested Range Not Satisfiable")
        offset, end = ranges[0]
        headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen)
        headers["Content-Length"] = str(end - offset)
        if body: body = _file_iter_range(body, offset, end - offset)
        return HTTPResponse(body, status=206, **headers)
    return HTTPResponse(body, **headers)


def handle_archive_viewing(localpath, mimetype):
    """Handle direct visit of HTZ/MAFF file.
    """
    def list_maff_pages(pages):
        """List available web pages in a MAFF file.
        """
        return template('maff_index.tpl',
                sitename=runtime['name'],
                is_local=is_local_access(),
                base=get_base(),
                path=request.path,
                pages=pages,
                )

    if not os.access(localpath, os.R_OK):
        return http_error(403, "You do not have permission to access this file.")

    if mimetype == "application/html+zip":
        subpath = "index.html"
    else:
        pages = util.get_maff_pages(localpath)

        if len(pages) > 1:
            # multiple index files
            return list_maff_pages(pages)
        elif len(pages) > 0:
            subpath = pages[0].indexfilename
        else:
            # no valid index file found
            return list_maff_pages([])

    parts = request.urlparts
    new_parts = (parts[0], parts[1], parts[2] + '!/' + quote(subpath), parts[3], parts[4])
    new_url = urlunsplit(new_parts)
    return redirect(new_url)


def handle_markdown_output(filepath, filename):
    """Output processed markdown.
    """
    from bottle import parse_date, tob

    headers = {}

    # check 304
    stats = os.stat(filename)
    headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, usegmt=True)

    getenv = request.environ.get

    etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime,
                               stats.st_size, filename)
    etag = hashlib.sha1(tob(etag)).hexdigest()

    headers['ETag'] = etag
    check = getenv('HTTP_IF_NONE_MATCH')
    if check and check == etag:
        return HTTPResponse(status=304, **headers)

    if not check:
        ims = getenv('HTTP_IF_MODIFIED_SINCE')
        if ims:
            ims = parse_date(ims.split(";")[0].strip())
        if ims is not None and ims >= int(stats.st_mtime):
            return HTTPResponse(status=304, **headers)

    # output processed content
    with open(filename, 'r', encoding='UTF-8') as f:
        body = f.read()
        f.close()
    
    body = template('markdown',
            sitename=runtime['name'],
            is_local=is_local_access(),
            base=get_base(),
            path=request.path,
            content=commonmark.commonmark(body),
            )

    return HTTPResponse(body, **headers)


@app.route('<filepath:path>', method=['GET', 'HEAD', 'POST'])
def handle_request(filepath):
    """Handle an HTTP request (HEAD, GET, POST).
    """
    action = request.params.getunicode('a', encoding='UTF-8', default='view')
    action = request.params.getunicode('action', encoding='UTF-8', default=action)

    format = request.params.getunicode('f', encoding='UTF-8')
    format = request.params.getunicode('format', encoding='UTF-8', default=format)

    # handle authentication
    runtime['permission'] = 'all'
    auth_result = handle_authorization(format=format)
    if auth_result is not None:
        return auth_result

    # determine primary variables
    #
    # filepath: the URL path below app base (not percent encoded)
    # localpath: the file system path corresponding to filepath
    # localtargetpath: localpath with symbolic link resolved
    # mimetype: the mimetype from localtargetpath
    # archivefile: the file system path of the ZIP archive file, or None
    # subarchivepath: the URL path below archivefile (not percent encoded)
    localpath = os.path.abspath(os.path.join(runtime['root'], filepath.strip('/\\')))
    localtargetpath = os.path.realpath(localpath)
    archivefile, subarchivepath = get_archive_path(filepath, localpath)
    mimetype, encoding = mimetypes.guess_type(localtargetpath)

    # handle action
    if action == 'static':
        return static_file(filepath, root=runtime['static'])

    elif action == 'source':
        # show text-like files as plain text
        if mimetype and (
                mimetype.startswith('text/') or
                mimetype.endswith('+xml') or
                mimetype.endswith('+json') or
                mimetype in ('application/javascript')
                ):
            mimetype = 'text/plain; charset=UTF-8'

        return static_file(filepath, root=runtime['root'], mimetype=mimetype)

    elif action in ('exec', 'browse'):
        if is_local_access():
            if os.path.lexists(localpath):
                if action == 'browse':
                    util.view_in_explorer(localpath)

                elif action == 'exec':
                    util.launch(localpath)

                if format:
                    return http_response('Command run successfully.', format=format)
                else:
                    return http_response(status=204, format=format)
       
            return http_error(404, "File does not exist.", format=format)

        else:
            return http_error(400, "Command can only run on local device.", format=format)

    elif action == 'token':
        if runtime['permission'] != 'all':
            return http_error(401, "You are not permitted to do this.", format=format)

        return http_response(token_handler.acquire(), format=format)

    elif action == 'list':
        if not format:
            return http_error(400, "Action not supported.", format=format)

        if os.path.isdir(localpath):
            return handle_directory_listing(localtargetpath, format=format)

        return http_error(400, "This is not a directory.", format=format)

    elif action == 'config':
        if not format:
            return http_error(400, "Action not supported.", format=format)

        # slightly adjusted some value for client to better know the server
        data = config.dump_object()
        data['app']['is_local'] = is_local_access()
        data['app']['root'] = runtime['root']
        data['app']['base'] = get_base()
        return http_response(data, format=format)

    elif action == 'edit':
        if format:
            return http_error(400, "Action not supported.", format=format)

        if os.path.lexists(localpath) and not os.path.isfile(localpath):
            return http_error(400, "Found a non-file here.", format=format)

        try:
            with open(localpath, 'rb') as f:
                body = f.read()
                f.close()
        except FileNotFoundError:
            body = b''

        try:
            body = body.decode('UTF-8')
            is_utf8 = True
        except UnicodeDecodeError:
            body = body.decode('ISO-8859-1')
            is_utf8 = False

        body = template('edit.tpl',
                sitename=runtime['name'],
                is_local=is_local_access(),
                base=get_base(),
                path=request.path,
                body=body,
                is_utf8=is_utf8,
                )

        return http_response(body, format=format)

    elif action == 'editx':
        if format:
            return http_error(400, "Action not supported.", format=format)

        if not os.path.lexists(localpath):
            return http_error(404, "File does not exist.", format=format)

        if os.path.lexists(localpath) and not os.path.isfile(localpath):
            return http_error(400, "Found a non-file here.", format=format)

        if not mimetype in ("text/html", "application/xhtml+xml"):
            return http_error(400, "This is not an HTML file.", format=format)

        body = template('editx.tpl',
                sitename=runtime['name'],
                is_local=is_local_access(),
                base=get_base(),
                path=request.path,
                )

        return http_response(body, format=format)

    elif action in ('mkdir', 'save', 'upload', 'delete', 'move', 'copy'):
        if request.method != 'POST':
            headers = {
                'Allow': 'POST',
                }
            return http_error(405, 'Method "{}" not allowed.'.format(request.method), format=format, **headers)

        token = request.params.get('token') or ''

        if not token_handler.validate(token):
            return http_error(401, 'Invalid access token.', format=format)

        token_handler.delete(token)

        if os.path.abspath(localpath) == runtime['root']:
            return http_error(403, "Unable to operate the root directory.", format=format)

        targetpath = target = request.forms.get('target')

        if target is not None:
            targetpath = os.path.normpath(os.path.join(runtime['root'], target.strip('/')))
            if not targetpath.startswith(os.path.join(runtime['root'], '')):
                return http_error(403, "Unable to operate beyond the root directory.", format=format)

        if action == 'mkdir':
            if os.path.lexists(localpath) and not os.path.isdir(localpath):
                return http_error(400, "Found a non-directory here.", format=format)

            try:
                os.makedirs(localpath, exist_ok=True)
            except OSError:
                traceback.print_exc()
                return http_error(500, "Unable to create a directory here.", format=format)

            if format:
                return http_response('Command run successfully.', format=format)

            return redirect(request.url)

        elif action == 'save':
            if os.path.lexists(localpath) and not os.path.isfile(localpath):
                return http_error(400, "Found a non-file here.", format=format)

            try:
                os.makedirs(os.path.dirname(localpath), exist_ok=True)
                bytes = request.forms.get('text').encode('ISO-8859-1') or b''
                with open(localpath, 'wb') as f:
                    f.write(bytes)
                    f.close()
            except OSError:
                traceback.print_exc()
                return http_error(500, "Unable to write to this file.", format=format)

            if format:
                return http_response('Command run successfully.', format=format)

            return redirect(request.url)

        elif action == 'upload':
            if os.path.lexists(localpath):
                if not os.path.isfile(localpath):
                    return http_error(400, "Found a non-file here.", format=format)
                else:
                    os.remove(localpath)

            try:
                os.makedirs(os.path.dirname(localpath), exist_ok=True)
                file = request.files.get('upload')
                file.save(localpath)
            except OSError:
                traceback.print_exc()
                return http_error(500, "Unable to upload this file.", format=format)

            if format:
                return http_response('Command run successfully.', format=format)

            return redirect(request.url)

        elif action == 'delete':
            if not os.path.lexists(localpath):
                return http_error(404, "File does not exist.", format=format)

            if os.path.islink(localpath):
                try:
                    os.remove(localpath)
                except:
                    traceback.print_exc()
                    return http_error(500, "Unable to delete this link.", format=format)
            elif os.path.isfile(localpath):
                try:
                    os.remove(localpath)
                except:
                    traceback.print_exc()
                    return http_error(500, "Unable to delete this file.", format=format)
            elif os.path.isdir(localpath):
                try:
                    shutil.rmtree(localpath)
                except:
                    traceback.print_exc()
                    return http_error(500, "Unable to delete this directory.", format=format)

            if format:
                return http_response('Command run successfully.', format=format)

            parts = request.urlparts
            new_parts = (parts[0], parts[1], re.sub(r'[^/]+/?$', r'', parts[2]), '', '')
            new_url = urlunsplit(new_parts)
            return redirect(new_url)

        elif action == 'move':
            if not os.path.lexists(localpath):
                return http_error(404, "File does not exist.", format=format)

            if os.path.lexists(targetpath):
                return http_error(400, 'Found something at target "{}".'.format(target), format=format)
            else:
                os.makedirs(os.path.dirname(targetpath), exist_ok=True)

            try:
                os.rename(localpath, targetpath)
            except:
                traceback.print_exc()
                return http_error(500, 'Unable to move to target "{}".'.format(target), format=format)

            if format:
                return http_response('Command run successfully.', format=format)

            parts = request.urlparts
            new_parts = (parts[0], parts[1], re.sub(r'[^/]+/?$', r'', parts[2]), '', '')
            new_url = urlunsplit(new_parts)
            return redirect(new_url)

        elif action == 'copy':
            if not os.path.lexists(localpath):
                return http_error(404, "File does not exist.", format=format)

            if os.path.lexists(targetpath):
                return http_error(400, 'Found something at target "{}".'.format(target), format=format)
            else:
                os.makedirs(os.path.dirname(targetpath), exist_ok=True)

            try:
                shutil.copytree(localpath, targetpath)
            except:
                try:
                    shutil.copy2(localpath, targetpath)
                except:
                    traceback.print_exc()
                    return http_error(500, 'Unable to copy to target "{}".'.format(target), format=format)

            if format:
                return http_response('Command run successfully.', format=format)

            return redirect(new_url)

    # "view" or unknown actions
    else:
        # show file information for other output formats
        if format:
            info = util.file_info(localpath)
            data = {
                'name': info.name,
                'type': info.type,
                'size': info.size,
                'last_modified': info.last_modified,
                'mime': mimetype,
                }
            return http_response(data, format=format)

        # handle directory
        if os.path.isdir(localpath):
            return handle_directory_listing(localtargetpath, format=format)

        # handle file
        elif os.path.isfile(localpath):
            # view archive file
            if mimetype in ("application/html+zip", "application/x-maff"):
                return handle_archive_viewing(localtargetpath, mimetype)

            # view markdown
            if mimetype == "text/markdown":
                return handle_markdown_output(filepath, localtargetpath)

            # convert meta refresh to 302 redirect
            if localtargetpath.lower().endswith('.htm'):
                target = util.parse_meta_refresh(localtargetpath).target

                if target is not None:
                    # Keep several chars as javascript encodeURI do,
                    # plus "!" to keep "!/" redirect work.
                    new_url = urljoin(request.url, quote(target, ',/?:@&=+$#!'))
                    return redirect(new_url)

            # show static file for other cases
            return static_file(filepath, root=runtime['root'], mimetype=mimetype)

        # handle sub-archive path
        elif archivefile:
            return handle_subarchive_path(os.path.realpath(archivefile), subarchivepath,
                    mimetype, encoding, format=format)

        # probably 404 not found here
        return static_file(filepath, root=runtime['root'], mimetype=mimetype)


def debug(*msg):
    """Quick dirty logging for testing purpose.
    """
    os.makedirs(os.path.dirname(runtime['log']), exist_ok=True)
    with open(runtime['log'], 'a', encoding='UTF-8') as f:
        f.write(' '.join(str(s) for s in msg))
        f.write('\n')
        f.close()


def init_app():
    """Run initialization and return the app.
    """
    runtime['root'] = os.path.abspath(config['app']['root'])
    runtime['name'] = config['app']['name']

    # if a local theme is found, overwrite the default theme
    runtime['theme'] = os.path.join(runtime['root'], WSB_DIR, 'themes', config['app']['theme'])
    if not os.path.isdir(runtime['theme']):
        runtime['theme'] = os.path.join(os.path.dirname(__file__), 'themes', config['app']['theme'])

    runtime['static'] = os.path.join(runtime['theme'], 'static')
    runtime['templates'] = os.path.join(runtime['theme'], 'templates')
    bottle.TEMPLATE_PATH.insert(0, runtime['templates'])

    # init token_handler
    global token_handler
    token_handler = util.TokenHandler(os.path.join(runtime['root'], WSB_DIR, 'server', 'token'))

    # init debugging logger
    runtime['log'] = os.path.join(runtime['root'], WSB_DIR, 'server', 'logs', 'debug.log')

    return app
