diff --git a/docs/cli.rst b/docs/cli.rst index 013d323..f3892ca 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -65,10 +65,10 @@ Binary strings are not valid JSON, so BLOB columns containing binary data will b $ sqlite-utils dogs.db "select name, content from images" | python -mjson.tool [ { - "name": "smile.gif", + "name": "transparent.gif", "content": { "$base64": true, - "encoded": "eJzt0c1x..." + "encoded": "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" } } ] @@ -295,6 +295,18 @@ If you feed it a JSON list it will insert multiple records. For example, if ``do } ] +You can insert binary data into a BLOB column by first encoding it using base64 and then structuring it like this:: + + [ + { + "name": "transparent.gif", + "content": { + "$base64": true, + "encoded": "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" + } + } + ] + You can import all three records into an automatically created ``dogs`` table and set the ``id`` column as the primary key like so:: $ sqlite-utils insert dogs.db dogs dogs.json --pk=id diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 155ea54..219c0bf 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -8,7 +8,7 @@ import json import sys import csv as csv_std import tabulate -from .utils import sqlite3 +from .utils import sqlite3, decode_base64_values VALID_COLUMN_TYPES = ("INTEGER", "TEXT", "FLOAT", "BLOB") @@ -451,6 +451,8 @@ def insert_upsert_implementation( extra_kwargs["defaults"] = dict(default) if upsert: extra_kwargs["upsert"] = upsert + # Apply {"$base64": true, ...} decoding, if needed + docs = (decode_base64_values(doc) for doc in docs) db[table].insert_all( docs, pk=pk, batch_size=batch_size, alter=alter, **extra_kwargs ) diff --git a/sqlite_utils/utils.py b/sqlite_utils/utils.py index ecb3568..e2d3451 100644 --- a/sqlite_utils/utils.py +++ b/sqlite_utils/utils.py @@ -1,3 +1,5 @@ +import base64 + try: import pysqlite3 as sqlite3 import pysqlite3.dbapi2 @@ -58,3 +60,17 @@ def column_affinity(column_type): return float # Default is 'NUMERIC', which we currently also treat as float return float + + +def decode_base64_values(doc): + # Looks for '{"$base64": true..., "encoded": ...}' values and decodes them + to_fix = [ + k + for k in doc + if isinstance(doc[k], dict) + and doc[k].get("$base64") is True + and "encoded" in doc[k] + ] + if not to_fix: + return doc + return dict(doc, **{k: base64.b64decode(doc[k]["encoded"]) for k in to_fix}) diff --git a/tests/test_cli.py b/tests/test_cli.py index 4012c97..2156def 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -554,6 +554,18 @@ def test_insert_not_null_default(db_path, tmpdir): ) == db["dogs"].schema +def test_insert_binary_base64(db_path): + result = CliRunner().invoke( + cli.cli, + ["insert", db_path, "files", "-"], + input=r'{"content": {"$base64": true, "encoded": "aGVsbG8="}}', + ) + assert 0 == result.exit_code, result.output + db = Database(db_path) + actual = db.execute_returning_dicts("select content from files") + assert actual == [{"content": b"hello"}] + + def test_insert_newline_delimited(db_path): result = CliRunner().invoke( cli.cli, diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..6489f2b --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,22 @@ +from sqlite_utils import utils +import pytest + + +@pytest.mark.parametrize( + "input,expected,should_be_is", + [ + ({}, None, True), + ({"foo": "bar"}, None, True), + ( + {"content": {"$base64": True, "encoded": "aGVsbG8="}}, + {"content": b"hello"}, + False, + ), + ], +) +def test_decode_base64_values(input, expected, should_be_is): + actual = utils.decode_base64_values(input) + if should_be_is: + assert actual is input + else: + assert actual == expected