#!/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()