From e981ac7d4d1e1603257369eb5edba0fc4fdf5ae9 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 3 Dec 2017 08:33:36 -0800 Subject: [PATCH] --static option for datasette serve You can now tell Datasette to serve static files from a specific location at a specific mountpoint. For example: datasette serve mydb.db --static extra-css:/tmp/static/css Now if you visit this URL: http://localhost:8001/extra-css/blah.css The following file will be served: /tmp/static/css/blah.css Refs #160 --- datasette/app.py | 6 +++++- datasette/cli.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index eb5bd541..f1278043 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -773,7 +773,8 @@ class Datasette: def __init__( self, files, num_threads=3, cache_headers=True, page_size=100, max_returned_rows=1000, sql_time_limit_ms=1000, cors=False, - inspect_data=None, metadata=None, sqlite_extensions=None, template_dir=None): + inspect_data=None, metadata=None, sqlite_extensions=None, + template_dir=None, static_mounts=None): self.files = files self.num_threads = num_threads self.executor = futures.ThreadPoolExecutor( @@ -789,6 +790,7 @@ class Datasette: self.sqlite_functions = [] self.sqlite_extensions = sqlite_extensions or [] self.template_dir = template_dir + self.static_mounts = static_mounts or [] def asset_urls(self, key): for url_or_dict in (self.metadata.get(key) or []): @@ -918,6 +920,8 @@ class Datasette: # TODO: /favicon.ico and /-/static/ deserve far-future cache expires app.add_route(favicon, '/favicon.ico') app.static('/-/static/', str(app_root / 'datasette' / 'static')) + for path, dirname in self.static_mounts: + app.static(path, dirname) app.add_route( DatabaseView.as_view(self), '/' diff --git a/datasette/cli.py b/datasette/cli.py index 664e9255..0072f148 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -1,6 +1,7 @@ import click from click_default_group import DefaultGroup import json +import os import shutil from subprocess import call, check_output import sys @@ -140,6 +141,18 @@ def package(files, tag, metadata, extra_options, branch, **extra_metadata): call(args) +class StaticMount(click.ParamType): + name = 'static mount' + + def convert(self, value, param, ctx): + if ':' not in value: + self.fail('"%s" should be of format mountpoint:directory' % value, param, ctx) + path, dirpath = value.split(':') + if not os.path.exists(dirpath) or not os.path.isdir(dirpath): + self.fail('%s is not a valid directory path' % value, param, ctx) + return path, dirpath + + @cli.command() @click.argument('files', type=click.Path(exists=True), nargs=-1) @click.option('-h', '--host', default='127.0.0.1', help='host for server, defaults to 127.0.0.1') @@ -157,7 +170,8 @@ def package(files, tag, metadata, extra_options, branch, **extra_metadata): @click.option('--inspect-file', help='Path to JSON file created using "datasette build"') @click.option('-m', '--metadata', type=click.File(mode='r'), help='Path to JSON file containing license/source metadata') @click.option('-t', '--template-dir', type=click.Path(exists=True, file_okay=False, dir_okay=True), help='Path to directory containing custom templates') -def serve(files, host, port, debug, reload, cors, page_size, max_returned_rows, sql_time_limit_ms, sqlite_extensions, inspect_file, metadata, template_dir): +@click.option('-s', '--static', type=StaticMount(), help='mountpoint:path-to-directory for serving static files', multiple=True) +def serve(files, host, port, debug, reload, cors, page_size, max_returned_rows, sql_time_limit_ms, sqlite_extensions, inspect_file, metadata, template_dir, static): """Serve up specified SQLite database files with a web UI""" if reload: import hupper @@ -183,6 +197,7 @@ def serve(files, host, port, debug, reload, cors, page_size, max_returned_rows, metadata=metadata_data, sqlite_extensions=sqlite_extensions, template_dir=template_dir, + static_mounts=static, ) # Force initial hashing/table counting ds.inspect()