#!/usr/bin/env python # Copyright (C) 2012 Aleksey Lim # # 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 . import os import re import sys import shlex import locale from json import dumps, loads from os.path import join, exists from gevent import monkey from sugar_network import db, client, toolkit from sugar_network.resources.volume import Volume from sugar_network.client import IPCRouter from sugar_network.client.commands import ClientCommands from sugar_network.toolkit import printf, application, coroutine, util from sugar_network.toolkit import Option, BUFFER_SIZE, enforce porcelain = Option( 'give the output in an easy-to-parse format for scripts', default=False, type_cast=Option.bool_cast, action='store_true', name='porcelain') post_data = Option( 'send content as a string from POST or PUT command', name='post_data', short_option='-d') post_file = Option( 'send content of the specified file from POST or PUT command', name='post_file', short_option='-f') json = Option( 'treat POST or PUT command content as a JSON data', name='json', short_option='-j', default=False, type_cast=Option.bool_cast, action='store_true') offline = Option( 'do not connect to Sugar Network server', default=False, type_cast=Option.bool_cast, action='store_true', name='offline') _ESCAPE_VALUE_RE = re.compile('([^\[\]\{\}0-9][^\]\[\{\}]+)') class Application(application.Application): def __init__(self, **kwargs): application.Application.__init__(self, **kwargs) application.rundir.value = join(client.local_root.value, 'run') util.init_logging(application.debug.value) if not exists(toolkit.cachedir.value): os.makedirs(toolkit.cachedir.value) @application.command( 'send POST API request') def POST(self): self._call('POST', True) @application.command( 'send PUT API request') def PUT(self): self._call('PUT', True) @application.command( 'send DELETE API request') def DELETE(self): self._call('DELETE', False) @application.command( 'send GET API request') def GET(self): self._call('GET', False) def _call(self, method, post): request = db.Request(method=method) request.allow_redirects = True response = db.Response() reply = [] if post: if post_data.value is None and post_file.value is None: json.value = True post_data.value = sys.stdin.read() if post_data.value: request.content = post_data.value.strip() elif post_file.value: with file(post_file.value, 'rb') as f: # TODO Avoid loading entire file request.content = f.read() request.content_type = 'application/octet-stream' if json.value: try: request.content = loads(request.content) request.content_type = 'application/json' except Exception: # TODO pass if self.args and self.args[0].startswith('/'): path = self.args.pop(0).strip('/').split('/') request['document'] = path.pop(0) if path: request['guid'] = path.pop(0) if path: request['prop'] = path.pop(0) for arg in self.args: arg = shlex.split(arg) if not arg: continue arg = arg[0] if '=' not in arg: reply.append(arg) continue arg, value = arg.split('=', 1) arg = arg.strip() enforce(arg, 'No argument name in %r expression', arg) if arg in request: if isinstance(request[arg], basestring): request[arg] = [request[arg]] request[arg].append(value) else: request[arg] = value pid_path = None server = None cp = None try: if self.check_for_instance(): cp = client.IPCClient() else: pid_path = self.new_instance() if not client.anonymous.value: util.ensure_key(client.key_path()) home = Volume(client.path('db')) cp = ClientCommands(home, offline=offline.value, no_subscription=True) if not offline.value: for __ in cp.subscribe(event='inline', state='online'): break coroutine.dispatch() server = coroutine.WSGIServer( ('localhost', client.ipc_port.value), IPCRouter(cp)) coroutine.spawn(server.serve_forever) coroutine.dispatch() result = cp.call(request, response) finally: if server is not None: server.close() if cp is not None: cp.close() if pid_path: os.unlink(pid_path) if result is None: return if response.content_type == 'application/json': if porcelain.value: if type(result) in (list, tuple): for i in result: # TODO print i else: # TODO print result elif reply: for key in reply: key = _ESCAPE_VALUE_RE.sub("'\\1'", key) print eval('result%s' % key) else: print dumps(result, indent=2) elif response.content_type == 'text/event-stream': while True: chunk = util.readline(result) if not chunk: break sys.stdout.write(chunk) elif hasattr(result, 'read'): while True: chunk = result.read(BUFFER_SIZE) if not chunk: break sys.stdout.write(chunk) else: sys.stdout.write(result) # Let toolkit.http work in concurrence # XXX No DNS because `toolkit.network.res_init()` doesn't work otherwise monkey.patch_socket(dns=False) monkey.patch_select() monkey.patch_ssl() monkey.patch_time() # New defaults application.debug.value = client.logger_level() # If tmpfs is mounted to /tmp, `os.fstat()` will return 0 free space # and will brake offline synchronization logic toolkit.cachedir.value = client.profile_path('tmp') Option.seek('main', [ application.debug, porcelain, post_data, post_file, json, offline, ]) Option.seek('client', [ client.api_url, client.layers, client.ipc_port, client.local_root, client.no_dbus, client.anonymous, ]) locale.setlocale(locale.LC_ALL, '') app = Application( name='sugar-network-client', description='Sugar Network client utility', epilog='See http://wiki.sugarlabs.org/go/Sugar_Network ' \ 'for details.', config_files=[ '/etc/sweets.conf', '~/.config/sweets/config', client.profile_path('sweets.conf'), ], stop_args=['launch']) app.start()