|
18 | 18 | import time |
19 | 19 | import webbrowser |
20 | 20 | from collections import namedtuple, OrderedDict |
| 21 | +from functools import reduce |
21 | 22 | from functools import wraps |
22 | 23 | from getpass import getpass |
| 24 | +from io import StringIO |
23 | 25 | from io import TextIOWrapper |
24 | 26 | from logging.handlers import WatchedFileHandler |
25 | 27 | from werkzeug.routing import BaseConverter |
26 | 28 | from werkzeug.utils import secure_filename |
27 | 29 |
|
28 | | -# Py2k compat. |
29 | | -if sys.version_info[0] == 2: |
30 | | - PY2 = True |
31 | | - binary_types = (buffer, bytes, bytearray) |
32 | | - decode_handler = 'replace' |
33 | | - numeric = (int, long, float) |
34 | | - unicode_type = unicode |
35 | | - from StringIO import StringIO |
36 | | -else: |
37 | | - PY2 = False |
38 | | - binary_types = (bytes, bytearray) |
39 | | - decode_handler = 'backslashreplace' |
40 | | - numeric = (int, float) |
41 | | - unicode_type = str |
42 | | - from functools import reduce |
43 | | - from io import StringIO |
44 | 30 |
|
45 | 31 | try: |
46 | 32 | from flask import ( |
@@ -90,6 +76,8 @@ def syntax_highlight(data): |
90 | 76 |
|
91 | 77 | CUR_DIR = os.path.realpath(os.path.dirname(__file__)) |
92 | 78 | DEBUG = False |
| 79 | + |
| 80 | +BLOB_AS_BASE64 = False # Default is hex. |
93 | 81 | ROWS_PER_PAGE = 50 |
94 | 82 | QUERY_ROWS_PER_PAGE = 1000 |
95 | 83 | TRUNCATE_VALUES = True |
@@ -887,10 +875,16 @@ def minimal_validate_field(field, value): |
887 | 875 | return value, 'Value must be 1, 0, true, false, t or f.' |
888 | 876 | value = True if value.lower() in ('1', 't', 'true') else False |
889 | 877 | elif isinstance(field, BlobField): |
890 | | - try: |
891 | | - value = base64.b64decode(value) |
892 | | - except Exception as exc: |
893 | | - return value, 'Value must be base64-encoded binary data.' |
| 878 | + if app.config['BLOB_AS_BASE64']: |
| 879 | + try: |
| 880 | + value = base64.b64decode(value) |
| 881 | + except Exception as exc: |
| 882 | + return value, 'Value must be base64-encoded binary data.' |
| 883 | + else: |
| 884 | + try: |
| 885 | + value = bytes.fromhex(value) |
| 886 | + except Exception as exc: |
| 887 | + return value, 'Value must be valid hex representation.' |
894 | 888 | try: |
895 | 889 | field.db_value(value) |
896 | 890 | except Exception as exc: |
@@ -1014,7 +1008,10 @@ def table_update(table, pk): |
1014 | 1008 | if value is None: |
1015 | 1009 | row[field.name] = None |
1016 | 1010 | elif isinstance(field, BlobField): |
1017 | | - row[field.name] = base64.b64encode(value).decode('utf8') |
| 1011 | + if app.config['BLOB_AS_BASE64']: |
| 1012 | + row[field.name] = base64.b64encode(value).decode('utf8') |
| 1013 | + else: |
| 1014 | + row[field.name] = value.hex() |
1018 | 1015 | else: |
1019 | 1016 | row[field.name] = value |
1020 | 1017 |
|
@@ -1128,6 +1125,9 @@ def export(query, export_format, table=None): |
1128 | 1125 | # Avoid any special chars in export filename. |
1129 | 1126 | filename = re.sub(r'[^\w\d\-\.]+', '', filename) |
1130 | 1127 |
|
| 1128 | + if peewee_version >= (4, 0, 2): |
| 1129 | + kwargs['base64_bytes'] = app.config['BLOB_AS_BASE64'] |
| 1130 | + |
1131 | 1131 | dataset.freeze(query, export_format, file_obj=buf, **kwargs) |
1132 | 1132 |
|
1133 | 1133 | response_data = buf.getvalue() |
@@ -1193,25 +1193,26 @@ def table_import(table): |
1193 | 1193 | # compatible with Python's CSV module. We'd need to reach pretty |
1194 | 1194 | # far into Flask's internals to modify this behavior, so instead |
1195 | 1195 | # we'll just translate the stream into utf8-decoded unicode. |
1196 | | - if not PY2: |
1197 | | - try: |
1198 | | - stream = TextIOWrapper(file_obj, encoding='utf8') |
1199 | | - except AttributeError: |
1200 | | - # The SpooledTemporaryFile used by werkzeug does not |
1201 | | - # implement an API that the TextIOWrapper expects, so we'll |
1202 | | - # just consume the whole damn thing and decode it. |
1203 | | - # Fixed in werkzeug 0.15. |
1204 | | - stream = StringIO(file_obj.read().decode('utf8')) |
1205 | | - else: |
1206 | | - stream = file_obj.stream |
1207 | | - |
| 1196 | + try: |
| 1197 | + stream = TextIOWrapper(file_obj, encoding='utf8') |
| 1198 | + except AttributeError: |
| 1199 | + # The SpooledTemporaryFile used by werkzeug does not |
| 1200 | + # implement an API that the TextIOWrapper expects, so we'll |
| 1201 | + # just consume the whole damn thing and decode it. |
| 1202 | + # Fixed in werkzeug 0.15. |
| 1203 | + stream = StringIO(file_obj.read().decode('utf8')) |
| 1204 | + |
| 1205 | + kwargs = {} |
| 1206 | + if peewee_version >= (4, 0, 2): |
| 1207 | + kwargs['base64_bytes'] = app.config['BLOB_AS_BASE64'] |
1208 | 1208 | try: |
1209 | 1209 | with dataset.transaction(): |
1210 | 1210 | count = dataset.thaw( |
1211 | 1211 | table, |
1212 | 1212 | format=format, |
1213 | 1213 | file_obj=stream, |
1214 | | - strict=strict) |
| 1214 | + strict=strict, |
| 1215 | + **kwargs) |
1215 | 1216 | except Exception as exc: |
1216 | 1217 | flash('Error importing file: %s' % exc, 'danger') |
1217 | 1218 | app.logger.exception('Error importing file.') |
@@ -1292,17 +1293,18 @@ def pk_display(table_pk, pk): |
1292 | 1293 |
|
1293 | 1294 | @app.template_filter('value_filter') |
1294 | 1295 | def value_filter(value, max_length=50): |
1295 | | - if isinstance(value, numeric): |
| 1296 | + if isinstance(value, (int, float)): |
1296 | 1297 | return value |
1297 | 1298 |
|
1298 | | - if isinstance(value, binary_types): |
1299 | | - if not isinstance(value, (bytes, bytearray)): |
1300 | | - value = bytes(value) # Handle `buffer` type. |
| 1299 | + if isinstance(value, (bytes, bytearray, memoryview)): |
1301 | 1300 | try: |
1302 | 1301 | value = value.decode('utf8') |
1303 | 1302 | except UnicodeDecodeError: |
1304 | | - value = base64.b64encode(value)[:1024].decode('utf8') |
1305 | | - if isinstance(value, unicode_type): |
| 1303 | + if app.config['BLOB_AS_BASE64']: |
| 1304 | + value = base64.b64encode(value)[:1024].decode('utf8') |
| 1305 | + else: |
| 1306 | + value = value.hex()[:1024] |
| 1307 | + if isinstance(value, str): |
1306 | 1308 | if link_re.match(value): |
1307 | 1309 | label = value |
1308 | 1310 | if len(value) > max_length and app.config['TRUNCATE_VALUES']: |
@@ -1481,6 +1483,12 @@ def get_option_parser(): |
1481 | 1483 | help=('Disable truncating long text values. By default text values ' |
1482 | 1484 | 'are ellipsized after 50 characters and the full text is shown ' |
1483 | 1485 | 'on click.')) |
| 1486 | + parser.add_option( |
| 1487 | + '-B', |
| 1488 | + '--base64', |
| 1489 | + action='store_true', |
| 1490 | + dest='base64', |
| 1491 | + help='BLOB data as base64 (default is hex)') |
1484 | 1492 | parser.add_option( |
1485 | 1493 | '-u', |
1486 | 1494 | '--url-prefix', |
@@ -1667,6 +1675,8 @@ def configure_app(): |
1667 | 1675 | app.config['ROWS_PER_PAGE'] = options.rows_per_page |
1668 | 1676 | if options.query_rows_per_page: |
1669 | 1677 | app.config['QUERY_ROWS_PER_PAGE'] = options.query_rows_per_page |
| 1678 | + if options.base64: |
| 1679 | + app.config['BLOB_AS_BASE64'] = options.base64 |
1670 | 1680 |
|
1671 | 1681 | app.config['TRUNCATE_VALUES'] = options.truncate_values |
1672 | 1682 |
|
|
0 commit comments