/db/table/-/blob/pk/column.blob download URL, refs #1036

This commit is contained in:
Simon Willison 2020-10-24 16:09:18 -07:00 committed by GitHub
commit 5a15197960
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 183 additions and 66 deletions

View file

@ -38,7 +38,7 @@ from .views.special import (
PermissionsDebugView,
MessagesDebugView,
)
from .views.table import RowView, TableView
from .views.table import RowView, TableView, BlobView
from .renderer import json_renderer
from .database import Database, QueryInterrupted
@ -923,6 +923,10 @@ class Datasette:
+ renderer_regex
+ r")?$",
)
add_route(
BlobView.as_view(self),
r"/(?P<db_name>[^/]+)/(?P<table>[^/]+?)/\-/blob/(?P<pk_path>[^/]+?)/(?P<column>[^/]+)\.blob$",
)
self._register_custom_units()
async def setup_db():

View file

@ -51,6 +51,9 @@ class DatasetteError(Exception):
class BaseView:
ds = None
def __init__(self, datasette):
self.ds = datasette
async def head(self, *args, **kwargs):
response = await self.get(*args, **kwargs)
response.body = b""
@ -151,9 +154,6 @@ class DataView(BaseView):
name = ""
re_named_parameter = re.compile(":([a-zA-Z0-9_]+)")
def __init__(self, datasette):
self.ds = datasette
def options(self, request, *args, **kwargs):
r = Response.text("ok")
if self.ds.cors:

View file

@ -18,9 +18,6 @@ COUNT_DB_SIZE_LIMIT = 100 * 1024 * 1024
class IndexView(BaseView):
name = "index"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request, as_format):
await self.check_permission(request, "view-instance")
databases = []

View file

@ -44,9 +44,6 @@ class JsonDataView(BaseView):
class PatternPortfolioView(BaseView):
name = "patterns"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request):
await self.check_permission(request, "view-instance")
return await self.render(["patterns.html"], request=request)
@ -55,9 +52,6 @@ class PatternPortfolioView(BaseView):
class AuthTokenView(BaseView):
name = "auth_token"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request):
token = request.args.get("token") or ""
if not self.ds._root_token:
@ -76,9 +70,6 @@ class AuthTokenView(BaseView):
class LogoutView(BaseView):
name = "logout"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request):
if not request.actor:
return Response.redirect(self.ds.urls.instance())
@ -98,9 +89,6 @@ class LogoutView(BaseView):
class PermissionsDebugView(BaseView):
name = "permissions_debug"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request):
await self.check_permission(request, "view-instance")
if not await self.ds.permission_allowed(request.actor, "permissions-debug"):
@ -115,9 +103,6 @@ class PermissionsDebugView(BaseView):
class AllowDebugView(BaseView):
name = "allow_debug"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request):
errors = []
actor_input = request.args.get("actor") or '{"id": "root"}'
@ -152,9 +137,6 @@ class AllowDebugView(BaseView):
class MessagesDebugView(BaseView):
name = "messages_debug"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request):
await self.check_permission(request, "view-instance")
return await self.render(["messages_debug.html"], request)

View file

@ -23,9 +23,9 @@ from datasette.utils import (
urlsafe_components,
value_as_boolean,
)
from datasette.utils.asgi import NotFound
from datasette.utils.asgi import NotFound, Response
from datasette.filters import Filters
from .base import DataView, DatasetteError, ureg
from .base import BaseView, DataView, DatasetteError, ureg
from .database import QueryView
LINK_WITH_LABEL = (
@ -903,28 +903,38 @@ class TableView(RowTableShared):
)
async def _sql_params_pks(db, table, pk_values):
pks = await db.primary_keys(table)
use_rowid = not pks
select = "*"
if use_rowid:
select = "rowid, *"
pks = ["rowid"]
wheres = ['"{}"=:p{}'.format(pk, i) for i, pk in enumerate(pks)]
sql = "select {} from {} where {}".format(
select, escape_sqlite(table), " AND ".join(wheres)
)
params = {}
for i, pk_value in enumerate(pk_values):
params["p{}".format(i)] = pk_value
return sql, params, pks
class RowView(RowTableShared):
name = "row"
async def data(self, request, database, hash, table, pk_path, default_labels=False):
pk_values = urlsafe_components(pk_path)
await self.check_permission(request, "view-instance")
await self.check_permission(request, "view-database", database)
await self.check_permission(request, "view-table", (database, table))
db = self.ds.databases[database]
pks = await db.primary_keys(table)
use_rowid = not pks
select = "*"
if use_rowid:
select = "rowid, *"
pks = ["rowid"]
wheres = ['"{}"=:p{}'.format(pk, i) for i, pk in enumerate(pks)]
sql = "select {} from {} where {}".format(
select, escape_sqlite(table), " AND ".join(wheres)
await self.check_permissions(
request,
[
("view-table", (database, table)),
("view-database", database),
"view-instance",
],
)
params = {}
for i, pk_value in enumerate(pk_values):
params["p{}".format(i)] = pk_value
pk_values = urlsafe_components(pk_path)
db = self.ds.databases[database]
sql, params, pks = await _sql_params_pks(db, table, pk_values)
results = await db.execute(sql, params, truncate=True)
columns = [r[0] for r in results.description]
rows = list(results.rows)
@ -1024,3 +1034,50 @@ 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],
status=200,
headers=headers,
content_type="application/binary",
)