.blob output renderer

* _blob_hash= checking plus refactored to use new BadRequest class, refs #1050
* Replace BlobView with new .blob renderer, closes #1050
* .blob downloads on arbitrary queries, closes #1051
This commit is contained in:
Simon Willison 2020-10-29 15:01:38 -07:00 committed by GitHub
commit 78b3eeaad9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 165 additions and 121 deletions

View file

@ -26,6 +26,7 @@ from datasette.utils.asgi import (
Forbidden,
NotFound,
Response,
BadRequest,
)
ureg = pint.UnitRegistry()
@ -260,9 +261,9 @@ class DataView(BaseView):
if stream:
# Some quick sanity checks
if not self.ds.config("allow_csv_stream"):
raise DatasetteError("CSV streaming is disabled", status=400)
raise BadRequest("CSV streaming is disabled")
if request.args.get("_next"):
raise DatasetteError("_next not allowed for CSV streaming", status=400)
raise BadRequest("_next not allowed for CSV streaming")
kwargs["_size"] = "max"
# Fetch the first page
try:

View file

@ -1,4 +1,5 @@
import os
import hashlib
import itertools
import jinja2
import json
@ -10,6 +11,7 @@ from datasette.utils import (
validate_sql_select,
is_url,
path_with_added_args,
path_with_format,
path_with_removed_args,
InvalidSql,
)
@ -342,6 +344,24 @@ class QueryView(DataView):
url=jinja2.escape(value.strip())
)
)
elif isinstance(display_value, bytes):
blob_url = path_with_format(
request,
"blob",
extra_qs={
"_blob_column": column,
"_blob_hash": hashlib.sha256(
display_value
).hexdigest(),
},
)
display_value = jinja2.Markup(
'<a class="blob-download" href="{}">&lt;Binary:&nbsp;{}&nbsp;byte{}&gt;</a>'.format(
blob_url,
len(display_value),
"" if len(value) == 1 else "s",
)
)
display_row.append(display_value)
display_rows.append(display_row)

View file

@ -23,9 +23,9 @@ from datasette.utils import (
urlsafe_components,
value_as_boolean,
)
from datasette.utils.asgi import NotFound, Response
from datasette.utils.asgi import BadRequest, NotFound
from datasette.filters import Filters
from .base import BaseView, DataView, DatasetteError, ureg
from .base import DataView, DatasetteError, ureg
from .database import QueryView
LINK_WITH_LABEL = (
@ -469,7 +469,7 @@ class TableView(RowTableShared):
for i, (key, search_text) in enumerate(search_args.items()):
search_col = key.split("_search_", 1)[1]
if search_col not in await db.table_columns(fts_table):
raise DatasetteError("Cannot search by that column", status=400)
raise BadRequest("Cannot search by that column")
where_clauses.append(
"rowid in (select rowid from {fts_table} where {search_col} match {match_clause})".format(
@ -614,11 +614,11 @@ class TableView(RowTableShared):
raise ValueError
except ValueError:
raise DatasetteError("_size must be a positive integer", status=400)
raise BadRequest("_size must be a positive integer")
if page_size > self.ds.max_returned_rows:
raise DatasetteError(
"_size must be <= {}".format(self.ds.max_returned_rows), status=400
raise BadRequest(
"_size must be <= {}".format(self.ds.max_returned_rows)
)
extra_args["page_size"] = page_size
@ -665,7 +665,7 @@ class TableView(RowTableShared):
if not self.ds.config("allow_facet") and any(
arg.startswith("_facet") for arg in request.args
):
raise DatasetteError("_facet= is not allowed", status=400)
raise BadRequest("_facet= is not allowed")
# pylint: disable=no-member
facet_classes = list(
@ -1041,50 +1041,3 @@ class RowView(RowTableShared):
)
foreign_key_tables.append({**fk, **{"count": count}})
return foreign_key_tables
class BlobView(BaseView):
async def get(self, request, db_name, table, pk_path, column):
await self.check_permissions(
request,
[
("view-table", (db_name, table)),
("view-database", db_name),
"view-instance",
],
)
try:
db = self.ds.get_database(db_name)
except KeyError:
raise NotFound("Database {} does not exist".format(db_name))
if not await db.table_exists(table):
raise NotFound("Table {} does not exist".format(table))
# Ensure the column exists and is of type BLOB
column_types = {c.name: c.type for c in await db.table_column_details(table)}
if column not in column_types:
raise NotFound("Table {} does not have column {}".format(table, column))
if column_types[column].upper() not in ("BLOB", ""):
raise NotFound(
"Table {} does not have column {} of type BLOB".format(table, column)
)
# Ensure the row exists for the pk_path
pk_values = urlsafe_components(pk_path)
sql, params, _ = await _sql_params_pks(db, table, pk_values)
results = await db.execute(sql, params, truncate=True)
rows = list(results.rows)
if not rows:
raise NotFound("Record not found: {}".format(pk_values))
# Serve back the binary data
filename_bits = [to_css_class(table), pk_path, to_css_class(column)]
filename = "-".join(filename_bits) + ".blob"
headers = {
"X-Content-Type-Options": "nosniff",
"Content-Disposition": 'attachment; filename="{}"'.format(filename),
}
return Response(
body=rows[0][column] or b"",
status=200,
headers=headers,
content_type="application/binary",
)