From bc8a8ecf5f819facc8dbe088a3829ebd6acfb1e2 Mon Sep 17 00:00:00 2001 From: robot3498712 Date: Thu, 15 Jun 2017 12:49:00 +0200 Subject: [PATCH 1/5] fix /issues/2592: web: Use Unicode paths to send files on Windows under Python 2 --- beetsplug/web/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 290f25b48..14e338ba6 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -217,12 +217,13 @@ def all_items(): @app.route('/item//file') def item_file(item_id): item = g.lib.get_item(item_id) + item_path = util.syspath(item.path) if os.name == 'nt' else util.py3_path(item.path) response = flask.send_file( - util.py3_path(item.path), + item_path, as_attachment=True, attachment_filename=os.path.basename(util.py3_path(item.path)), ) - response.headers['Content-Length'] = os.path.getsize(item.path) + response.headers['Content-Length'] = os.path.getsize(item_path) return response From cafbb2438e90727ae48c00324cda5f8309ab544c Mon Sep 17 00:00:00 2001 From: robot3498712 Date: Thu, 15 Jun 2017 13:27:28 +0200 Subject: [PATCH 2/5] fixed failing test - line too long --- beetsplug/web/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 14e338ba6..00718dbfb 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -217,7 +217,8 @@ def all_items(): @app.route('/item//file') def item_file(item_id): item = g.lib.get_item(item_id) - item_path = util.syspath(item.path) if os.name == 'nt' else util.py3_path(item.path) + item_path = util.syspath(item.path) if (os.name == 'nt') else ( + util.py3_path(item.path)) response = flask.send_file( item_path, as_attachment=True, From c840fea1254325940bc4e48c1c8e82a5f9678f0d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 15 Jun 2017 17:49:59 -0400 Subject: [PATCH 3/5] Changelog for #2593 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 299d380f9..b3bee8a44 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,8 @@ Fixes: * :doc:`/plugins/lastgenre`: Fix a crash when using the `prefer_specific` and `canonical` options together. Thanks to :user:`yacoob`. :bug:`2459` :bug:`2583` +* :doc:`/plugins/web`: Fix a crash on Windows under Python 2 when serving + non-ASCII filenames. Thanks to :user:`robot3498712`. :bug:`2592` :bug:`2593` 1.4.4 (June 10, 2017) From 009c6a4f6d76496109ab9b9751ff22864cf612cd Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 15 Jun 2017 17:51:14 -0400 Subject: [PATCH 4/5] Slightly clearer layout for #2593, and comments --- beetsplug/web/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 00718dbfb..635c2f5a8 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -217,8 +217,14 @@ def all_items(): @app.route('/item//file') def item_file(item_id): item = g.lib.get_item(item_id) - item_path = util.syspath(item.path) if (os.name == 'nt') else ( - util.py3_path(item.path)) + + # On Windows under Python 2, Flask wants a Unicode path. On Python 3, it + # *always* wants a Unicode path. + if os.name == 'nt': + item_path = util.syspath(item.path) + else: + item_path = util.py3_path(item.path) + response = flask.send_file( item_path, as_attachment=True, From 2144882290111b908293fceb542fe3047ce62de5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 15 Jun 2017 17:59:40 -0400 Subject: [PATCH 5/5] Correct some PEP8 post-docstring whitespace To better match #2597 for a cleaner diff. --- beets/dbcore/query.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 51790b9fa..31756aeac 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -40,6 +40,7 @@ class InvalidQueryError(ParsingError): The query should be a unicode string or a list, which will be space-joined. """ + def __init__(self, query, explanation): if isinstance(query, list): query = " ".join(query) @@ -53,6 +54,7 @@ class InvalidQueryArgumentValueError(ParsingError): It exists to be caught in upper stack levels so a meaningful (i.e. with the query) InvalidQueryError can be raised. """ + def __init__(self, what, expected, detail=None): message = u"'{0}' is not {1}".format(what, expected) if detail: @@ -63,6 +65,7 @@ class InvalidQueryArgumentValueError(ParsingError): class Query(object): """An abstract class representing a query into the item database. """ + def clause(self): """Generate an SQLite expression implementing the query. @@ -95,6 +98,7 @@ class FieldQuery(Query): string. Subclasses may also provide `col_clause` to implement the same matching functionality in SQLite. """ + def __init__(self, field, pattern, fast=True): self.field = field self.pattern = pattern @@ -134,6 +138,7 @@ class FieldQuery(Query): class MatchQuery(FieldQuery): """A query that looks for exact matches in an item field.""" + def col_clause(self): return self.field + " = ?", [self.pattern] @@ -143,6 +148,7 @@ class MatchQuery(FieldQuery): class NoneQuery(FieldQuery): + """A query that checks whether a field is null.""" def __init__(self, field, fast=True): super(NoneQuery, self).__init__(field, None, fast) @@ -165,6 +171,7 @@ class StringFieldQuery(FieldQuery): """A FieldQuery that converts values to strings before matching them. """ + @classmethod def value_match(cls, pattern, value): """Determine whether the value matches the pattern. The value @@ -182,11 +189,12 @@ class StringFieldQuery(FieldQuery): class SubstringQuery(StringFieldQuery): """A query that matches a substring in a specific item field.""" + def col_clause(self): pattern = (self.pattern - .replace('\\', '\\\\') - .replace('%', '\\%') - .replace('_', '\\_')) + .replace('\\', '\\\\') + .replace('%', '\\%') + .replace('_', '\\_')) search = '%' + pattern + '%' clause = self.field + " like ? escape '\\'" subvals = [search] @@ -204,6 +212,7 @@ class RegexpQuery(StringFieldQuery): Raises InvalidQueryError when the pattern is not a valid regular expression. """ + def __init__(self, field, pattern, fast=True): super(RegexpQuery, self).__init__(field, pattern, fast) pattern = self._normalize(pattern) @@ -231,6 +240,7 @@ class BooleanQuery(MatchQuery): """Matches a boolean field. Pattern should either be a boolean or a string reflecting a boolean. """ + def __init__(self, field, pattern, fast=True): super(BooleanQuery, self).__init__(field, pattern, fast) if isinstance(pattern, six.string_types): @@ -244,6 +254,7 @@ class BytesQuery(MatchQuery): `unicode` equivalently in Python 2. Always use this query instead of `MatchQuery` when matching on BLOB values. """ + def __init__(self, field, pattern): super(BytesQuery, self).__init__(field, pattern) @@ -270,6 +281,7 @@ class NumericQuery(FieldQuery): Raises InvalidQueryError when the pattern does not represent an int or a float. """ + def _convert(self, s): """Convert a string to a numeric type (float or int). @@ -337,6 +349,7 @@ class CollectionQuery(Query): """An abstract query class that aggregates other queries. Can be indexed like a list to access the sub-queries. """ + def __init__(self, subqueries=()): self.subqueries = subqueries @@ -389,6 +402,7 @@ class AnyFieldQuery(CollectionQuery): any field. The individual field query class is provided to the constructor. """ + def __init__(self, pattern, fields, cls): self.pattern = pattern self.fields = fields @@ -424,6 +438,7 @@ class MutableCollectionQuery(CollectionQuery): """A collection query whose subqueries may be modified after the query is initialized. """ + def __setitem__(self, key, value): self.subqueries[key] = value @@ -433,6 +448,7 @@ class MutableCollectionQuery(CollectionQuery): class AndQuery(MutableCollectionQuery): """A conjunction of a list of other queries.""" + def clause(self): return self.clause_with_joiner('and') @@ -442,6 +458,7 @@ class AndQuery(MutableCollectionQuery): class OrQuery(MutableCollectionQuery): """A conjunction of a list of other queries.""" + def clause(self): return self.clause_with_joiner('or') @@ -453,6 +470,7 @@ class NotQuery(Query): """A query that matches the negation of its `subquery`, as a shorcut for performing `not(subquery)` without using regular expressions. """ + def __init__(self, subquery): self.subquery = subquery @@ -481,6 +499,7 @@ class NotQuery(Query): class TrueQuery(Query): """A query that always matches.""" + def clause(self): return '1', () @@ -490,6 +509,7 @@ class TrueQuery(Query): class FalseQuery(Query): """A query that never matches.""" + def clause(self): return '0', ()