Configuration

Before you can begin using Flask S3Viewer, you should set up authentication credentials. Credentials for your AWS account can be found in the IAM Console. You can create or use an existing user. Go to manage access keys and generate a new set of keys.

Configure credentials

Install AWS CLI.

pip install awscli

If you have the AWS CLI installed, then you can use it to configure your credentials file:

aws configure

Alternatively, you can create the credential file yourself. By default, its location is at ~/.aws/credentials. and Flask S3Viewer is going to use the credential file.

Minimum settings

This is a minimal setup for using flask s3viewer. First install the dependency packages.

pip install flask flask_s3_viewer

Import flask and flask_s3_viewer

1from flask import Flask
2
3from flask_s3_viewer import FlaskS3Viewer
4from flask_s3_viewer.aws.ref import Region

Initiailize Flask application and FlaskS3Viewer.

 1# Init Flask
 2app = Flask(__name__)
 3
 4# Init Flask S3Viewer (auto-registers in v1.0+)
 5FlaskS3Viewer(
 6    # Flask App
 7    app,
 8    # Namespace must be unique
 9    namespace='flask-s3-viewer',
10    # Hostname, e.g. Cloudfront endpoint
11    object_hostname='http://flask-s3-viewer.com',
12    # Put your AWS's profile name and Bucket name
13    config={
14        'profile_name': 'PROFILE_NAME',
15        'bucket_name': 'S3_BUCKET_NAME'
16    }
17)
18
19if __name__ == '__main__':
20    app.run(debug=True, port=3000)

Note

In v1.0+, the constructor auto-registers the blueprint via Flask extension pattern. The legacy s3viewer.register() call has been removed. For deferred registration, pass app=None and call viewer.init_app(app) later.

The values in the code above are mandatory. If the setting is finished, run your Flask application and visit http://localhost/{namespace}/files, e.g. http://localhost:3000/flask-s3-viewer/files.

You can get example codes over here.


User Guides

It is about various advanced settings.

Multiple bucket settings

You can also initiailize multiple bucket.

 1...
 2
 3s3viewer = FlaskS3Viewer(
 4    ...
 5)
 6
 7# Init another bucket
 8s3viewer.add_new_one(
 9    namespace='another_namespace',
10    object_hostname='http://anotherbucket.com',
11    config={
12        'profile_name': 'PROFILE_NAME',
13        'bucket_name': 'S3_BUCKET_NAME'
14    }
15)

Mount a specific path in a bucket for browsing

You can mount a specific path in the bucket to the browser. ( Be careful not to end the path with / )

 1...
 2
 3s3viewer = FlaskS3Viewer(
 4    ...
 5)
 6
 7# Init another bucket
 8s3viewer.add_new_one(
 9    namespace='another_namespace',
10    object_hostname='http://anotherbucket.com',
11    config={
12        'profile_name': 'PROFILE_NAME',
13        'bucket_name': 'S3_BUCKET_NAME',
14        'base_path': 'path/to/your/folder',
15    }
16)

Limit the file extensions

You can limit the file extensions that are uploaded, if you want.

1s3viewer = FlaskS3Viewer(
2    ...
3
4    # allowed extension
5    allowed_extensions={'jpg', 'jpeg'},
6    config={
7        ...
8    }
9)

Design template

Since v1.0, Flask S3 Viewer ships a single unified design built with Tailwind CSS + HTMX, with light/dark mode and inline heroicons.

Note

The template_namespace='base'|'mdl' argument is deprecated. Passing it emits a DeprecationWarning and is otherwise ignored. The previous base/ and mdl/ template directories have been removed.

Template overrides

The recommended path is the CLI scaffold plus the template_folder= constructor argument:

# Copy just the Jinja templates (most common)
flask_s3_viewer -p ./fsv-templates

# Fork the whole UI bundle (templates + static/css/app.css + htmx + core.js)
flask_s3_viewer -p ./fsv-templates --with-static

