Compare commits

...

5 commits

Author SHA1 Message Date
Simon Willison
ee718b98b7 Verify SQLite DBs with check_connection 2020-03-26 18:03:41 -07:00
Simon Willison
ec0d68da70 I moved this import 2020-03-26 08:34:49 -07:00
Simon Willison
f2fd7d20bf Run scan_dirs in a thread every 10 seconds 2020-03-26 08:23:46 -07:00
Simon Willison
55e633e09f Run scan_dirs() in a thread 2020-03-26 08:23:40 -07:00
Simon Willison
6ff261c1de --dirs scan mechanism, work in progress - refs #417 2020-03-26 08:23:11 -07:00
5 changed files with 66 additions and 1 deletions

View file

@ -7,6 +7,7 @@ import os
import re import re
import sys import sys
import threading import threading
import time
import traceback import traceback
import urllib.parse import urllib.parse
from concurrent import futures from concurrent import futures
@ -31,6 +32,7 @@ from .utils import (
escape_css_string, escape_css_string,
escape_sqlite, escape_sqlite,
format_bytes, format_bytes,
is_valid_sqlite,
module_from_path, module_from_path,
sqlite3, sqlite3,
to_css_class, to_css_class,
@ -149,6 +151,7 @@ class Datasette:
def __init__( def __init__(
self, self,
files, files,
dirs=None,
immutables=None, immutables=None,
cache_headers=True, cache_headers=True,
cors=False, cors=False,
@ -163,6 +166,7 @@ class Datasette:
version_note=None, version_note=None,
): ):
immutables = immutables or [] immutables = immutables or []
self.dirs = dirs or []
self.files = tuple(files) + tuple(immutables) self.files = tuple(files) + tuple(immutables)
self.immutables = set(immutables) self.immutables = set(immutables)
if not self.files: if not self.files:
@ -182,6 +186,11 @@ class Datasette:
if db.name in self.databases: if db.name in self.databases:
raise Exception("Multiple files with same stem: {}".format(db.name)) raise Exception("Multiple files with same stem: {}".format(db.name))
self.add_database(db.name, db) self.add_database(db.name, db)
if dirs:
self.scan_dirs_thread = threading.Thread(
target=self.scan_dirs, name="scan-dirs", daemon=True
)
self.scan_dirs_thread.start()
self.cache_headers = cache_headers self.cache_headers = cache_headers
self.cors = cors self.cors = cors
self._metadata = metadata or {} self._metadata = metadata or {}
@ -217,6 +226,27 @@ class Datasette:
def remove_database(self, name): def remove_database(self, name):
self.databases.pop(name) self.databases.pop(name)
def scan_dirs(self):
# Recurse through self.dirs looking for new SQLite DBs. Runs in a thread.
while True:
current_filepaths = {
d.path for d in list(self.databases.values()) if d.path is not None
}
for dir in self.dirs:
for filepath in Path(dir).glob("**/*.db"):
if str(filepath) in current_filepaths:
continue
print(filepath)
if is_valid_sqlite(filepath):
self.add_database(
str(filepath)
.replace("../", "")
.replace("/", "_")
.replace(".db", ""),
Database(self, str(filepath), is_mutable=True),
)
time.sleep(10)
def config(self, key): def config(self, key):
return self._config.get(key, None) return self._config.get(key, None)

View file

@ -232,6 +232,13 @@ def package(
@cli.command() @cli.command()
@click.argument("files", type=click.Path(exists=True), nargs=-1) @click.argument("files", type=click.Path(exists=True), nargs=-1)
@click.option(
"-d",
"--dir",
type=click.Path(exists=True),
help="Directories to scan for SQLite files to serve",
multiple=True,
)
@click.option( @click.option(
"-i", "-i",
"--immutable", "--immutable",
@ -310,6 +317,7 @@ def package(
@click.option("--help-config", is_flag=True, help="Show available config options") @click.option("--help-config", is_flag=True, help="Show available config options")
def serve( def serve(
files, files,
dir,
immutable, immutable,
host, host,
port, port,
@ -361,6 +369,7 @@ def serve(
) )
ds = Datasette( ds = Datasette(
files, files,
dir,
immutables=immutable, immutables=immutable,
cache_headers=not debug and not reload, cache_headers=not debug and not reload,
cors=cors, cors=cors,

View file

@ -588,6 +588,28 @@ def to_css_class(s):
return "-".join(bits) return "-".join(bits)
SQLITE_MAGIC = b"SQLite format 3\x00"
def is_valid_sqlite(path):
if not path.is_file():
return False
try:
with open(path, "rb") as fp:
has_magic = fp.read(len(SQLITE_MAGIC)) == SQLITE_MAGIC
except PermissionError:
return False
if not has_magic:
return False
# Check we can run `select * from sqlite_master`
try:
conn = sqlite3.connect(str(path))
check_connection(conn)
except Exception:
return False
return True
def link_or_copy(src, dst): def link_or_copy(src, dst):
# Intended for use in populating a temp directory. We link if possible, # Intended for use in populating a temp directory. We link if possible,
# but fall back to copying if the temp directory is on a different device # but fall back to copying if the temp directory is on a different device

View file

@ -23,7 +23,10 @@ class IndexView(BaseView):
async def get(self, request, as_format): async def get(self, request, as_format):
databases = [] databases = []
for name, db in self.ds.databases.items(): # Using list() here because scan_dirs() running in a thread might
# modify self.ds.databases while we are iterating it, which could
# cause 'RuntimeError: OrderedDict mutated during iteration'
for name, db in list(self.ds.databases.items()):
table_names = await db.table_names() table_names = await db.table_names()
hidden_table_names = set(await db.hidden_table_names()) hidden_table_names = set(await db.hidden_table_names())
views = await db.view_names() views = await db.view_names()

View file

@ -5,6 +5,7 @@ Usage: datasette serve [OPTIONS] [FILES]...
Serve up specified SQLite database files with a web UI Serve up specified SQLite database files with a web UI
Options: Options:
-d, --dir PATH Directories to scan for SQLite files to serve
-i, --immutable PATH Database files to open in immutable mode -i, --immutable PATH Database files to open in immutable mode
-h, --host TEXT Host for server. Defaults to 127.0.0.1 which means only -h, --host TEXT Host for server. Defaults to 127.0.0.1 which means only
connections from the local machine will be allowed. Use connections from the local machine will be allowed. Use