Working with Files
The archive repository stores source files as blobs, content-addressed by SHA256 checksum: identical files are stored once per dataset, and every file can be verified against its checksum. A file's name, path and type live in its FollowTheMoney entity; anything else a caller wants to keep goes next to the blob as raw data.
Archiving Files
store takes a local path or a URL and returns the checksum:
from ftm_lakehouse import ensure_dataset, get_archive
ensure_dataset("my_dataset")
archive = get_archive("my_dataset")
checksum = archive.store("/path/to/document.pdf")
checksum = archive.store("https://example.com/report.pdf")
Reading Files
# File-like handle
with archive.open(checksum) as fh:
content = fh.read()
# Chunks, for large files
for chunk in archive.stream(checksum):
process_chunk(chunk)
# A local path, for tools that need one
with archive.local_path(checksum) as path:
subprocess.run(["pdftotext", str(path), "output.txt"])
local_path copies a remote blob to a temporary file, removed when the context exits.
Warning
On a local archive local_path returns the blob's actual path. Do not modify or delete it.
Listing Files
archive.exists(checksum) # is the blob there?
for checksum in archive.iterate_checksums(): # every blob in the archive
print(checksum)
Extra Metadata
Callers keep their own metadata about a file – where it came from, who crawled it – next to its blob: put_data writes raw bytes at a relative path in the blob's directory, get_data reads them back:
import json
archive.put_data(checksum, "my_crawler.json", json.dumps({"url": url}).encode())
meta = json.loads(archive.get_data(checksum, "my_crawler.json"))
blob and top-level *.txt paths are reserved – the content and the extracted text (put_txt / get_txt).
File to Entity
make_file_entities builds the Document entity of an archived file – its schema picked by mime type – after the Folder entities of its path. The info is the file's anystore Stats: store.info(key) for a file in a crawled store (the key is its path), UriResource(uri).info() for a single uri (the key is just its file name).
from anystore.store.resource import UriResource
from ftm_lakehouse import get_entities
from ftm_lakehouse.helpers.file import make_file_entities
uri = "/path/to/document.pdf"
checksum = archive.store(uri)
entities = get_entities("my_dataset")
info = UriResource(uri).info()
entities.add_many(make_file_entities(info, checksum, "my_dataset"), origin="archive")
entities.flush()
CLI Usage
# List the checksums of all files
ftm-lakehouse -d my_dataset archive ls
# Retrieve file content
ftm-lakehouse -d my_dataset archive get <checksum> -o output.pdf
# Download every file to a directory, under the paths from exports/documents.csv
ftm-lakehouse -d my_dataset archive download -o ./files
Storage Layout
The checksum is split into directory segments:
my_dataset/
archive/
00/
de/
ad/
00deadbeef123456789012345678901234567890/
blob # file blob (raw bytes)
{origin}.txt # (optional) extracted text
The full dataset layout is in Conventions.
Public Blob URLs
A public URL prefix (e.g. a CDN, or the nginx in front of the lakehouse API) is joined with the blob's archive path – https://cdn.example.com/my_dataset/archive/ab/cd/ef/<checksum>/blob – and written as public_url into the documents export (documents.csv) and the index.json resource links.
Per dataset in config.yml:
Or globally via environment variable (supports a ${dataset} placeholder):
Without a prefix, the lakehouse API serves blobs under /{dataset}/archive/...; access control is up to the reverse proxy (see API deployment).