Edit any of layout.html / files.html / _file_list.html / _pagination.html / _upload_form.html / error.html in the scaffolded directory, then point the viewer at it:

1FlaskS3Viewer(
2    app,
3    namespace='my-bucket',
4    template_folder='./fsv-templates',
5    config={...},
6)

Behind the scenes the extension prepends a FileSystemLoader to the Flask app’s Jinja loader via ChoiceLoader, so any not-overridden template still resolves against the bundle and other blueprints’ templates are unaffected.

layout.html also exposes a {% block extra_head %} hook for the common case where you only need to inject CSS / JS / <meta> tags:

{% extends "flask_s3_viewer/layout.html" %}
{% block extra_head %}
  <link rel="stylesheet" href="{{ url_for('static', filename='custom.css') }}">
{% endblock %}

Controll large files

If you want to controll large files (maybe larger than 5MB ~ maximum 5TB), I recommand to set like below. Flask S3Viewer is going to use S3’s presigned URL. It’s nice to controll large files.

1s3viewer = FlaskS3Viewer(
2    ...
3    # Change upload type to 'presign'
4    upload_type='presign',
5    config={
6        ...
7    }
8)

but you must do S3’s CORS settings before like set above.

STS AssumeRole / MFA

For cross-account or multi-tenant deployments, the viewer can run sts:AssumeRole on top of the base credentials (profile / env / IRSA / IMDS — whatever boto3 resolves by default). Pass the role config inside the config dict:

 1FlaskS3Viewer(
 2    app,
 3    namespace='cross-account',
 4    config={
 5        'bucket_name': 'target-bucket',
 6        'region_name': 'us-east-1',
 7        # Base credentials still come from boto3's default chain.
 8        'role_arn': 'arn:aws:iam::123456789012:role/AppRole',
 9        'external_id': 'shared-secret',     # optional
10        'role_session_name': 'my-app',      # default: flask-s3-viewer
11        'duration_seconds': 3600,           # 15 min ~ 12 h
12    },
13)

For MFA-protected roles, supply either token_code directly or a token_code_callback callable that returns the current code on demand (useful for interactive prompts that mustn’t expire):

 1FlaskS3Viewer(
 2    app,
 3    namespace='mfa-account',
 4    config={
 5        'bucket_name': 'secure-bucket',
 6        'region_name': 'us-east-1',
 7        'role_arn': 'arn:aws:iam::123456789012:role/AdminRole',
 8        'mfa_serial': 'arn:aws:iam::123456789012:mfa/alice',
 9        'token_code_callback': lambda: input('MFA code: ').strip(),
10    },
11)

If role_arn is omitted, no STS call happens — the direct credential path is used. That covers static keys, named profiles (including profiles that themselves declare role_arn``+``source_profile in ~/.aws/config — boto3 handles AssumeRole automatically), env vars, EC2 IMDS, ECS task role, AWS SSO, and EKS IRSA.

Automatic credential refresh

Since v1.2, AssumeRole temporary credentials are wrapped in botocore’s RefreshableCredentials whenever role_arn is set and either MFA is not used or a token_code_callback is supplied. boto3 re-invokes sts:AssumeRole automatically when the cached credentials approach expiry. botocore’s defaults are a 15-minute advisory window (best-effort background refresh) and a 10-minute mandatory window (synchronous refresh — the next S3 call blocks until new credentials are in place). A long-running viewer no longer hits ExpiredToken once DurationSeconds elapses.

The legacy single-shot path is preserved for the mfa_serial + literal token_code combination — once the OTP is consumed there is no way to obtain the next one without prompting the user, so the session keeps the v1.1.x behaviour and surfaces ExpiredToken after the session expires. Use this only for short-lived workflows.

