Release v0.1.1 #1

Merged
kqjy merged 20 commits from next into main 2025-11-22 12:31:27 +00:00
4 changed files with 49 additions and 4 deletions
Showing only changes of commit 840fd176d3 - Show all commits

View File

@@ -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:

View File

@@ -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}"

View File

@@ -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,

View File

@@ -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>