AWS S3
presigned URLs
cloud storage
folder access
secure file sharing

s3 presigned url for access to entire folder

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

An S3 presigned URL is great when you want to grant temporary access to one object without sharing AWS credentials. The important limitation is that S3 does not have real folders, only object keys with prefixes. That means you cannot create one presigned URL that magically grants access to every object under a prefix.

Why A Folder Presigned URL Does Not Exist

When you see a folder in the S3 console, you are looking at a UI convenience built on top of key names such as reports/2026/january.csv. The slash characters are part of the key, not a separate directory resource.

A presigned URL signs one HTTP request to one S3 API operation. For example, it can sign GetObject for reports/2026/january.csv. It cannot sign a wildcard request for reports/2026/ and let the client fetch everything under that prefix.

That is why the usual answer is: generate one presigned URL per object, or use a different access model for bulk access.

Generating One URL Per Object Under A Prefix

If the goal is to let a client download several objects from what looks like a folder, list the keys under that prefix and generate a presigned URL for each one.

python
1import boto3
2
3s3 = boto3.client("s3")
4
5
6def presigned_urls_for_prefix(bucket: str, prefix: str, expires_in: int = 900) -> list[dict]:
7    paginator = s3.get_paginator("list_objects_v2")
8    results = []
9
10    for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
11        for obj in page.get("Contents", []):
12            key = obj["Key"]
13            if key.endswith("/"):
14                continue
15
16            url = s3.generate_presigned_url(
17                "get_object",
18                Params={"Bucket": bucket, "Key": key},
19                ExpiresIn=expires_in,
20            )
21            results.append({"key": key, "url": url})
22
23    return results
24
25
26for item in presigned_urls_for_prefix("my-bucket", "reports/2026/"):
27    print(item["key"])
28    print(item["url"])

This approach works well if the number of objects is modest and your application can hand the client a manifest of URLs.

Better Options For Bulk Access

If you need a user to browse or download many objects, per-object presigned URLs can become awkward. In that case, temporary credentials or CloudFront are often a better fit.

One option is to assume a role and return temporary AWS credentials scoped to a specific bucket and prefix. Then the client can call ListObjectsV2 and GetObject directly for that prefix during the credential lifetime.

A policy for that pattern usually grants s3:ListBucket with a prefix condition and s3:GetObject for arn:aws:s3:::my-bucket/reports/2026/*.

Another option is CloudFront signed URLs or signed cookies. That works especially well when the files are being distributed over HTTP and you want CDN caching plus temporary access control. Signed cookies are convenient when many files share the same access rules.

Packaging A Prefix Into One Download

Sometimes the real requirement is not "access a folder" but "download everything in this folder as one file." In that case, a common pattern is to build a ZIP archive on demand, upload that archive to S3, and create a single presigned URL for the ZIP file.

python
1import io
2import zipfile
3import boto3
4
5s3 = boto3.client("s3")
6
7
8def create_zip_and_presign(source_bucket: str, prefix: str, zip_bucket: str, zip_key: str) -> str:
9    buffer = io.BytesIO()
10
11    with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
12        response = s3.list_objects_v2(Bucket=source_bucket, Prefix=prefix)
13        for obj in response.get("Contents", []):
14            key = obj["Key"]
15            if key.endswith("/"):
16                continue
17            body = s3.get_object(Bucket=source_bucket, Key=key)["Body"].read()
18            zf.writestr(key.removeprefix(prefix), body)
19
20    buffer.seek(0)
21    s3.put_object(Bucket=zip_bucket, Key=zip_key, Body=buffer.getvalue())
22
23    return s3.generate_presigned_url(
24        "get_object",
25        Params={"Bucket": zip_bucket, "Key": zip_key},
26        ExpiresIn=900,
27    )

This is a good fit when users want one click and you control the server side workflow.

Common Pitfalls

The most common mistake is assuming that S3 folders are real resources with their own permissions and URLs. They are not. Access control is always happening against bucket operations and object keys.

Another issue is forgetting pagination when listing objects. A single list_objects_v2 call does not guarantee all results if the prefix contains many objects.

Expiration also matters. If you hand out hundreds of presigned URLs with a short lifetime, clients may not finish downloading before the first ones expire.

Finally, do not confuse presigned URLs with authorization for listing. A presigned GetObject URL lets a client download one object, but it does not let the client discover sibling keys under the same prefix.

Summary

  • A presigned URL can grant access to one S3 operation on one object, not an entire prefix.
  • S3 folders are key prefixes, not first-class directory resources.
  • Generate one presigned URL per object when a client needs a small set of files.
  • Use temporary credentials or CloudFront for broader prefix-based access.
  • If users want one download, create an archive and presign that single object.

Course illustration
Course illustration

All Rights Reserved.