For headless deployments that still need MFA, supply a token_code_callback that fetches the current OTP from your secret store. The callback is invoked on every refresh, so each AssumeRole call carries a fresh code:

 1def fetch_otp() -> str:
 2    # Pull the current TOTP from your secret manager / hardware HSM /
 3    # short-lived broker — anything except interactive stdin in a
 4    # daemon context.
 5    return secrets_client.get_current_totp('flask-s3-viewer')
 6
 7FlaskS3Viewer(
 8    app,
 9    namespace='mfa-account',
10    config={
11        'bucket_name': 'secure-bucket',
12        'role_arn': 'arn:aws:iam::123456789012:role/AdminRole',
13        'mfa_serial': 'arn:aws:iam::123456789012:mfa/headless',
14        'token_code_callback': fetch_otp,
15    },
16)

Thread-safety is delegated to botocore’s standard RefreshableCredentials locking. Each Flask process/worker gets its own FlaskS3Viewer instance and its own refresh schedule — the library does not share credentials across workers.

Presigned URL TTL with temporary credentials

A presigned URL signed with STS-issued credentials is bounded by ``min(Expires, STS session expiry)``. boto3 writes the requested X-Amz-Expires into the URL query verbatim, but S3 rejects the request at access time once the underlying STS session expires. Concrete consequence: a viewer started with duration_seconds=3600 (1 hour) that issues a presigned URL with Expires=86400 (24 hours) still produces a URL the client can only use for ~1 hour. Automatic refresh (above) does not extend URLs that were already signed — refreshed credentials only affect new signatures.

If you need long-lived presigned URLs:

  • Sign with a long-lived IAM user (skip role_arn for that namespace), or

  • Set duration_seconds ≥ the longest Expires value your application requests (within the STS maximum of 12 h for chained AssumeRole, or 43200 s when explicitly allowed by the role).

Choosing duration_seconds

STS AssumeRole quotas are per-account (default ~30 TPS) and botocore’s 15-minute advisory / 10-minute mandatory refresh windows pull each renewal that much earlier than Expiration. The practical guidance:

  • ≥ 3600 (1 h) recommended. With a 1-hour session, the advisory window kicks in ~45 min after issuance and renews once per hour.

  • 900 s (the STS minimum) is risky. Because the advisory window is also 900 s, every S3 call after issuance falls inside the advisory band and triggers a background refresh — effectively rate-limited by botocore’s per-credential lock, but still hard on STS quota under high worker counts.

  • Typical sweet spot: 3600 – 43200 (1 h – 12 h).

  • For viewers behind gunicorn --workers N, each worker maintains its own refresh schedule. Multiply your expected refresh frequency by N when sizing against the STS account quota; a larger duration_seconds (e.g. 12 h) keeps the per-second refresh rate well under quota even at high worker counts.

Using with EKS IRSA

In EKS, IAM Roles for Service Accounts (IRSA) issues web-identity tokens via the projected ServiceAccount token. When you combine IRSA with an explicit role_arn in the viewer config, two credential layers stack:

  1. Base — boto3 calls sts:AssumeRoleWithWebIdentity against the IRSA-projected token and gets a refresh-capable Credentials object out of the box (boto3 manages this layer; flask-s3-viewer is not involved).

  2. Working — flask-s3-viewer then calls sts:AssumeRole against that base and produces the working session. The v1.2 refresh wiring renews this second layer transparently.

Both layers refresh independently, so a viewer running on EKS for days keeps working without manual intervention.

STS endpoint selection

For non-us-east-1 deployments, prefer the regional STS endpoint to reduce latency and improve availability. boto3 1.30+ uses regional STS by default; older configurations may need AWS_STS_REGIONAL_ENDPOINTS=regional in the environment. Concretely, calls to sts.amazonaws.com (global, us-east-1) from Seoul measure 150 – 200 ms RTT, while sts.ap-northeast-2.amazonaws.com is in the 5 – 10 ms range.

Mapping the web user to a CloudTrail identity

