S3-Compatible Storage
AnonDrop speaks the S3 REST API. Point any S3 client at the endpoint below and it works unchanged — the AWS CLI, boto3, rclone, s3cmd, Cyberduck and anything else that signs with AWS Signature Version 4. There is no SDK to install and nothing to rewrite.
Start in 30 seconds
- Create an access key on your account page. Pro or Business plan required.
- Point the AWS CLI at the endpoint.
- Copy a file. That is the whole integration.
aws configure set aws_access_key_id YOUR_KEY_ID --profile anondrop
aws configure set aws_secret_access_key YOUR_SECRET --profile anondrop
aws configure set region us-east-1 --profile anondrop
aws --profile anondrop --endpoint-url https://anondrop.net/s3 s3 mb s3://my-bucket
aws --profile anondrop --endpoint-url https://anondrop.net/s3 s3 cp report.pdf s3://my-bucket/
aws --profile anondrop --endpoint-url https://anondrop.net/s3 s3 ls s3://my-bucket/Using boto3, rclone, s3cmd or Cyberduck instead? Every configuration is below, and nothing about your code changes.
Endpoint
Both domains serve the same account and the same buckets, so a key minted on one works on the other. Use path-style addressing: the bucket is the first path segment after /s3, as in https://anondrop.net/s3/my-bucket/reports/q3.pdf.
Get an access key
Access keys are minted on your account page. The S3 API is part of the Pro and Business plans; a request signed with a key whose plan has lapsed is answered with SubscriptionRequired.
AKAD and is safe to put in configuration files and CI variables.read only, and to a named list of buckets. A scoped key touching anything else is answered with AccessDenied.Pro includes 10 keys, Business includes an unlimited number. Keys are per account, not per bucket, so one account can hand a read-only key to a CDN and a write key to a build server.
What's behind the API
Behind the API you get the same storage the rest of AnonDrop runs on: files are encrypted server-side, split into encrypted chunks that live at their own storage addresses, and erasure coded EC 14+4 across 18 storage nodes, so any 14 fragments rebuild the object. Requests are served under our strict no-log policy — we keep counters, never request logs.
Configure your tools
Every snippet below is complete. Replace the key id and secret with the pair from your account page and you are done.
AWS CLI
Write a dedicated profile so your existing AWS credentials stay untouched:
aws configure set aws_access_key_id AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI --profile anondrop
aws configure set aws_secret_access_key wJalrXUtnFEMI-K7MDENG-bPxRfiCYEXAMPLEKEY --profile anondrop
aws configure set region us-east-1 --profile anondrop
aws configure set s3.addressing_style path --profile anondropThen pass --endpoint-url on each call:
export AWS_PROFILE=anondrop
export AWS_ENDPOINT_URL=https://anondrop.net/s3
aws s3 mb s3://my-bucket
aws s3 cp ./release.tar.gz s3://my-bucket/builds/release.tar.gz
aws s3 sync ./site s3://my-bucket/site --delete
aws s3 ls s3://my-bucket/builds/
aws s3 cp s3://my-bucket/builds/release.tar.gz ./release.tar.gzIf your AWS CLI is older than version 2.13 it will not read AWS_ENDPOINT_URL; pass the flag explicitly instead:
aws --profile anondrop --endpoint-url https://anondrop.net/s3 \
s3 sync ./site s3://my-bucket/site --deleteThe low-level s3api commands work too, which is how you reach headers and multipart controls directly:
aws --profile anondrop --endpoint-url https://anondrop.net/s3 \
s3api list-objects-v2 --bucket my-bucket --prefix builds/ --delimiter /
aws --profile anondrop --endpoint-url https://anondrop.net/s3 \
s3api put-object --bucket my-bucket --key builds/notes.txt \
--body notes.txt --content-type text/plainboto3
Construct the client with endpoint_url and path addressing:
import boto3
from botocore.client import Config
s3 = boto3.client(
"s3",
endpoint_url="https://anondrop.net/s3",
aws_access_key_id="AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI",
aws_secret_access_key="wJalrXUtnFEMI-K7MDENG-bPxRfiCYEXAMPLEKEY",
region_name="us-east-1",
config=Config(
signature_version="s3v4",
s3={"addressing_style": "path"},
),
)
s3.create_bucket(Bucket="my-bucket")
with open("release.tar.gz", "rb") as fh:
s3.put_object(
Bucket="my-bucket",
Key="builds/release.tar.gz",
Body=fh,
ContentType="application/gzip",
Metadata={"build": "1841", "branch": "main"},
)
obj = s3.get_object(Bucket="my-bucket", Key="builds/release.tar.gz")
data = obj["Body"].read()
print(obj["ContentLength"], obj["ETag"], obj["Metadata"])Ranged reads, listing with pagination and presigned links all behave the way you expect:
head = s3.get_object(
Bucket="my-bucket", Key="builds/release.tar.gz", Range="bytes=0-1023")
first_kib = head["Body"].read()
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket="my-bucket", Prefix="builds/"):
for item in page.get("Contents", []):
print(item["Key"], item["Size"])
link = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "my-bucket", "Key": "builds/release.tar.gz"},
ExpiresIn=3600,
)
print(link)rclone
Add this block to your rclone.conf, or answer the equivalent prompts under rclone config:
[anondrop]
type = s3
provider = Other
env_auth = false
access_key_id = AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI
secret_access_key = wJalrXUtnFEMI-K7MDENG-bPxRfiCYEXAMPLEKEY
region = us-east-1
endpoint = https://anondrop.net/s3
location_constraint =
force_path_style = true
acl = private
chunk_size = 64M
upload_concurrency = 4Then use it like any other remote:
rclone mkdir anondrop:my-bucket
rclone copy ./photos anondrop:my-bucket/photos --progress
rclone sync ./site anondrop:my-bucket/site --progress
rclone lsl anondrop:my-bucket/photos
rclone cat anondrop:my-bucket/photos/list.txts3cmd
Put this in ~/.s3cfg. The bucket sits in the path, which is what keeps s3cmd in path-style mode:
[default]
access_key = AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI
secret_key = wJalrXUtnFEMI-K7MDENG-bPxRfiCYEXAMPLEKEY
host_base = anondrop.net/s3
host_bucket = anondrop.net/s3/%(bucket)s
bucket_location = us-east-1
use_https = True
signature_v2 = False
check_ssl_certificate = True
multipart_chunk_size_mb = 64 s3cmd mb s3://my-bucket
s3cmd put release.tar.gz s3://my-bucket/builds/release.tar.gz
s3cmd sync ./site/ s3://my-bucket/site/
s3cmd ls s3://my-bucket/builds/
s3cmd get s3://my-bucket/builds/release.tar.gzEvery client above negotiates SigV4 on its own. Nothing needs an SDK patch, a custom signer or a shim — if a tool can talk to S3, it can talk to AnonDrop.
Supported operations
Paths are shown against the /s3 mount. Anything not listed here is answered with NotImplemented.
Buckets
GET /s3/Every bucket on the account, listed in name order.PUT /s3/{bucket}Names follow S3 rules and are globally unique. Re-creating a bucket you already own is a no-op.DELETE /s3/{bucket}The bucket must be empty, otherwise BucketNotEmpty.HEAD /s3/{bucket}Existence and ownership probe.GET /s3/{bucket}?locationEmpty location constraint, which S3 clients read as us-east-1.GET /s3/{bucket}?list-type=2Supports prefix, delimiter, max-keys, start-after, continuation-token and encoding-type=url. Truncated pages return NextContinuationToken.GET /s3/{bucket}The original marker-based listing is answered too, with marker and NextMarker.Objects
PUT /s3/{bucket}/{key}Plain and aws-chunked bodies, signed or unsigned, with or without trailing checksums. Content-MD5 and x-amz-content-sha256 are verified when sent.GET /s3/{bucket}/{key}Ranged reads via Range: bytes= returning 206 and Content-Range. If-Match and If-None-Match are honoured.HEAD /s3/{bucket}/{key}Size, ETag, content type and user metadata without a body. Never counts against a download limit.DELETE /s3/{bucket}/{key}Answers 204 whether or not the key existed, as S3 does.POST /s3/{bucket}?deleteUp to 1000 keys per request. <Quiet>true</Quiet> suppresses the per-key result rows.PUT /s3/{bucket}/{key}Set x-amz-copy-source. A real byte copy, so deleting either side never touches the other. x-amz-metadata-directive takes COPY (default) or REPLACE.Multipart uploads
POST /s3/{bucket}/{key}?uploadsReturns the UploadId. Content type and x-amz-meta-* are captured here.PUT /s3/{bucket}/{key}?uploadId=&partNumber=Part numbers 1 to 10000, uploaded in any order and in parallel.PUT /s3/{bucket}/{key}?uploadId=&partNumber=Set x-amz-copy-source. Copies the whole source object into the part; a source byte range is not applied.POST /s3/{bucket}/{key}?uploadId=Parts must be listed in ascending order and every part except the last must be at least 5 MB.DELETE /s3/{bucket}/{key}?uploadId=Discards the parts already uploaded.GET /s3/{bucket}/{key}?uploadId=Part number, size, ETag and timestamp for everything received so far.GET /s3/{bucket}?uploadsAccepted for tool compatibility and always answers with an empty list, so keep your own record of upload ids.Presigned links
GET or PUT with X-Amz-SignatureQuery-signed links for reads and writes, valid for up to 7 days.A presigned URL carries the signature in the query string, so anyone holding the link can use it without a key of their own until it expires. The link inherits the permissions and bucket scoping of the key that signed it, and dies the moment that key is revoked.
aws --profile anondrop --endpoint-url https://anondrop.net/s3 \
s3 presign s3://my-bucket/builds/release.tar.gz --expires-in 3600Not offered
Bucket ACLs, bucket policies, lifecycle rules, object tagging, CORS configuration, replication, encryption configuration, object lock, static website hosting, access logging, notification configuration, transfer acceleration, requester pays, analytics, inventory, metrics, public access block and ownership controls are all answered with NotImplemented. Versioning is not offered either; the versioning subresource answers with an empty configuration so clients that probe it keep working.
AnonDrop extension headers
Three request headers add behaviour S3 has no vocabulary for. They are plain extension headers, so any client that lets you set a header can use them, and any client that cannot simply ignores them.
x-anondrop-expiresIntegerSelf-expiring object. A value of 1000000000 or less is read as seconds from now; anything larger is read as an absolute Unix timestamp. After that moment the object stops resolving and drops out of listings, and responses before then carry x-amz-expiration.x-amz-meta-anondrop-expiresIntegerThe same control, spelled as user metadata for clients that only expose --metadata. When both are present this one wins.x-anondrop-max-downloadsIntegerCaps how many times the object may be fetched. Once the count is reached, GetObject answers 403 AccessDenied. HeadObject never counts.x-anondrop-passwordStringAttaches a download password to the object. Only a PBKDF2-HMAC-SHA256 hash is kept, never the password itself. Your own SigV4-signed reads are authenticated by the key and are never prompted for it.AWS CLI
The metadata spelling is the one the AWS CLI can reach without any extra code:
aws --profile anondrop --endpoint-url https://anondrop.net/s3 \
s3api put-object --bucket my-bucket --key drops/build.zip \
--body build.zip --metadata anondrop-expires=86400boto3
In boto3 the headers go on before the request is signed, which means they are covered by the signature:
def anondrop_headers(request, **kwargs):
request.headers.add_header("x-anondrop-expires", "3600")
request.headers.add_header("x-anondrop-max-downloads", "5")
request.headers.add_header("x-anondrop-password", "correct-horse-battery")
s3.meta.events.register("before-sign.s3.PutObject", anondrop_headers)
with open("build.zip", "rb") as fh:
s3.put_object(Bucket="my-bucket", Key="drops/build.zip", Body=fh)
s3.meta.events.unregister("before-sign.s3.PutObject", anondrop_headers)curl
With curl against a presigned PUT the headers are simply extra headers on the request:
curl -X PUT --upload-file build.zip \
-H "x-anondrop-expires: 86400" \
-H "x-anondrop-max-downloads: 5" \
"$PRESIGNED_PUT_URL"Standard metadata
x-amz-meta-*is stored verbatim and returned onGetObjectandHeadObject.Content-Typeis stored and replayed; objects uploaded without one are served asapplication/octet-stream.Content-MD5is checked against the received bytes and a mismatch is rejected withBadDigest, with nothing written.- The ETag of a single-shot upload is the MD5 of the bytes stored. A multipart object uses the S3 composite form instead, the MD5 of the concatenated part digests followed by a dash and the part count.
- Every object reports storage class
STANDARD.
Limits
The S3 API is included with Pro and Business. Storage and transfer are the plan allowances; nothing is metered per request and there are no Class A or Class B operation charges.
Protocol limits that apply on both plans
- 1000 keys per listing page, and 1000 is also the ceiling for
max-keys. - 1000 keys per
DeleteObjectsrequest. - Part numbers 1 to 10000, and every part except the last must be at least 5 MB.
- 7 days is the longest a presigned URL may be valid for.
- 15 minutes of clock skew is tolerated; past that a request is refused with
RequestTimeTooSkewed, so keep NTP running. - Bucket names are 3 to 63 characters, lowercase letters, digits, dots and hyphens, must start and end alphanumerically, and may not look like an IPv4 address.
Going over a limit
Going over the request ceiling is answered with SlowDown and going over a storage or transfer allowance with QuotaExceeded; both clear on their own, the first at the top of the next minute and the second when you free space or the month rolls over. Every S3 client already retries SlowDown with backoff.
Error codes
Errors come back as the standard S3 XML document, so your client parses them without help:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchKey</Code>
<Message>The specified key does not exist.</Message>
<Resource>builds/release.tar.gz</Resource>
<RequestId>4F2A9C1E</RequestId>
</Error>Request ids and presigned errors
Quote the RequestId if you ever need to ask us about a request. It is also returned in the x-amz-request-id response header.
A presigned URL whose X-Amz- query parameters are missing, malformed or out of range is refused with AuthorizationQueryParametersError and status 400 before any of the codes below are reached.
All error codes
Content-Range with the real size.If-Match condition did not hold.Content-MD5 did not match the bytes received; nothing was written.Where next