dwas-manager / dwas_manager / app.py
app.py
Raw
#!/usr/bin/env python3

# Copyright 2020 Cyanic
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from flask import Flask, request

import base64
import configparser
import glob
import json
import os
import re
import shutil
import stat
import subprocess
import urllib
import yaml

app = Flask(__name__)

dwas_dir = os.path.join(os.path.expanduser('~'), '.dwas')


def dotglob(s):
    match = glob.glob(s.replace('/*', '/.*'))
    match.extend(glob.glob(s))
    return match


def app_dir_valid(directory):
    un = urllib.parse.unquote(directory)
    re = urllib.parse.quote(un, safe=' ')
    return directory == re


def get_app_path():
    fmt = dwas_dir + '/{}'

    files = dotglob(fmt.format('*'))

    rapp = request.args.get('app', '')
    app = urllib.parse.quote(rapp, safe=' ')

    filename = fmt.format(app)
    if filename in files:
        return filename


def get_icon_path(directory):
    return os.path.join(dwas_dir, directory, 'favicon.png')


@app.route('/runapp')
def runapp():
    path = get_app_path()
    if path:
        subprocess.Popen(['python3', '-m', 'dwas', path])

    return ''


@app.route('/createapp')
def createapp():
    dirs = dotglob('{}/*'.format(dwas_dir))
    names = [os.path.basename(d) for d in dirs]
    name = 'New app'

    count = 0
    while name in names:
        count += 1
        name = 'New app - {}'.format(count)

    dirpath = os.path.join(dwas_dir, name)
    os.mkdir(dirpath)
    with open(os.path.join(dirpath, 'manifest.yaml'), 'w') as f:
        f.write('name: {}'.format(name))

    return json.dumps({'name': name, 'app': name, 'favicon': None})


@app.route('/deleteapp')
def deleteapp():
    path = get_app_path()
    if path:
        shutil.rmtree(path)
    return ''


@app.route('/addtodesktop')
def addtodesktop():
    path = get_app_path()
    if not path:
        return ''

    mpath = os.path.join(path, 'manifest.yaml')
    deskdir = subprocess.check_output(['xdg-user-dir', 'DESKTOP'])
    try:
        with open(mpath) as f:
            m = yaml.safe_load(f)
        name = str(m.get('name'))
    except Exception as e:
        print('Error while loading \'%s\':' % mpath, e)
        return ''
    directory = os.path.basename(path)
    filename = 'DWAS-' + directory + '.desktop'
    deskpath = os.path.join(deskdir.decode().strip(), filename)

    icon = get_icon_path(directory)
    if not os.path.isfile(icon):
        icon = 'dwas-app'

    config = configparser.ConfigParser(interpolation=None)
    config.optionxform = lambda x: x  # Allow uppercase letters
    config['Desktop Entry'] = {
        'Name': name,
        'Exec': 'python3 -m dwas .',
        'Path': path,
        'Icon': icon,
        'Type': 'Application',
        'StartupNotify': 'true',
    }

    with open(deskpath, 'w') as f:
        config.write(f, space_around_delimiters=False)

    os.chmod(deskpath, os.stat(deskpath).st_mode | stat.S_IEXEC)

    return ''


def load_b64png(directory):
    iconpath = get_icon_path(directory)
    try:
        with open(iconpath, 'rb') as f:
            b64 = base64.b64encode(f.read()).decode('ascii')
        return 'data:image/png;base64,' + b64
    except Exception:
        pass


@app.route('/apps')
def apps():
    files = dotglob('{}/*/manifest.yaml'.format(dwas_dir))

    manifests = []
    for filename in files:
        directory = os.path.basename(os.path.dirname(filename))
        if not app_dir_valid(directory):
            continue

        try:
            with open(filename) as f:
                m = yaml.safe_load(f)
        except Exception as e:
            print('Error while loading \'%s\':' % filename, e)
            continue

        if not isinstance(m, dict):
            continue

        name = str(m.get('name', directory))
        favicon = m.get('favicon')
        if not favicon:
            favicon = load_b64png(directory)

        manifest = {
            'name': name,
            'app': directory,
            'favicon': favicon,
        }
        manifests.append(manifest)

    def natural(s):
        return [
            int(x) if x.isdigit() else x.lower()
            for x in re.split(r'(\d+)', s)
        ]
    manifests = sorted(manifests, key=lambda x: x['name'])
    manifests = sorted(manifests, key=lambda x: natural(x['name']))

    return json.dumps(manifests)


@app.route('/getapp')
def getapp():
    path = get_app_path()
    if path:
        mpath = os.path.join(path, 'manifest.yaml')
        try:
            with open(mpath) as f:
                text = f.read()
        except Exception as e:
            print(e)
            return 'Forbidden', 403
        return text
    return 'Not Found', 404


@app.route('/setapp', methods=['GET', 'POST'])
def setapp():
    path = get_app_path()
    if not path:
        return json.dumps({'error': 'app not found'})

    mpath = os.path.join(path, 'manifest.yaml')

    body = request.stream.read()
    try:
        m = yaml.safe_load(body)
    except Exception as e:
        return json.dumps({'error': str(e)})

    if not isinstance(m, dict):
        return json.dumps({'error': 'YAML root must be an object'})

    try:
        with open(mpath, 'wb') as f:
            f.write(body)
    except Exception as e:
        return json.dumps({'error': str(e)})

    directory = os.path.basename(path)
    name = str(m.get('name', directory))

    safename = urllib.parse.quote(str(name), safe=' ')
    newpath = os.path.join(dwas_dir, safename)
    try:
        os.rename(path, newpath)
    except Exception as e:
        print(e)
    else:
        directory = os.path.basename(newpath)

    favicon = m.get('favicon')
    if not favicon:
        favicon = load_b64png(directory)

    manifest = {
        'name': name,
        'app': directory,
        'favicon': favicon,
    }

    return json.dumps(manifest)


@app.route('/')
def root():
    return app.send_static_file('app.html')


if __name__ == '__main__':
    app.run()