.. meta:: :description: Flask S3 Viewer is a powerful extension that makes it easy to browse S3 in any Flask application :keywords: Flask, s3, aws, upload, uploader, browsing, python3, python, mount, objectstorage, s3viewer 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. .. code-block:: bash pip install awscli If you have the AWS CLI installed, then you can use it to configure your credentials file: .. code-block:: bash 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. .. code-block:: bash pip install flask flask_s3_viewer Import flask and flask_s3_viewer .. code-block:: python :linenos: :emphasize-lines: 3-4 from flask import Flask from flask_s3_viewer import FlaskS3Viewer from flask_s3_viewer.aws.ref import Region Initiailize Flask application and FlaskS3Viewer. .. code-block:: python :linenos: :emphasize-lines: 4-20 # Init Flask app = Flask(__name__) # Init Flask S3Viewer (auto-registers in v1.0+) FlaskS3Viewer( # Flask App app, # Namespace must be unique namespace='flask-s3-viewer', # Hostname, e.g. Cloudfront endpoint object_hostname='http://flask-s3-viewer.com', # Put your AWS's profile name and Bucket name config={ 'profile_name': 'PROFILE_NAME', 'bucket_name': 'S3_BUCKET_NAME' } ) if __name__ == '__main__': 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_. .. _here: https://github.com/hidekuma/flask-s3-viewer/tree/master/example ---- User Guides ================= It is about various advanced settings. Multiple bucket settings ------------------------ You can also initiailize multiple bucket. .. code-block:: python :linenos: :emphasize-lines: 7-8 ... s3viewer = FlaskS3Viewer( ... ) # Init another bucket s3viewer.add_new_one( namespace='another_namespace', object_hostname='http://anotherbucket.com', config={ 'profile_name': 'PROFILE_NAME', 'bucket_name': 'S3_BUCKET_NAME' } ) 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 / ) .. code-block:: python :linenos: :emphasize-lines: 14 ... s3viewer = FlaskS3Viewer( ... ) # Init another bucket s3viewer.add_new_one( namespace='another_namespace', object_hostname='http://anotherbucket.com', config={ 'profile_name': 'PROFILE_NAME', 'bucket_name': 'S3_BUCKET_NAME', 'base_path': 'path/to/your/folder', } ) Limit the file extensions -------------------------- You can limit the file extensions that are uploaded, if you want. .. code-block:: python :linenos: :emphasize-lines: 4-5 s3viewer = FlaskS3Viewer( ... # allowed extension allowed_extensions={'jpg', 'jpeg'}, config={ ... } ) 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 :class:`DeprecationWarning` and is otherwise ignored. The previous ``base/`` and ``mdl/`` template directories have been removed. Branding (title + logo) ----------------------- Four constructor options let you brand the UI without overriding templates: .. code-block:: python :linenos: FlaskS3Viewer( app, namespace='my-bucket', title='ACME File Vault', logo_path='/opt/acme/assets/logo.svg', # local file, auto-inlined # logo_url='https://cdn.acme.io/logo.svg', # alternatively, any URL logo_link_url='https://intranet.acme.io/dashboard', # optional config={...}, ) ``logo_path`` reads the file once at construction time and embeds it as a ``data:`` URI so you don't need to expose it via a separate static route. ``logo_url`` accepts any browser-resolvable URL (CDN, ``url_for("static", filename=...)`` result, or absolute URL). ``logo_path`` takes precedence over ``logo_url`` when both are provided. ``logo_link_url`` (since v1.3) overrides the click target of the header logo + title anchor. When set, the anchor renders as a plain ```` pointing at the configured URL — useful when the deployer wants the brand mark to return users to an external dashboard / home page rather than the namespace's own root listing. The HTMX swap attributes (``hx-get`` / ``hx-target`` / ``hx-push-url``) are intentionally omitted in this mode so the browser performs a standard full-page navigation; the listing's HTMX flows (file rows, pagination, search, bucket switcher) are unaffected. Omit ``logo_link_url`` (or leave it as ``None``) to keep the v1.2 behaviour where the anchor performs an in-place HTMX listing reset back to the namespace root. For multi-namespace deployments using :meth:`add_new_one`, omit the kwarg on the child to inherit the parent's value; pass ``None`` explicitly to drop the parent's override on this child namespace; pass a different string to override the parent value only on the child. Template overrides ------------------ The recommended path is the CLI scaffold plus the ``template_folder=`` constructor argument: .. code-block:: bash # 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: .. code-block:: python :linenos: FlaskS3Viewer( app, namespace='my-bucket', template_folder='./fsv-templates', config={...}, ) 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 / ```` tags: .. code-block:: jinja {% extends "flask_s3_viewer/layout.html" %} {% block extra_head %} {% 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. .. code-block:: python :linenos: :emphasize-lines: 3-4 s3viewer = FlaskS3Viewer( ... # Change upload type to 'presign' upload_type='presign', config={ ... } ) 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: .. code-block:: python :linenos: FlaskS3Viewer( app, namespace='cross-account', config={ 'bucket_name': 'target-bucket', 'region_name': 'us-east-1', # Base credentials still come from boto3's default chain. 'role_arn': 'arn:aws:iam::123456789012:role/AppRole', 'external_id': 'shared-secret', # optional 'role_session_name': 'my-app', # default: flask-s3-viewer 'duration_seconds': 3600, # 15 min ~ 12 h }, ) 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): .. code-block:: python :linenos: FlaskS3Viewer( app, namespace='mfa-account', config={ 'bucket_name': 'secure-bucket', 'region_name': 'us-east-1', 'role_arn': 'arn:aws:iam::123456789012:role/AdminRole', 'mfa_serial': 'arn:aws:iam::123456789012:mfa/alice', 'token_code_callback': lambda: input('MFA code: ').strip(), }, ) 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: .. code-block:: python :linenos: def fetch_otp() -> str: # Pull the current TOTP from your secret manager / hardware HSM / # short-lived broker — anything except interactive stdin in a # daemon context. return secrets_client.get_current_totp('flask-s3-viewer') FlaskS3Viewer( app, namespace='mfa-account', config={ 'bucket_name': 'secure-bucket', 'role_arn': 'arn:aws:iam::123456789012:role/AdminRole', 'mfa_serial': 'arn:aws:iam::123456789012:mfa/headless', 'token_code_callback': fetch_otp, }, ) 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``: .. code-block:: python :linenos: FlaskS3Viewer( app, namespace='cross-account', config={ 'bucket_name': 'target-bucket', 'role_arn': 'arn:aws:iam::123456789012:role/AppRole', # CloudTrail surfaces this string in every API call. Keep it # opaque but trace-able. 'role_session_name': f'fs3v-{user_id_hash}', }, ) .. 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 //files/`` 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 ``