The audit log records the web user (Flask session / header) under the user field; CloudTrail records the ``RoleSessionName`` passed to sts:AssumeRole. Bridge the two by embedding a stable per-user identifier into role_session_name:

 1FlaskS3Viewer(
 2    app,
 3    namespace='cross-account',
 4    config={
 5        'bucket_name': 'target-bucket',
 6        'role_arn': 'arn:aws:iam::123456789012:role/AppRole',
 7        # CloudTrail surfaces this string in every API call. Keep it
 8        # opaque but trace-able.
 9        'role_session_name': f'fs3v-{user_id_hash}',
10    },
11)

Warning

RoleSessionName is recorded in cleartext in CloudTrail and surfaces in many AWS Console screens. Do not embed PII such as full email addresses, Korean RRN (주민등록번호), phone numbers, or any other regulated identifier. Use a short hash (e.g. hashlib.sha256(email).hexdigest()[:16]) or an opaque numeric user id instead. The audit user field can keep the email for the operator’s own log pipeline.

Note

Per-namespace role assumption is already supported — each add_new_one(config={...}) call builds an independent AWSSession, so namespace A can assume role X while namespace B assumes role Y. The v1.2 refresh wiring applies independently per namespace.

Range requests / partial downloads

Since v1.0, GET /<namespace>/files/<key> honors the HTTP Range header (RFC 7233). A well-formed range returns 206 Partial Content with Content-Range and Content-Length populated; every download response advertises Accept-Ranges: bytes. Malformed or unsatisfiable ranges return 416 Range Not Satisfiable.

This is what lets curl -C -, video/audio <video>/<audio> players, and chunked mobile downloaders resume or seek without re-fetching the whole object.

1# Resume a partially downloaded file
2curl -C - -O http://localhost:3000/flask-s3-viewer/files/big.bin
3
4# Or request a specific byte range explicitly
5curl -H "Range: bytes=0-1023" -O \
6    http://localhost:3000/flask-s3-viewer/files/big.bin
 1[
 2    {
 3        "AllowedHeaders": [
 4            "*"
 5        ],
 6        "AllowedMethods": [
 7            "POST",
 8            "PUT",
 9            "GET",
10            "HEAD",
11            "DELETE"
12        ],
13        "AllowedOrigins": [
14            "http://localhost:3000"
15        ],
16    }
17]

Authentication & permissions

Two opt-in layers, both off by default — the package keeps the legacy anonymous experience verbatim until you wire something up.

Hook framework — bring your own login. Two callables:

 1from flask_s3_viewer.auth import ACTION_DELETE
 2
 3def auth_callback(request):
 4    # Return the user's email/id, or None for anonymous.
 5    return request.headers.get("X-Forwarded-Email")
 6
 7def permission_callback(email, action, namespace, key):
 8    # ``action`` is one of ACTION_LIST / ACTION_DOWNLOAD /
 9    # ACTION_UPLOAD / ACTION_DELETE / ACTION_PRESIGN.
10    if action == ACTION_DELETE:
11        return email.endswith("@admin.example.com")
12    return True
13
14FlaskS3Viewer(
15    app, namespace="bucket",
16    auth_callback=auth_callback,
17    permission_callback=permission_callback,
18    config={...},
19)

Returning None from auth_callback triggers a 401 (or a Google login redirect — see below). permission_callback returning False triggers a 403.

Built-in Google OAuth — requires the optional [auth] extra:

pip install "flask_s3_viewer[auth]"
 1app.secret_key = "..."  # required — signs the session cookie
 2
 3FlaskS3Viewer(
 4    app, namespace="bucket",
 5    google_client_id="...apps.googleusercontent.com",
 6    google_client_secret="...",
 7    allowed_emails=["alice@example.com"],
 8    allowed_domains=["example.com"],
 9    config={...},
10)

Routes /auth/login, /auth/callback, /auth/logout are registered as app-level routes — they live OUTSIDE the FlaskS3Viewer namespace prefix. Configure the redirect URI as https://<host>/auth/callback in Google Cloud Console. One URI per app even when you mount multiple namespaces via add_new_one(); renaming a namespace does not require updating Google Console. Anonymous browser GETs are redirected through Google sign-in; non-browser clients still get a bare 401.

