alocals3: a Rust S3-like server and Rust-backed Python client
Project description
alocals3
alocals3 is a local S3-like object store focused on fast local development and internal workloads.
The current main branch is Rust-first:
- Server: pure Rust binary, no Python runtime required.
- Metadata backend: SQLite or PostgreSQL.
- Object payloads: local filesystem with SHA-256 based sharding.
- Python client: wheel package backed by Rust networking through
reqwest. - Python target: Python 3.12+ with PyO3
abi3-py312limited API.
The project implements an S3-compatible subset, not the full AWS S3 API surface.
Quick Start
Build and run the Rust server:
PYO3_NO_PYTHON=1 cargo build --release --no-default-features --features server,server-binary --bin alocals3-server
target/release/alocals3-server \
--host 127.0.0.1 \
--port 8000 \
--database-url "sqlite:///./alocals3.db" \
--storage-root ./data
Background garbage collection is enabled by default. It periodically removes object files that are no longer referenced by the database and removes database records whose files are missing. Candidates must be older than the GC grace period before they are deleted.
target/release/alocals3-server \
--gc-interval-secs 300 \
--gc-grace-secs 300 \
--gc-start-delay-secs 30
Set --disable-gc to disable background GC. --gc-interval-secs 0 and ALOCALS3_GC_INTERVAL_SECS=0 are equivalent.
Run the reproducible GC correctness suite:
./examples/gc_correctness.sh
It covers orphan files, missing-file records, stale temp files, live/shared object retention, grace-period behavior, GC disable modes, and orphan-path reuse races. Latest local result:
GC correctness and race scenarios passed
orphan_files_deleted=1 missing_records_deleted=1 stale_tmp_files_deleted=1
remaining_objects: grace=1 disable_flag=1 interval_zero=1 race=30
Use PostgreSQL instead of SQLite:
target/release/alocals3-server \
--host 127.0.0.1 \
--port 8000 \
--database-url "postgresql://user:password@127.0.0.1:5432/alocals3" \
--storage-root ./data
Install the Python client from source:
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip maturin
python -m pip install -e .
Build Artifacts
Release helper scripts live in scripts:
# Linux static server plus Python 3.12+ ABI3 wheel
scripts/build-linux-release.sh
# macOS arm64, macOS 11 deployment baseline
scripts/build-macos-release.sh
# Windows 10+ PowerShell
.\scripts\build-windows-release.ps1
Linux server builds default to x86_64-unknown-linux-musl; Linux wheels default to manylinux_2_28. macOS builds default to aarch64-apple-darwin with MACOSX_DEPLOYMENT_TARGET=11.0.
The wheel is configured as Python 3.12+ ABI3 via PyO3 abi3-py312. It is not a cp312-cp312 wheel unless the ABI3 feature is removed.
Platform wheels include the Rust server executable and install an alocals3-server command. Standalone server artifacts are also named alocals3-server on Unix-like platforms and alocals3-server.exe on Windows.
Configuration
Server CLI flags:
--host: bind host, default127.0.0.1--port: bind port, default8000--database-url: SQLite or PostgreSQL URL--storage-root: object payload root directory--log-level: log level ortracing_subscriberfilter directive, defaultinfo--max-upload-size: maximum upload body size in bytes, default0disables the axum body limit--version: print server version and author information
Environment variables:
ALOCALS3_DATABASE_URL: defaultsqlite:///./alocals3.dbALOCALS3_STORAGE_ROOT: default./dataALOCALS3_LOG_LEVEL: defaultinfoALOCALS3_MAX_UPLOAD_SIZE: default0
Database URL examples:
- SQLite:
sqlite:///./alocals3.db - PostgreSQL:
postgresql://user:password@127.0.0.1:5432/alocals3
Use an absolute SQLite path in scripts and services to avoid accidentally writing to different database files from different working directories. PostgreSQL is recommended for sustained concurrent workloads.
Logging is written to stderr. Common levels are debug, info, warn, and error; full EnvFilter directives such as warn,alocals3_server=debug are also accepted.
Uploads are not capped by the server body extractor by default. The current implementation buffers each uploaded object in memory before writing it to disk, so practical limits are still determined by process memory and available storage. Set --max-upload-size to restore an explicit byte limit.
Storage Layout
- Bucket and object metadata is stored in SQLite or PostgreSQL.
- Object bytes are stored on local disk.
- Blob paths are content-addressed and sharded:
sha256(<object bytes>) = <digest>{storage_root}/objects/{digest[:2]}/{digest[2:4]}/{digest}
Object keys, bucket names, prefixes, delimiters, and continuation tokens are UTF-8 text. Client path parameters are UTF-8 percent-encoded automatically; pass raw strings such as logs/data.txt or logs/数据.txt, not pre-encoded URL fragments.
HTTP API
GET /healthz: health checkGET /s3: list bucketsPUT /s3/{bucket}: create bucketDELETE /s3/{bucket}: delete empty bucketGET /s3/{bucket}/objects: list objectsGET /s3/{bucket}?list-type=2: S3-style ListObjectsV2PUT /s3/{bucket}/{key}: upload objectGET /s3/{bucket}/{key}: download objectHEAD /s3/{bucket}/{key}: object metadataDELETE /s3/{bucket}/{key}: delete object
Supported object features:
ETagis the MD5 hex digest of the object body.Rangerequests return206or416.If-None-MatchandIf-Matchare supported forPUT.Content-MD5is validated onPUT.If-None-Matchis supported forGETandHEAD.
PUT /s3/{bucket}/{key} returns:
201: new object created200: existing object overwritten400: invalidContent-MD5412: conditional request failed
Client Usage
The Python runtime dependency list is intentionally empty. HTTP networking is implemented in Rust, not httpx.
import asyncio
from pathlib import Path
from alocals3.client import ALocalS3Client, ALocalS3ClientAsync
with ALocalS3Client("http://127.0.0.1:8000", disable_proxy=True) as client:
client.create_bucket("demo")
info = client.put_object("demo", "logs/数据.txt", Path("data.txt"))
print(info["etag"])
data, headers = client.get_object_range("demo", "logs/数据.txt", "bytes=0-99")
print(len(data), headers.get("content-range"))
with client.open("s3://demo/logs/数据.txt", "r") as f:
print(f.read())
with client.open("s3://demo/logs/from-open.txt", "wb") as f:
f.write(b"hello from file-like API\n")
client.get_object_to_file("demo", "logs/数据.txt", Path("copy.txt"))
async def main() -> None:
async with ALocalS3ClientAsync("http://127.0.0.1:8000", disable_proxy=True) as client:
print(await client.list_buckets())
async with client.open("s3://demo/logs/from-async-open.txt", "wb") as f:
await f.write(b"hello from async file-like API\n")
async with client.open("s3://demo/logs/from-async-open.txt", "rb") as f:
print(await f.read())
asyncio.run(main())
CLI:
python -m alocals3.client --endpoint http://127.0.0.1:8000 CREATE_BUCKET demo
python -m alocals3.client --endpoint http://127.0.0.1:8000 PUT demo file.bin ./file.bin
python -m alocals3.client --endpoint http://127.0.0.1:8000 GET demo file.bin ./copy.bin
python -m alocals3.client --endpoint http://127.0.0.1:8000 LIST_OBJECTS_V2 demo --prefix logs/ --delimiter /
Set disable_proxy=True or pass --disable-proxy to ignore proxy environment variables such as HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY.
client.open() is file-like but not an in-memory object wrapper:
- Read modes (
"rb"/"r") create a Rust-backed streaming HTTP reader.open()sends the request and reads response headers, but object bytes are pulled from the network when the returned file object'sread()path runs. - Write modes (
"wb"/"w") spool writes to a Rust-owned temporary file. The HTTPPUTis sent when the file is closed or thewithblock exits successfully. If thewithblock exits with an exception, the upload is discarded. cache_path=is best-effort and is populated as bytes pass through the file-like object; cache write failures do not fail the network operation.- For asyncio code,
ALocalS3ClientAsync.open()returns an async file-like object forasync with. It provides awaitableread(),readline(),readinto(),write(),flush(),close(), anddiscard()methods backed by the same Rust implementation.
Benchmark the Python file-like API with a large object:
./examples/benchmark_file_like.sh
Validate stream multiplexing and Range-based resume reads:
./examples/benchmark_stream_multiplex.sh
The current file-like API supports resume reads through range_header="bytes=N-". Upload resume is not implemented yet; write modes spool to a Rust-owned temporary file and use one HTTP PUT on close.
Curl Examples
curl -i -X PUT http://127.0.0.1:8000/s3/demo
curl -i -X PUT --data-binary @file.bin http://127.0.0.1:8000/s3/demo/file.bin
curl -i http://127.0.0.1:8000/s3/demo/file.bin
curl -i -H "Range: bytes=0-99" http://127.0.0.1:8000/s3/demo/file.bin
curl -sS "http://127.0.0.1:8000/s3/demo?list-type=2&prefix=logs/&delimiter=/&max-keys=100"
Conditional PUT:
curl -i -X PUT -H "If-None-Match: *" --data-binary @file.bin \
http://127.0.0.1:8000/s3/demo/file.bin
curl -i -X PUT -H 'If-Match: "d41d8cd98f00b204e9800998ecf8427e"' --data-binary @file.bin \
http://127.0.0.1:8000/s3/demo/file.bin
MD5_B64=$(openssl md5 -binary file.bin | openssl base64)
curl -i -X PUT -H "Content-MD5: ${MD5_B64}" --data-binary @file.bin \
http://127.0.0.1:8000/s3/demo/file.bin
Consistency Notes
- Object bytes are written through a temporary file and atomic rename.
- Metadata updates are committed through the selected database backend.
- This is not a single distributed transaction across database and filesystem.
- Under process or machine failure, orphan blob files may exist. The Python package no longer ships an alternate storage backend or Python server path; operational cleanup should be handled outside the request path.
Updates
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file alocals3-0.7.1-cp312-abi3-win_amd64.whl.
File metadata
- Download URL: alocals3-0.7.1-cp312-abi3-win_amd64.whl
- Upload date:
- Size: 6.7 MB
- Tags: CPython 3.12+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f20cb7b59635f8fc28fff1b24e7273f24fceaa22431f46970d6dd4ccd0e828b3
|
|
| MD5 |
a11ae4e141efdab3928354e07e7d62d5
|
|
| BLAKE2b-256 |
de096b53d60366c59e293717d55d6704d5b55c07aff37844140a9c6c16370a8b
|
File details
Details for the file alocals3-0.7.1-cp312-abi3-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: alocals3-0.7.1-cp312-abi3-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 8.0 MB
- Tags: CPython 3.12+, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8ac9b1f400c4c46aea531467ee3c89fdaf63168e5070806f2f41ae1dc58faa5
|
|
| MD5 |
357399d6dddfe4156a52f0a5df4cfd92
|
|
| BLAKE2b-256 |
c488b1797936fffa8db82bcab8cf2425bd22638e11353af1d86e9cb2860fa099
|
File details
Details for the file alocals3-0.7.1-cp312-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: alocals3-0.7.1-cp312-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 7.1 MB
- Tags: CPython 3.12+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d72e1b6dedced3bc7ef5e32d0dba6555763f4fee0af3dc1ed6eac143a5b238a
|
|
| MD5 |
06476218f38105e4bf2427237b2e1464
|
|
| BLAKE2b-256 |
009348bf654652d153f0618d489286a6cb8c2f775e72f414e629f9e2b608e9a6
|