Skip to content

Commit

Permalink
add put_file_upload()
Browse files Browse the repository at this point in the history
  • Loading branch information
wang0618 committed Mar 12, 2023
1 parent cf55382 commit 30b4da0
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 51 deletions.
11 changes: 1 addition & 10 deletions pywebio/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,16 +649,7 @@ def file_upload(label: str = '', accept: Union[List, str] = None, name: str = No
raise ValueError('The `max_size` and `max_total_size` value can not exceed the backend payload size limit. '
'Please increase the `max_total_size` of `start_server()`/`path_deploy()`')

def read_file(data):
for file in data:
# Security fix: to avoid interpreting file name as path
file['filename'] = os.path.basename(file['filename'])

if not multiple:
return data[0] if len(data) >= 1 else None
return data

return single_input(item_spec, valid_func, read_file, onchange_func)
return single_input(item_spec, valid_func, lambda d: d, onchange_func)


def slider(label: str = '', *, name: str = None, value: Union[int, float] = 0, min_value: Union[int, float] = 0,
Expand Down
18 changes: 15 additions & 3 deletions pywebio/pin.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
Pin widgets
------------------
Each pin widget function corresponds to an input function of :doc:`input <./input>` module.
(For performance reasons, no pin widget for `file_upload() <pywebio.input.file_upload>` input function)
The function of pin widget supports most of the parameters of the corresponding input function.
Here lists the difference between the two in parameters:
Expand All @@ -88,6 +87,7 @@
.. autofunction:: put_radio
.. autofunction:: put_slider
.. autofunction:: put_actions
.. autofunction:: put_file_upload
Pin utils
------------------
Expand Down Expand Up @@ -137,7 +137,7 @@
_pin_name_chars = set(string.ascii_letters + string.digits + '_-')

__all__ = ['put_input', 'put_textarea', 'put_select', 'put_checkbox', 'put_radio', 'put_slider', 'put_actions',
'pin', 'pin_update', 'pin_wait_change', 'pin_on_change']
'put_file_upload', 'pin', 'pin_update', 'pin_wait_change', 'pin_on_change']


def _pin_output(single_input_return, scope, position):
Expand Down Expand Up @@ -238,6 +238,17 @@ def put_actions(name: str, *, label: str = '', buttons: List[Union[Dict[str, Any
return _pin_output(input_kwargs, scope, position)


def put_file_upload(name: str, *, label: str = '', accept: Union[List, str] = None, placeholder: str = 'Choose file',
multiple: bool = False, max_size: Union[int, str] = 0, max_total_size: Union[int, str] = 0,
help_text: str = None, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output:
"""Output a file uploading widget. Refer to: `pywebio.input.file_upload()`"""
from pywebio.input import file_upload
check_dom_name_value(name, 'pin `name`')
single_input_return = file_upload(label=label, accept=accept, name=name, placeholder=placeholder, multiple=multiple,
max_size=max_size, max_total_size=max_total_size, help_text=help_text)
return _pin_output(single_input_return, scope, position)


@chose_impl
def get_client_val():
res = yield next_client_event()
Expand Down Expand Up @@ -365,7 +376,8 @@ def pin_on_change(name: str, onchange: Callable[[Any], None] = None, clear: bool
"""
assert not (onchange is None and clear is False), "When `onchange` is `None`, `clear` must be `True`"
if onchange is not None:
callback_id = output_register_callback(onchange, **callback_options)
callback = lambda data: onchange(data['value'])
callback_id = output_register_callback(callback, **callback_options)
if init_run:
onchange(pin[name])
else:
Expand Down
31 changes: 26 additions & 5 deletions pywebio/platform/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fnmatch
import json
import os
import socket
import urllib.parse
from collections import defaultdict
Expand Down Expand Up @@ -54,16 +55,18 @@ def is_same_site(origin, host):

def deserialize_binary_event(data: bytes):
"""
Data format:
Binary event message is used to submit data with files upload to server.
Data message format:
| event | file_header | file_data | file_header | file_data | ...
The 8 bytes at the beginning of each segment indicate the number of bytes remaining in the segment.
event: {
event: "from_submit",
task_id: that.task_id,
...
data: {
input_name => input_data
input_name => input_data,
...
}
}
Expand All @@ -75,9 +78,18 @@ def deserialize_binary_event(data: bytes):
'input_name': name of input field
}
file_data is the file content in bytes.
- When a form field is not a file input, the `event['data'][input_name]` will be the value of the form field.
- When a form field is a single file, the `event['data'][input_name]` is None,
and there will only be one file_header+file_data at most.
- When a form field is a multiple files, the `event['data'][input_name]` is [],
and there may be multiple file_header+file_data.
Example:
b'\x00\x00\x00\x00\x00\x00\x00E{"event":"from_submit","task_id":"main-4788341456","data":{"data":1}}\x00\x00\x00\x00\x00\x00\x00Y{"filename":"hello.txt","size":2,"mime_type":"text/plain","last_modified":1617119937.276}\x00\x00\x00\x00\x00\x00\x00\x02ss'
"""
# split data into segments
parts = []
start_idx = 0
while start_idx < len(data):
Expand All @@ -88,17 +100,26 @@ def deserialize_binary_event(data: bytes):
start_idx += size

event = json.loads(parts[0])

# deserialize file data
files = defaultdict(list)
for idx in range(1, len(parts), 2):
f = json.loads(parts[idx])
f['content'] = parts[idx + 1]

# Security fix: to avoid interpreting file name as path
f['filename'] = os.path.basename(f['filename'])

input_name = f.pop('input_name')
files[input_name].append(f)

# fill file data to event
for input_name in list(event['data'].keys()):
if input_name in files:
init = event['data'][input_name]
event['data'][input_name] = files[input_name]

if init is None: # the file is not multiple
event['data'][input_name] = files[input_name][0] if len(files[input_name]) else None
return event


Expand Down
24 changes: 14 additions & 10 deletions webiojs/src/handlers/input.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Command, Session} from "../session";
import {error_alert, LRUMap, make_set, serialize_json} from "../utils";
import {error_alert, LRUMap, make_set, serialize_json, serialize_file} from "../utils";
import {InputItem} from "../models/input/base"
import {state} from '../state'
import {all_input_items} from "../models/input"
Expand Down Expand Up @@ -234,11 +234,11 @@ class FormController {
}


let data_keys: string[] = [];
let data_values: any[] = [];
let input_names: string[] = [];
let input_values: any[] = [];
$.each(that.name2input, (name, ctrl) => {
data_keys.push(name as string);
data_values.push(ctrl.get_value());
input_names.push(name as string);
input_values.push(ctrl.get_value());
});

let on_process = (loaded: number, total: number) => {
Expand All @@ -250,14 +250,17 @@ class FormController {
break;
}
}
Promise.all(data_values).then((values) => {
Promise.all(input_values).then((values) => {
let input_data: { [i: string]: any } = {};
let files: Blob[] = [];
for (let idx in data_keys) {
input_data[data_keys[idx]] = values[idx];
for (let idx in input_names) {
let name = input_names[idx], value = values[idx];
input_data[name] = value;
if (that.spec.inputs[idx].type == 'file') {
input_data[data_keys[idx]] = [];
files.push(...values[idx]);
input_data[name] = value.multiple ? [] : null;
value.files.forEach((file: File) => {
files.push(serialize_file(file, name))
});
}
}
let msg = {
Expand All @@ -266,6 +269,7 @@ class FormController {
data: input_data
};
if (files.length) {
// see also: `py:pywebio.platform.utils.deserialize_binary_event()`
that.session.send_buffer(new Blob([serialize_json(msg), ...files], {type: 'application/octet-stream'}), on_process);
} else {
that.session.send_message(msg, on_process);
Expand Down
55 changes: 45 additions & 10 deletions webiojs/src/handlers/pin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {Command, Session} from "../session";
import {ClientEvent, Command, Session} from "../session";
import {CommandHandler} from "./base";
import {GetPinValue, PinChangeCallback, PinUpdate, WaitChange} from "../models/pin";
import {GetPinValue, PinChangeCallback, PinUpdate, WaitChange, IsFileInput} from "../models/pin";
import {state} from "../state";
import {serialize_file, serialize_json} from "../utils";


export class PinHandler implements CommandHandler {
Expand All @@ -15,22 +16,56 @@ export class PinHandler implements CommandHandler {

handle_message(msg: Command) {
if (msg.command === 'pin_value') {
let val = GetPinValue(msg.spec.name);
let data = val===undefined? null : {value: val};
state.CurrentSession.send_message({event: "js_yield", task_id: msg.task_id, data: data});
let val = GetPinValue(msg.spec.name); // undefined or value
let send_msg = {
event: "js_yield", task_id: msg.task_id,
data: val === undefined ? null : {value: val}
};
this.submit(send_msg, IsFileInput(msg.spec.name));
} else if (msg.command === 'pin_update') {
PinUpdate(msg.spec.name, msg.spec.attributes);
} else if (msg.command === 'pin_wait') {
let p = WaitChange(msg.spec.names, msg.spec.timeout);
Promise.resolve(p).then(function (value) {
state.CurrentSession.send_message({event: "js_yield", task_id: msg.task_id, data: value});
Promise.resolve(p).then((change_info: (null | { name: string, value: any })) => {
// change_info: null or {'name': name, 'value': value}
let send_msg = {event: "js_yield", task_id: msg.task_id, data: change_info}
this.submit(send_msg, IsFileInput(change_info.name));
}).catch((error) => {
console.error('error in `pin_wait`: %s', error);
state.CurrentSession.send_message({event: "js_yield", task_id: msg.task_id, data: null});
this.submit({event: "js_yield", task_id: msg.task_id, data: null});
});
}else if (msg.command === 'pin_onchange') {
PinChangeCallback(msg.spec.name, msg.spec.callback_id, msg.spec.clear);
} else if (msg.command === 'pin_onchange') {
let onchange = (val: any) => {
let send_msg = {
event: "callback",
task_id: msg.spec.callback_id,
data: {value: val}
}
this.submit(send_msg, IsFileInput(msg.spec.name));
}
PinChangeCallback(msg.spec.name, msg.spec.callback_id ? onchange : null, msg.spec.clear);
}
}

/*
* Send pin value to server.
* `msg.data` may be null, or {value: any, ...}
* `msg.data.value` stores the value of the pin.
* when submit files, `msg.data.value` is {multiple: bool, files: File[] }
* */
submit(msg: ClientEvent, is_file: boolean = false) {
if (is_file && msg.data !== null) {
// msg.data.value: {multiple: bool, files: File[]}
let {multiple, files} = msg.data.value;
msg.data.value = multiple ? [] : null; // replace file value with initial value
state.CurrentSession.send_buffer(
new Blob([
serialize_json(msg),
...files.map((file: File) => serialize_file(file, 'value'))
], {type: 'application/octet-stream'})
);
} else {
state.CurrentSession.send_message(msg);
}
}
}
17 changes: 11 additions & 6 deletions webiojs/src/models/input/file.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {InputItem} from "./base";
import {deep_copy, serialize_file} from "../../utils";
import {deep_copy} from "../../utils";
import {t} from "../../i18n";

const file_input_tpl = `
Expand All @@ -14,10 +14,10 @@ const file_input_tpl = `
</div>
</div>`;

export class File extends InputItem {
export class FileUpload extends InputItem {
static accept_input_types: string[] = ["file"];

files: Blob[] = []; // Files to be uploaded
files: File[] = []; // Files to be uploaded
valid = true;

constructor(spec: any, task_id: string, on_input_event: (event_name: string, input_item: InputItem) => void) {
Expand Down Expand Up @@ -72,10 +72,12 @@ export class File extends InputItem {
if (!that.valid) return;
that.update_input_helper(-1, {'valid_status': 0});

that.files.push(serialize_file(f, spec.name));

that.files.push(f);
}

if (spec.onchange) {
that.on_input_event("change", that);
}
});

return this.element;
Expand All @@ -100,7 +102,10 @@ export class File extends InputItem {
}

get_value(): any {
return this.files;
return {
multiple: this.spec.multiple,
files: this.files
}
}

after_add_to_dom(): any {
Expand Down
4 changes: 2 additions & 2 deletions webiojs/src/models/input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import {Input} from "./input"
import {Actions} from "./actions"
import {CheckboxRadio} from "./checkbox_radio"
import {Textarea} from "./textarea"
import {File} from "./file"
import {FileUpload} from "./file"
import {Select} from "./select"
import {Slider} from "./slider"
import {InputItem} from "./base";


export const all_input_items = [Input, Actions, CheckboxRadio, Textarea, File, Select, Slider];
export const all_input_items = [Input, Actions, CheckboxRadio, Textarea, FileUpload, Select, Slider];

export function get_input_item_from_type(type: string) {
return type2item[type];
Expand Down
12 changes: 7 additions & 5 deletions webiojs/src/models/pin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {pushData} from "../session";

let name2input: { [k: string]: InputItem } = {};

export function IsFileInput(name: string): boolean {
return name2input[name] !== undefined && name2input[name].spec.type == "file";
}

export function GetPinValue(name: string) {
if (name2input[name] == undefined || !document.contains(name2input[name].element[0]))
return undefined;
Expand Down Expand Up @@ -47,14 +51,12 @@ export function WaitChange(names: string[], timeout: number) {
});
}

export function PinChangeCallback(name: string, callback_id: string, clear: boolean) {
export function PinChangeCallback(name: string, callback: null | ((val: any) => void), clear: boolean) {
if (!(name in resident_onchange_callbacks) || clear)
resident_onchange_callbacks[name] = [];

if (callback_id) {
resident_onchange_callbacks[name].push((val) => {
pushData(val, callback_id)
})
if (callback) {
resident_onchange_callbacks[name].push(callback)
}
}

Expand Down

0 comments on commit 30b4da0

Please sign in to comment.