allowed_emails / allowed_domains are a shortcut for the common allow-list case — internally they wire up the email_allowlist(emails=..., domains=...) builder as the permission_callback. Pass your own permission_callback for fine-grained per-action policy.

Audit logging

Every S3 CRUD action that flows through the blueprint emits a single structured record on the flask_s3_viewer.audit logger. The logger is always present — host applications opt in by attaching a handler and/or adjusting its level via the standard logging API. No constructor flag toggles audit on or off; the v1.0 public API is unchanged.

Logger name: flask_s3_viewer.audit Default level: unset (records propagate to root and are filtered by the host’s effective level). Successful actions emit at INFO; permission denials emit at WARNING; unexpected exceptions emit at ERROR.

Record fields (attached as LogRecord attributes via extra=):

  • action — one of list, download, upload, delete, presign

  • namespace — viewer namespace the request landed on

  • bucket — S3 bucket name resolved from the viewer config for the current request. Populated automatically by the blueprint’s url_value_preprocessor; the empty string when emit() is called outside a Flask request context unless the caller pre-sets g.FSV_AUDIT_BUCKET themselves.

  • key — canonical S3 key / prefix (post-base_path)

  • user — authenticated email or the literal string anonymous

  • resultok / denied / error

  • status_code — HTTP status emitted to the client. Reflects the planned response code at emit time. If a render-phase exception fires after a successful upload row was emitted (e.g. the listing template rendering hits an I/O error), the audit row may carry 201 while the client receives a 500 from Flask’s error handler.

  • client_iprequest.remote_addr

  • user_agent — capped at 256 bytes; sanitised

  • request_id — 8 hex chars; rows emitted within the same Flask request share one id. Records emitted from host code outside a request context get a fresh id each call.

  • error — present only when an exception was attached

The human-readable message is a single space-separated key=value line:

action=download namespace=fsv-test bucket=fsv-bucket
key=docs/report.pdf user=alice@example.com result=ok status=200
req=a1b2c3d4

Newlines, carriage returns, and other ASCII control bytes inside attacker-controllable fields (key, email, User-Agent, exception message) are escaped as \\xNN before the record is built, so a crafted request cannot smuggle a fake row into the log stream.

Multi-file requests emit one row per file. An upload of three files produces three action=upload records — each key is the per-object S3 target the request would write (<prefix><safe_name>). presign follows the same rule: one row per file_list entry, each with its own status_code (200 ok / 409 conflict / 403 disallowed / 500 error). The “no files iterated” cases (mkdir-only upload, empty file_list presign, invalid prefix, denied auth) still emit a single aggregate row keyed by the prefix. Note: when a multi-file upload aborts with 403 (disallowed extension) or returns 409 (duplicate / overwrite conflict) the HTTP response is a single status but the audit stream carries one row per violating file — response status != row count by design. Plan for the row volume: uploading 100 files in one request produces 100 audit records.

If the same target key appears N times in a single upload request, the audit stream records one 409 row per unique conflicting key — not N. The duplicate-detection step dedupes targets before emit (see view.py duplicate_targets set), so two uploads of a.txt in one request produce one row, not two.

Plain file handler example:

1import logging
2
3handler = logging.FileHandler('/var/log/flask_s3_viewer/audit.log')
4handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
5logging.getLogger('flask_s3_viewer.audit').addHandler(handler)
6logging.getLogger('flask_s3_viewer.audit').setLevel(logging.INFO)

Structured JSON handler example (uses python-json-logger):

 1import logging
 2from pythonjsonlogger import jsonlogger
 3
 4audit_handler = logging.FileHandler('/var/log/flask_s3_viewer/audit.jsonl')
 5audit_handler.setFormatter(jsonlogger.JsonFormatter(
 6    '%(asctime)s %(levelname)s %(action)s %(namespace)s '
 7    '%(bucket)s %(key)s %(user)s %(result)s %(status_code)s '
 8    '%(client_ip)s %(user_agent)s'
 9))
