MyFSIO v0.3.0 Release #22
137
app/s3_api.py
137
app/s3_api.py
@@ -1039,6 +1039,7 @@ def _maybe_handle_bucket_subresource(bucket_name: str) -> Response | None:
|
|||||||
"logging": _bucket_logging_handler,
|
"logging": _bucket_logging_handler,
|
||||||
"uploads": _bucket_uploads_handler,
|
"uploads": _bucket_uploads_handler,
|
||||||
"policy": _bucket_policy_handler,
|
"policy": _bucket_policy_handler,
|
||||||
|
"policyStatus": _bucket_policy_status_handler,
|
||||||
"replication": _bucket_replication_handler,
|
"replication": _bucket_replication_handler,
|
||||||
"website": _bucket_website_handler,
|
"website": _bucket_website_handler,
|
||||||
}
|
}
|
||||||
@@ -1321,8 +1322,8 @@ def _bucket_cors_handler(bucket_name: str) -> Response:
|
|||||||
|
|
||||||
|
|
||||||
def _bucket_encryption_handler(bucket_name: str) -> Response:
|
def _bucket_encryption_handler(bucket_name: str) -> Response:
|
||||||
if request.method not in {"GET", "PUT"}:
|
if request.method not in {"GET", "PUT", "DELETE"}:
|
||||||
return _method_not_allowed(["GET", "PUT"])
|
return _method_not_allowed(["GET", "PUT", "DELETE"])
|
||||||
principal, error = _require_principal()
|
principal, error = _require_principal()
|
||||||
if error:
|
if error:
|
||||||
return error
|
return error
|
||||||
@@ -1343,6 +1344,13 @@ def _bucket_encryption_handler(bucket_name: str) -> Response:
|
|||||||
404,
|
404,
|
||||||
)
|
)
|
||||||
return _xml_response(_render_encryption_document(config))
|
return _xml_response(_render_encryption_document(config))
|
||||||
|
if request.method == "DELETE":
|
||||||
|
try:
|
||||||
|
storage.set_bucket_encryption(bucket_name, None)
|
||||||
|
except StorageError as exc:
|
||||||
|
return _error_response("NoSuchBucket", str(exc), 404)
|
||||||
|
current_app.logger.info("Bucket encryption deleted", extra={"bucket": bucket_name})
|
||||||
|
return Response(status=204)
|
||||||
ct_error = _require_xml_content_type()
|
ct_error = _require_xml_content_type()
|
||||||
if ct_error:
|
if ct_error:
|
||||||
return ct_error
|
return ct_error
|
||||||
@@ -1439,6 +1447,99 @@ def _bucket_acl_handler(bucket_name: str) -> Response:
|
|||||||
return _xml_response(root)
|
return _xml_response(root)
|
||||||
|
|
||||||
|
|
||||||
|
def _object_acl_handler(bucket_name: str, object_key: str) -> Response:
|
||||||
|
from .acl import create_canned_acl, GRANTEE_ALL_USERS, GRANTEE_AUTHENTICATED_USERS
|
||||||
|
|
||||||
|
if request.method not in {"GET", "PUT"}:
|
||||||
|
return _method_not_allowed(["GET", "PUT"])
|
||||||
|
storage = _storage()
|
||||||
|
try:
|
||||||
|
path = storage.get_object_path(bucket_name, object_key)
|
||||||
|
except (StorageError, FileNotFoundError):
|
||||||
|
return _error_response("NoSuchKey", "Object not found", 404)
|
||||||
|
|
||||||
|
if request.method == "PUT":
|
||||||
|
principal, error = _object_principal("write", bucket_name, object_key)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
owner_id = principal.access_key if principal else "anonymous"
|
||||||
|
canned_acl = request.headers.get("x-amz-acl", "private")
|
||||||
|
acl = create_canned_acl(canned_acl, owner_id)
|
||||||
|
acl_service = _acl()
|
||||||
|
metadata = storage.get_object_metadata(bucket_name, object_key)
|
||||||
|
metadata.update(acl_service.create_object_acl_metadata(acl))
|
||||||
|
safe_key = storage._sanitize_object_key(object_key, storage._object_key_max_length_bytes)
|
||||||
|
storage._write_metadata(bucket_name, safe_key, metadata)
|
||||||
|
current_app.logger.info("Object ACL set", extra={"bucket": bucket_name, "key": object_key, "acl": canned_acl})
|
||||||
|
return Response(status=200)
|
||||||
|
|
||||||
|
principal, error = _object_principal("read", bucket_name, object_key)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
owner_id = principal.access_key if principal else "anonymous"
|
||||||
|
acl_service = _acl()
|
||||||
|
metadata = storage.get_object_metadata(bucket_name, object_key)
|
||||||
|
acl = acl_service.get_object_acl(bucket_name, object_key, metadata)
|
||||||
|
if not acl:
|
||||||
|
acl = create_canned_acl("private", owner_id)
|
||||||
|
|
||||||
|
root = Element("AccessControlPolicy")
|
||||||
|
owner_el = SubElement(root, "Owner")
|
||||||
|
SubElement(owner_el, "ID").text = acl.owner
|
||||||
|
SubElement(owner_el, "DisplayName").text = acl.owner
|
||||||
|
acl_el = SubElement(root, "AccessControlList")
|
||||||
|
for grant in acl.grants:
|
||||||
|
grant_el = SubElement(acl_el, "Grant")
|
||||||
|
grantee = SubElement(grant_el, "Grantee")
|
||||||
|
if grant.grantee == GRANTEE_ALL_USERS:
|
||||||
|
grantee.set("{http://www.w3.org/2001/XMLSchema-instance}type", "Group")
|
||||||
|
SubElement(grantee, "URI").text = "http://acs.amazonaws.com/groups/global/AllUsers"
|
||||||
|
elif grant.grantee == GRANTEE_AUTHENTICATED_USERS:
|
||||||
|
grantee.set("{http://www.w3.org/2001/XMLSchema-instance}type", "Group")
|
||||||
|
SubElement(grantee, "URI").text = "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
|
||||||
|
else:
|
||||||
|
grantee.set("{http://www.w3.org/2001/XMLSchema-instance}type", "CanonicalUser")
|
||||||
|
SubElement(grantee, "ID").text = grant.grantee
|
||||||
|
SubElement(grantee, "DisplayName").text = grant.grantee
|
||||||
|
SubElement(grant_el, "Permission").text = grant.permission
|
||||||
|
return _xml_response(root)
|
||||||
|
|
||||||
|
|
||||||
|
def _object_attributes_handler(bucket_name: str, object_key: str) -> Response:
|
||||||
|
if request.method != "GET":
|
||||||
|
return _method_not_allowed(["GET"])
|
||||||
|
principal, error = _object_principal("read", bucket_name, object_key)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
storage = _storage()
|
||||||
|
try:
|
||||||
|
path = storage.get_object_path(bucket_name, object_key)
|
||||||
|
file_stat = path.stat()
|
||||||
|
metadata = storage.get_object_metadata(bucket_name, object_key)
|
||||||
|
except (StorageError, FileNotFoundError):
|
||||||
|
return _error_response("NoSuchKey", "Object not found", 404)
|
||||||
|
|
||||||
|
requested = request.headers.get("x-amz-object-attributes", "")
|
||||||
|
attrs = {a.strip() for a in requested.split(",") if a.strip()}
|
||||||
|
|
||||||
|
root = Element("GetObjectAttributesResponse")
|
||||||
|
if "ETag" in attrs:
|
||||||
|
etag = metadata.get("__etag__") or storage._compute_etag(path)
|
||||||
|
SubElement(root, "ETag").text = etag
|
||||||
|
if "StorageClass" in attrs:
|
||||||
|
SubElement(root, "StorageClass").text = "STANDARD"
|
||||||
|
if "ObjectSize" in attrs:
|
||||||
|
SubElement(root, "ObjectSize").text = str(file_stat.st_size)
|
||||||
|
if "Checksum" in attrs:
|
||||||
|
SubElement(root, "Checksum")
|
||||||
|
if "ObjectParts" in attrs:
|
||||||
|
SubElement(root, "ObjectParts")
|
||||||
|
|
||||||
|
response = _xml_response(root)
|
||||||
|
response.headers["Last-Modified"] = http_date(file_stat.st_mtime)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _bucket_list_versions_handler(bucket_name: str) -> Response:
|
def _bucket_list_versions_handler(bucket_name: str) -> Response:
|
||||||
"""Handle ListObjectVersions (GET /<bucket>?versions)."""
|
"""Handle ListObjectVersions (GET /<bucket>?versions)."""
|
||||||
if request.method != "GET":
|
if request.method != "GET":
|
||||||
@@ -2669,6 +2770,12 @@ def object_handler(bucket_name: str, object_key: str):
|
|||||||
if "legal-hold" in request.args:
|
if "legal-hold" in request.args:
|
||||||
return _object_legal_hold_handler(bucket_name, object_key)
|
return _object_legal_hold_handler(bucket_name, object_key)
|
||||||
|
|
||||||
|
if "acl" in request.args:
|
||||||
|
return _object_acl_handler(bucket_name, object_key)
|
||||||
|
|
||||||
|
if "attributes" in request.args:
|
||||||
|
return _object_attributes_handler(bucket_name, object_key)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if "uploads" in request.args:
|
if "uploads" in request.args:
|
||||||
return _initiate_multipart_upload(bucket_name, object_key)
|
return _initiate_multipart_upload(bucket_name, object_key)
|
||||||
@@ -2993,6 +3100,32 @@ def _bucket_policy_handler(bucket_name: str) -> Response:
|
|||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
|
|
||||||
|
def _bucket_policy_status_handler(bucket_name: str) -> Response:
|
||||||
|
if request.method != "GET":
|
||||||
|
return _method_not_allowed(["GET"])
|
||||||
|
principal, error = _require_principal()
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
try:
|
||||||
|
_authorize_action(principal, bucket_name, "policy")
|
||||||
|
except IamError as exc:
|
||||||
|
return _error_response("AccessDenied", str(exc), 403)
|
||||||
|
storage = _storage()
|
||||||
|
if not storage.bucket_exists(bucket_name):
|
||||||
|
return _error_response("NoSuchBucket", "Bucket does not exist", 404)
|
||||||
|
store = _bucket_policies()
|
||||||
|
policy = store.get_policy(bucket_name)
|
||||||
|
is_public = False
|
||||||
|
if policy:
|
||||||
|
for statement in policy.get("Statement", []):
|
||||||
|
if statement.get("Effect") == "Allow" and statement.get("Principal") == "*":
|
||||||
|
is_public = True
|
||||||
|
break
|
||||||
|
root = Element("PolicyStatus")
|
||||||
|
SubElement(root, "IsPublic").text = "TRUE" if is_public else "FALSE"
|
||||||
|
return _xml_response(root)
|
||||||
|
|
||||||
|
|
||||||
def _bucket_replication_handler(bucket_name: str) -> Response:
|
def _bucket_replication_handler(bucket_name: str) -> Response:
|
||||||
if request.method not in {"GET", "PUT", "DELETE"}:
|
if request.method not in {"GET", "PUT", "DELETE"}:
|
||||||
return _method_not_allowed(["GET", "PUT", "DELETE"])
|
return _method_not_allowed(["GET", "PUT", "DELETE"])
|
||||||
|
|||||||
Reference in New Issue
Block a user