Release v0.1.1 #1
@@ -66,6 +66,25 @@ class ReplicationManager:
|
|||||||
del self._rules[bucket_name]
|
del self._rules[bucket_name]
|
||||||
self.save_rules()
|
self.save_rules()
|
||||||
|
|
||||||
|
def create_remote_bucket(self, connection_id: str, bucket_name: str) -> None:
|
||||||
|
"""Create a bucket on the remote connection."""
|
||||||
|
connection = self.connections.get(connection_id)
|
||||||
|
if not connection:
|
||||||
|
raise ValueError(f"Connection {connection_id} not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
s3 = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=connection.endpoint_url,
|
||||||
|
aws_access_key_id=connection.access_key,
|
||||||
|
aws_secret_access_key=connection.secret_key,
|
||||||
|
region_name=connection.region,
|
||||||
|
)
|
||||||
|
s3.create_bucket(Bucket=bucket_name)
|
||||||
|
except ClientError as e:
|
||||||
|
logger.error(f"Failed to create remote bucket {bucket_name}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
def trigger_replication(self, bucket_name: str, object_key: str) -> None:
|
def trigger_replication(self, bucket_name: str, object_key: str) -> None:
|
||||||
rule = self.get_rule(bucket_name)
|
rule = self.get_rule(bucket_name)
|
||||||
if not rule or not rule.enabled:
|
if not rule or not rule.enabled:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import re
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from urllib.parse import quote, urlencode
|
from urllib.parse import quote, urlencode, urlparse
|
||||||
from xml.etree.ElementTree import Element, SubElement, tostring, fromstring, ParseError
|
from xml.etree.ElementTree import Element, SubElement, tostring, fromstring, ParseError
|
||||||
|
|
||||||
from flask import Blueprint, Response, current_app, jsonify, request
|
from flask import Blueprint, Response, current_app, jsonify, request
|
||||||
@@ -468,7 +468,17 @@ def _generate_presigned_url(
|
|||||||
"X-Amz-Content-Sha256": "UNSIGNED-PAYLOAD",
|
"X-Amz-Content-Sha256": "UNSIGNED-PAYLOAD",
|
||||||
}
|
}
|
||||||
canonical_query = _encode_query_params(query_params)
|
canonical_query = _encode_query_params(query_params)
|
||||||
host = request.host
|
|
||||||
|
# Determine host and scheme from config or request
|
||||||
|
api_base = current_app.config.get("API_BASE_URL")
|
||||||
|
if api_base:
|
||||||
|
parsed = urlparse(api_base)
|
||||||
|
host = parsed.netloc
|
||||||
|
scheme = parsed.scheme
|
||||||
|
else:
|
||||||
|
host = request.host
|
||||||
|
scheme = request.scheme or "http"
|
||||||
|
|
||||||
canonical_headers = f"host:{host}\n"
|
canonical_headers = f"host:{host}\n"
|
||||||
canonical_request = "\n".join(
|
canonical_request = "\n".join(
|
||||||
[
|
[
|
||||||
@@ -492,7 +502,6 @@ def _generate_presigned_url(
|
|||||||
signing_key = _derive_signing_key(secret_key, date_stamp, region, service)
|
signing_key = _derive_signing_key(secret_key, date_stamp, region, service)
|
||||||
signature = hmac.new(signing_key, string_to_sign.encode(), hashlib.sha256).hexdigest()
|
signature = hmac.new(signing_key, string_to_sign.encode(), hashlib.sha256).hexdigest()
|
||||||
query_with_sig = canonical_query + f"&X-Amz-Signature={signature}"
|
query_with_sig = canonical_query + f"&X-Amz-Signature={signature}"
|
||||||
scheme = request.scheme or "http"
|
|
||||||
return f"{scheme}://{host}{_canonical_uri(bucket_name, object_key)}?{query_with_sig}"
|
return f"{scheme}://{host}{_canonical_uri(bucket_name, object_key)}?{query_with_sig}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
app/ui.py
10
app/ui.py
@@ -1105,6 +1105,16 @@ def update_bucket_replication(bucket_name: str):
|
|||||||
if not target_conn_id or not target_bucket:
|
if not target_conn_id or not target_bucket:
|
||||||
flash("Target connection and bucket are required", "danger")
|
flash("Target connection and bucket are required", "danger")
|
||||||
else:
|
else:
|
||||||
|
# Check if user wants to create the remote bucket
|
||||||
|
create_remote = request.form.get("create_remote_bucket") == "on"
|
||||||
|
if create_remote:
|
||||||
|
try:
|
||||||
|
_replication().create_remote_bucket(target_conn_id, target_bucket)
|
||||||
|
flash(f"Created remote bucket '{target_bucket}'", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Failed to create remote bucket: {e}", "warning")
|
||||||
|
# We continue to set the rule even if creation fails (maybe it exists?)
|
||||||
|
|
||||||
rule = ReplicationRule(
|
rule = ReplicationRule(
|
||||||
bucket_name=bucket_name,
|
bucket_name=bucket_name,
|
||||||
target_connection_id=target_conn_id,
|
target_connection_id=target_conn_id,
|
||||||
|
|||||||
@@ -409,6 +409,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('ui.update_bucket_replication', bucket_name=bucket_name) }}" onsubmit="return confirm('Are you sure you want to disable replication?');">
|
<form method="POST" action="{{ url_for('ui.update_bucket_replication', bucket_name=bucket_name) }}" onsubmit="return confirm('Are you sure you want to disable replication?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input type="hidden" name="action" value="delete">
|
<input type="hidden" name="action" value="delete">
|
||||||
<button type="submit" class="btn btn-danger">Disable Replication</button>
|
<button type="submit" class="btn btn-danger">Disable Replication</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -418,6 +419,7 @@
|
|||||||
|
|
||||||
{% if connections %}
|
{% if connections %}
|
||||||
<form method="POST" action="{{ url_for('ui.update_bucket_replication', bucket_name=bucket_name) }}">
|
<form method="POST" action="{{ url_for('ui.update_bucket_replication', bucket_name=bucket_name) }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input type="hidden" name="action" value="create">
|
<input type="hidden" name="action" value="create">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -434,7 +436,12 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="target_bucket" class="form-label">Target Bucket Name</label>
|
<label for="target_bucket" class="form-label">Target Bucket Name</label>
|
||||||
<input type="text" class="form-control" id="target_bucket" name="target_bucket" required placeholder="e.g. my-backup-bucket">
|
<input type="text" class="form-control" id="target_bucket" name="target_bucket" required placeholder="e.g. my-backup-bucket">
|
||||||
<div class="form-text">The bucket on the remote service must already exist.</div>
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="create_remote_bucket" name="create_remote_bucket">
|
||||||
|
<label class="form-check-label" for="create_remote_bucket">
|
||||||
|
Create this bucket on the remote server if it doesn't exist
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Enable Replication</button>
|
<button type="submit" class="btn btn-primary">Enable Replication</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user