10audit = logging.getLogger('flask_s3_viewer.audit')
11audit.addHandler(audit_handler)
12audit.setLevel(logging.INFO)
13# The library leaves propagate=True by default — disable it here
14# if you do NOT also want these records flowing to root handlers.
15audit.propagate = False

PII / secret redaction. Emails and S3 keys are written verbatim, which may be sensitive depending on deployment policy. Attach a logging.Filter if you need to mask, hash, or drop fields before they hit disk — for example to GDPR-truncate the user field, or to strip ARNs/bucket names from error messages produced by boto3 ClientError stringification.

1class RedactFilter(logging.Filter):
2    def filter(self, record):
3        if getattr(record, 'user', None):
4            user = record.user
5            record.user = user.split('@', 1)[0][:2] + '***@' + user.split('@', 1)[-1]
6        return True
7
8audit.addFilter(RedactFilter())

Multi-file uploads / presigns emit one record per file (see Multi-file requests emit one row per file above), so the filter above runs N times per request — keep it side-effect free and allocation-light.

For key and error — which can carry full S3 paths and boto3 ClientError text containing bucket names / ARNs / request IDs — attach a second filter that keeps just enough breadcrumb to trace the incident without leaking the rest of the path or the AWS account topology:

 1import re
 2
 3_ARN_RE = re.compile(r'arn:aws:[^\s"\']+')
 4_BUCKET_RE = re.compile(r'(?i)\bbucket[\s:=]+[^\s"\',]+')
 5
 6class KeyErrorRedactFilter(logging.Filter):
 7    """Redact prefix tails on ``key`` and AWS identifiers on ``error``."""
 8    def filter(self, record):
 9        key = getattr(record, 'key', None)
10        if key:
11            # Keep only the first path segment ("docs/...") so the
12            # audit trail still distinguishes top-level folders but
13            # the leaf filename / nested path is masked.
14            head, sep, _tail = key.partition('/')
15            record.key = f'{head}{sep}***' if sep else '***'
16        err = getattr(record, 'error', None)
17        if err:
18            err = _ARN_RE.sub('arn:aws:***', err)
19            err = _BUCKET_RE.sub('bucket=***', err)
20            record.error = err
21        # ``bucket`` is exposed as a record extra (and a
22        # ``bucket=<name>`` token in the message body) on every emit.
23        # Mask it the same way if your retention policy treats live
24        # S3 bucket names as sensitive metadata.
25        if getattr(record, 'bucket', None):
26            record.bucket = '***'
27        return True
28
29audit.addFilter(KeyErrorRedactFilter())

The two filters compose — install both if you want the user, key, and error fields all masked. Tune the regex set to your environment; ClientError text varies by API call. The bucket record extra is populated for every blueprint emit, so masking it here keeps the audit pipeline from accidentally fanning bucket names out to secondary handlers (Splunk indexers, SIEM forwards, etc.) when the deployer wants to keep that detail bounded to a single trusted sink.

Capturing the real client IP behind a reverse proxy. client_ip is sourced from request.remote_addr, which Werkzeug fills from the last hop on the TCP connection. When the app sits behind a load balancer, ALB / ELB / nginx / Cloudflare, that hop is the proxy and every audit row records the proxy IP — not the originating client. For the audit trail to actually identify clients you must install Werkzeug’s ProxyFix middleware (or an equivalent) so X-Forwarded-For / Forwarded headers are honored:

1from werkzeug.middleware.proxy_fix import ProxyFix
2
3# ``x_for=1`` trusts exactly one X-Forwarded-For hop (your edge LB).
4# If the request transits N reverse proxies you control end-to-end,
5# raise this to N. Trusting too many hops lets clients spoof the IP.
6app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)

