First porting of Python to Rust - update docs and bug fixes

This commit is contained in:
2026-04-20 21:27:02 +08:00
parent c2ef37b84e
commit 476b9bd2e4
82 changed files with 24682 additions and 4132 deletions

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{% if principal %}<meta name="csrf-token" content="{{ csrf_token() }}" />{% endif %}
{% if principal %}<meta name="csrf-token" content="{{ csrf_token_value }}" />{% endif %}
<title>MyFSIO Console</title>
<link rel="icon" type="image/png" href="{{ url_for(endpoint="static", filename="images/MyFSIO.png") }}" />
<link rel="icon" type="image/x-icon" href="{{ url_for(endpoint="static", filename="images/MyFSIO.ico") }}" />
@@ -145,7 +145,7 @@
</div>
</div>
<form method="post" action="{{ url_for(endpoint="ui.logout") }}" class="w-100">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<button class="sidebar-logout-btn" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
@@ -264,7 +264,7 @@
</div>
</div>
<form method="post" action="{{ url_for(endpoint="ui.logout") }}" class="w-100">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<button class="sidebar-logout-btn" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>

View File

@@ -395,14 +395,8 @@
{% endif %}
{% if can_edit_policy %}
{% set preset_choice = "custom" %}
{% if not bucket_policy %}
{% set preset_choice = "private" %}
{% elif bucket_policy_text and bucket_policy_text | trim == default_policy | trim %}
{% set preset_choice = "public" %}
{% endif %}
<form method="post" action="{{ url_for(endpoint="ui.update_bucket_policy", bucket_name=bucket_name) }}" id="bucketPolicyForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="mode" value="upsert" id="policyMode" />
<div class="mb-4">
@@ -454,7 +448,7 @@
</button>
</label>
<div class="position-relative">
<textarea class="form-control font-monospace" rows="14" name="policy_document" id="policyDocument" data-public-template='{{ default_policy | json_encode | safe }}' spellcheck="false" style="font-size: 0.85rem; line-height: 1.5; tab-size: 2;">{{ bucket_policy_text or default_policy }}</textarea>
<textarea class="form-control font-monospace" rows="14" name="policy_document" id="policyDocument" data-public-template='{{ default_policy | json_encode | safe }}' spellcheck="false" style="font-size: 0.85rem; line-height: 1.5; tab-size: 2;">{% if bucket_policy_text %}{{ bucket_policy_text }}{% else %}{{ default_policy }}{% endif %}</textarea>
<div id="policyValidationStatus" class="position-absolute top-0 end-0 m-2 d-none">
<span class="badge bg-success-subtle text-success" id="policyValidBadge">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
@@ -655,7 +649,7 @@
</button>
{% else %}
<form method="post" action="{{ url_for(endpoint="ui.update_bucket_versioning", bucket_name=bucket_name) }}" id="enableVersioningForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="state" value="enable" />
<button class="btn btn-success" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
@@ -783,7 +777,7 @@
{% if can_manage_encryption %}
<form method="post" action="{{ url_for(endpoint="ui.update_bucket_encryption", bucket_name=bucket_name) }}" id="encryptionForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="action" value="enable" id="encryptionAction" />
<div class="mb-3">
@@ -817,7 +811,7 @@
<option value="">Use default KMS key</option>
{% for key in kms_keys %}
<option value="{{ key.key_id }}" {% if key.key_id == enc_kms_key %}selected{% else %}{% endif %}>
{{ key.description or key.key_id }} ({{ key.key_id | slice(start=0, end=8) }}...)
{% if key.description %}{{ key.description }}{% else %}{{ key.key_id }}{% endif %} ({{ key.key_id | slice(start=0, end=8) }}...)
</option>
{% endfor %}
</select>
@@ -832,14 +826,14 @@
</svg>
Save Encryption Settings
</button>
{% if enc_algorithm %}
<button type="button" class="btn btn-outline-danger" id="disableEncryptionBtn">
<button type="button" class="btn btn-outline-danger" id="disableEncryptionBtn"
data-bs-toggle="modal" data-bs-target="#disableEncryptionModal"
{% if not enc_algorithm %}style="display: none;"{% endif %}>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M11 1a2 2 0 0 0-2 2v4a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h5V3a3 3 0 0 1 6 0v4a.5.5 0 0 1-1 0V3a2 2 0 0 0-2-2z"/>
</svg>
Disable Encryption
</button>
{% endif %}
</div>
</form>
{% else %}
@@ -864,7 +858,6 @@
<div class="card-body">
{% set max_bytes = bucket_quota.max_bytes %}
{% set max_objects = bucket_quota.max_objects %}
{% set has_quota = max_bytes != null or max_objects != null %}
{% set current_objects = bucket_stats.objects | default(value=0) %}
{% set version_count = bucket_stats.version_count | default(value=0) %}
{% set total_objects = bucket_stats.total_objects | default(value=current_objects) %}
@@ -879,10 +872,10 @@
<div class="border rounded p-3 text-center">
<div class="fs-4 fw-bold text-primary">{{ total_objects }}</div>
<div class="small text-muted">Total Objects</div>
{% if max_objects != null %}
{% if has_max_objects %}
<div class="progress mt-2" style="height: 4px;">
{% if max_objects > 0 %}{% set obj_pct = total_objects / max_objects * 100 | int %}{% else %}{% set obj_pct = 0 %}{% endif %}
<div class="progress-bar {% if obj_pct >= 90 %}bg-danger{% elif obj_pct >= 75 %}bg-warning{% else %}bg-success{% endif %}" style="width: {{ [obj_pct, 100] | min }}%"></div>
<div class="progress-bar {% if obj_pct >= 90 %}bg-danger{% elif obj_pct >= 75 %}bg-warning{% else %}bg-success{% endif %}" style="width: {% if obj_pct > 100 %}100{% else %}{{ obj_pct }}{% endif %}%"></div>
</div>
<div class="small text-muted mt-1">{{ obj_pct }}% of {{ max_objects }} limit</div>
{% else %}
@@ -899,10 +892,10 @@
<div class="border rounded p-3 text-center">
<div class="fs-4 fw-bold text-primary">{{ total_bytes | filesizeformat }}</div>
<div class="small text-muted">Total Storage</div>
{% if max_bytes != null %}
{% if has_max_bytes %}
<div class="progress mt-2" style="height: 4px;">
{% if max_bytes > 0 %}{% set bytes_pct = total_bytes / max_bytes * 100 | int %}{% else %}{% set bytes_pct = 0 %}{% endif %}
<div class="progress-bar {% if bytes_pct >= 90 %}bg-danger{% elif bytes_pct >= 75 %}bg-warning{% else %}bg-success{% endif %}" style="width: {{ [bytes_pct, 100] | min }}%"></div>
<div class="progress-bar {% if bytes_pct >= 90 %}bg-danger{% elif bytes_pct >= 75 %}bg-warning{% else %}bg-success{% endif %}" style="width: {% if bytes_pct > 100 %}100{% else %}{{ bytes_pct }}{% endif %}%"></div>
</div>
<div class="small text-muted mt-1">{{ bytes_pct }}% of {{ max_bytes | filesizeformat }} limit</div>
{% else %}
@@ -924,14 +917,14 @@
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</svg>
<div>
<strong>Storage quota enabled</strong>
<strong>Storage quota active</strong>
<p class="mb-0 small">
{% if max_bytes != null and max_objects != null %}
Limited to {{ max_bytes | filesizeformat }} and {{ max_objects }} objects.
{% elif max_bytes != null %}
Limited to {{ max_bytes | filesizeformat }} storage.
{% else %}
Limited to {{ max_objects }} objects.
{% if has_max_bytes and has_max_objects %}
This bucket is limited to {{ max_bytes | filesizeformat }} storage and {{ max_objects }} objects.
{% elif has_max_bytes %}
This bucket is limited to {{ max_bytes | filesizeformat }} storage.
{% elif has_max_objects %}
This bucket is limited to {{ max_objects }} objects.
{% endif %}
</p>
</div>
@@ -951,14 +944,14 @@
{% if can_manage_quota %}
<form method="post" action="{{ url_for(endpoint="ui.update_bucket_quota", bucket_name=bucket_name) }}" id="quotaForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="action" value="set" id="quotaAction" />
<div class="mb-3">
<label for="max_mb" class="form-label fw-medium">Maximum Storage Size</label>
<div class="input-group">
<input type="number" class="form-control" id="max_mb" name="max_mb"
value="{% if max_bytes != null %}{{ (max_bytes / 1048576) | int }}{% else %}{{ "" }}{% endif %}"
value="{% if has_max_bytes %}{{ (max_bytes / 1048576) | int }}{% endif %}"
min="1" step="1" placeholder="Unlimited">
<span class="input-group-text">MB</span>
</div>
@@ -968,7 +961,7 @@
<div class="mb-4">
<label for="max_objects" class="form-label fw-medium">Maximum Object Count</label>
<input type="number" class="form-control" id="max_objects" name="max_objects"
value="{% if max_objects != null %}{{ max_objects }}{% else %}{{ "" }}{% endif %}"
value="{% if has_max_objects %}{{ max_objects }}{% endif %}"
min="0" step="1" placeholder="Unlimited">
<div class="form-text">Maximum number of objects allowed. Leave empty for unlimited.</div>
</div>
@@ -1058,7 +1051,7 @@
{% if can_manage_website %}
<form method="post" action="{{ url_for(endpoint="ui.update_bucket_website", bucket_name=bucket_name) }}" id="websiteForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="action" value="enable" id="websiteAction" />
<div class="mb-3">
@@ -1423,7 +1416,7 @@
Refresh
</a>
<form id="pause-replication-form" method="POST" action="{{ url_for(endpoint="ui.update_bucket_replication", bucket_name=bucket_name) }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="action" value="pause">
<button type="submit" class="btn btn-outline-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
@@ -1499,7 +1492,7 @@
<div class="d-flex gap-2">
<form method="POST" action="{{ url_for(endpoint="ui.update_bucket_replication", bucket_name=bucket_name) }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="action" value="resume">
<button type="submit" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
@@ -1538,7 +1531,7 @@
{% if is_replication_admin and connections %}
<form method="POST" action="{{ url_for(endpoint="ui.update_bucket_replication", bucket_name=bucket_name) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="action" value="create">
<div class="mb-3">
@@ -1713,7 +1706,7 @@
{% endif %}
<div class="row g-4">
<div class="col-lg-8">
<div class="card shadow-sm" id="lifecycle-rules-card" data-lifecycle-url="{{ lifecycle_url }}" data-lifecycle-enabled="{{ lifecycle_enabled|lower }}">
<div class="card shadow-sm" id="lifecycle-rules-card" data-lifecycle-url="{{ lifecycle_url }}" data-lifecycle-enabled="{% if lifecycle_enabled %}true{% else %}false{% endif %}">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-primary me-2" viewBox="0 0 16 16">
@@ -2024,7 +2017,7 @@
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="post" action="{{ url_for(endpoint="ui.update_bucket_policy", bucket_name=bucket_name) }}" class="d-inline" id="deletePolicyForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="mode" value="delete" />
<button type="submit" class="btn btn-danger">Delete Policy</button>
</form>
@@ -2056,7 +2049,7 @@
data-multipart-complete-template="{{ url_for(endpoint="ui.complete_multipart_upload", bucket_name=bucket_name, upload_id="UPLOAD_ID_PLACEHOLDER") }}"
data-multipart-abort-template="{{ url_for(endpoint="ui.abort_multipart_upload", bucket_name=bucket_name, upload_id="UPLOAD_ID_PLACEHOLDER") }}"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<div class="modal-body">
<p class="text-muted small mb-3">Upload files to <code>{{ bucket_name }}</code>. You can select multiple files at once.</p>
<div class="row g-3">
@@ -2212,7 +2205,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{{ url_for(endpoint="ui.delete_bucket", bucket_name=bucket_name) }}" id="deleteBucketForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<div class="modal-body">
<div class="alert alert-danger d-flex align-items-center mb-3" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 me-2" viewBox="0 0 16 16">
@@ -2322,7 +2315,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" id="deleteObjectForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<div class="modal-body">
<p class="mb-3">
Are you sure you want to delete this object?
@@ -2440,7 +2433,7 @@
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" action="{{ url_for(endpoint="ui.update_bucket_versioning", bucket_name=bucket_name) }}" class="d-inline" id="suspendVersioningForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="state" value="suspend" />
<button type="submit" class="btn btn-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
@@ -2454,6 +2447,41 @@
</div>
</div>
<div class="modal fade" id="disableEncryptionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-semibold">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-warning" viewBox="0 0 16 16">
<path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z"/>
<path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z"/>
</svg>
Disable Default Encryption
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning d-flex align-items-start mb-3" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 me-2 mt-1" viewBox="0 0 16 16">
<path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z"/>
</svg>
<div>
<strong>Are you sure?</strong><br>
<span class="small">New objects will not be encrypted automatically.</span>
</div>
</div>
<p class="text-muted small mb-0">Existing encrypted objects remain encrypted and readable.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDisableEncryptionBtn">
Disable Encryption
</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="disableReplicationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
@@ -2483,7 +2511,7 @@
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" action="{{ url_for(endpoint="ui.update_bucket_replication", bucket_name=bucket_name) }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" name="action" value="delete">
<button type="submit" class="btn btn-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">

View File

@@ -113,10 +113,10 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{{ url_for(endpoint="ui.create_bucket") }}" id="createBucketForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<div class="modal-body pt-0">
<label class="form-label fw-medium">Bucket name</label>
<input class="form-control" type="text" name="bucket_name" pattern="[a-z0-9.-]{3,63}" placeholder="my-bucket-name" required autofocus />
<input class="form-control" type="text" name="bucket_name" pattern="[a-z0-9.\-]{3,63}" placeholder="my-bucket-name" required autofocus />
<div class="form-text">Use 3-63 characters: lowercase letters, numbers, dots, or hyphens.</div>
</div>
<div class="modal-footer">

View File

@@ -36,7 +36,7 @@
</div>
<div class="card-body px-4 pb-4">
<form method="POST" action="{{ url_for(endpoint="ui.create_connection") }}" id="createConnectionForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="mb-3">
<label for="name" class="form-label fw-medium">Name</label>
<input type="text" class="form-control" id="name" name="name" required placeholder="Production Backup">
@@ -197,7 +197,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="editConnectionForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="modal-body">
<div class="mb-3">
<label for="edit_name" class="form-label fw-medium">Name</label>
@@ -274,7 +274,7 @@
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" id="deleteConnectionForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<button type="submit" class="btn btn-danger">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
@@ -291,7 +291,7 @@
<script src="{{ url_for(endpoint="static", filename="js/connections-management.js") }}"></script>
<script>
ConnectionsManagement.init({
csrfToken: "{{ csrf_token() }}",
csrfToken: "{{ csrf_token_value }}",
endpoints: {
test: "{{ url_for(endpoint="ui.test_connection") }}",
updateTemplate: "{{ url_for(endpoint="ui.update_connection", connection_id="CONNECTION_ID") }}",

View File

@@ -71,28 +71,35 @@
<span class="docs-section-kicker">01</span>
<h2 class="h4 mb-0">Set up &amp; run locally</h2>
</div>
<p class="text-muted">Prepare a virtual environment, install dependencies, and launch both servers for a complete console + API experience.</p>
<p class="text-muted">Build or run the Rust server and launch the API plus web UI from a single process.</p>
<div class="alert alert-light border small mb-3">
Runtime note: MyFSIO now runs from the Rust server in <code>rust/myfsio-engine</code>. For the verified runtime configuration list, use the repository <code>docs.md</code>.
</div>
<ol class="docs-steps">
<li>Install Python 3.11+ plus system build tools.</li>
<li>Create a virtual environment and install <code>requirements.txt</code>.</li>
<li>Start the services with <code>python run.py</code>.</li>
<li>Install a current Rust toolchain.</li>
<li>Change into <code>rust/myfsio-engine</code>.</li>
<li>Start the server with <code>cargo run -p myfsio-server --</code>.</li>
</ol>
<pre class="mb-3"><code class="language-bash">python -m venv .venv
. .venv/Scripts/activate # PowerShell: .\\.venv\\Scripts\\Activate.ps1
pip install -r requirements.txt
<pre class="mb-3"><code class="language-bash">cd rust/myfsio-engine
# Run both API and UI (Development)
python run.py
# Run API + UI
cargo run -p myfsio-server --
# Run in Production (Granian server)
python run.py --prod
# Show resolved configuration
cargo run -p myfsio-server -- --show-config
# Or run individually
python run.py --mode api
python run.py --mode ui
# Validate configuration
cargo run -p myfsio-server -- --check-config
# API only
UI_ENABLED=false cargo run -p myfsio-server --
# Release build
cargo build --release -p myfsio-server
./target/release/myfsio-server
</code></pre>
<h3 class="h6 mt-4 mb-2">Configuration</h3>
<p class="text-muted small">Configuration defaults live in <code>app/config.py</code>. You can override them using environment variables. This is critical for production deployments behind proxies.</p>
<p class="text-muted small">Configuration is driven by the Rust server environment. See <code>docs.md</code> for the authoritative runtime variables.</p>
<div class="table-responsive">
<table class="table table-sm table-bordered small mb-0">
<thead class="table-light">
@@ -121,15 +128,15 @@ python run.py --mode ui
<tr>
<td><code>SECRET_KEY</code></td>
<td>(Auto-generated)</td>
<td>Flask session key. Auto-generates if not set. <strong>Set explicitly in production.</strong></td>
<td>Session signing and IAM-at-rest encryption key. <strong>Set explicitly in production.</strong></td>
</tr>
<tr>
<td><code>APP_HOST</code></td>
<td><code>0.0.0.0</code></td>
<td><code>HOST</code></td>
<td><code>127.0.0.1</code></td>
<td>Bind interface.</td>
</tr>
<tr>
<td><code>APP_PORT</code></td>
<td><code>PORT</code></td>
<td><code>5000</code></td>
<td>Listen port (UI uses 5100).</td>
</tr>
@@ -454,19 +461,19 @@ python run.py --mode ui
<h3 class="h6 text-uppercase text-muted mt-4">Quick Start (nohup)</h3>
<p class="text-muted small">Simplest way to run in background—survives terminal close:</p>
<pre class="mb-3"><code class="language-bash"># Using Python
nohup python run.py --prod > /dev/null 2>&1 &
<pre class="mb-3"><code class="language-bash"># From the repository
nohup cargo run -p myfsio-server -- > /dev/null 2>&1 &
# Using compiled binary
nohup ./myfsio > /dev/null 2>&1 &
# Using a compiled binary
nohup ./myfsio-server > /dev/null 2>&1 &
# Check if running
ps aux | grep myfsio</code></pre>
ps aux | grep myfsio-server</code></pre>
<h3 class="h6 text-uppercase text-muted mt-4">Screen / Tmux</h3>
<p class="text-muted small">Attach/detach from a persistent session:</p>
<pre class="mb-3"><code class="language-bash"># Start in a detached screen session
screen -dmS myfsio ./myfsio
screen -dmS myfsio ./myfsio-server
# Attach to view logs
screen -r myfsio
@@ -507,7 +514,7 @@ sudo journalctl -u myfsio -f # View logs</code></pre>
<span class="docs-section-kicker">03</span>
<h2 class="h4 mb-0">Authenticate &amp; manage IAM</h2>
</div>
<p class="text-muted">On first startup, MyFSIO generates random admin credentials and prints them to the console. Set <code>ADMIN_ACCESS_KEY</code> and <code>ADMIN_SECRET_KEY</code> env vars for custom credentials. When <code>SECRET_KEY</code> is configured, the IAM config is encrypted at rest. To reset credentials, run <code>python run.py --reset-cred</code>.</p>
<p class="text-muted">On first startup, MyFSIO generates random admin credentials and prints them to the console. Set <code>ADMIN_ACCESS_KEY</code> and <code>ADMIN_SECRET_KEY</code> for custom credentials. When <code>SECRET_KEY</code> is configured, the IAM config is encrypted at rest. To reset credentials, run <code>cargo run -p myfsio-server -- --reset-cred</code> or the installed binary with <code>--reset-cred</code>.</p>
<div class="docs-highlight mb-3">
<ol class="mb-0">
<li>Check the console output for the generated <code>Access Key</code> and <code>Secret Key</code>, then visit <code>/ui/login</code>.</li>
@@ -517,7 +524,7 @@ sudo journalctl -u myfsio -f # View logs</code></pre>
<li>Bucket policies layer on top of IAM. Apply Private/Public presets or paste custom JSON; changes reload instantly.</li>
</ol>
</div>
<p class="mb-0 text-muted">All API calls require <code>X-Access-Key</code> and <code>X-Secret-Key</code> headers. The UI stores them in the Flask session after you log in.</p>
<p class="mb-0 text-muted">All API calls require <code>X-Access-Key</code> and <code>X-Secret-Key</code> headers. The UI stores them in the server-managed session after you log in.</p>
</div>
</article>
<article id="console" class="card shadow-sm docs-section">
@@ -737,7 +744,7 @@ curl -X PUT {{ api_base }}/demo/notes.txt \
</tbody>
</table>
</div>
<p class="small text-muted mt-3 mb-0">All responses include <code>X-Request-Id</code> for tracing. See the <a href="#api-matrix">Full API Reference</a> for the complete endpoint list. Logs land in <code>logs/api.log</code> and <code>logs/ui.log</code>.</p>
<p class="small text-muted mt-3 mb-0">All responses include <code>X-Request-Id</code> for tracing. See the <a href="#api-matrix">Full API Reference</a> for the complete endpoint list. When running the Rust server, logs go to stdout and whatever service manager or container runtime is supervising the process.</p>
</div>
</article>
<article id="examples" class="card shadow-sm docs-section">
@@ -1174,7 +1181,7 @@ export SITE_ID=us-west-1
export SITE_ENDPOINT=https://s3.us-west-1.example.com
export SITE_REGION=us-west-1
export SITE_PRIORITY=100
python run.py</code></pre>
cargo run -p myfsio-server --</code></pre>
<h3 class="h6 text-uppercase text-muted mt-4">Using the Sites UI</h3>
<p class="small text-muted">Navigate to <a href="{{ url_for(endpoint="ui.sites_dashboard") }}">Sites</a> in the sidebar to manage site configuration:</p>
@@ -1442,12 +1449,12 @@ curl -X PUT "{{ api_base }}/bucket/&lt;bucket&gt;?quota" \
<pre class="mb-2"><code class="language-bash"># PowerShell
$env:ENCRYPTION_ENABLED = "true"
$env:KMS_ENABLED = "true" # Optional
python run.py
cargo run -p myfsio-server --
# Bash
export ENCRYPTION_ENABLED=true
export KMS_ENABLED=true
python run.py</code></pre>
cargo run -p myfsio-server --</code></pre>
</li>
<li>
<strong>Configure bucket encryption:</strong> Navigate to your bucket → <strong>Properties</strong> tab → <strong>Default Encryption</strong> card → Click <strong>Enable Encryption</strong>.
@@ -1557,7 +1564,7 @@ curl "{{ api_base }}/my-bucket/secret.txt" \
<h3 class="h6 text-uppercase text-muted mt-4">How It Works</h3>
<p class="small text-muted mb-3">
Lifecycle rules run on a background timer (Python <code>threading.Timer</code>), not a system cronjob. The enforcement cycle triggers every <strong>3600 seconds (1 hour)</strong> by default. Each cycle scans all buckets with lifecycle configurations and applies matching rules.
Lifecycle rules run in a Tokio background task, not a system cronjob. The enforcement cycle triggers every <strong>3600 seconds (1 hour)</strong> by default. Each cycle scans all buckets with lifecycle configurations and applies matching rules.
</p>
<h3 class="h6 text-uppercase text-muted mt-4">Expiration Types</h3>
@@ -1639,7 +1646,7 @@ curl "{{ api_base }}/&lt;bucket&gt;?lifecycle" \
<h3 class="h6 text-uppercase text-muted mt-4">Enabling GC</h3>
<p class="small text-muted">Disabled by default. Enable via environment variable:</p>
<pre class="mb-3"><code class="language-bash">GC_ENABLED=true python run.py</code></pre>
<pre class="mb-3"><code class="language-bash">GC_ENABLED=true cargo run -p myfsio-server --</code></pre>
<h3 class="h6 text-uppercase text-muted mt-4">Configuration</h3>
<div class="table-responsive mb-3">
@@ -1742,7 +1749,7 @@ curl "{{ api_base }}/admin/gc/history?limit=10" \
<h3 class="h6 text-uppercase text-muted mt-4">Enabling Integrity Scanner</h3>
<p class="small text-muted">Disabled by default. Enable via environment variable:</p>
<pre class="mb-3"><code class="language-bash">INTEGRITY_ENABLED=true python run.py</code></pre>
<pre class="mb-3"><code class="language-bash">INTEGRITY_ENABLED=true cargo run -p myfsio-server --</code></pre>
<h3 class="h6 text-uppercase text-muted mt-4">Configuration</h3>
<div class="table-responsive mb-3">
@@ -1848,11 +1855,11 @@ curl "{{ api_base }}/admin/integrity/history?limit=10" \
<p class="small text-muted">Set the environment variable to opt-in:</p>
<pre class="mb-3"><code class="language-bash"># PowerShell
$env:METRICS_HISTORY_ENABLED = "true"
python run.py
cargo run -p myfsio-server --
# Bash
export METRICS_HISTORY_ENABLED=true
python run.py</code></pre>
cargo run -p myfsio-server --</code></pre>
<h3 class="h6 text-uppercase text-muted mt-4">Configuration Options</h3>
<div class="table-responsive mb-3">
@@ -1932,11 +1939,11 @@ curl -X PUT "{{ api_base | replace(from="/api", to="/ui") }}/metrics/settings" \
<p class="small text-muted">Set the environment variable to opt-in:</p>
<pre class="mb-3"><code class="language-bash"># PowerShell
$env:OPERATION_METRICS_ENABLED = "true"
python run.py
cargo run -p myfsio-server --
# Bash
export OPERATION_METRICS_ENABLED=true
python run.py</code></pre>
cargo run -p myfsio-server --</code></pre>
<h3 class="h6 text-uppercase text-muted mt-4">Configuration Options</h3>
<div class="table-responsive mb-3">
@@ -2751,8 +2758,8 @@ cp -r logs/ logs-backup/</code></pre>
<ol class="docs-steps mb-3">
<li><strong>Stop the service:</strong> <code>sudo systemctl stop myfsio</code> (or kill the process)</li>
<li><strong>Pull new version:</strong> <code>git pull origin main</code> or download the new binary</li>
<li><strong>Install dependencies:</strong> <code>pip install -r requirements.txt</code></li>
<li><strong>Validate config:</strong> <code>python run.py --check-config</code></li>
<li><strong>Build or install the new Rust binary:</strong> <code>cargo build --release -p myfsio-server</code></li>
<li><strong>Validate config:</strong> <code>./myfsio-server --check-config</code></li>
<li><strong>Start the service:</strong> <code>sudo systemctl start myfsio</code></li>
<li><strong>Verify:</strong> <code>curl http://localhost:5000/myfsio/health</code></li>
</ol>
@@ -2892,11 +2899,11 @@ POST /kms/generate-random # Generate random bytes</code></pre>
</div>
<div>
<div class="small text-uppercase text-muted">Logs</div>
<span class="text-muted small">logs/api.log · logs/ui.log</span>
<span class="text-muted small">stdout, journald, or container logs</span>
</div>
</div>
<div class="mt-4">
<p class="small text-muted mb-1">Need more automation? Extend <code>app/s3_api.py</code> or wrap <code>run_api.py</code> with gunicorn for production-style deployments.</p>
<p class="small text-muted mb-1">Need more automation? Integrate against the S3, admin, or KMS routes directly, or package the Rust binary behind systemd or containers for production deployments.</p>
</div>
</div>
</aside>

View File

@@ -133,24 +133,8 @@
{% endif %}
<div class="row g-3">
{% for user in users %}
{% set_global is_admin = false %}
{% set_global is_expired = false %}
{% set_global is_expiring_soon = false %}
{% for policy in user.policies %}
{% if "iam:*" in policy.actions or "*" in policy.actions %}
{% set_global is_admin = true %}
{% endif %}
{% endfor %}
{% if user.expires_at %}
{% set exp_str = user.expires_at %}
{% if exp_str <= now_iso %}
{% set_global is_expired = true %}
{% elif exp_str <= soon_iso %}
{% set_global is_expiring_soon = true %}
{% endif %}
{% endif %}
<div class="col-md-6 col-xl-4 iam-user-item" data-display-name="{{ user.display_name|lower }}" data-access-key-filter="{{ user.access_key|lower }}">
<div class="card h-100 iam-user-card{% if is_admin %} iam-admin-card{% else %}{% endif %}">
<div class="col-md-6 col-xl-4 iam-user-item" data-user-id="{{ user.user_id }}" data-access-key="{{ user.access_key }}" data-display-name="{{ user.display_name|lower }}" data-access-key-filter="{{ user.access_key|lower }}">
<div class="card h-100 iam-user-card{% if user.is_admin %} iam-admin-card{% else %}{% endif %}">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between mb-3">
<div class="d-flex align-items-center gap-3 min-width-0 overflow-hidden">
@@ -162,14 +146,14 @@
<div class="min-width-0">
<div class="d-flex align-items-center gap-2 mb-0">
<h6 class="fw-semibold mb-0 text-truncate" title="{{ user.display_name }}">{{ user.display_name }}</h6>
{% if is_admin %}
{% if user.is_admin %}
<span class="iam-role-badge iam-role-admin" data-role-badge>Admin</span>
{% else %}
<span class="iam-role-badge iam-role-user" data-role-badge>User</span>
{% endif %}
{% if is_expired %}
{% if user.is_expired %}
<span class="badge text-bg-danger" style="font-size: .65rem">Expired</span>
{% elif is_expiring_soon %}
{% elif user.is_expiring_soon %}
<span class="badge text-bg-warning" style="font-size: .65rem">Expiring soon</span>
{% endif %}
</div>
@@ -192,7 +176,7 @@
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item" type="button" data-edit-user="{{ user.access_key }}" data-display-name="{{ user.display_name }}">
<button class="dropdown-item" type="button" data-edit-user data-user-id="{{ user.user_id }}" data-access-key="{{ user.access_key }}" data-display-name="{{ user.display_name }}">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5z"/>
</svg>
@@ -200,7 +184,7 @@
</button>
</li>
<li>
<button class="dropdown-item" type="button" data-expiry-user="{{ user.access_key }}" data-expires-at="{{ user.expires_at or "" }}">
<button class="dropdown-item" type="button" data-expiry-user data-user-id="{{ user.user_id }}" data-access-key="{{ user.access_key }}" data-expires-at="{{ user.expires_at | default(value="") }}">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
@@ -209,7 +193,7 @@
</button>
</li>
<li>
<button class="dropdown-item" type="button" data-rotate-user="{{ user.access_key }}">
<button class="dropdown-item" type="button" data-rotate-user data-user-id="{{ user.user_id }}" data-access-key="{{ user.access_key }}">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/>
<path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
@@ -219,7 +203,7 @@
</li>
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item text-danger" type="button" data-delete-user="{{ user.access_key }}">
<button class="dropdown-item text-danger" type="button" data-delete-user data-user-id="{{ user.user_id }}" data-access-key="{{ user.access_key }}">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M5.5 5.5a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0v-6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 .5v6a.5.5 0 0 1-1 0v-6a.5.5 0 0 1 1 0z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
@@ -260,7 +244,7 @@
{% endfor %}
</div>
</div>
<button class="btn btn-outline-primary btn-sm w-100" type="button" data-policy-editor data-access-key="{{ user.access_key }}">
<button class="btn btn-outline-primary btn-sm w-100" type="button" data-policy-editor data-user-id="{{ user.user_id }}" data-access-key="{{ user.access_key }}">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319z"/>
@@ -313,7 +297,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{{ url_for(endpoint="ui.create_iam_user") }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-medium">Display Name</label>
@@ -393,11 +377,11 @@
<form
id="policyEditorForm"
method="post"
data-action-template="{{ url_for(endpoint="ui.update_iam_policies", access_key="ACCESS_KEY_PLACEHOLDER") }}"
data-action-template="{{ url_for(endpoint="ui.update_iam_policies", user_id="USER_ID_PLACEHOLDER") }}"
class="d-flex flex-column gap-3"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" id="policyEditorUser" name="access_key" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<input type="hidden" id="policyEditorUserId" name="user_id" />
<div>
<label class="form-label fw-medium">Inline Policies (JSON array)</label>
@@ -440,7 +424,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" id="editUserForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-medium">Display Name</label>
@@ -490,7 +474,7 @@
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="post" id="deleteUserForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<button class="btn btn-danger" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
@@ -575,7 +559,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" id="expiryForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<div class="modal-body">
<p class="text-muted small mb-3">Set expiration for <code id="expiryUserLabel"></code></p>
<div class="mb-3">
@@ -618,14 +602,14 @@
users: JSON.parse(document.getElementById('iamUsersJson').textContent || '[]'),
currentUserKey: {{ principal.access_key | json_encode | safe }},
iamLocked: {{ iam_locked | json_encode | safe }},
csrfToken: "{{ csrf_token() }}",
csrfToken: "{{ csrf_token_value }}",
endpoints: {
createUser: "{{ url_for(endpoint="ui.create_iam_user") }}",
updateUser: "{{ url_for(endpoint="ui.update_iam_user", access_key="ACCESS_KEY") }}",
deleteUser: "{{ url_for(endpoint="ui.delete_iam_user", access_key="ACCESS_KEY") }}",
updatePolicies: "{{ url_for(endpoint="ui.update_iam_policies", access_key="ACCESS_KEY") }}",
rotateSecret: "{{ url_for(endpoint="ui.rotate_iam_secret", access_key="ACCESS_KEY") }}",
updateExpiry: "{{ url_for(endpoint="ui.update_iam_expiry", access_key="ACCESS_KEY") }}"
updateUser: "{{ url_for(endpoint="ui.update_iam_user", user_id="USER_ID") }}",
deleteUser: "{{ url_for(endpoint="ui.delete_iam_user", user_id="USER_ID") }}",
updatePolicies: "{{ url_for(endpoint="ui.update_iam_policies", user_id="USER_ID") }}",
rotateSecret: "{{ url_for(endpoint="ui.rotate_iam_secret", user_id="USER_ID") }}",
updateExpiry: "{{ url_for(endpoint="ui.update_iam_expiry", user_id="USER_ID") }}"
}
});
</script>

View File

@@ -41,7 +41,7 @@
<h2 class="h4 mb-1 d-none d-lg-block">Sign in</h2>
<p class="text-muted mb-4 d-none d-lg-block">Enter your credentials to continue</p>
<form method="post" action="{{ url_for(endpoint="ui.login") }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}" />
<div class="mb-3">
<label class="form-label fw-medium">Access key</label>
<div class="input-group">

View File

@@ -78,7 +78,7 @@
<div class="card-body px-4 pb-4">
{% if buckets %}
<form method="POST" action="{{ url_for(endpoint="ui.create_peer_replication_rules", site_id=peer.site_id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="mb-4">
<label for="mode" class="form-label fw-medium">Replication Mode</label>

View File

@@ -43,18 +43,18 @@
</div>
<div class="card-body px-4 pb-4">
<form method="POST" action="{{ url_for(endpoint="ui.update_local_site") }}" id="localSiteForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="mb-3">
<label for="site_id" class="form-label fw-medium">Site ID</label>
<input type="text" class="form-control" id="site_id" name="site_id" required
value="{% if local_site %}{{ local_site.site_id }}{% else %}{{ config_site_id or "" }}{% endif %}"
value="{% if local_site %}{{ local_site.site_id }}{% else %}{{ config_site_id | default(value="") }}{% endif %}"
placeholder="us-west-1">
<div class="form-text">Unique identifier for this site</div>
</div>
<div class="mb-3">
<label for="endpoint" class="form-label fw-medium">Endpoint URL</label>
<input type="url" class="form-control" id="endpoint" name="endpoint"
value="{% if local_site %}{{ local_site.endpoint }}{% else %}{{ config_site_endpoint or "" }}{% endif %}"
value="{% if local_site %}{{ local_site.endpoint }}{% else %}{{ config_site_endpoint | default(value="") }}{% endif %}"
placeholder="https://s3.us-west-1.example.com">
<div class="form-text">Public URL for this site</div>
</div>
@@ -109,7 +109,7 @@
<div class="collapse" id="addPeerCollapse">
<div class="card-body px-4 pb-4">
<form method="POST" action="{{ url_for(endpoint="ui.add_peer_site") }}" id="addPeerForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="mb-3">
<label for="peer_site_id" class="form-label fw-medium">Site ID</label>
<input type="text" class="form-control" id="peer_site_id" name="site_id" required placeholder="us-east-1">
@@ -198,7 +198,7 @@
<tr data-site-id="{{ peer.site_id }}">
<td class="text-center">
<span class="peer-health-status" data-site-id="{{ peer.site_id }}"
data-last-checked="{{ peer.last_health_check or "" }}"
data-last-checked="{{ peer.last_health_check | default(value="") }}"
title="{% if peer.is_healthy == true %}Healthy{% elif peer.is_healthy == false %}Unhealthy{% else %}Not checked{% endif %}{% if peer.last_health_check %} (checked {{ peer.last_health_check }}){% endif %}"
style="cursor: help;">
{% if peer.is_healthy == true %}
@@ -277,7 +277,7 @@
data-region="{{ peer.region }}"
data-priority="{{ peer.priority }}"
data-display-name="{{ peer.display_name }}"
data-connection-id="{{ peer.connection_id or "" }}"
data-connection-id="{{ peer.connection_id | default(value="") }}"
title="Edit peer">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5z"/>
@@ -381,7 +381,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="editPeerForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-medium">Site ID</label>
@@ -454,7 +454,7 @@
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" id="deletePeerForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<button type="submit" class="btn btn-danger">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
@@ -775,7 +775,7 @@
var blockingErrors = ['NO_CONNECTION', 'CONNECTION_NOT_FOUND', 'REMOTE_UNREACHABLE', 'ENDPOINT_NOT_ALLOWED'];
var hasBlockingError = data.issues && data.issues.some(function(i) { return blockingErrors.indexOf(i.code) !== -1; });
if (!hasBlockingError) {
wizardLink.href = '/ui/sites/peers/' + encodeURIComponent(siteId) + '/replication-wizard';
wizardLink.href = '/ui/replication/new?site_id=' + encodeURIComponent(siteId);
wizardLink.classList.remove('d-none');
}
}

View File

@@ -37,13 +37,8 @@
<tr><td class="text-muted" style="width:40%">Version</td><td class="fw-medium">{{ app_version }}</td></tr>
<tr><td class="text-muted">Storage Root</td><td><code>{{ storage_root }}</code></td></tr>
<tr><td class="text-muted">Platform</td><td>{{ platform }}</td></tr>
<tr><td class="text-muted">Python</td><td>{{ python_version }}</td></tr>
<tr><td class="text-muted">Rust Extension</td><td>
{% if has_rust %}
<span class="badge bg-success bg-opacity-10 text-success">Loaded</span>
{% else %}
<span class="badge bg-secondary bg-opacity-10 text-secondary">Not loaded</span>
{% endif %}
<tr><td class="text-muted">Engine</td><td>
<span class="badge bg-success bg-opacity-10 text-success">Rust (native)</span>
</td></tr>
</tbody>
</table>
@@ -420,7 +415,8 @@
function _gcRefreshHistory() {
fetch('{{ url_for(endpoint="ui.system_gc_history") }}?limit=10', {
headers: {'X-CSRFToken': csrfToken}
headers: {'X-CSRFToken': csrfToken},
cache: 'no-store'
})
.then(function (r) { return r.json(); })
.then(function (hist) {
@@ -459,7 +455,8 @@
function _integrityRefreshHistory() {
fetch('{{ url_for(endpoint="ui.system_integrity_history") }}?limit=10', {
headers: {'X-CSRFToken': csrfToken}
headers: {'X-CSRFToken': csrfToken},
cache: 'no-store'
})
.then(function (r) { return r.json(); })
.then(function (hist) {
@@ -555,7 +552,8 @@
function _gcPoll() {
fetch('{{ url_for(endpoint="ui.system_gc_status") }}', {
headers: {'X-CSRFToken': csrfToken}
headers: {'X-CSRFToken': csrfToken},
cache: 'no-store'
})
.then(function (r) { return r.json(); })
.then(function (status) {
@@ -567,7 +565,8 @@
_gcSetScanning(false);
_gcRefreshHistory();
fetch('{{ url_for(endpoint="ui.system_gc_history") }}?limit=1', {
headers: {'X-CSRFToken': csrfToken}
headers: {'X-CSRFToken': csrfToken},
cache: 'no-store'
})
.then(function (r) { return r.json(); })
.then(function (hist) {
@@ -608,7 +607,13 @@
body.textContent = data.error;
return;
}
_gcPollTimer = setTimeout(_gcPoll, 2000);
if (data.status === 'started' || data.scanning === true || data.running === true) {
_gcPollTimer = setTimeout(_gcPoll, 2000);
return;
}
_gcSetScanning(false);
_gcShowResult(data, dryRun);
_gcRefreshHistory();
})
.catch(function (err) {
_gcSetScanning(false);
@@ -685,7 +690,8 @@
function _integrityPoll() {
fetch('{{ url_for(endpoint="ui.system_integrity_status") }}', {
headers: {'X-CSRFToken': csrfToken}
headers: {'X-CSRFToken': csrfToken},
cache: 'no-store'
})
.then(function (r) { return r.json(); })
.then(function (status) {
@@ -697,7 +703,8 @@
_integritySetScanning(false);
_integrityRefreshHistory();
fetch('{{ url_for(endpoint="ui.system_integrity_history") }}?limit=1', {
headers: {'X-CSRFToken': csrfToken}
headers: {'X-CSRFToken': csrfToken},
cache: 'no-store'
})
.then(function (r) { return r.json(); })
.then(function (hist) {
@@ -738,7 +745,13 @@
body.textContent = data.error;
return;
}
_integrityPollTimer = setTimeout(_integrityPoll, 2000);
if (data.status === 'started' || data.scanning === true || data.running === true) {
_integrityPollTimer = setTimeout(_integrityPoll, 2000);
return;
}
_integritySetScanning(false);
_integrityShowResult(data, dryRun, autoHeal);
_integrityRefreshHistory();
})
.catch(function (err) {
_integritySetScanning(false);

View File

@@ -35,7 +35,7 @@
</div>
<div class="card-body px-4 pb-4">
<form method="POST" action="{{ url_for(endpoint="ui.create_website_domain") }}" id="createDomainForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="mb-3">
<label for="domain" class="form-label fw-medium">Domain</label>
<input type="text" class="form-control" id="domain" name="domain" required
@@ -204,7 +204,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="editDomainForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-medium">Domain</label>
@@ -241,7 +241,7 @@
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form method="POST" id="deleteDomainForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}"/>
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-semibold">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-danger" viewBox="0 0 16 16">