Merge pull request #258 from YunoHost/enh-file-args

[enh] Allow file type in actionmaps
This commit is contained in:
Alexandre Aubin 2021-08-23 15:25:22 +02:00 committed by GitHub
commit 4332860e39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -5,14 +5,17 @@ import errno
import logging import logging
import argparse import argparse
from json import dumps as json_encode from json import dumps as json_encode
from tempfile import mkdtemp
from gevent import sleep from gevent import sleep
from gevent.queue import Queue from gevent.queue import Queue
from geventwebsocket import WebSocketError from geventwebsocket import WebSocketError
from bottle import request, response, Bottle, HTTPResponse from bottle import request, response, Bottle, HTTPResponse, FileUpload
from bottle import abort from bottle import abort
from shutil import rmtree
from moulinette import msignals, m18n, env from moulinette import msignals, m18n, env
from moulinette.actionsmap import ActionsMap from moulinette.actionsmap import ActionsMap
from moulinette.core import MoulinetteError, MoulinetteValidationError from moulinette.core import MoulinetteError, MoulinetteValidationError
@ -29,6 +32,8 @@ logger = log.getLogger("moulinette.interface.api")
# API helpers ---------------------------------------------------------- # API helpers ----------------------------------------------------------
# We define a global variable to manage in a dirty way the upload...
UPLOAD_DIR = None
CSRF_TYPES = set( CSRF_TYPES = set(
["text/plain", "application/x-www-form-urlencoded", "multipart/form-data"] ["text/plain", "application/x-www-form-urlencoded", "multipart/form-data"]
@ -111,6 +116,7 @@ class _HTTPArgumentParser(object):
self._positional = [] # list(arg_name) self._positional = [] # list(arg_name)
self._optional = {} # dict({arg_name: option_strings}) self._optional = {} # dict({arg_name: option_strings})
self._upload_dir = None
def set_defaults(self, **kwargs): def set_defaults(self, **kwargs):
return self._parser.set_defaults(**kwargs) return self._parser.set_defaults(**kwargs)
@ -145,9 +151,9 @@ class _HTTPArgumentParser(object):
# Append newly created action # Append newly created action
if len(action.option_strings) == 0: if len(action.option_strings) == 0:
self._positional.append(action.dest) self._positional.append(action)
else: else:
self._optional[action.dest] = action.option_strings self._optional[action.dest] = action
return action return action
@ -155,11 +161,26 @@ class _HTTPArgumentParser(object):
arg_strings = [] arg_strings = []
# Append an argument to the current one # Append an argument to the current one
def append(arg_strings, value, option_string=None): def append(arg_strings, value, action):
if isinstance(value, bool): option_string = None
if len(action.option_strings) > 0:
option_string = action.option_strings[0]
if isinstance(value, bool) or isinstance(action.const, bool):
# Append the option string only # Append the option string only
if option_string is not None and value != 0:
arg_strings.append(option_string)
elif isinstance(value, FileUpload) and (
isinstance(action.type, argparse.FileType) or action.type == open
):
# Upload the file in a temp directory
global UPLOAD_DIR
if UPLOAD_DIR is None:
UPLOAD_DIR = mkdtemp(prefix="moulinette_upload_")
value.save(UPLOAD_DIR)
if option_string is not None: if option_string is not None:
arg_strings.append(option_string) arg_strings.append(option_string)
arg_strings.append(UPLOAD_DIR + "/" + value.filename)
elif isinstance(value, str): elif isinstance(value, str):
if option_string is not None: if option_string is not None:
arg_strings.append(option_string) arg_strings.append(option_string)
@ -192,14 +213,14 @@ class _HTTPArgumentParser(object):
return arg_strings return arg_strings
# Iterate over positional arguments # Iterate over positional arguments
for dest in self._positional: for action in self._positional:
if dest in args: if action.dest in args:
arg_strings = append(arg_strings, args[dest]) arg_strings = append(arg_strings, args[action.dest], action)
# Iterate over optional arguments # Iterate over optional arguments
for dest, opt in self._optional.items(): for dest, action in self._optional.items():
if dest in args: if dest in args:
arg_strings = append(arg_strings, args[dest], opt[0]) arg_strings = append(arg_strings, args[dest], action)
return self._parser.parse_args(arg_strings, namespace) return self._parser.parse_args(arg_strings, namespace)
@ -319,8 +340,12 @@ class _ActionsMapPlugin(object):
# Format boolean params # Format boolean params
for a in args: for a in args:
params[a] = True params[a] = True
# Append other request params # Append other request params
for k, v in request.params.decode().dict.items(): req_params = list(request.params.decode().dict.items())
# TODO test special chars in filename
req_params+=list(request.files.dict.items())
for k, v in req_params:
v = _format(v) v = _format(v)
if k not in params.keys(): if k not in params.keys():
params[k] = v params[k] = v
@ -495,6 +520,14 @@ class _ActionsMapPlugin(object):
else: else:
return format_for_response(ret) return format_for_response(ret)
finally: finally:
# Clean upload directory
# FIXME do that in a better way
global UPLOAD_DIR
if UPLOAD_DIR is not None:
rmtree(UPLOAD_DIR, True)
UPLOAD_DIR = None
# Close opened WebSocket by putting StopIteration in the queue # Close opened WebSocket by putting StopIteration in the queue
try: try:
queue = self.log_queues[request.get_cookie("session.id")] queue = self.log_queues[request.get_cookie("session.id")]