Without ProxyFix (or a host-supplied equivalent), every client_ip field in the audit stream is the LB’s address and the audit trail loses most of its forensic value. The value of x_for is deployment-specific — adjust it for nested LB / CDN topologies, and only trust hops you operate.

Calling :func:`emit` from host code. The emit function is part of the public surface: host integrations may import it and emit extra audit lines for non-CRUD operations they layer on top of the viewer (e.g. a custom admin route that bulk-tags objects). Usage:

 1from flask_s3_viewer.audit import emit as audit_emit
 2from flask_s3_viewer.auth import ACTION_LIST  # or ACTION_DOWNLOAD/...
 3
 4# Call from inside a Flask request context so client_ip / user_agent
 5# are populated automatically; outside a request both fields emit as
 6# empty strings.
 7audit_emit(
 8    action=ACTION_LIST,
 9    namespace='my-bucket',
10    key='reports/2026/',   # caller pre-normalises (post-base_path)
11    user=current_user_email,
12    result='ok',
13    status_code=200,
14)

Prefer the flask_s3_viewer.auth.ACTION_* constants over raw strings; action and result are sanitised but the level mapping (ok``→INFO, ``denied``→WARNING, ``error``→ERROR) depends on ``result. The signature is part of v1.x stability — additions will be backwards-compatible.

Use Caching

S3 is charged per call. Therefore, Flask S3Viewer supports caching (currently only supports file caching, in-memory database will be supported later).

 1s3viewer = FlaskS3Viewer(
 2    ...
 3    config={
 4        ...
 5        # Flask S3Viewer will cache the list of s3 objects, if you set True
 6        'use_cache': True,
 7        # Where cached files will be written
 8        'cache_dir': '/tmp/flask_s3_viewer',
 9        # Time To Live
10        'ttl': 86400
11    }
12)

Full example

 1...
 2
 3 FlaskS3Viewer(
 4     # Flask app
 5     app,
 6     # Namespace must be unique
 7     namespace='flask-s3-viewer',
 8     # File's hostname
 9     object_hostname='http://flask-s3-viewer.com',
10     # Allowed extension
11     allowed_extensions={},
12     # Bucket configs and else
13     config={
14         # Required
15         'profile_name': 'PROFILE_NAME',
16         # Required
17         'bucket_name': 'S3_BUCKET_NAME',
18         'region_name': Region.SEOUL.value,
19         # Not necessary, if you configure aws settings, e.g. ~/.aws
20         'access_key': 'AWS_IAM_ACCESS_KEY',
21         'secret_key': 'AWS_IAM_SECRET_KEY',
22         # For S3 compatible
23         'endpoint_url': None,
24         # Flask S3Viewer will cache the list of s3 objects, if you set True
25         'use_cache': True,
26         # Where cached files will be written
27         'cache_dir': '/tmp/flask_s3_viewer',
28         # Time To Live
29         'ttl': 86400,
30     }
31 )

Things to know

Searching

Case-insensitive substring match applied in Python. Notable properties:

  • Unicode-safe. Korean, Japanese, accented Latin, and emoji all match — the EN-only JMESPath limitation in earlier versions is gone.

  • NFC-normalised. macOS Finder uploads land in S3 as NFD-decomposed Hangul while the browser IME emits NFC; both sides are normalised before comparison so the bytes line up.

  • Recursive. When the search box is non-empty the listing switches to a flat (delimiter='') S3 call scoped to the current prefix, so a matching filename three folders deep is visible without manual drill-in.

  • Folder rows are synthesized. Any sub-prefix whose own segment contains the query appears as a clickable folder, alongside the matching files.

  • ``base_path`` is excluded from the comparison. The namespace mount point itself doesn’t bleed into every match.

  • Cache-bypassed. Search results are not persisted to the disk cache, so a code-level change in matching semantics never gets served stale from an old deployment.

Matching is bounded by max_items × max_pages per request (default 1000 keys). For very large prefixes a query may need to be combined with a narrower prefix to surface deeper matches.