__init__.py 5.41 KB
Newer Older
Michael Murtaugh's avatar
Michael Murtaugh committed
1
from aiohttp import web
2
import sys, os, asyncio, json
Michael Murtaugh's avatar
Michael Murtaugh committed
3
from asyncio import create_subprocess_exec
4 5
from urllib.parse import urlparse, unquote as urlunquote, quote as urlquote
import argparse
6

Michael Murtaugh's avatar
Michael Murtaugh committed
7

8 9 10 11
"""
Todo: use proper http error responses for errors, see aiohttp/http_exceptions.py
"""

Michael Murtaugh's avatar
Michael Murtaugh committed
12 13 14
async def is_makeable (path, cwd=".", makefile="makefile"):
    rpath = os.path.relpath(path, cwd)
    p = await create_subprocess_exec("make", "--question", "-f", makefile, rpath, cwd = cwd)
15
    # retcode 0=file is up to date, 1=file needs remaking, 2=file is not makeable
Michael Murtaugh's avatar
Michael Murtaugh committed
16
    ret = await p.wait() == 1
17
    # print (f"is_makeable {path}: {ret}", file=sys.stderr)
Michael Murtaugh's avatar
Michael Murtaugh committed
18 19 20 21 22 23
    return ret

async def make (path, cwd=".", makefile="makefile"):
    rpath = os.path.relpath(path, cwd)
    p = await create_subprocess_exec("make", "-f", makefile, rpath, cwd = cwd)
    ret = await p.wait() == 0
24
    # print (f"make {path}: {ret}", file=sys.stderr)
Michael Murtaugh's avatar
Michael Murtaugh committed
25 26
    return ret

Michael Murtaugh's avatar
Michael Murtaugh committed
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
textchars = bytearray({7,8,9,10,12,13,27} | set(range(0x20, 0x100)) - {0x7f})
is_binary_string = lambda bytes: bool(bytes.translate(None, textchars))

def is_binary_file (p):
    """ returns none on ioerror """
    try:
        return not os.path.isdir(p) and is_binary_string(open(p, 'rb').read(1024))
    except IOError:
        return None

def editable (path):
    _, ext = os.path.splitext(path)
    ext = ext.lower()[1:]
    print (f"editable? ext {ext}", file=sys.stderr)
    return ((ext not in ("html", "htm")) and not is_binary_file(path))

43
# From aiohttp/web_urldispatcher.py
Michael Murtaugh's avatar
Michael Murtaugh committed
44
def directory_as_html(filepath, directory, prefix, editor_url=None):
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
    # returns directory's index as html

    # sanity check
    assert os.path.isdir(filepath)

    # relative_path_to_dir = filepath.relative_to(self._directory)
    relative_path_to_dir = os.path.relpath(filepath, directory)
    
    index_of = "Index of /{}".format(relative_path_to_dir)
    h1 = "<h1>{}</h1>".format(index_of)

    index_list = []
    dir_index = os.listdir(filepath)
    for _file in sorted(dir_index):
        # show file url as relative to static path
        # rel_path = _file.relative_to(self._directory).as_posix()
        # rel_path = os.path.relpath(_file, directory)
        # file_url = prefix + '/' + rel_path
Michael Murtaugh's avatar
Michael Murtaugh committed
63 64 65 66 67 68
        fp = os.path.join(filepath, _file)
        file_url = '/' + urlquote(fp)
        target = None
        if editable(fp) and editor_url:
            file_url = f"{editor_url}#{file_url}"
            target = "editor"
69 70 71 72 73
        # if file is a directory, add '/' to the end of the name
        if os.path.isdir(_file):
            file_name = "{}/".format(_file)
        else:
            file_name = _file
Michael Murtaugh's avatar
Michael Murtaugh committed
74 75 76
        if target:
            link_src = '<li><a href="{url}" target="{target}">{name}</a></li>'.format(
                url=file_url,
77 78
                target="editor",
                name=file_name)
Michael Murtaugh's avatar
Michael Murtaugh committed
79 80 81 82 83
        else:
            link_src = '<li><a href="{url}">{name}</a></li>'.format(
                url=file_url,
                name=file_name)
        index_list.append(link_src)
84 85 86 87 88 89 90 91
    ul = "<ul>\n{}\n</ul>".format('\n'.join(index_list))
    body = "<body>\n{}\n{}\n</body>".format(h1, ul)

    head_str = "<head>\n<title>{}</title>\n</head>".format(index_of)
    html = "<html>\n{}\n{}\n</html>".format(head_str, body)

    return html

92 93 94 95 96 97 98 99 100 101
async def route_get (request):
    # return FileResponse(filepath, chunk_size=self._chunk_size)
    path = urlunquote(urlparse(request.rel_url.raw_path.lstrip("/")).path)
    print ("route_get", path, file=sys.stderr)
    if path == '':
        path = '.'
    if path and await is_makeable(path, makefile=request.app['makefile']):
        await make(path, makefile=request.app['makefile'])
    if os.path.exists(path):
        if os.path.isdir(path):
Michael Murtaugh's avatar
Michael Murtaugh committed
102
            return web.Response(text=directory_as_html(path, '', '', editor_url=request.app['editor']), content_type="text/html")
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
        else:
            return web.FileResponse(path, chunk_size=256*1024)
    else:
        return web.Response(text=f"404 on path {path}")

async def route_post (request):
    """ write posted text value to file """
    path = urlunquote(urlparse(request.rel_url.raw_path.lstrip("/")).path)
    print ("route_post", path, file=sys.stderr)
    if os.path.exists(path) and os.path.isfile(path):
        data = await request.post()
        if 'text' in data:
            text = data['text']
            # doing file io inline here is not strictly speaking very async ;)
            with open(path, "w") as f:
                f.write(text)
            return web.Response(text=json.dumps({"response": "ok"}), content_type="application/json")
        else:
            return web.Response(text=f"no text")
    else:
        return web.Response(text=f"post not allowed on {path}")
124

Michael Murtaugh's avatar
Michael Murtaugh committed
125
def main ():
126 127
    ap = argparse.ArgumentParser("make & serve")
    ap.add_argument("--makefile", default="makefile")
Michael Murtaugh's avatar
Michael Murtaugh committed
128
    ap.add_argument("--editor", default=None)
129
    args = ap.parse_args()
Michael Murtaugh's avatar
Michael Murtaugh committed
130 131 132 133 134 135 136
    if sys.platform == 'win32':
        # Folowing: https://docs.python.org/3/library/asyncio-subprocess.html
        # On Windows, the default event loop is SelectorEventLoop which does not support subprocesses.
        # ProactorEventLoop should be used instead.
        loop = asyncio.ProactorEventLoop()
        asyncio.set_event_loop(loop)
    app = web.Application()
137
    app['makefile'] = args.makefile
Michael Murtaugh's avatar
Michael Murtaugh committed
138
    app['editor'] = args.editor
139 140
    app.add_routes([web.get('/{make:.*}', route_get)])
    app.add_routes([web.post('/{make:.*}', route_post)])
Michael Murtaugh's avatar
Michael Murtaugh committed
141 142
    web.run_app(app)

143

Michael Murtaugh's avatar
Michael Murtaugh committed
144 145
if __name__ == "__main__":
    main()