Skip to content

Commit 30b4da0

Browse files
committed
add put_file_upload()
1 parent cf55382 commit 30b4da0

File tree

8 files changed

+121
-51
lines changed

8 files changed

+121
-51
lines changed

pywebio/input.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -649,16 +649,7 @@ def file_upload(label: str = '', accept: Union[List, str] = None, name: str = No
649649
raise ValueError('The `max_size` and `max_total_size` value can not exceed the backend payload size limit. '
650650
'Please increase the `max_total_size` of `start_server()`/`path_deploy()`')
651651

652-
def read_file(data):
653-
for file in data:
654-
# Security fix: to avoid interpreting file name as path
655-
file['filename'] = os.path.basename(file['filename'])
656-
657-
if not multiple:
658-
return data[0] if len(data) >= 1 else None
659-
return data
660-
661-
return single_input(item_spec, valid_func, read_file, onchange_func)
652+
return single_input(item_spec, valid_func, lambda d: d, onchange_func)
662653

663654

664655
def slider(label: str = '', *, name: str = None, value: Union[int, float] = 0, min_value: Union[int, float] = 0,

pywebio/pin.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@
7070
Pin widgets
7171
------------------
7272
Each pin widget function corresponds to an input function of :doc:`input <./input>` module.
73-
(For performance reasons, no pin widget for `file_upload() <pywebio.input.file_upload>` input function)
7473
7574
The function of pin widget supports most of the parameters of the corresponding input function.
7675
Here lists the difference between the two in parameters:
@@ -88,6 +87,7 @@
8887
.. autofunction:: put_radio
8988
.. autofunction:: put_slider
9089
.. autofunction:: put_actions
90+
.. autofunction:: put_file_upload
9191
9292
Pin utils
9393
------------------
@@ -137,7 +137,7 @@
137137
_pin_name_chars = set(string.ascii_letters + string.digits + '_-')
138138

139139
__all__ = ['put_input', 'put_textarea', 'put_select', 'put_checkbox', 'put_radio', 'put_slider', 'put_actions',
140-
'pin', 'pin_update', 'pin_wait_change', 'pin_on_change']
140+
'put_file_upload', 'pin', 'pin_update', 'pin_wait_change', 'pin_on_change']
141141

142142

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

240240

241+
def put_file_upload(name: str, *, label: str = '', accept: Union[List, str] = None, placeholder: str = 'Choose file',
242+
multiple: bool = False, max_size: Union[int, str] = 0, max_total_size: Union[int, str] = 0,
243+
help_text: str = None, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output:
244+
"""Output a file uploading widget. Refer to: `pywebio.input.file_upload()`"""
245+
from pywebio.input import file_upload
246+
check_dom_name_value(name, 'pin `name`')
247+
single_input_return = file_upload(label=label, accept=accept, name=name, placeholder=placeholder, multiple=multiple,
248+
max_size=max_size, max_total_size=max_total_size, help_text=help_text)
249+
return _pin_output(single_input_return, scope, position)
250+
251+
241252
@chose_impl
242253
def get_client_val():
243254
res = yield next_client_event()
@@ -365,7 +376,8 @@ def pin_on_change(name: str, onchange: Callable[[Any], None] = None, clear: bool
365376
"""
366377
assert not (onchange is None and clear is False), "When `onchange` is `None`, `clear` must be `True`"
367378
if onchange is not None:
368-
callback_id = output_register_callback(onchange, **callback_options)
379+
callback = lambda data: onchange(data['value'])
380+
callback_id = output_register_callback(callback, **callback_options)
369381
if init_run:
370382
onchange(pin[name])
371383
else:

pywebio/platform/utils.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fnmatch
22
import json
3+
import os
34
import socket
45
import urllib.parse
56
from collections import defaultdict
@@ -54,16 +55,18 @@ def is_same_site(origin, host):
5455

5556
def deserialize_binary_event(data: bytes):
5657
"""
57-
Data format:
58+
Binary event message is used to submit data with files upload to server.
59+
60+
Data message format:
5861
| event | file_header | file_data | file_header | file_data | ...
5962
6063
The 8 bytes at the beginning of each segment indicate the number of bytes remaining in the segment.
6164
6265
event: {
63-
event: "from_submit",
64-
task_id: that.task_id,
66+
...
6567
data: {
66-
input_name => input_data
68+
input_name => input_data,
69+
...
6770
}
6871
}
6972
@@ -75,9 +78,18 @@ def deserialize_binary_event(data: bytes):
7578
'input_name': name of input field
7679
}
7780
81+
file_data is the file content in bytes.
82+
83+
- When a form field is not a file input, the `event['data'][input_name]` will be the value of the form field.
84+
- When a form field is a single file, the `event['data'][input_name]` is None,
85+
and there will only be one file_header+file_data at most.
86+
- When a form field is a multiple files, the `event['data'][input_name]` is [],
87+
and there may be multiple file_header+file_data.
88+
7889
Example:
7990
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'
8091
"""
92+
# split data into segments
8193
parts = []
8294
start_idx = 0
8395
while start_idx < len(data):
@@ -88,17 +100,26 @@ def deserialize_binary_event(data: bytes):
88100
start_idx += size
89101

90102
event = json.loads(parts[0])
103+
104+
# deserialize file data
91105
files = defaultdict(list)
92106
for idx in range(1, len(parts), 2):
93107
f = json.loads(parts[idx])
94108
f['content'] = parts[idx + 1]
109+
110+
# Security fix: to avoid interpreting file name as path
111+
f['filename'] = os.path.basename(f['filename'])
112+
95113
input_name = f.pop('input_name')
96114
files[input_name].append(f)
97115

116+
# fill file data to event
98117
for input_name in list(event['data'].keys()):
99118
if input_name in files:
119+
init = event['data'][input_name]
100120
event['data'][input_name] = files[input_name]
101-
121+
if init is None: # the file is not multiple
122+
event['data'][input_name] = files[input_name][0] if len(files[input_name]) else None
102123
return event
103124

104125

webiojs/src/handlers/input.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Command, Session} from "../session";
2-
import {error_alert, LRUMap, make_set, serialize_json} from "../utils";
2+
import {error_alert, LRUMap, make_set, serialize_json, serialize_file} from "../utils";
33
import {InputItem} from "../models/input/base"
44
import {state} from '../state'
55
import {all_input_items} from "../models/input"
@@ -234,11 +234,11 @@ class FormController {
234234
}
235235

236236

237-
let data_keys: string[] = [];
238-
let data_values: any[] = [];
237+
let input_names: string[] = [];
238+
let input_values: any[] = [];
239239
$.each(that.name2input, (name, ctrl) => {
240-
data_keys.push(name as string);
241-
data_values.push(ctrl.get_value());
240+
input_names.push(name as string);
241+
input_values.push(ctrl.get_value());
242242
});
243243

244244
let on_process = (loaded: number, total: number) => {
@@ -250,14 +250,17 @@ class FormController {
250250
break;
251251
}
252252
}
253-
Promise.all(data_values).then((values) => {
253+
Promise.all(input_values).then((values) => {
254254
let input_data: { [i: string]: any } = {};
255255
let files: Blob[] = [];
256-
for (let idx in data_keys) {
257-
input_data[data_keys[idx]] = values[idx];
256+
for (let idx in input_names) {
257+
let name = input_names[idx], value = values[idx];
258+
input_data[name] = value;
258259
if (that.spec.inputs[idx].type == 'file') {
259-
input_data[data_keys[idx]] = [];
260-
files.push(...values[idx]);
260+
input_data[name] = value.multiple ? [] : null;
261+
value.files.forEach((file: File) => {
262+
files.push(serialize_file(file, name))
263+
});
261264
}
262265
}
263266
let msg = {
@@ -266,6 +269,7 @@ class FormController {
266269
data: input_data
267270
};
268271
if (files.length) {
272+
// see also: `py:pywebio.platform.utils.deserialize_binary_event()`
269273
that.session.send_buffer(new Blob([serialize_json(msg), ...files], {type: 'application/octet-stream'}), on_process);
270274
} else {
271275
that.session.send_message(msg, on_process);

webiojs/src/handlers/pin.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {Command, Session} from "../session";
1+
import {ClientEvent, Command, Session} from "../session";
22
import {CommandHandler} from "./base";
3-
import {GetPinValue, PinChangeCallback, PinUpdate, WaitChange} from "../models/pin";
3+
import {GetPinValue, PinChangeCallback, PinUpdate, WaitChange, IsFileInput} from "../models/pin";
44
import {state} from "../state";
5+
import {serialize_file, serialize_json} from "../utils";
56

67

78
export class PinHandler implements CommandHandler {
@@ -15,22 +16,56 @@ export class PinHandler implements CommandHandler {
1516

1617
handle_message(msg: Command) {
1718
if (msg.command === 'pin_value') {
18-
let val = GetPinValue(msg.spec.name);
19-
let data = val===undefined? null : {value: val};
20-
state.CurrentSession.send_message({event: "js_yield", task_id: msg.task_id, data: data});
19+
let val = GetPinValue(msg.spec.name); // undefined or value
20+
let send_msg = {
21+
event: "js_yield", task_id: msg.task_id,
22+
data: val === undefined ? null : {value: val}
23+
};
24+
this.submit(send_msg, IsFileInput(msg.spec.name));
2125
} else if (msg.command === 'pin_update') {
2226
PinUpdate(msg.spec.name, msg.spec.attributes);
2327
} else if (msg.command === 'pin_wait') {
2428
let p = WaitChange(msg.spec.names, msg.spec.timeout);
25-
Promise.resolve(p).then(function (value) {
26-
state.CurrentSession.send_message({event: "js_yield", task_id: msg.task_id, data: value});
29+
Promise.resolve(p).then((change_info: (null | { name: string, value: any })) => {
30+
// change_info: null or {'name': name, 'value': value}
31+
let send_msg = {event: "js_yield", task_id: msg.task_id, data: change_info}
32+
this.submit(send_msg, IsFileInput(change_info.name));
2733
}).catch((error) => {
2834
console.error('error in `pin_wait`: %s', error);
29-
state.CurrentSession.send_message({event: "js_yield", task_id: msg.task_id, data: null});
35+
this.submit({event: "js_yield", task_id: msg.task_id, data: null});
3036
});
31-
}else if (msg.command === 'pin_onchange') {
32-
PinChangeCallback(msg.spec.name, msg.spec.callback_id, msg.spec.clear);
37+
} else if (msg.command === 'pin_onchange') {
38+
let onchange = (val: any) => {
39+
let send_msg = {
40+
event: "callback",
41+
task_id: msg.spec.callback_id,
42+
data: {value: val}
43+
}
44+
this.submit(send_msg, IsFileInput(msg.spec.name));
45+
}
46+
PinChangeCallback(msg.spec.name, msg.spec.callback_id ? onchange : null, msg.spec.clear);
3347
}
48+
}
3449

50+
/*
51+
* Send pin value to server.
52+
* `msg.data` may be null, or {value: any, ...}
53+
* `msg.data.value` stores the value of the pin.
54+
* when submit files, `msg.data.value` is {multiple: bool, files: File[] }
55+
* */
56+
submit(msg: ClientEvent, is_file: boolean = false) {
57+
if (is_file && msg.data !== null) {
58+
// msg.data.value: {multiple: bool, files: File[]}
59+
let {multiple, files} = msg.data.value;
60+
msg.data.value = multiple ? [] : null; // replace file value with initial value
61+
state.CurrentSession.send_buffer(
62+
new Blob([
63+
serialize_json(msg),
64+
...files.map((file: File) => serialize_file(file, 'value'))
65+
], {type: 'application/octet-stream'})
66+
);
67+
} else {
68+
state.CurrentSession.send_message(msg);
69+
}
3570
}
3671
}

webiojs/src/models/input/file.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {InputItem} from "./base";
2-
import {deep_copy, serialize_file} from "../../utils";
2+
import {deep_copy} from "../../utils";
33
import {t} from "../../i18n";
44

55
const file_input_tpl = `
@@ -14,10 +14,10 @@ const file_input_tpl = `
1414
</div>
1515
</div>`;
1616

17-
export class File extends InputItem {
17+
export class FileUpload extends InputItem {
1818
static accept_input_types: string[] = ["file"];
1919

20-
files: Blob[] = []; // Files to be uploaded
20+
files: File[] = []; // Files to be uploaded
2121
valid = true;
2222

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

75-
that.files.push(serialize_file(f, spec.name));
76-
75+
that.files.push(f);
7776
}
7877

78+
if (spec.onchange) {
79+
that.on_input_event("change", that);
80+
}
7981
});
8082

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

102104
get_value(): any {
103-
return this.files;
105+
return {
106+
multiple: this.spec.multiple,
107+
files: this.files
108+
}
104109
}
105110

106111
after_add_to_dom(): any {

webiojs/src/models/input/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import {Input} from "./input"
22
import {Actions} from "./actions"
33
import {CheckboxRadio} from "./checkbox_radio"
44
import {Textarea} from "./textarea"
5-
import {File} from "./file"
5+
import {FileUpload} from "./file"
66
import {Select} from "./select"
77
import {Slider} from "./slider"
88
import {InputItem} from "./base";
99

1010

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

1313
export function get_input_item_from_type(type: string) {
1414
return type2item[type];

webiojs/src/models/pin.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import {pushData} from "../session";
88

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

11+
export function IsFileInput(name: string): boolean {
12+
return name2input[name] !== undefined && name2input[name].spec.type == "file";
13+
}
14+
1115
export function GetPinValue(name: string) {
1216
if (name2input[name] == undefined || !document.contains(name2input[name].element[0]))
1317
return undefined;
@@ -47,14 +51,12 @@ export function WaitChange(names: string[], timeout: number) {
4751
});
4852
}
4953

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

54-
if (callback_id) {
55-
resident_onchange_callbacks[name].push((val) => {
56-
pushData(val, callback_id)
57-
})
58+
if (callback) {
59+
resident_onchange_callbacks[name].push(callback)
5860
}
5961
}
6062

0 commit comments

Comments
 (0)