mirror of
https://github.com/chrislusf/seaweedfs
synced 2025-07-27 05:52:48 +02:00
Compare commits
No commits in common. "master" and "3.95" have entirely different histories.
44 changed files with 1016 additions and 4229 deletions
1
.github/workflows/helm_chart_release.yml
vendored
1
.github/workflows/helm_chart_release.yml
vendored
|
@ -20,4 +20,3 @@ jobs:
|
||||||
charts_dir: k8s/charts
|
charts_dir: k8s/charts
|
||||||
target_dir: helm
|
target_dir: helm
|
||||||
branch: gh-pages
|
branch: gh-pages
|
||||||
helm_version: v3.18.4
|
|
||||||
|
|
146
.github/workflows/s3tests.yml
vendored
146
.github/workflows/s3tests.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
|
|
||||||
|
@ -131,32 +131,18 @@ jobs:
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_many \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_many \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_many \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_many \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_basic \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_encoding_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_encoding_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_encoding_basic \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_prefix \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix_ends_with_delimiter \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix_ends_with_delimiter \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_prefix_ends_with_delimiter \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_alt \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_alt \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_alt \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix_underscore \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix_underscore \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_prefix_underscore \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_percentage \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_percentage \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_percentage \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_whitespace \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_whitespace \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_whitespace \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_dot \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_dot \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_dot \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_unreadable \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_unreadable \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_unreadable \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_empty \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_empty \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_empty \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_none \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_none \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_none \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_not_exist \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_not_exist \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_not_exist \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_not_skip_special \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_delimiter_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_delimiter_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_alt \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_alt \
|
||||||
|
@ -168,8 +154,6 @@ jobs:
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_prefix_delimiter_not_exist \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_prefix_delimiter_not_exist \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_delimiter_prefix_delimiter_not_exist \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_delimiter_prefix_delimiter_not_exist \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_fetchowner_notempty \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_fetchowner_notempty \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_fetchowner_defaultempty \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_fetchowner_empty \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_alt \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_alt \
|
||||||
|
@ -186,11 +170,6 @@ jobs:
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_one \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_one \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_maxkeys_zero \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_maxkeys_zero \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_zero \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_zero \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_maxkeys_none \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_none \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_unordered \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_unordered \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_maxkeys_invalid \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_none \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_none \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_empty \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_empty \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_continuationtoken_empty \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_continuationtoken_empty \
|
||||||
|
@ -202,9 +181,6 @@ jobs:
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_startafter_not_in_list \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_startafter_not_in_list \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_after_list \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_after_list \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_startafter_after_list \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_startafter_after_list \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_return_data \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_objects_anonymous \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_objects_anonymous \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_objects_anonymous_fail \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_objects_anonymous_fail \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_objects_anonymous_fail \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_objects_anonymous_fail \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_long_name \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_long_name \
|
||||||
|
@ -322,7 +298,7 @@ jobs:
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
|
|
||||||
|
@ -423,12 +399,7 @@ jobs:
|
||||||
echo "S3 connection test failed, retrying... ($i/10)"
|
echo "S3 connection test failed, retrying... ($i/10)"
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
# tox -- s3tests_boto3/functional/test_s3.py -k "object_lock or (versioning and not test_versioning_obj_suspend_versions and not test_bucket_list_return_data_versioning and not test_versioning_concurrent_multi_object_delete)" --tb=short
|
tox -- s3tests_boto3/functional/test_s3.py -k "object_lock or (versioning and not test_versioning_obj_suspend_versions and not test_bucket_list_return_data_versioning and not test_versioning_concurrent_multi_object_delete)" --tb=short
|
||||||
# Run all versioning and object lock tests including specific list object versions tests
|
|
||||||
tox -- \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_return_data_versioning \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_versioning_obj_list_marker \
|
|
||||||
s3tests_boto3/functional/test_s3.py -k "object_lock or versioning" --tb=short
|
|
||||||
kill -9 $pid || true
|
kill -9 $pid || true
|
||||||
# Clean up data directory
|
# Clean up data directory
|
||||||
rm -rf "$WEED_DATA_DIR" || true
|
rm -rf "$WEED_DATA_DIR" || true
|
||||||
|
@ -448,7 +419,7 @@ jobs:
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
|
|
||||||
|
@ -671,7 +642,7 @@ jobs:
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
|
|
||||||
|
@ -904,32 +875,18 @@ jobs:
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_many \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_many \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_many \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_many \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_basic \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_encoding_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_encoding_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_encoding_basic \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_prefix \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix_ends_with_delimiter \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix_ends_with_delimiter \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_prefix_ends_with_delimiter \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_alt \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_alt \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_alt \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix_underscore \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_prefix_underscore \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_prefix_underscore \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_percentage \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_percentage \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_percentage \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_whitespace \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_whitespace \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_whitespace \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_dot \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_dot \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_dot \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_unreadable \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_unreadable \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_unreadable \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_empty \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_empty \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_empty \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_none \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_none \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_none \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_not_exist \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_delimiter_not_exist \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_not_exist \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_delimiter_not_skip_special \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_delimiter_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_delimiter_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_alt \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_alt \
|
||||||
|
@ -941,8 +898,6 @@ jobs:
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_prefix_delimiter_not_exist \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_delimiter_prefix_delimiter_not_exist \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_delimiter_prefix_delimiter_not_exist \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_delimiter_prefix_delimiter_not_exist \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_fetchowner_notempty \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_fetchowner_notempty \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_fetchowner_defaultempty \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_fetchowner_empty \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_basic \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_prefix_basic \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_alt \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_prefix_alt \
|
||||||
|
@ -959,11 +914,6 @@ jobs:
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_one \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_one \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_maxkeys_zero \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_maxkeys_zero \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_zero \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_zero \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_maxkeys_none \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_maxkeys_none \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_unordered \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_unordered \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_maxkeys_invalid \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_none \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_none \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_empty \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_empty \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_continuationtoken_empty \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_continuationtoken_empty \
|
||||||
|
@ -975,107 +925,23 @@ jobs:
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_startafter_not_in_list \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_startafter_not_in_list \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_after_list \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_marker_after_list \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_startafter_after_list \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_startafter_after_list \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_return_data \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_objects_anonymous \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_objects_anonymous \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_objects_anonymous_fail \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_objects_anonymous_fail \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_objects_anonymous_fail \
|
s3tests_boto3/functional/test_s3.py::test_bucket_listv2_objects_anonymous_fail \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_long_name \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_long_name \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_special_prefix \
|
s3tests_boto3/functional/test_s3.py::test_bucket_list_special_prefix \
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_delete_notexist \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_create_delete \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_read_not_exist \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multi_object_delete \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multi_objectv2_delete \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_head_zero_bytes \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_write_check_etag \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_write_cache_control \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_write_expires \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_write_read_update_read_delete \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_metadata_replaced_on_put \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_write_file \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_invalid_date_format \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_no_key_specified \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_missing_signature \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_condition_is_case_sensitive \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_expires_is_case_sensitive \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_missing_expires_condition \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_missing_conditions_list \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_upload_size_limit_exceeded \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_missing_content_length_argument \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_invalid_content_length_argument \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_upload_size_below_minimum \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_post_object_empty_conditions \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_get_object_ifmatch_good \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_get_object_ifnonematch_good \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_get_object_ifmatch_failed \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_get_object_ifnonematch_failed \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_get_object_ifmodifiedsince_good \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_get_object_ifmodifiedsince_failed \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_get_object_ifunmodifiedsince_failed \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_head \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_head_notexist \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_raw_authenticated \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_raw_authenticated_bucket_acl \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_raw_authenticated_object_acl \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_raw_authenticated_object_gone \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_raw_get_x_amz_expires_out_range_zero \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_anon_put \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_put_authenticated \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_recreate_overwrite_acl \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_recreate_new_acl \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_buckets_create_then_list \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_buckets_list_ctime \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_list_buckets_invalid_auth \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_list_buckets_bad_auth \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_create_naming_good_contains_period \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_create_naming_good_contains_hyphen \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_bucket_list_special_prefix \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_copy_zero_size \
|
s3tests_boto3/functional/test_s3.py::test_object_copy_zero_size \
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_copy_same_bucket \
|
s3tests_boto3/functional/test_s3.py::test_object_copy_same_bucket \
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_copy_to_itself \
|
s3tests_boto3/functional/test_s3.py::test_object_copy_to_itself \
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_copy_diff_bucket \
|
s3tests_boto3/functional/test_s3.py::test_object_copy_diff_bucket \
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_copy_canned_acl \
|
s3tests_boto3/functional/test_s3.py::test_object_copy_canned_acl \
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_copy_bucket_not_found \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_object_copy_key_not_found \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_copy_small \
|
s3tests_boto3/functional/test_s3.py::test_multipart_copy_small \
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_copy_without_range \
|
s3tests_boto3/functional/test_s3.py::test_multipart_copy_without_range \
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_copy_special_names \
|
s3tests_boto3/functional/test_s3.py::test_multipart_copy_special_names \
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_copy_multiple_sizes \
|
s3tests_boto3/functional/test_s3.py::test_multipart_copy_multiple_sizes \
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_get_part \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload_empty \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload_multiple_sizes \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload_contents \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload_overwrite_existing_object \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload_size_too_small \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_resend_first_finishes_last \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload_resend_part \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload_missing_part \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_multipart_upload_incorrect_etag \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_abort_multipart_upload \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_list_multipart_upload \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_read_1mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_read_4mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_read_8mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_write_1mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_write_4mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_write_8mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_dual_write_1mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_dual_write_4mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_dual_write_8mb \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_atomic_multipart_upload_write \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_ranged_request_response_code \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_ranged_big_request_response_code \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_ranged_request_skip_leading_bytes_response_code \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_ranged_request_return_trailing_bytes_response_code \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_copy_object_ifmatch_good \
|
s3tests_boto3/functional/test_s3.py::test_copy_object_ifmatch_good \
|
||||||
s3tests_boto3/functional/test_s3.py::test_copy_object_ifnonematch_failed \
|
s3tests_boto3/functional/test_s3.py::test_copy_object_ifnonematch_failed \
|
||||||
s3tests_boto3/functional/test_s3.py::test_copy_object_ifmatch_failed \
|
s3tests_boto3/functional/test_s3.py::test_copy_object_ifmatch_failed \
|
||||||
s3tests_boto3/functional/test_s3.py::test_copy_object_ifnonematch_good \
|
s3tests_boto3/functional/test_s3.py::test_copy_object_ifnonematch_good
|
||||||
s3tests_boto3/functional/test_s3.py::test_lifecycle_set \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_lifecycle_get \
|
|
||||||
s3tests_boto3/functional/test_s3.py::test_lifecycle_set_filter
|
|
||||||
kill -9 $pid || true
|
kill -9 $pid || true
|
||||||
# Clean up data directory
|
# Clean up data directory
|
||||||
rm -rf "$WEED_DATA_DIR" || true
|
rm -rf "$WEED_DATA_DIR" || true
|
||||||
|
|
|
@ -84,7 +84,6 @@ Table of Contents
|
||||||
|
|
||||||
## Quick Start with Single Binary ##
|
## Quick Start with Single Binary ##
|
||||||
* Download the latest binary from https://github.com/seaweedfs/seaweedfs/releases and unzip a single binary file `weed` or `weed.exe`. Or run `go install github.com/seaweedfs/seaweedfs/weed@latest`.
|
* Download the latest binary from https://github.com/seaweedfs/seaweedfs/releases and unzip a single binary file `weed` or `weed.exe`. Or run `go install github.com/seaweedfs/seaweedfs/weed@latest`.
|
||||||
* `export AWS_ACCESS_KEY_ID=admin ; export AWS_SECRET_ACCESS_KEY=key` as the admin credentials to access the object store.
|
|
||||||
* Run `weed server -dir=/some/data/dir -s3` to start one master, one volume server, one filer, and one S3 gateway.
|
* Run `weed server -dir=/some/data/dir -s3` to start one master, one volume server, one filer, and one S3 gateway.
|
||||||
|
|
||||||
Also, to increase capacity, just add more volume servers by running `weed volume -dir="/some/data/dir2" -mserver="<master_host>:9333" -port=8081` locally, or on a different machine, or on thousands of machines. That is it!
|
Also, to increase capacity, just add more volume servers by running `weed volume -dir="/some/data/dir2" -mserver="<master_host>:9333" -port=8081` locally, or on a different machine, or on thousands of machines. That is it!
|
||||||
|
|
98
go.mod
98
go.mod
|
@ -5,7 +5,7 @@ go 1.24
|
||||||
toolchain go1.24.1
|
toolchain go1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.121.4 // indirect
|
cloud.google.com/go v0.121.1 // indirect
|
||||||
cloud.google.com/go/pubsub v1.49.0
|
cloud.google.com/go/pubsub v1.49.0
|
||||||
cloud.google.com/go/storage v1.55.0
|
cloud.google.com/go/storage v1.55.0
|
||||||
github.com/Azure/azure-pipeline-go v0.2.3
|
github.com/Azure/azure-pipeline-go v0.2.3
|
||||||
|
@ -29,17 +29,18 @@ require (
|
||||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||||
github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4
|
github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4
|
||||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
|
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/go-redsync/redsync/v4 v4.13.0
|
github.com/go-redsync/redsync/v4 v4.13.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/go-zookeeper/zk v1.0.3 // indirect
|
github.com/go-zookeeper/zk v1.0.3 // indirect
|
||||||
github.com/gocql/gocql v1.7.0
|
github.com/gocql/gocql v1.7.0
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/golang/protobuf v1.5.4
|
github.com/golang/protobuf v1.5.4
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/google/btree v1.1.3
|
github.com/google/btree v1.1.3
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/google/wire v0.6.0 // indirect
|
github.com/google/wire v0.6.0 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
@ -52,7 +53,7 @@ require (
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/karlseguin/ccache/v2 v2.0.8
|
github.com/karlseguin/ccache/v2 v2.0.8
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/reedsolomon v1.12.5
|
github.com/klauspost/reedsolomon v1.12.4
|
||||||
github.com/kurin/blazer v0.5.3
|
github.com/kurin/blazer v0.5.3
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/linxGnu/grocksdb v1.10.1
|
github.com/linxGnu/grocksdb v1.10.1
|
||||||
|
@ -96,9 +97,9 @@ require (
|
||||||
go.etcd.io/etcd/client/v3 v3.6.2
|
go.etcd.io/etcd/client/v3 v3.6.2
|
||||||
go.mongodb.org/mongo-driver v1.17.4
|
go.mongodb.org/mongo-driver v1.17.4
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
gocloud.dev v0.43.0
|
gocloud.dev v0.42.0
|
||||||
gocloud.dev/pubsub/natspubsub v0.42.0
|
gocloud.dev/pubsub/natspubsub v0.42.0
|
||||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0
|
gocloud.dev/pubsub/rabbitpubsub v0.42.0
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.40.0
|
||||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476
|
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476
|
||||||
golang.org/x/image v0.29.0
|
golang.org/x/image v0.29.0
|
||||||
|
@ -108,8 +109,8 @@ require (
|
||||||
golang.org/x/text v0.27.0 // indirect
|
golang.org/x/text v0.27.0 // indirect
|
||||||
golang.org/x/tools v0.35.0
|
golang.org/x/tools v0.35.0
|
||||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||||
google.golang.org/api v0.242.0
|
google.golang.org/api v0.241.0
|
||||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||||
google.golang.org/grpc v1.73.0
|
google.golang.org/grpc v1.73.0
|
||||||
google.golang.org/protobuf v1.36.6
|
google.golang.org/protobuf v1.36.6
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
@ -123,19 +124,19 @@ require (
|
||||||
require (
|
require (
|
||||||
github.com/Jille/raft-grpc-transport v1.6.1
|
github.com/Jille/raft-grpc-transport v1.6.1
|
||||||
github.com/ThreeDotsLabs/watermill v1.4.7
|
github.com/ThreeDotsLabs/watermill v1.4.7
|
||||||
github.com/a-h/templ v0.3.920
|
github.com/a-h/templ v0.3.906
|
||||||
github.com/arangodb/go-driver v1.6.6
|
github.com/arangodb/go-driver v1.6.6
|
||||||
github.com/armon/go-metrics v0.4.1
|
github.com/armon/go-metrics v0.4.1
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.6
|
github.com/aws/aws-sdk-go-v2 v1.36.5
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.18
|
github.com/aws/aws-sdk-go-v2/config v1.29.17
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.71
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.70
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0
|
||||||
github.com/cognusion/imaging v1.0.2
|
github.com/cognusion/imaging v1.0.2
|
||||||
github.com/fluent/fluent-logger-golang v1.10.0
|
github.com/fluent/fluent-logger-golang v1.10.0
|
||||||
github.com/getsentry/sentry-go v0.34.1
|
github.com/getsentry/sentry-go v0.34.1
|
||||||
github.com/gin-contrib/sessions v1.0.4
|
github.com/gin-contrib/sessions v1.0.4
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/flatbuffers/go v0.0.0-20230108230133-3b8644d32c50
|
github.com/google/flatbuffers/go v0.0.0-20230108230133-3b8644d32c50
|
||||||
github.com/hanwen/go-fuse/v2 v2.8.0
|
github.com/hanwen/go-fuse/v2 v2.8.0
|
||||||
github.com/hashicorp/raft v1.7.3
|
github.com/hashicorp/raft v1.7.3
|
||||||
|
@ -153,7 +154,7 @@ require (
|
||||||
github.com/tarantool/go-tarantool/v2 v2.4.0
|
github.com/tarantool/go-tarantool/v2 v2.4.0
|
||||||
github.com/tikv/client-go/v2 v2.0.7
|
github.com/tikv/client-go/v2 v2.0.7
|
||||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.0
|
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.0
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.113.1
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.112.0
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.6.2
|
go.etcd.io/etcd/client/pkg/v3 v3.6.2
|
||||||
go.uber.org/atomic v1.11.0
|
go.uber.org/atomic v1.11.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.16.0
|
||||||
|
@ -164,28 +165,29 @@ require github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // ind
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||||
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cel.dev/expr v0.24.0 // indirect
|
cel.dev/expr v0.23.0 // indirect
|
||||||
cloud.google.com/go/auth v0.16.3 // indirect
|
cloud.google.com/go/auth v0.16.2 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||||
cloud.google.com/go/iam v1.5.2 // indirect
|
cloud.google.com/go/iam v1.5.2 // indirect
|
||||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.1 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.1 // indirect
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||||
github.com/Files-com/files-sdk-go/v3 v3.2.173 // indirect
|
github.com/Files-com/files-sdk-go/v3 v3.2.173 // indirect
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||||
github.com/IBM/go-sdk-core/v5 v5.20.0 // indirect
|
github.com/IBM/go-sdk-core/v5 v5.20.0 // indirect
|
||||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect
|
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
@ -203,21 +205,21 @@ require (
|
||||||
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e // indirect
|
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.77 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
|
||||||
github.com/aws/smithy-go v1.22.4 // indirect
|
github.com/aws/smithy-go v1.22.4 // indirect
|
||||||
github.com/boltdb/bolt v1.3.1 // indirect
|
github.com/boltdb/bolt v1.3.1 // indirect
|
||||||
github.com/bradenaw/juniper v0.15.3 // indirect
|
github.com/bradenaw/juniper v0.15.3 // indirect
|
||||||
|
@ -232,7 +234,7 @@ require (
|
||||||
github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc // indirect
|
github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc // indirect
|
||||||
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc // indirect
|
github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||||
github.com/colinmarc/hdfs/v2 v2.4.0 // indirect
|
github.com/colinmarc/hdfs/v2 v2.4.0 // indirect
|
||||||
github.com/creasty/defaults v1.8.0 // indirect
|
github.com/creasty/defaults v1.8.0 // indirect
|
||||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||||
|
@ -254,7 +256,7 @@ require (
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.2 // indirect
|
github.com/go-chi/chi/v5 v5.2.2 // indirect
|
||||||
github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 // indirect
|
github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
@ -276,7 +278,7 @@ require (
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.4.0 // indirect
|
github.com/gorilla/sessions v1.4.0 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
|
@ -378,21 +380,21 @@ require (
|
||||||
go.etcd.io/bbolt v1.4.0 // indirect
|
go.etcd.io/bbolt v1.4.0 // indirect
|
||||||
go.etcd.io/etcd/api/v3 v3.6.2 // indirect
|
go.etcd.io/etcd/api/v3 v3.6.2 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect
|
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
golang.org/x/arch v0.16.0 // indirect
|
golang.org/x/arch v0.16.0 // indirect
|
||||||
golang.org/x/term v0.33.0 // indirect
|
golang.org/x/term v0.33.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
|
200
go.sum
200
go.sum
|
@ -1,5 +1,5 @@
|
||||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss=
|
||||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
@ -38,8 +38,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY
|
||||||
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
|
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
|
||||||
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
|
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
|
||||||
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
||||||
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
|
cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw=
|
||||||
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
|
cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
|
||||||
cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
|
cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
|
||||||
cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
|
cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
|
||||||
cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
|
cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
|
||||||
|
@ -86,8 +86,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
|
||||||
cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
|
cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
|
||||||
cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
|
cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
|
||||||
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
|
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
|
||||||
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
|
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
|
||||||
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
|
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
|
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
|
||||||
|
@ -541,10 +541,10 @@ gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zum
|
||||||
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
|
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
|
||||||
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
|
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
|
||||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 h1:j8BorDEigD8UFOSZQiSqAMOOleyQOOQPnUAwV+Ls1gA=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||||
|
@ -580,14 +580,14 @@ github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3
|
||||||
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||||
github.com/Files-com/files-sdk-go/v3 v3.2.173 h1:OPDjpkEWXO+WSGX1qQ10Y51do178i9z4DdFpI25B+iY=
|
github.com/Files-com/files-sdk-go/v3 v3.2.173 h1:OPDjpkEWXO+WSGX1qQ10Y51do178i9z4DdFpI25B+iY=
|
||||||
github.com/Files-com/files-sdk-go/v3 v3.2.173/go.mod h1:HnPrW1lljxOjdkR5Wm6DjtdHwWdcm/afts2N6O+iiJo=
|
github.com/Files-com/files-sdk-go/v3 v3.2.173/go.mod h1:HnPrW1lljxOjdkR5Wm6DjtdHwWdcm/afts2N6O+iiJo=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||||
github.com/IBM/go-sdk-core/v5 v5.20.0 h1:rG1fn5GmJfFzVtpDKndsk6MgcarluG8YIWf89rVqLP8=
|
github.com/IBM/go-sdk-core/v5 v5.20.0 h1:rG1fn5GmJfFzVtpDKndsk6MgcarluG8YIWf89rVqLP8=
|
||||||
github.com/IBM/go-sdk-core/v5 v5.20.0/go.mod h1:Q3BYO6iDA2zweQPDGbNTtqft5tDcEpm6RTuqMlPcvbw=
|
github.com/IBM/go-sdk-core/v5 v5.20.0/go.mod h1:Q3BYO6iDA2zweQPDGbNTtqft5tDcEpm6RTuqMlPcvbw=
|
||||||
github.com/Jille/raft-grpc-transport v1.6.1 h1:gN3sjapb+fVbiebS7AfQQgbV2ecTOI7ur7NPPC7Mhoc=
|
github.com/Jille/raft-grpc-transport v1.6.1 h1:gN3sjapb+fVbiebS7AfQQgbV2ecTOI7ur7NPPC7Mhoc=
|
||||||
|
@ -624,8 +624,8 @@ github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT
|
||||||
github.com/Shopify/toxiproxy/v2 v2.5.0/go.mod h1:yhM2epWtAmel9CB8r2+L+PCmhH6yH2pITaPAo7jxJl0=
|
github.com/Shopify/toxiproxy/v2 v2.5.0/go.mod h1:yhM2epWtAmel9CB8r2+L+PCmhH6yH2pITaPAo7jxJl0=
|
||||||
github.com/ThreeDotsLabs/watermill v1.4.7 h1:LiF4wMP400/psRTdHL/IcV1YIv9htHYFggbe2d6cLeI=
|
github.com/ThreeDotsLabs/watermill v1.4.7 h1:LiF4wMP400/psRTdHL/IcV1YIv9htHYFggbe2d6cLeI=
|
||||||
github.com/ThreeDotsLabs/watermill v1.4.7/go.mod h1:Ks20MyglVnqjpha1qq0kjaQ+J9ay7bdnjszQ4cW9FMU=
|
github.com/ThreeDotsLabs/watermill v1.4.7/go.mod h1:Ks20MyglVnqjpha1qq0kjaQ+J9ay7bdnjszQ4cW9FMU=
|
||||||
github.com/a-h/templ v0.3.920 h1:IQjjTu4KGrYreHo/ewzSeS8uefecisPayIIc9VflLSE=
|
github.com/a-h/templ v0.3.906 h1:ZUThc8Q9n04UATaCwaG60pB1AqbulLmYEAMnWV63svg=
|
||||||
github.com/a-h/templ v0.3.920/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
github.com/a-h/templ v0.3.906/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
||||||
github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3 h1:hhdWprfSpFbN7lz3W1gM40vOgvSh1WCSMxYD6gGB4Hs=
|
github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3 h1:hhdWprfSpFbN7lz3W1gM40vOgvSh1WCSMxYD6gGB4Hs=
|
||||||
github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3/go.mod h1:XaUnRxSCYgL3kkgX0QHIV0D+znljPIDImxlv2kbGv0Y=
|
github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3/go.mod h1:XaUnRxSCYgL3kkgX0QHIV0D+znljPIDImxlv2kbGv0Y=
|
||||||
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
|
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
|
||||||
|
@ -659,46 +659,46 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU=
|
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I=
|
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU=
|
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.77 h1:xaRN9fags7iJznsMEjtcEuON1hGfCZ0y5MVfEMKtrx8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.77/go.mod h1:lolsiGkT47AZ3DWqtxgEQM/wVMpayi7YWNjl3wHSRx8=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37 h1:XTZZ0I3SZUHAtBLBU6395ad+VOblE0DwQP6MuaNeics=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37/go.mod h1:Pi6ksbniAWVwu2S8pEzcYPyhUkAcLaufxN7PfAUQjBk=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5 h1:M5/B8JUaCI8+9QD+u3S/f4YHpvqE9RpSkV3rf0Iks2w=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5/go.mod h1:Bktzci1bwdbpuLiu3AOksiNPMl/LLKmX1TWmqp2xbvs=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18 h1:OS2e0SKqsU2LiJPqL8u9x41tKc6MMEHrWjLVLn3oysg=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18/go.mod h1:+Yrk+MDGzlNGxCXieljNeWpoZTCQUQVL+Jk9hGGJ8qM=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1 h1:RkHXU9jP0DptGy7qKI8CBGsUJruWz0v5IgwBa2DwWcU=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 h1:5Y75q0RPQoAbieyOuGLhjV9P3txvYgXv2lg0UwJOfmE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1/go.mod h1:3xAOf7tdKF+qbb+XpU+EPhNXAdun3Lu1RcDrj8KC24I=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE=
|
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 h1:PajtbJ/5bEo6iUAIGMYnK8ljqg2F1h4mMCGh1acjN30=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7/go.mod h1:4WYoZAhHt+dWYpoOQUgkUKfuQbE6Gg/hW4oXE0pKS9U=
|
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2/go.mod h1:PJtxxMdj747j8DeZENRTTYAz/lx/pADn/U0k7YNNiUY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ=
|
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 h1:j5BchjfDoS7K26vPdyJlyxBIIBGDflq3qjjJKBDlbcI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI=
|
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3/go.mod h1:Bar4MrRxeqdn6XIh8JGfiXuFRmyrrsZNTJotxEJmWW0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||||
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
|
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
|
||||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
@ -781,8 +781,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
|
||||||
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
|
||||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||||
github.com/cognusion/imaging v1.0.2 h1:BQwBV8V8eF3+dwffp8Udl9xF1JKh5Z0z5JkJwAi98Mc=
|
github.com/cognusion/imaging v1.0.2 h1:BQwBV8V8eF3+dwffp8Udl9xF1JKh5Z0z5JkJwAi98Mc=
|
||||||
github.com/cognusion/imaging v1.0.2/go.mod h1:mj7FvH7cT2dlFogQOSUQRtotBxJ4gFQ2ySMSmBm5dSk=
|
github.com/cognusion/imaging v1.0.2/go.mod h1:mj7FvH7cT2dlFogQOSUQRtotBxJ4gFQ2ySMSmBm5dSk=
|
||||||
github.com/colinmarc/hdfs/v2 v2.4.0 h1:v6R8oBx/Wu9fHpdPoJJjpGSUxo8NhHIwrwsfhFvU9W0=
|
github.com/colinmarc/hdfs/v2 v2.4.0 h1:v6R8oBx/Wu9fHpdPoJJjpGSUxo8NhHIwrwsfhFvU9W0=
|
||||||
|
@ -887,8 +887,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
|
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
|
||||||
|
@ -916,8 +916,8 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
@ -985,8 +985,8 @@ github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||||
|
@ -1118,8 +1118,8 @@ github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK
|
||||||
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
|
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
|
||||||
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
|
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
|
||||||
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
|
||||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||||
|
@ -1141,8 +1141,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||||
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
||||||
|
@ -1262,8 +1262,8 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/klauspost/reedsolomon v1.12.5 h1:4cJuyH926If33BeDgiZpI5OU0pE+wUHZvMSyNGqN73Y=
|
github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA=
|
||||||
github.com/klauspost/reedsolomon v1.12.5/go.mod h1:LkXRjLYGM8K/iQfujYnaPeDmhZLqkrGUyG9p7zs5L68=
|
github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
@ -1668,8 +1668,8 @@ github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.0 h1:/NyPd9KnCJgzrEXCArqk1T
|
||||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.0/go.mod h1:9YzkhlIymWaJGX6KMU3vh5sOf3UKbCXkG/ZdjaI3zNM=
|
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.0/go.mod h1:9YzkhlIymWaJGX6KMU3vh5sOf3UKbCXkG/ZdjaI3zNM=
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.44.0/go.mod h1:oSLwnuilwIpaF5bJJMAofnGgzPJusoI3zWMNb8I+GnM=
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.44.0/go.mod h1:oSLwnuilwIpaF5bJJMAofnGgzPJusoI3zWMNb8I+GnM=
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.3/go.mod h1:bWnOIcUHd7+Sl7DN+yhyY1H/I61z53GczvwJgXMgvj0=
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.3/go.mod h1:bWnOIcUHd7+Sl7DN+yhyY1H/I61z53GczvwJgXMgvj0=
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.113.1 h1:VRRUtl0JlovbiZOEwqpreVYJNixY7IdgGvEkXRO2mK0=
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.112.0 h1:jOtznRBsagoZjuOS8u+jbjRbqZGX4tq579yWMoj0KYg=
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.113.1/go.mod h1:Pp1w2xxUoLQ3NCNAwV7pvDq0TVQOdtAqs+ZiC+i8r14=
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.112.0/go.mod h1:Pp1w2xxUoLQ3NCNAwV7pvDq0TVQOdtAqs+ZiC+i8r14=
|
||||||
github.com/ydb-platform/ydb-go-yc v0.12.1 h1:qw3Fa+T81+Kpu5Io2vYHJOwcrYrVjgJlT6t/0dOXJrA=
|
github.com/ydb-platform/ydb-go-yc v0.12.1 h1:qw3Fa+T81+Kpu5Io2vYHJOwcrYrVjgJlT6t/0dOXJrA=
|
||||||
github.com/ydb-platform/ydb-go-yc v0.12.1/go.mod h1:t/ZA4ECdgPWjAb4jyDe8AzQZB5dhpGbi3iCahFaNwBY=
|
github.com/ydb-platform/ydb-go-yc v0.12.1/go.mod h1:t/ZA4ECdgPWjAb4jyDe8AzQZB5dhpGbi3iCahFaNwBY=
|
||||||
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 h1:9E5q8Nsy2RiJMZDNVy0A3KUrIMBPakJ2VgloeWbcI84=
|
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 h1:9E5q8Nsy2RiJMZDNVy0A3KUrIMBPakJ2VgloeWbcI84=
|
||||||
|
@ -1720,24 +1720,24 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA=
|
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU=
|
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||||
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||||
|
@ -1762,12 +1762,12 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M=
|
gocloud.dev v0.42.0 h1:qzG+9ItUL3RPB62/Amugws28n+4vGZXEoJEAMfjutzw=
|
||||||
gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4=
|
gocloud.dev v0.42.0/go.mod h1:zkaYAapZfQisXOA4bzhsbA4ckiStGQ3Psvs9/OQ5dPM=
|
||||||
gocloud.dev/pubsub/natspubsub v0.42.0 h1:sjz9PNIT28us6UVctyZZVDlBoGfUXSqvBX5rcT36nKQ=
|
gocloud.dev/pubsub/natspubsub v0.42.0 h1:sjz9PNIT28us6UVctyZZVDlBoGfUXSqvBX5rcT36nKQ=
|
||||||
gocloud.dev/pubsub/natspubsub v0.42.0/go.mod h1:Y25oPmk9vWg1pathkY85+u+9zszMGhI+xhdFUSWnins=
|
gocloud.dev/pubsub/natspubsub v0.42.0/go.mod h1:Y25oPmk9vWg1pathkY85+u+9zszMGhI+xhdFUSWnins=
|
||||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0 h1:6nNZFSlJ1dk2GujL8PFltfLz3vC6IbrpjGS4FTduo1s=
|
gocloud.dev/pubsub/rabbitpubsub v0.42.0 h1:eqpm8LGNAVkZ0J0/M/6LgazXI6dLcNWbivOby/Kuaag=
|
||||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0/go.mod h1:sEaueAGat+OASRoB3QDkghCtibKttgg7X6zsPTm1pl0=
|
gocloud.dev/pubsub/rabbitpubsub v0.42.0/go.mod h1:m3N1YQV8nXGepLuu/qPBtM8Rvey90Tw1uMhVf8GO37w=
|
||||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
@ -2287,8 +2287,8 @@ google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
|
||||||
google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
||||||
google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
||||||
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||||
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
|
google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE=
|
||||||
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
@ -2422,12 +2422,12 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ
|
||||||
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
|
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
|
||||||
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
|
|
@ -51,7 +51,7 @@ spec:
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.allInOne.topologySpreadConstraints }}
|
{{- if .Values.allInOne.topologySpreadConstraints }}
|
||||||
topologySpreadConstraints:
|
topologySpreadConstraints:
|
||||||
{{ tpl .Values.allInOne.topologySpreadConstraints . | nindent 8 | trim }}
|
{{ tpl .Values.allInOne.topologySpreadConstraint . | nindent 8 | trim }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.allInOne.tolerations }}
|
{{- if .Values.allInOne.tolerations }}
|
||||||
tolerations:
|
tolerations:
|
||||||
|
@ -142,9 +142,6 @@ spec:
|
||||||
{{- if .Values.allInOne.disableHttp }}
|
{{- if .Values.allInOne.disableHttp }}
|
||||||
-disableHttp={{ .Values.allInOne.disableHttp }} \
|
-disableHttp={{ .Values.allInOne.disableHttp }} \
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if and (.Values.volume.dataDirs) (index .Values.volume.dataDirs 0 "maxVolumes") }}
|
|
||||||
-volume.max={{ index .Values.volume.dataDirs 0 "maxVolumes" }} \
|
|
||||||
{{- end }}
|
|
||||||
-master.port={{ .Values.master.port }} \
|
-master.port={{ .Values.master.port }} \
|
||||||
{{- if .Values.global.enableReplication }}
|
{{- if .Values.global.enableReplication }}
|
||||||
-master.defaultReplication={{ .Values.global.replicationPlacement }} \
|
-master.defaultReplication={{ .Values.global.replicationPlacement }} \
|
||||||
|
|
|
@ -1,169 +0,0 @@
|
||||||
package basic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestS3ListDelimiterWithDirectoryKeyObjects tests the specific scenario from
|
|
||||||
// test_bucket_list_delimiter_not_skip_special where directory key objects
|
|
||||||
// should be properly grouped into common prefixes when using delimiters
|
|
||||||
func TestS3ListDelimiterWithDirectoryKeyObjects(t *testing.T) {
|
|
||||||
bucketName := fmt.Sprintf("test-delimiter-dir-key-%d", rand.Int31())
|
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
_, err := svc.CreateBucket(&s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer cleanupBucket(t, bucketName)
|
|
||||||
|
|
||||||
// Create objects matching the failing test scenario:
|
|
||||||
// ['0/'] + ['0/1000', '0/1001', '0/1002'] + ['1999', '1999#', '1999+', '2000']
|
|
||||||
objects := []string{
|
|
||||||
"0/", // Directory key object
|
|
||||||
"0/1000", // Objects under 0/ prefix
|
|
||||||
"0/1001",
|
|
||||||
"0/1002",
|
|
||||||
"1999", // Objects without delimiter
|
|
||||||
"1999#",
|
|
||||||
"1999+",
|
|
||||||
"2000",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create all objects
|
|
||||||
for _, key := range objects {
|
|
||||||
_, err := svc.PutObject(&s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
Body: strings.NewReader(fmt.Sprintf("content for %s", key)),
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Failed to create object %s", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with delimiter='/'
|
|
||||||
resp, err := svc.ListObjects(&s3.ListObjectsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Delimiter: aws.String("/"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Extract keys and prefixes
|
|
||||||
var keys []string
|
|
||||||
for _, content := range resp.Contents {
|
|
||||||
keys = append(keys, *content.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefixes []string
|
|
||||||
for _, prefix := range resp.CommonPrefixes {
|
|
||||||
prefixes = append(prefixes, *prefix.Prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expected results:
|
|
||||||
// Keys should be: ['1999', '1999#', '1999+', '2000'] (objects without delimiters)
|
|
||||||
// Prefixes should be: ['0/'] (grouping '0/' and all '0/xxxx' objects)
|
|
||||||
|
|
||||||
expectedKeys := []string{"1999", "1999#", "1999+", "2000"}
|
|
||||||
expectedPrefixes := []string{"0/"}
|
|
||||||
|
|
||||||
t.Logf("Actual keys: %v", keys)
|
|
||||||
t.Logf("Actual prefixes: %v", prefixes)
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, expectedKeys, keys, "Keys should only include objects without delimiters")
|
|
||||||
assert.ElementsMatch(t, expectedPrefixes, prefixes, "CommonPrefixes should group directory key object with other objects sharing prefix")
|
|
||||||
|
|
||||||
// Additional validation
|
|
||||||
assert.Equal(t, "/", *resp.Delimiter, "Delimiter should be set correctly")
|
|
||||||
assert.Contains(t, prefixes, "0/", "Directory key object '0/' should be grouped into common prefix '0/'")
|
|
||||||
assert.NotContains(t, keys, "0/", "Directory key object '0/' should NOT appear as individual key when delimiter is used")
|
|
||||||
|
|
||||||
// Verify none of the '0/xxxx' objects appear as individual keys
|
|
||||||
for _, key := range keys {
|
|
||||||
assert.False(t, strings.HasPrefix(key, "0/"), "No object with '0/' prefix should appear as individual key, found: %s", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestS3ListWithoutDelimiter tests that directory key objects appear as individual keys when no delimiter is used
|
|
||||||
func TestS3ListWithoutDelimiter(t *testing.T) {
|
|
||||||
bucketName := fmt.Sprintf("test-no-delimiter-%d", rand.Int31())
|
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
_, err := svc.CreateBucket(&s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer cleanupBucket(t, bucketName)
|
|
||||||
|
|
||||||
// Create objects
|
|
||||||
objects := []string{"0/", "0/1000", "1999"}
|
|
||||||
|
|
||||||
for _, key := range objects {
|
|
||||||
_, err := svc.PutObject(&s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
Body: strings.NewReader(fmt.Sprintf("content for %s", key)),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test without delimiter
|
|
||||||
resp, err := svc.ListObjects(&s3.ListObjectsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
// No delimiter specified
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Extract keys
|
|
||||||
var keys []string
|
|
||||||
for _, content := range resp.Contents {
|
|
||||||
keys = append(keys, *content.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When no delimiter is used, all objects should be returned as individual keys
|
|
||||||
expectedKeys := []string{"0/", "0/1000", "1999"}
|
|
||||||
assert.ElementsMatch(t, expectedKeys, keys, "All objects should be individual keys when no delimiter is used")
|
|
||||||
|
|
||||||
// No common prefixes should be present
|
|
||||||
assert.Empty(t, resp.CommonPrefixes, "No common prefixes should be present when no delimiter is used")
|
|
||||||
assert.Contains(t, keys, "0/", "Directory key object '0/' should appear as individual key when no delimiter is used")
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanupBucket(t *testing.T, bucketName string) {
|
|
||||||
// Delete all objects
|
|
||||||
resp, err := svc.ListObjects(&s3.ListObjectsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Failed to list objects for cleanup: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, obj := range resp.Contents {
|
|
||||||
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: obj.Key,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Failed to delete object %s: %v", *obj.Key, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give some time for eventual consistency
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Delete bucket
|
|
||||||
_, err = svc.DeleteBucket(&s3.DeleteBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Failed to delete bucket %s: %v", bucketName, err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -41,9 +40,6 @@ func TestCORSPreflightRequest(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to put CORS configuration")
|
require.NoError(t, err, "Should be able to put CORS configuration")
|
||||||
|
|
||||||
// Wait for metadata subscription to update cache
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Test preflight request with raw HTTP
|
// Test preflight request with raw HTTP
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
@ -73,29 +69,6 @@ func TestCORSPreflightRequest(t *testing.T) {
|
||||||
|
|
||||||
// TestCORSActualRequest tests CORS behavior with actual requests
|
// TestCORSActualRequest tests CORS behavior with actual requests
|
||||||
func TestCORSActualRequest(t *testing.T) {
|
func TestCORSActualRequest(t *testing.T) {
|
||||||
// Temporarily clear AWS environment variables to ensure truly anonymous requests
|
|
||||||
// This prevents AWS SDK from auto-signing requests in GitHub Actions
|
|
||||||
originalAccessKey := os.Getenv("AWS_ACCESS_KEY_ID")
|
|
||||||
originalSecretKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
|
|
||||||
originalSessionToken := os.Getenv("AWS_SESSION_TOKEN")
|
|
||||||
originalProfile := os.Getenv("AWS_PROFILE")
|
|
||||||
originalRegion := os.Getenv("AWS_REGION")
|
|
||||||
|
|
||||||
os.Setenv("AWS_ACCESS_KEY_ID", "")
|
|
||||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
|
|
||||||
os.Setenv("AWS_SESSION_TOKEN", "")
|
|
||||||
os.Setenv("AWS_PROFILE", "")
|
|
||||||
os.Setenv("AWS_REGION", "")
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
// Restore original environment variables
|
|
||||||
os.Setenv("AWS_ACCESS_KEY_ID", originalAccessKey)
|
|
||||||
os.Setenv("AWS_SECRET_ACCESS_KEY", originalSecretKey)
|
|
||||||
os.Setenv("AWS_SESSION_TOKEN", originalSessionToken)
|
|
||||||
os.Setenv("AWS_PROFILE", originalProfile)
|
|
||||||
os.Setenv("AWS_REGION", originalRegion)
|
|
||||||
}()
|
|
||||||
|
|
||||||
client := getS3Client(t)
|
client := getS3Client(t)
|
||||||
bucketName := createTestBucket(t, client)
|
bucketName := createTestBucket(t, client)
|
||||||
defer cleanupTestBucket(t, client, bucketName)
|
defer cleanupTestBucket(t, client, bucketName)
|
||||||
|
@ -119,9 +92,6 @@ func TestCORSActualRequest(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to put CORS configuration")
|
require.NoError(t, err, "Should be able to put CORS configuration")
|
||||||
|
|
||||||
// Wait for CORS configuration to be fully processed
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// First, put an object using S3 client
|
// First, put an object using S3 client
|
||||||
objectKey := "test-cors-object"
|
objectKey := "test-cors-object"
|
||||||
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
|
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||||
|
@ -132,75 +102,23 @@ func TestCORSActualRequest(t *testing.T) {
|
||||||
require.NoError(t, err, "Should be able to put object")
|
require.NoError(t, err, "Should be able to put object")
|
||||||
|
|
||||||
// Test GET request with CORS headers using raw HTTP
|
// Test GET request with CORS headers using raw HTTP
|
||||||
// Create a completely isolated HTTP client to avoid AWS SDK auto-signing
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
transport := &http.Transport{
|
|
||||||
// Completely disable any proxy or middleware
|
|
||||||
Proxy: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := &http.Client{
|
req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/%s", getDefaultConfig().Endpoint, bucketName, objectKey), nil)
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
// Use a completely clean transport to avoid any AWS SDK middleware
|
|
||||||
Transport: transport,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create URL manually to avoid any AWS SDK endpoint processing
|
|
||||||
// Use the same endpoint as the S3 client to ensure compatibility with GitHub Actions
|
|
||||||
config := getDefaultConfig()
|
|
||||||
endpoint := config.Endpoint
|
|
||||||
// Remove any protocol prefix and ensure it's http for anonymous requests
|
|
||||||
if strings.HasPrefix(endpoint, "https://") {
|
|
||||||
endpoint = strings.Replace(endpoint, "https://", "http://", 1)
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(endpoint, "http://") {
|
|
||||||
endpoint = "http://" + endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
requestURL := fmt.Sprintf("%s/%s/%s", endpoint, bucketName, objectKey)
|
|
||||||
req, err := http.NewRequest("GET", requestURL, nil)
|
|
||||||
require.NoError(t, err, "Should be able to create GET request")
|
require.NoError(t, err, "Should be able to create GET request")
|
||||||
|
|
||||||
// Add Origin header to simulate CORS request
|
// Add Origin header to simulate CORS request
|
||||||
req.Header.Set("Origin", "https://example.com")
|
req.Header.Set("Origin", "https://example.com")
|
||||||
|
|
||||||
// Explicitly ensure no AWS headers are present (defensive programming)
|
|
||||||
// Clear ALL potential AWS-related headers that might be auto-added
|
|
||||||
req.Header.Del("Authorization")
|
|
||||||
req.Header.Del("X-Amz-Content-Sha256")
|
|
||||||
req.Header.Del("X-Amz-Date")
|
|
||||||
req.Header.Del("Amz-Sdk-Invocation-Id")
|
|
||||||
req.Header.Del("Amz-Sdk-Request")
|
|
||||||
req.Header.Del("X-Amz-Security-Token")
|
|
||||||
req.Header.Del("X-Amz-Session-Token")
|
|
||||||
req.Header.Del("AWS-Session-Token")
|
|
||||||
req.Header.Del("X-Amz-Target")
|
|
||||||
req.Header.Del("X-Amz-User-Agent")
|
|
||||||
|
|
||||||
// Ensure User-Agent doesn't indicate AWS SDK
|
|
||||||
req.Header.Set("User-Agent", "anonymous-cors-test/1.0")
|
|
||||||
|
|
||||||
// Verify no AWS-related headers are present
|
|
||||||
for name := range req.Header {
|
|
||||||
headerLower := strings.ToLower(name)
|
|
||||||
if strings.Contains(headerLower, "aws") ||
|
|
||||||
strings.Contains(headerLower, "amz") ||
|
|
||||||
strings.Contains(headerLower, "authorization") {
|
|
||||||
t.Fatalf("Found AWS-related header in anonymous request: %s", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the request
|
// Send the request
|
||||||
resp, err := httpClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err, "Should be able to send GET request")
|
require.NoError(t, err, "Should be able to send GET request")
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Verify CORS headers are present
|
// Verify CORS headers in response
|
||||||
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header")
|
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header")
|
||||||
assert.Contains(t, resp.Header.Get("Access-Control-Expose-Headers"), "ETag", "Should expose ETag header")
|
assert.Contains(t, resp.Header.Get("Access-Control-Expose-Headers"), "ETag", "Should expose ETag header")
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "GET request should return 200")
|
||||||
// Anonymous requests should succeed when anonymous read permission is configured in IAM
|
|
||||||
// The server configuration allows anonymous users to have Read permissions
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Anonymous GET request should succeed when anonymous read is configured")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCORSOriginMatching tests origin matching with different patterns
|
// TestCORSOriginMatching tests origin matching with different patterns
|
||||||
|
@ -268,9 +186,6 @@ func TestCORSOriginMatching(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to put CORS configuration")
|
require.NoError(t, err, "Should be able to put CORS configuration")
|
||||||
|
|
||||||
// Wait for metadata subscription to update cache
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Test preflight request
|
// Test preflight request
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
@ -364,9 +279,6 @@ func TestCORSHeaderMatching(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to put CORS configuration")
|
require.NoError(t, err, "Should be able to put CORS configuration")
|
||||||
|
|
||||||
// Wait for metadata subscription to update cache
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Test preflight request
|
// Test preflight request
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
@ -448,9 +360,6 @@ func TestCORSMethodMatching(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to put CORS configuration")
|
require.NoError(t, err, "Should be able to put CORS configuration")
|
||||||
|
|
||||||
// Wait for metadata subscription to update cache
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
method string
|
method string
|
||||||
shouldAllow bool
|
shouldAllow bool
|
||||||
|
@ -522,9 +431,6 @@ func TestCORSMultipleRulesMatching(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to put CORS configuration")
|
require.NoError(t, err, "Should be able to put CORS configuration")
|
||||||
|
|
||||||
// Wait for metadata subscription to update cache
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Test first rule
|
// Test first rule
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
|
|
@ -78,9 +78,6 @@ func createTestBucket(t *testing.T, client *s3.Client) string {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Wait for bucket metadata to be fully processed
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
return bucketName
|
return bucketName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,9 +139,6 @@ func TestCORSConfigurationManagement(t *testing.T) {
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "Should be able to put CORS configuration")
|
assert.NoError(t, err, "Should be able to put CORS configuration")
|
||||||
|
|
||||||
// Wait for metadata subscription to update cache
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Test 3: Get CORS configuration
|
// Test 3: Get CORS configuration
|
||||||
getResp, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
getResp, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
|
@ -177,38 +171,14 @@ func TestCORSConfigurationManagement(t *testing.T) {
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
CORSConfiguration: updatedCorsConfig,
|
CORSConfiguration: updatedCorsConfig,
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to update CORS configuration")
|
assert.NoError(t, err, "Should be able to update CORS configuration")
|
||||||
|
|
||||||
// Wait for CORS configuration update to be fully processed
|
// Verify the update
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify the update with retries for robustness
|
|
||||||
var updateSuccess bool
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
getResp, err = client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
getResp, err = client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
})
|
})
|
||||||
if err != nil {
|
assert.NoError(t, err, "Should be able to get updated CORS configuration")
|
||||||
t.Logf("Attempt %d: Failed to get updated CORS config: %v", i+1, err)
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(getResp.CORSRules) > 0 {
|
|
||||||
rule = getResp.CORSRules[0]
|
rule = getResp.CORSRules[0]
|
||||||
// Check if the update actually took effect
|
|
||||||
if len(rule.AllowedHeaders) > 0 && rule.AllowedHeaders[0] == "Content-Type" &&
|
|
||||||
len(rule.AllowedOrigins) > 1 {
|
|
||||||
updateSuccess = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.Logf("Attempt %d: CORS config not updated yet, retrying...", i+1)
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err, "Should be able to get updated CORS configuration")
|
|
||||||
require.True(t, updateSuccess, "CORS configuration should be updated after retries")
|
|
||||||
assert.Equal(t, []string{"Content-Type"}, rule.AllowedHeaders, "Updated allowed headers should match")
|
assert.Equal(t, []string{"Content-Type"}, rule.AllowedHeaders, "Updated allowed headers should match")
|
||||||
assert.Equal(t, []string{"https://example.com", "https://another.com"}, rule.AllowedOrigins, "Updated allowed origins should match")
|
assert.Equal(t, []string{"https://example.com", "https://another.com"}, rule.AllowedOrigins, "Updated allowed origins should match")
|
||||||
|
|
||||||
|
@ -216,30 +186,13 @@ func TestCORSConfigurationManagement(t *testing.T) {
|
||||||
_, err = client.DeleteBucketCors(context.TODO(), &s3.DeleteBucketCorsInput{
|
_, err = client.DeleteBucketCors(context.TODO(), &s3.DeleteBucketCorsInput{
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to delete CORS configuration")
|
assert.NoError(t, err, "Should be able to delete CORS configuration")
|
||||||
|
|
||||||
// Wait for deletion to be fully processed
|
// Verify deletion
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Verify deletion - should get NoSuchCORSConfiguration error
|
|
||||||
_, err = client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
_, err = client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
})
|
})
|
||||||
|
assert.Error(t, err, "Should get error after deleting CORS configuration")
|
||||||
// Check that we get the expected error type
|
|
||||||
if err != nil {
|
|
||||||
// Log the error for debugging
|
|
||||||
t.Logf("Got expected error after CORS deletion: %v", err)
|
|
||||||
// Check if it's the correct error type (NoSuchCORSConfiguration)
|
|
||||||
errMsg := err.Error()
|
|
||||||
if !strings.Contains(errMsg, "NoSuchCORSConfiguration") && !strings.Contains(errMsg, "404") {
|
|
||||||
t.Errorf("Expected NoSuchCORSConfiguration error, got: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no error, this might be a SeaweedFS implementation difference
|
|
||||||
// Some implementations might return empty config instead of error
|
|
||||||
t.Logf("CORS deletion test: No error returned - this may be implementation-specific behavior")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCORSMultipleRules tests CORS configuration with multiple rules
|
// TestCORSMultipleRules tests CORS configuration with multiple rules
|
||||||
|
@ -279,30 +232,14 @@ func TestCORSMultipleRules(t *testing.T) {
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
CORSConfiguration: corsConfig,
|
CORSConfiguration: corsConfig,
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to put CORS configuration with multiple rules")
|
assert.NoError(t, err, "Should be able to put CORS configuration with multiple rules")
|
||||||
|
|
||||||
// Wait for CORS configuration to be fully processed
|
// Get and verify the configuration
|
||||||
time.Sleep(100 * time.Millisecond)
|
getResp, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||||
|
|
||||||
// Get and verify the configuration with retries for robustness
|
|
||||||
var getResp *s3.GetBucketCorsOutput
|
|
||||||
var getErr error
|
|
||||||
|
|
||||||
// Retry getting CORS config up to 3 times to handle timing issues
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
getResp, getErr = client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
})
|
})
|
||||||
if getErr == nil {
|
assert.NoError(t, err, "Should be able to get CORS configuration")
|
||||||
break
|
assert.Len(t, getResp.CORSRules, 3, "Should have three CORS rules")
|
||||||
}
|
|
||||||
t.Logf("Attempt %d: Failed to get multiple rules CORS config: %v", i+1, getErr)
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, getErr, "Should be able to get CORS configuration after retries")
|
|
||||||
require.NotNil(t, getResp, "GetBucketCors response should not be nil")
|
|
||||||
require.Len(t, getResp.CORSRules, 3, "Should have three CORS rules")
|
|
||||||
|
|
||||||
// Verify first rule
|
// Verify first rule
|
||||||
rule1 := getResp.CORSRules[0]
|
rule1 := getResp.CORSRules[0]
|
||||||
|
@ -405,33 +342,16 @@ func TestCORSWithWildcards(t *testing.T) {
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
CORSConfiguration: corsConfig,
|
CORSConfiguration: corsConfig,
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "Should be able to put CORS configuration with wildcards")
|
assert.NoError(t, err, "Should be able to put CORS configuration with wildcards")
|
||||||
|
|
||||||
// Wait for CORS configuration to be fully processed and available
|
// Get and verify the configuration
|
||||||
time.Sleep(100 * time.Millisecond)
|
getResp, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||||
|
|
||||||
// Get and verify the configuration with retries for robustness
|
|
||||||
var getResp *s3.GetBucketCorsOutput
|
|
||||||
var getErr error
|
|
||||||
|
|
||||||
// Retry getting CORS config up to 3 times to handle timing issues
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
getResp, getErr = client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
})
|
})
|
||||||
if getErr == nil {
|
assert.NoError(t, err, "Should be able to get CORS configuration")
|
||||||
break
|
assert.Len(t, getResp.CORSRules, 1, "Should have one CORS rule")
|
||||||
}
|
|
||||||
t.Logf("Attempt %d: Failed to get CORS config: %v", i+1, getErr)
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, getErr, "Should be able to get CORS configuration after retries")
|
|
||||||
require.NotNil(t, getResp, "GetBucketCors response should not be nil")
|
|
||||||
require.Len(t, getResp.CORSRules, 1, "Should have one CORS rule")
|
|
||||||
|
|
||||||
rule := getResp.CORSRules[0]
|
rule := getResp.CORSRules[0]
|
||||||
require.NotNil(t, rule, "CORS rule should not be nil")
|
|
||||||
assert.Equal(t, []string{"*"}, rule.AllowedHeaders, "Wildcard headers should be preserved")
|
assert.Equal(t, []string{"*"}, rule.AllowedHeaders, "Wildcard headers should be preserved")
|
||||||
assert.Equal(t, []string{"https://*.example.com"}, rule.AllowedOrigins, "Wildcard origins should be preserved")
|
assert.Equal(t, []string{"https://*.example.com"}, rule.AllowedOrigins, "Wildcard origins should be preserved")
|
||||||
assert.Equal(t, []string{"*"}, rule.ExposeHeaders, "Wildcard expose headers should be preserved")
|
assert.Equal(t, []string{"*"}, rule.ExposeHeaders, "Wildcard expose headers should be preserved")
|
||||||
|
@ -592,9 +512,6 @@ func TestCORSCaching(t *testing.T) {
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "Should be able to put initial CORS configuration")
|
assert.NoError(t, err, "Should be able to put initial CORS configuration")
|
||||||
|
|
||||||
// Wait for metadata subscription to update cache
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Get the configuration
|
// Get the configuration
|
||||||
getResp1, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
getResp1, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
|
@ -620,9 +537,6 @@ func TestCORSCaching(t *testing.T) {
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "Should be able to update CORS configuration")
|
assert.NoError(t, err, "Should be able to update CORS configuration")
|
||||||
|
|
||||||
// Wait for metadata subscription to update cache
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Get the updated configuration (should reflect the changes)
|
// Get the updated configuration (should reflect the changes)
|
||||||
getResp2, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
getResp2, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||||
Bucket: aws.String(bucketName),
|
Bucket: aws.String(bucketName),
|
||||||
|
|
|
@ -1,861 +0,0 @@
|
||||||
package s3api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/config"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestListObjectVersionsIncludesDirectories tests that directories are included in list-object-versions response
|
|
||||||
// This ensures compatibility with Minio and AWS S3 behavior
|
|
||||||
func TestListObjectVersionsIncludesDirectories(t *testing.T) {
|
|
||||||
bucketName := "test-versioning-directories"
|
|
||||||
|
|
||||||
client := setupS3Client(t)
|
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
defer func() {
|
|
||||||
cleanupBucket(t, client, bucketName)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Enable versioning
|
|
||||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
VersioningConfiguration: &types.VersioningConfiguration{
|
|
||||||
Status: types.BucketVersioningStatusEnabled,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// First create explicit directory objects (keys ending with "/")
|
|
||||||
// These are the directories that should appear in list-object-versions
|
|
||||||
explicitDirectories := []string{
|
|
||||||
"Veeam/",
|
|
||||||
"Veeam/Archive/",
|
|
||||||
"Veeam/Archive/vbr/",
|
|
||||||
"Veeam/Backup/",
|
|
||||||
"Veeam/Backup/vbr/",
|
|
||||||
"Veeam/Backup/vbr/Clients/",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create explicit directory objects
|
|
||||||
for _, dirKey := range explicitDirectories {
|
|
||||||
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(dirKey),
|
|
||||||
Body: strings.NewReader(""), // Empty content for directories
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Failed to create directory object %s", dirKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now create some test files
|
|
||||||
testFiles := []string{
|
|
||||||
"Veeam/test-file.txt",
|
|
||||||
"Veeam/Archive/test-file2.txt",
|
|
||||||
"Veeam/Archive/vbr/test-file3.txt",
|
|
||||||
"Veeam/Backup/test-file4.txt",
|
|
||||||
"Veeam/Backup/vbr/test-file5.txt",
|
|
||||||
"Veeam/Backup/vbr/Clients/test-file6.txt",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload test files
|
|
||||||
for _, objectKey := range testFiles {
|
|
||||||
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader("test content"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Failed to create file %s", objectKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List object versions
|
|
||||||
listResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Extract all keys from versions
|
|
||||||
var allKeys []string
|
|
||||||
for _, version := range listResp.Versions {
|
|
||||||
allKeys = append(allKeys, *version.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expected directories that should be included (with trailing slash)
|
|
||||||
expectedDirectories := []string{
|
|
||||||
"Veeam/",
|
|
||||||
"Veeam/Archive/",
|
|
||||||
"Veeam/Archive/vbr/",
|
|
||||||
"Veeam/Backup/",
|
|
||||||
"Veeam/Backup/vbr/",
|
|
||||||
"Veeam/Backup/vbr/Clients/",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that directories are included in the response
|
|
||||||
t.Logf("Found %d total versions", len(listResp.Versions))
|
|
||||||
t.Logf("All keys: %v", allKeys)
|
|
||||||
|
|
||||||
for _, expectedDir := range expectedDirectories {
|
|
||||||
found := false
|
|
||||||
for _, version := range listResp.Versions {
|
|
||||||
if *version.Key == expectedDir {
|
|
||||||
found = true
|
|
||||||
// Verify directory properties
|
|
||||||
assert.Equal(t, "null", *version.VersionId, "Directory %s should have VersionId 'null'", expectedDir)
|
|
||||||
assert.Equal(t, int64(0), *version.Size, "Directory %s should have size 0", expectedDir)
|
|
||||||
assert.True(t, *version.IsLatest, "Directory %s should be marked as latest", expectedDir)
|
|
||||||
assert.Equal(t, "\"d41d8cd98f00b204e9800998ecf8427e\"", *version.ETag, "Directory %s should have MD5 of empty string as ETag", expectedDir)
|
|
||||||
assert.Equal(t, types.ObjectStorageClassStandard, version.StorageClass, "Directory %s should have STANDARD storage class", expectedDir)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "Directory %s should be included in list-object-versions response", expectedDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also verify that actual files are included
|
|
||||||
for _, objectKey := range testFiles {
|
|
||||||
found := false
|
|
||||||
for _, version := range listResp.Versions {
|
|
||||||
if *version.Key == objectKey {
|
|
||||||
found = true
|
|
||||||
assert.NotEqual(t, "null", *version.VersionId, "File %s should have a real version ID", objectKey)
|
|
||||||
assert.Greater(t, *version.Size, int64(0), "File %s should have size > 0", objectKey)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found, "File %s should be included in list-object-versions response", objectKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count directories vs files
|
|
||||||
directoryCount := 0
|
|
||||||
fileCount := 0
|
|
||||||
for _, version := range listResp.Versions {
|
|
||||||
if strings.HasSuffix(*version.Key, "/") && *version.Size == 0 && *version.VersionId == "null" {
|
|
||||||
directoryCount++
|
|
||||||
} else {
|
|
||||||
fileCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Found %d directories and %d files", directoryCount, fileCount)
|
|
||||||
assert.Equal(t, len(expectedDirectories), directoryCount, "Should find exactly %d directories", len(expectedDirectories))
|
|
||||||
assert.Equal(t, len(testFiles), fileCount, "Should find exactly %d files", len(testFiles))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestListObjectVersionsDeleteMarkers tests that delete markers are properly separated from versions
|
|
||||||
// This test verifies the fix for the issue where delete markers were incorrectly categorized as versions
|
|
||||||
func TestListObjectVersionsDeleteMarkers(t *testing.T) {
|
|
||||||
bucketName := "test-delete-markers"
|
|
||||||
|
|
||||||
client := setupS3Client(t)
|
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
defer func() {
|
|
||||||
cleanupBucket(t, client, bucketName)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Enable versioning
|
|
||||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
VersioningConfiguration: &types.VersioningConfiguration{
|
|
||||||
Status: types.BucketVersioningStatusEnabled,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
objectKey := "test1/a"
|
|
||||||
|
|
||||||
// 1. Create one version of the file
|
|
||||||
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader("test content"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// 2. Delete the object 3 times to create 3 delete markers
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. List object versions and verify the response structure
|
|
||||||
listResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// 4. Verify that we have exactly 1 version and 3 delete markers
|
|
||||||
assert.Len(t, listResp.Versions, 1, "Should have exactly 1 file version")
|
|
||||||
assert.Len(t, listResp.DeleteMarkers, 3, "Should have exactly 3 delete markers")
|
|
||||||
|
|
||||||
// 5. Verify the version is for our test file
|
|
||||||
version := listResp.Versions[0]
|
|
||||||
assert.Equal(t, objectKey, *version.Key, "Version should be for our test file")
|
|
||||||
assert.NotEqual(t, "null", *version.VersionId, "File version should have a real version ID")
|
|
||||||
assert.Greater(t, *version.Size, int64(0), "File version should have size > 0")
|
|
||||||
|
|
||||||
// 6. Verify all delete markers are for our test file
|
|
||||||
for i, deleteMarker := range listResp.DeleteMarkers {
|
|
||||||
assert.Equal(t, objectKey, *deleteMarker.Key, "Delete marker %d should be for our test file", i)
|
|
||||||
assert.NotEqual(t, "null", *deleteMarker.VersionId, "Delete marker %d should have a real version ID", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Successfully verified: 1 version + 3 delete markers for object %s", objectKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVersionedObjectAcl tests that ACL operations work correctly on objects in versioned buckets
|
|
||||||
// This test verifies the fix for the NoSuchKey error when getting ACLs for objects in versioned buckets
|
|
||||||
func TestVersionedObjectAcl(t *testing.T) {
|
|
||||||
bucketName := "test-versioned-acl"
|
|
||||||
|
|
||||||
client := setupS3Client(t)
|
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
defer func() {
|
|
||||||
cleanupBucket(t, client, bucketName)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Enable versioning
|
|
||||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
VersioningConfiguration: &types.VersioningConfiguration{
|
|
||||||
Status: types.BucketVersioningStatusEnabled,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
objectKey := "test-acl-object"
|
|
||||||
|
|
||||||
// Create an object in the versioned bucket
|
|
||||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader("test content for ACL"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, putResp.VersionId, "Object should have a version ID")
|
|
||||||
|
|
||||||
// Test 1: Get ACL for the object (without specifying version ID - should get latest version)
|
|
||||||
getAclResp, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Should be able to get ACL for object in versioned bucket")
|
|
||||||
require.NotNil(t, getAclResp.Owner, "ACL response should have owner information")
|
|
||||||
|
|
||||||
// Test 2: Get ACL for specific version ID
|
|
||||||
getAclVersionResp, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
VersionId: putResp.VersionId,
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Should be able to get ACL for specific version")
|
|
||||||
require.NotNil(t, getAclVersionResp.Owner, "Versioned ACL response should have owner information")
|
|
||||||
|
|
||||||
// Test 3: Verify both ACL responses are the same (same object, same version)
|
|
||||||
assert.Equal(t, getAclResp.Owner.ID, getAclVersionResp.Owner.ID, "Owner ID should match for latest and specific version")
|
|
||||||
|
|
||||||
// Test 4: Create another version of the same object
|
|
||||||
putResp2, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader("updated content for ACL"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, putResp2.VersionId, "Second object version should have a version ID")
|
|
||||||
require.NotEqual(t, putResp.VersionId, putResp2.VersionId, "Version IDs should be different")
|
|
||||||
|
|
||||||
// Test 5: Get ACL for latest version (should be the second version)
|
|
||||||
getAclLatestResp, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Should be able to get ACL for latest version after update")
|
|
||||||
require.NotNil(t, getAclLatestResp.Owner, "Latest ACL response should have owner information")
|
|
||||||
|
|
||||||
// Test 6: Get ACL for the first version specifically
|
|
||||||
getAclFirstResp, err := client.GetObjectAcl(context.TODO(), &s3.GetObjectAclInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
VersionId: putResp.VersionId,
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Should be able to get ACL for first version specifically")
|
|
||||||
require.NotNil(t, getAclFirstResp.Owner, "First version ACL response should have owner information")
|
|
||||||
|
|
||||||
// Test 7: Verify we can put ACL on versioned objects
|
|
||||||
_, err = client.PutObjectAcl(context.TODO(), &s3.PutObjectAclInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
ACL: types.ObjectCannedACLPrivate,
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Should be able to put ACL on versioned object")
|
|
||||||
|
|
||||||
t.Logf("Successfully verified ACL operations on versioned object %s with versions %s and %s",
|
|
||||||
objectKey, *putResp.VersionId, *putResp2.VersionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConcurrentMultiObjectDelete tests that concurrent delete operations work correctly without race conditions
|
|
||||||
// This test verifies the fix for the race condition in deleteSpecificObjectVersion
|
|
||||||
func TestConcurrentMultiObjectDelete(t *testing.T) {
|
|
||||||
bucketName := "test-concurrent-delete"
|
|
||||||
numObjects := 5
|
|
||||||
numThreads := 5
|
|
||||||
|
|
||||||
client := setupS3Client(t)
|
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
defer func() {
|
|
||||||
cleanupBucket(t, client, bucketName)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Enable versioning
|
|
||||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
VersioningConfiguration: &types.VersioningConfiguration{
|
|
||||||
Status: types.BucketVersioningStatusEnabled,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Create objects
|
|
||||||
var objectKeys []string
|
|
||||||
var versionIds []string
|
|
||||||
|
|
||||||
for i := 0; i < numObjects; i++ {
|
|
||||||
objectKey := fmt.Sprintf("key_%d", i)
|
|
||||||
objectKeys = append(objectKeys, objectKey)
|
|
||||||
|
|
||||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader(fmt.Sprintf("content for key_%d", i)),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, putResp.VersionId)
|
|
||||||
versionIds = append(versionIds, *putResp.VersionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify objects were created
|
|
||||||
listResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, listResp.Versions, numObjects, "Should have created %d objects", numObjects)
|
|
||||||
|
|
||||||
// Create delete objects request
|
|
||||||
var objectsToDelete []types.ObjectIdentifier
|
|
||||||
for i, objectKey := range objectKeys {
|
|
||||||
objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
VersionId: aws.String(versionIds[i]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run concurrent delete operations
|
|
||||||
results := make([]*s3.DeleteObjectsOutput, numThreads)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for i := 0; i < numThreads; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(threadIdx int) {
|
|
||||||
defer wg.Done()
|
|
||||||
deleteResp, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Delete: &types.Delete{
|
|
||||||
Objects: objectsToDelete,
|
|
||||||
Quiet: aws.Bool(false),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Thread %d: delete objects failed: %v", threadIdx, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
results[threadIdx] = deleteResp
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
for i, result := range results {
|
|
||||||
require.NotNil(t, result, "Thread %d should have a result", i)
|
|
||||||
assert.Len(t, result.Deleted, numObjects, "Thread %d should have deleted all %d objects", i, numObjects)
|
|
||||||
|
|
||||||
if len(result.Errors) > 0 {
|
|
||||||
for _, deleteError := range result.Errors {
|
|
||||||
t.Errorf("Thread %d delete error: %s - %s (Key: %s, VersionId: %s)",
|
|
||||||
i, *deleteError.Code, *deleteError.Message, *deleteError.Key,
|
|
||||||
func() string {
|
|
||||||
if deleteError.VersionId != nil {
|
|
||||||
return *deleteError.VersionId
|
|
||||||
} else {
|
|
||||||
return "nil"
|
|
||||||
}
|
|
||||||
}())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.Empty(t, result.Errors, "Thread %d should have no delete errors", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify objects are deleted (bucket should be empty)
|
|
||||||
finalListResp, err := client.ListObjects(context.TODO(), &s3.ListObjectsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, finalListResp.Contents, "Bucket should be empty after all deletions")
|
|
||||||
|
|
||||||
t.Logf("Successfully verified concurrent deletion of %d objects from %d threads", numObjects, numThreads)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSuspendedVersioningDeleteBehavior tests that delete operations during suspended versioning
|
|
||||||
// actually delete the "null" version object rather than creating delete markers
|
|
||||||
func TestSuspendedVersioningDeleteBehavior(t *testing.T) {
|
|
||||||
bucketName := "test-suspended-versioning-delete"
|
|
||||||
objectKey := "testobj"
|
|
||||||
|
|
||||||
client := setupS3Client(t)
|
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
defer func() {
|
|
||||||
cleanupBucket(t, client, bucketName)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Enable versioning and create some versions
|
|
||||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
VersioningConfiguration: &types.VersioningConfiguration{
|
|
||||||
Status: types.BucketVersioningStatusEnabled,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Create 3 versions
|
|
||||||
var versionIds []string
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader(fmt.Sprintf("content version %d", i+1)),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, putResp.VersionId)
|
|
||||||
versionIds = append(versionIds, *putResp.VersionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify 3 versions exist
|
|
||||||
listResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, listResp.Versions, 3, "Should have 3 versions initially")
|
|
||||||
|
|
||||||
// Suspend versioning
|
|
||||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
VersioningConfiguration: &types.VersioningConfiguration{
|
|
||||||
Status: types.BucketVersioningStatusSuspended,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Create a new object during suspended versioning (this should be a "null" version)
|
|
||||||
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader("null version content"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify we still have 3 versions + 1 null version = 4 total
|
|
||||||
listResp, err = client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, listResp.Versions, 4, "Should have 3 versions + 1 null version")
|
|
||||||
|
|
||||||
// Find the null version
|
|
||||||
var nullVersionFound bool
|
|
||||||
for _, version := range listResp.Versions {
|
|
||||||
if *version.VersionId == "null" {
|
|
||||||
nullVersionFound = true
|
|
||||||
assert.True(t, *version.IsLatest, "Null version should be marked as latest during suspended versioning")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, nullVersionFound, "Should have found a null version")
|
|
||||||
|
|
||||||
// Delete the object during suspended versioning (should actually delete the null version)
|
|
||||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
// No VersionId specified - should delete the "null" version during suspended versioning
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify the null version was actually deleted (not a delete marker created)
|
|
||||||
listResp, err = client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, listResp.Versions, 3, "Should be back to 3 versions after deleting null version")
|
|
||||||
assert.Empty(t, listResp.DeleteMarkers, "Should have no delete markers during suspended versioning delete")
|
|
||||||
|
|
||||||
// Verify null version is gone
|
|
||||||
nullVersionFound = false
|
|
||||||
for _, version := range listResp.Versions {
|
|
||||||
if *version.VersionId == "null" {
|
|
||||||
nullVersionFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.False(t, nullVersionFound, "Null version should be deleted, not present")
|
|
||||||
|
|
||||||
// Create another null version and delete it multiple times to test idempotency
|
|
||||||
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader("another null version"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Delete it twice to test idempotency
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "Delete should be idempotent - iteration %d", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-enable versioning
|
|
||||||
_, err = client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
VersioningConfiguration: &types.VersioningConfiguration{
|
|
||||||
Status: types.BucketVersioningStatusEnabled,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Create a new version with versioning enabled
|
|
||||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader("new version after re-enabling"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, putResp.VersionId)
|
|
||||||
|
|
||||||
// Now delete without version ID (should create delete marker)
|
|
||||||
deleteResp, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "true", deleteResp.DeleteMarker, "Should create delete marker when versioning is enabled")
|
|
||||||
|
|
||||||
// Verify final state
|
|
||||||
listResp, err = client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, listResp.Versions, 4, "Should have 3 original versions + 1 new version")
|
|
||||||
assert.Len(t, listResp.DeleteMarkers, 1, "Should have 1 delete marker")
|
|
||||||
|
|
||||||
t.Logf("Successfully verified suspended versioning delete behavior")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVersionedObjectListBehavior tests that list operations show logical object names for versioned objects
|
|
||||||
// and that owner information is properly extracted from S3 metadata
|
|
||||||
func TestVersionedObjectListBehavior(t *testing.T) {
|
|
||||||
bucketName := "test-versioned-list"
|
|
||||||
objectKey := "testfile"
|
|
||||||
|
|
||||||
client := setupS3Client(t)
|
|
||||||
|
|
||||||
// Create bucket with object lock enabled (which enables versioning)
|
|
||||||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
ObjectLockEnabledForBucket: aws.Bool(true),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
defer func() {
|
|
||||||
cleanupBucket(t, client, bucketName)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Verify versioning is enabled
|
|
||||||
versioningResp, err := client.GetBucketVersioning(context.TODO(), &s3.GetBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, types.BucketVersioningStatusEnabled, versioningResp.Status, "Bucket versioning should be enabled")
|
|
||||||
|
|
||||||
// Create a versioned object
|
|
||||||
content := "test content for versioned object"
|
|
||||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader(content),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, putResp.VersionId)
|
|
||||||
|
|
||||||
versionId := *putResp.VersionId
|
|
||||||
t.Logf("Created versioned object with version ID: %s", versionId)
|
|
||||||
|
|
||||||
// Test list-objects operation - should show logical object name, not internal versioned path
|
|
||||||
listResp, err := client.ListObjects(context.TODO(), &s3.ListObjectsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, listResp.Contents, 1, "Should list exactly one object")
|
|
||||||
|
|
||||||
listedObject := listResp.Contents[0]
|
|
||||||
|
|
||||||
// Verify the object key is the logical name, not the internal versioned path
|
|
||||||
assert.Equal(t, objectKey, *listedObject.Key, "Should show logical object name, not internal versioned path")
|
|
||||||
assert.NotContains(t, *listedObject.Key, ".versions", "Object key should not contain .versions")
|
|
||||||
assert.NotContains(t, *listedObject.Key, versionId, "Object key should not contain version ID")
|
|
||||||
|
|
||||||
// Verify object properties
|
|
||||||
assert.Equal(t, int64(len(content)), listedObject.Size, "Object size should match")
|
|
||||||
assert.NotNil(t, listedObject.ETag, "Object should have ETag")
|
|
||||||
assert.NotNil(t, listedObject.LastModified, "Object should have LastModified")
|
|
||||||
|
|
||||||
// Verify owner information is present (even if anonymous)
|
|
||||||
require.NotNil(t, listedObject.Owner, "Object should have Owner information")
|
|
||||||
assert.NotEmpty(t, listedObject.Owner.ID, "Owner ID should not be empty")
|
|
||||||
assert.NotEmpty(t, listedObject.Owner.DisplayName, "Owner DisplayName should not be empty")
|
|
||||||
|
|
||||||
t.Logf("Listed object: Key=%s, Size=%d, Owner.ID=%s, Owner.DisplayName=%s",
|
|
||||||
*listedObject.Key, listedObject.Size, *listedObject.Owner.ID, *listedObject.Owner.DisplayName)
|
|
||||||
|
|
||||||
// Test list-objects-v2 operation as well
|
|
||||||
listV2Resp, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
FetchOwner: aws.Bool(true), // Explicitly request owner information
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, listV2Resp.Contents, 1, "ListObjectsV2 should also list exactly one object")
|
|
||||||
|
|
||||||
listedObjectV2 := listV2Resp.Contents[0]
|
|
||||||
assert.Equal(t, objectKey, *listedObjectV2.Key, "ListObjectsV2 should also show logical object name")
|
|
||||||
assert.NotNil(t, listedObjectV2.Owner, "ListObjectsV2 should include owner when FetchOwner=true")
|
|
||||||
|
|
||||||
// Create another version to ensure multiple versions don't appear in regular list
|
|
||||||
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: strings.NewReader("updated content"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// List again - should still show only one logical object (the latest version)
|
|
||||||
listRespAfterUpdate, err := client.ListObjects(context.TODO(), &s3.ListObjectsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, listRespAfterUpdate.Contents, 1, "Should still list exactly one object after creating second version")
|
|
||||||
|
|
||||||
// Compare with list-object-versions which should show both versions
|
|
||||||
versionsResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, versionsResp.Versions, 2, "list-object-versions should show both versions")
|
|
||||||
|
|
||||||
t.Logf("Successfully verified versioned object list behavior")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPrefixFilteringLogic tests the prefix filtering logic fix for list object versions
|
|
||||||
// This addresses the issue raised by gemini-code-assist bot where files could be incorrectly included
|
|
||||||
func TestPrefixFilteringLogic(t *testing.T) {
|
|
||||||
s3Client := setupS3Client(t)
|
|
||||||
bucketName := "test-bucket-" + fmt.Sprintf("%d", time.Now().UnixNano())
|
|
||||||
|
|
||||||
// Create bucket
|
|
||||||
_, err := s3Client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer cleanupBucket(t, s3Client, bucketName)
|
|
||||||
|
|
||||||
// Enable versioning
|
|
||||||
_, err = s3Client.PutBucketVersioning(context.Background(), &s3.PutBucketVersioningInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
VersioningConfiguration: &types.VersioningConfiguration{
|
|
||||||
Status: types.BucketVersioningStatusEnabled,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Create test files that could trigger the edge case:
|
|
||||||
// - File "a" (which should NOT be included when searching for prefix "a/b")
|
|
||||||
// - File "a/b" (which SHOULD be included when searching for prefix "a/b")
|
|
||||||
_, err = s3Client.PutObject(context.Background(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String("a"),
|
|
||||||
Body: strings.NewReader("content of file a"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = s3Client.PutObject(context.Background(), &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String("a/b"),
|
|
||||||
Body: strings.NewReader("content of file a/b"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Test list-object-versions with prefix "a/b" - should NOT include file "a"
|
|
||||||
versionsResponse, err := s3Client.ListObjectVersions(context.Background(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Prefix: aws.String("a/b"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify that only "a/b" is returned, not "a"
|
|
||||||
require.Len(t, versionsResponse.Versions, 1, "Should only find one version matching prefix 'a/b'")
|
|
||||||
assert.Equal(t, "a/b", aws.ToString(versionsResponse.Versions[0].Key), "Should only return 'a/b', not 'a'")
|
|
||||||
|
|
||||||
// Test list-object-versions with prefix "a/" - should include "a/b" but not "a"
|
|
||||||
versionsResponse, err = s3Client.ListObjectVersions(context.Background(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Prefix: aws.String("a/"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify that only "a/b" is returned, not "a"
|
|
||||||
require.Len(t, versionsResponse.Versions, 1, "Should only find one version matching prefix 'a/'")
|
|
||||||
assert.Equal(t, "a/b", aws.ToString(versionsResponse.Versions[0].Key), "Should only return 'a/b', not 'a'")
|
|
||||||
|
|
||||||
// Test list-object-versions with prefix "a" - should include both "a" and "a/b"
|
|
||||||
versionsResponse, err = s3Client.ListObjectVersions(context.Background(), &s3.ListObjectVersionsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Prefix: aws.String("a"),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Should find both files
|
|
||||||
require.Len(t, versionsResponse.Versions, 2, "Should find both versions matching prefix 'a'")
|
|
||||||
|
|
||||||
// Extract keys and sort them for predictable comparison
|
|
||||||
var keys []string
|
|
||||||
for _, version := range versionsResponse.Versions {
|
|
||||||
keys = append(keys, aws.ToString(version.Key))
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"a", "a/b"}, keys, "Should return both 'a' and 'a/b'")
|
|
||||||
|
|
||||||
t.Logf("✅ Prefix filtering logic correctly handles edge cases")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to setup S3 client
|
|
||||||
func setupS3Client(t *testing.T) *s3.Client {
|
|
||||||
// S3TestConfig holds configuration for S3 tests
|
|
||||||
type S3TestConfig struct {
|
|
||||||
Endpoint string
|
|
||||||
AccessKey string
|
|
||||||
SecretKey string
|
|
||||||
Region string
|
|
||||||
BucketPrefix string
|
|
||||||
UseSSL bool
|
|
||||||
SkipVerifySSL bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default test configuration - should match s3tests.conf
|
|
||||||
defaultConfig := &S3TestConfig{
|
|
||||||
Endpoint: "http://localhost:8333", // Default SeaweedFS S3 port
|
|
||||||
AccessKey: "some_access_key1",
|
|
||||||
SecretKey: "some_secret_key1",
|
|
||||||
Region: "us-east-1",
|
|
||||||
BucketPrefix: "test-versioning-",
|
|
||||||
UseSSL: false,
|
|
||||||
SkipVerifySSL: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.LoadDefaultConfig(context.TODO(),
|
|
||||||
config.WithRegion(defaultConfig.Region),
|
|
||||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
|
||||||
defaultConfig.AccessKey,
|
|
||||||
defaultConfig.SecretKey,
|
|
||||||
"",
|
|
||||||
)),
|
|
||||||
config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(
|
|
||||||
func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
|
||||||
return aws.Endpoint{
|
|
||||||
URL: defaultConfig.Endpoint,
|
|
||||||
SigningRegion: defaultConfig.Region,
|
|
||||||
HostnameImmutable: true,
|
|
||||||
}, nil
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return s3.NewFromConfig(cfg, func(o *s3.Options) {
|
|
||||||
o.UsePathStyle = true // Important for SeaweedFS
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to clean up bucket
|
|
||||||
func cleanupBucket(t *testing.T, client *s3.Client, bucketName string) {
|
|
||||||
// First, delete all objects and versions
|
|
||||||
err := deleteAllObjectVersions(t, client, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Warning: failed to delete all object versions: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then delete the bucket
|
|
||||||
_, err = client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Warning: failed to delete bucket %s: %v", bucketName, err)
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
|
@ -160,14 +160,6 @@ var cmdS3 = &Command{
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
Alternatively, you can use environment variables as fallback admin credentials:
|
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key weed s3
|
|
||||||
|
|
||||||
Environment variables are only used when no S3 configuration file is provided
|
|
||||||
and no configuration is available from the filer. This provides a simple way
|
|
||||||
to get started without requiring configuration files.
|
|
||||||
|
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,24 +33,3 @@ message S3CircuitBreakerOptions {
|
||||||
bool enabled=1;
|
bool enabled=1;
|
||||||
map<string, int64> actions = 2;
|
map<string, int64> actions = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
|
||||||
// Bucket Metadata
|
|
||||||
|
|
||||||
message CORSRule {
|
|
||||||
repeated string allowed_headers = 1;
|
|
||||||
repeated string allowed_methods = 2;
|
|
||||||
repeated string allowed_origins = 3;
|
|
||||||
repeated string expose_headers = 4;
|
|
||||||
int32 max_age_seconds = 5;
|
|
||||||
string id = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CORSConfiguration {
|
|
||||||
repeated CORSRule cors_rules = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message BucketMetadata {
|
|
||||||
map<string, string> tags = 1;
|
|
||||||
CORSConfiguration cors = 2;
|
|
||||||
}
|
|
||||||
|
|
|
@ -205,186 +205,6 @@ func (x *S3CircuitBreakerOptions) GetActions() map[string]int64 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CORSRule struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
AllowedHeaders []string `protobuf:"bytes,1,rep,name=allowed_headers,json=allowedHeaders,proto3" json:"allowed_headers,omitempty"`
|
|
||||||
AllowedMethods []string `protobuf:"bytes,2,rep,name=allowed_methods,json=allowedMethods,proto3" json:"allowed_methods,omitempty"`
|
|
||||||
AllowedOrigins []string `protobuf:"bytes,3,rep,name=allowed_origins,json=allowedOrigins,proto3" json:"allowed_origins,omitempty"`
|
|
||||||
ExposeHeaders []string `protobuf:"bytes,4,rep,name=expose_headers,json=exposeHeaders,proto3" json:"expose_headers,omitempty"`
|
|
||||||
MaxAgeSeconds int32 `protobuf:"varint,5,opt,name=max_age_seconds,json=maxAgeSeconds,proto3" json:"max_age_seconds,omitempty"`
|
|
||||||
Id string `protobuf:"bytes,6,opt,name=id,proto3" json:"id,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSRule) Reset() {
|
|
||||||
*x = CORSRule{}
|
|
||||||
mi := &file_s3_proto_msgTypes[4]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSRule) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*CORSRule) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *CORSRule) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_s3_proto_msgTypes[4]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use CORSRule.ProtoReflect.Descriptor instead.
|
|
||||||
func (*CORSRule) Descriptor() ([]byte, []int) {
|
|
||||||
return file_s3_proto_rawDescGZIP(), []int{4}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSRule) GetAllowedHeaders() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.AllowedHeaders
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSRule) GetAllowedMethods() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.AllowedMethods
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSRule) GetAllowedOrigins() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.AllowedOrigins
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSRule) GetExposeHeaders() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.ExposeHeaders
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSRule) GetMaxAgeSeconds() int32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.MaxAgeSeconds
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSRule) GetId() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Id
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type CORSConfiguration struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
CorsRules []*CORSRule `protobuf:"bytes,1,rep,name=cors_rules,json=corsRules,proto3" json:"cors_rules,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSConfiguration) Reset() {
|
|
||||||
*x = CORSConfiguration{}
|
|
||||||
mi := &file_s3_proto_msgTypes[5]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSConfiguration) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*CORSConfiguration) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *CORSConfiguration) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_s3_proto_msgTypes[5]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use CORSConfiguration.ProtoReflect.Descriptor instead.
|
|
||||||
func (*CORSConfiguration) Descriptor() ([]byte, []int) {
|
|
||||||
return file_s3_proto_rawDescGZIP(), []int{5}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CORSConfiguration) GetCorsRules() []*CORSRule {
|
|
||||||
if x != nil {
|
|
||||||
return x.CorsRules
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type BucketMetadata struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
Tags map[string]string `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
|
||||||
Cors *CORSConfiguration `protobuf:"bytes,2,opt,name=cors,proto3" json:"cors,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BucketMetadata) Reset() {
|
|
||||||
*x = BucketMetadata{}
|
|
||||||
mi := &file_s3_proto_msgTypes[6]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BucketMetadata) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*BucketMetadata) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *BucketMetadata) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_s3_proto_msgTypes[6]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use BucketMetadata.ProtoReflect.Descriptor instead.
|
|
||||||
func (*BucketMetadata) Descriptor() ([]byte, []int) {
|
|
||||||
return file_s3_proto_rawDescGZIP(), []int{6}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BucketMetadata) GetTags() map[string]string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Tags
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BucketMetadata) GetCors() *CORSConfiguration {
|
|
||||||
if x != nil {
|
|
||||||
return x.Cors
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var File_s3_proto protoreflect.FileDescriptor
|
var File_s3_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_s3_proto_rawDesc = "" +
|
const file_s3_proto_rawDesc = "" +
|
||||||
|
@ -404,23 +224,7 @@ const file_s3_proto_rawDesc = "" +
|
||||||
"\aactions\x18\x02 \x03(\v22.messaging_pb.S3CircuitBreakerOptions.ActionsEntryR\aactions\x1a:\n" +
|
"\aactions\x18\x02 \x03(\v22.messaging_pb.S3CircuitBreakerOptions.ActionsEntryR\aactions\x1a:\n" +
|
||||||
"\fActionsEntry\x12\x10\n" +
|
"\fActionsEntry\x12\x10\n" +
|
||||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||||
"\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"\xe4\x01\n" +
|
"\x05value\x18\x02 \x01(\x03R\x05value:\x028\x012_\n" +
|
||||||
"\bCORSRule\x12'\n" +
|
|
||||||
"\x0fallowed_headers\x18\x01 \x03(\tR\x0eallowedHeaders\x12'\n" +
|
|
||||||
"\x0fallowed_methods\x18\x02 \x03(\tR\x0eallowedMethods\x12'\n" +
|
|
||||||
"\x0fallowed_origins\x18\x03 \x03(\tR\x0eallowedOrigins\x12%\n" +
|
|
||||||
"\x0eexpose_headers\x18\x04 \x03(\tR\rexposeHeaders\x12&\n" +
|
|
||||||
"\x0fmax_age_seconds\x18\x05 \x01(\x05R\rmaxAgeSeconds\x12\x0e\n" +
|
|
||||||
"\x02id\x18\x06 \x01(\tR\x02id\"J\n" +
|
|
||||||
"\x11CORSConfiguration\x125\n" +
|
|
||||||
"\n" +
|
|
||||||
"cors_rules\x18\x01 \x03(\v2\x16.messaging_pb.CORSRuleR\tcorsRules\"\xba\x01\n" +
|
|
||||||
"\x0eBucketMetadata\x12:\n" +
|
|
||||||
"\x04tags\x18\x01 \x03(\v2&.messaging_pb.BucketMetadata.TagsEntryR\x04tags\x123\n" +
|
|
||||||
"\x04cors\x18\x02 \x01(\v2\x1f.messaging_pb.CORSConfigurationR\x04cors\x1a7\n" +
|
|
||||||
"\tTagsEntry\x12\x10\n" +
|
|
||||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
|
||||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x012_\n" +
|
|
||||||
"\tSeaweedS3\x12R\n" +
|
"\tSeaweedS3\x12R\n" +
|
||||||
"\tConfigure\x12 .messaging_pb.S3ConfigureRequest\x1a!.messaging_pb.S3ConfigureResponse\"\x00BI\n" +
|
"\tConfigure\x12 .messaging_pb.S3ConfigureRequest\x1a!.messaging_pb.S3ConfigureResponse\"\x00BI\n" +
|
||||||
"\x10seaweedfs.clientB\aS3ProtoZ,github.com/seaweedfs/seaweedfs/weed/pb/s3_pbb\x06proto3"
|
"\x10seaweedfs.clientB\aS3ProtoZ,github.com/seaweedfs/seaweedfs/weed/pb/s3_pbb\x06proto3"
|
||||||
|
@ -437,34 +241,27 @@ func file_s3_proto_rawDescGZIP() []byte {
|
||||||
return file_s3_proto_rawDescData
|
return file_s3_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_s3_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
|
var file_s3_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||||
var file_s3_proto_goTypes = []any{
|
var file_s3_proto_goTypes = []any{
|
||||||
(*S3ConfigureRequest)(nil), // 0: messaging_pb.S3ConfigureRequest
|
(*S3ConfigureRequest)(nil), // 0: messaging_pb.S3ConfigureRequest
|
||||||
(*S3ConfigureResponse)(nil), // 1: messaging_pb.S3ConfigureResponse
|
(*S3ConfigureResponse)(nil), // 1: messaging_pb.S3ConfigureResponse
|
||||||
(*S3CircuitBreakerConfig)(nil), // 2: messaging_pb.S3CircuitBreakerConfig
|
(*S3CircuitBreakerConfig)(nil), // 2: messaging_pb.S3CircuitBreakerConfig
|
||||||
(*S3CircuitBreakerOptions)(nil), // 3: messaging_pb.S3CircuitBreakerOptions
|
(*S3CircuitBreakerOptions)(nil), // 3: messaging_pb.S3CircuitBreakerOptions
|
||||||
(*CORSRule)(nil), // 4: messaging_pb.CORSRule
|
nil, // 4: messaging_pb.S3CircuitBreakerConfig.BucketsEntry
|
||||||
(*CORSConfiguration)(nil), // 5: messaging_pb.CORSConfiguration
|
nil, // 5: messaging_pb.S3CircuitBreakerOptions.ActionsEntry
|
||||||
(*BucketMetadata)(nil), // 6: messaging_pb.BucketMetadata
|
|
||||||
nil, // 7: messaging_pb.S3CircuitBreakerConfig.BucketsEntry
|
|
||||||
nil, // 8: messaging_pb.S3CircuitBreakerOptions.ActionsEntry
|
|
||||||
nil, // 9: messaging_pb.BucketMetadata.TagsEntry
|
|
||||||
}
|
}
|
||||||
var file_s3_proto_depIdxs = []int32{
|
var file_s3_proto_depIdxs = []int32{
|
||||||
3, // 0: messaging_pb.S3CircuitBreakerConfig.global:type_name -> messaging_pb.S3CircuitBreakerOptions
|
3, // 0: messaging_pb.S3CircuitBreakerConfig.global:type_name -> messaging_pb.S3CircuitBreakerOptions
|
||||||
7, // 1: messaging_pb.S3CircuitBreakerConfig.buckets:type_name -> messaging_pb.S3CircuitBreakerConfig.BucketsEntry
|
4, // 1: messaging_pb.S3CircuitBreakerConfig.buckets:type_name -> messaging_pb.S3CircuitBreakerConfig.BucketsEntry
|
||||||
8, // 2: messaging_pb.S3CircuitBreakerOptions.actions:type_name -> messaging_pb.S3CircuitBreakerOptions.ActionsEntry
|
5, // 2: messaging_pb.S3CircuitBreakerOptions.actions:type_name -> messaging_pb.S3CircuitBreakerOptions.ActionsEntry
|
||||||
4, // 3: messaging_pb.CORSConfiguration.cors_rules:type_name -> messaging_pb.CORSRule
|
3, // 3: messaging_pb.S3CircuitBreakerConfig.BucketsEntry.value:type_name -> messaging_pb.S3CircuitBreakerOptions
|
||||||
9, // 4: messaging_pb.BucketMetadata.tags:type_name -> messaging_pb.BucketMetadata.TagsEntry
|
0, // 4: messaging_pb.SeaweedS3.Configure:input_type -> messaging_pb.S3ConfigureRequest
|
||||||
5, // 5: messaging_pb.BucketMetadata.cors:type_name -> messaging_pb.CORSConfiguration
|
1, // 5: messaging_pb.SeaweedS3.Configure:output_type -> messaging_pb.S3ConfigureResponse
|
||||||
3, // 6: messaging_pb.S3CircuitBreakerConfig.BucketsEntry.value:type_name -> messaging_pb.S3CircuitBreakerOptions
|
5, // [5:6] is the sub-list for method output_type
|
||||||
0, // 7: messaging_pb.SeaweedS3.Configure:input_type -> messaging_pb.S3ConfigureRequest
|
4, // [4:5] is the sub-list for method input_type
|
||||||
1, // 8: messaging_pb.SeaweedS3.Configure:output_type -> messaging_pb.S3ConfigureResponse
|
4, // [4:4] is the sub-list for extension type_name
|
||||||
8, // [8:9] is the sub-list for method output_type
|
4, // [4:4] is the sub-list for extension extendee
|
||||||
7, // [7:8] is the sub-list for method input_type
|
0, // [0:4] is the sub-list for field type_name
|
||||||
7, // [7:7] is the sub-list for extension type_name
|
|
||||||
7, // [7:7] is the sub-list for extension extendee
|
|
||||||
0, // [0:7] is the sub-list for field type_name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_s3_proto_init() }
|
func init() { file_s3_proto_init() }
|
||||||
|
@ -478,7 +275,7 @@ func file_s3_proto_init() {
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_s3_proto_rawDesc), len(file_s3_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_s3_proto_rawDesc), len(file_s3_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 10,
|
NumMessages: 6,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -85,6 +85,22 @@ type Credential struct {
|
||||||
SecretKey string
|
SecretKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Identity) isAnonymous() bool {
|
||||||
|
return i.Account.Id == s3_constants.AccountAnonymousId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (action Action) isAdmin() bool {
|
||||||
|
return strings.HasPrefix(string(action), s3_constants.ACTION_ADMIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (action Action) isOwner(bucket string) bool {
|
||||||
|
return string(action) == s3_constants.ACTION_ADMIN+":"+bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func (action Action) overBucket(bucket string) bool {
|
||||||
|
return strings.HasSuffix(string(action), ":"+bucket) || strings.HasSuffix(string(action), ":*")
|
||||||
|
}
|
||||||
|
|
||||||
// "Permission": "FULL_CONTROL"|"WRITE"|"WRITE_ACP"|"READ"|"READ_ACP"
|
// "Permission": "FULL_CONTROL"|"WRITE"|"WRITE_ACP"|"READ"|"READ_ACP"
|
||||||
func (action Action) getPermission() Permission {
|
func (action Action) getPermission() Permission {
|
||||||
switch act := strings.Split(string(action), ":")[0]; act {
|
switch act := strings.Split(string(action), ":")[0]; act {
|
||||||
|
@ -131,72 +147,17 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, explicitSto
|
||||||
|
|
||||||
iam.credentialManager = credentialManager
|
iam.credentialManager = credentialManager
|
||||||
|
|
||||||
// Track whether any configuration was successfully loaded
|
|
||||||
configLoaded := false
|
|
||||||
|
|
||||||
// First, try to load configurations from file or filer
|
|
||||||
if option.Config != "" {
|
if option.Config != "" {
|
||||||
glog.V(3).Infof("loading static config file %s", option.Config)
|
glog.V(3).Infof("loading static config file %s", option.Config)
|
||||||
if err := iam.loadS3ApiConfigurationFromFile(option.Config); err != nil {
|
if err := iam.loadS3ApiConfigurationFromFile(option.Config); err != nil {
|
||||||
glog.Fatalf("fail to load config file %s: %v", option.Config, err)
|
glog.Fatalf("fail to load config file %s: %v", option.Config, err)
|
||||||
}
|
}
|
||||||
configLoaded = true
|
|
||||||
} else {
|
} else {
|
||||||
glog.V(3).Infof("no static config file specified... loading config from credential manager")
|
glog.V(3).Infof("no static config file specified... loading config from credential manager")
|
||||||
if err := iam.loadS3ApiConfigurationFromFiler(option); err != nil {
|
if err := iam.loadS3ApiConfigurationFromFiler(option); err != nil {
|
||||||
glog.Warningf("fail to load config: %v", err)
|
glog.Warningf("fail to load config: %v", err)
|
||||||
} else {
|
|
||||||
// Check if any identities were actually loaded from filer
|
|
||||||
iam.m.RLock()
|
|
||||||
if len(iam.identities) > 0 {
|
|
||||||
configLoaded = true
|
|
||||||
}
|
|
||||||
iam.m.RUnlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only use environment variables as fallback if no configuration was loaded
|
|
||||||
if !configLoaded {
|
|
||||||
accessKeyId := os.Getenv("AWS_ACCESS_KEY_ID")
|
|
||||||
secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
|
|
||||||
|
|
||||||
if accessKeyId != "" && secretAccessKey != "" {
|
|
||||||
glog.V(0).Infof("No S3 configuration found, using AWS environment variables as fallback")
|
|
||||||
|
|
||||||
// Create environment variable identity name
|
|
||||||
identityNameSuffix := accessKeyId
|
|
||||||
if len(accessKeyId) > 8 {
|
|
||||||
identityNameSuffix = accessKeyId[:8]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create admin identity with environment variable credentials
|
|
||||||
envIdentity := &Identity{
|
|
||||||
Name: "admin-" + identityNameSuffix,
|
|
||||||
Account: &AccountAdmin,
|
|
||||||
Credentials: []*Credential{
|
|
||||||
{
|
|
||||||
AccessKey: accessKeyId,
|
|
||||||
SecretKey: secretAccessKey,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Actions: []Action{
|
|
||||||
s3_constants.ACTION_ADMIN,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set as the only configuration
|
|
||||||
iam.m.Lock()
|
|
||||||
if len(iam.identities) == 0 {
|
|
||||||
iam.identities = []*Identity{envIdentity}
|
|
||||||
iam.accessKeyIdent = map[string]*Identity{accessKeyId: envIdentity}
|
|
||||||
iam.isAuthEnabled = true
|
|
||||||
}
|
|
||||||
iam.m.Unlock()
|
|
||||||
|
|
||||||
glog.V(0).Infof("Added admin identity from AWS environment variables: %s", envIdentity.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return iam
|
return iam
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,7 +343,6 @@ func (iam *IdentityAccessManagement) Auth(f http.HandlerFunc, action Action) htt
|
||||||
|
|
||||||
identity, errCode := iam.authRequest(r, action)
|
identity, errCode := iam.authRequest(r, action)
|
||||||
glog.V(3).Infof("auth error: %v", errCode)
|
glog.V(3).Infof("auth error: %v", errCode)
|
||||||
|
|
||||||
if errCode == s3err.ErrNone {
|
if errCode == s3err.ErrNone {
|
||||||
if identity != nil && identity.Name != "" {
|
if identity != nil && identity.Name != "" {
|
||||||
r.Header.Set(s3_constants.AmzIdentityId, identity.Name)
|
r.Header.Set(s3_constants.AmzIdentityId, identity.Name)
|
||||||
|
@ -577,5 +537,9 @@ func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromCredentialManager
|
||||||
return fmt.Errorf("failed to load configuration from credential manager: %w", err)
|
return fmt.Errorf("failed to load configuration from credential manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(s3ApiConfiguration.Identities) == 0 {
|
||||||
|
return fmt.Errorf("no identities found")
|
||||||
|
}
|
||||||
|
|
||||||
return iam.loadS3ApiConfiguration(s3ApiConfiguration)
|
return iam.loadS3ApiConfiguration(s3ApiConfiguration)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||||
|
@ -108,12 +107,12 @@ func (s3a *S3ApiServer) updateBucketConfigCacheFromEntry(entry *filer_pb.Entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket := entry.Name
|
bucket := entry.Name
|
||||||
|
glog.V(2).Infof("updateBucketConfigCacheFromEntry: updating cache for bucket %s", bucket)
|
||||||
|
|
||||||
// Create new bucket config from the entry
|
// Create new bucket config from the entry
|
||||||
config := &BucketConfig{
|
config := &BucketConfig{
|
||||||
Name: bucket,
|
Name: bucket,
|
||||||
Entry: entry,
|
Entry: entry,
|
||||||
IsPublicRead: false, // Explicitly default to false for private buckets
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract configuration from extended attributes
|
// Extract configuration from extended attributes
|
||||||
|
@ -126,11 +125,6 @@ func (s3a *S3ApiServer) updateBucketConfigCacheFromEntry(entry *filer_pb.Entry)
|
||||||
}
|
}
|
||||||
if acl, exists := entry.Extended[s3_constants.ExtAmzAclKey]; exists {
|
if acl, exists := entry.Extended[s3_constants.ExtAmzAclKey]; exists {
|
||||||
config.ACL = acl
|
config.ACL = acl
|
||||||
// Parse ACL and cache public-read status
|
|
||||||
config.IsPublicRead = parseAndCachePublicReadStatus(acl)
|
|
||||||
} else {
|
|
||||||
// No ACL means private bucket
|
|
||||||
config.IsPublicRead = false
|
|
||||||
}
|
}
|
||||||
if owner, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
if owner, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
||||||
config.Owner = string(owner)
|
config.Owner = string(owner)
|
||||||
|
@ -142,21 +136,12 @@ func (s3a *S3ApiServer) updateBucketConfigCacheFromEntry(entry *filer_pb.Entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load CORS configuration from bucket directory content
|
|
||||||
if corsConfig, err := s3a.loadCORSFromBucketContent(bucket); err != nil {
|
|
||||||
if !errors.Is(err, filer_pb.ErrNotFound) {
|
|
||||||
glog.Errorf("updateBucketConfigCacheFromEntry: failed to load CORS configuration for bucket %s: %v", bucket, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.CORS = corsConfig
|
|
||||||
glog.V(2).Infof("updateBucketConfigCacheFromEntry: loaded CORS config for bucket %s", bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update timestamp
|
// Update timestamp
|
||||||
config.LastModified = time.Now()
|
config.LastModified = time.Now()
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
s3a.bucketConfigCache.Set(bucket, config)
|
s3a.bucketConfigCache.Set(bucket, config)
|
||||||
|
glog.V(2).Infof("updateBucketConfigCacheFromEntry: updated bucket config cache for %s", bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// invalidateBucketConfigCache removes a bucket from the configuration cache
|
// invalidateBucketConfigCache removes a bucket from the configuration cache
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
|
||||||
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
@ -266,94 +264,3 @@ func TestLoadS3ApiConfiguration(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewIdentityAccessManagementWithStoreEnvVars(t *testing.T) {
|
|
||||||
// Save original environment
|
|
||||||
originalAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID")
|
|
||||||
originalSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
|
|
||||||
|
|
||||||
// Clean up after test
|
|
||||||
defer func() {
|
|
||||||
if originalAccessKeyId != "" {
|
|
||||||
os.Setenv("AWS_ACCESS_KEY_ID", originalAccessKeyId)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("AWS_ACCESS_KEY_ID")
|
|
||||||
}
|
|
||||||
if originalSecretAccessKey != "" {
|
|
||||||
os.Setenv("AWS_SECRET_ACCESS_KEY", originalSecretAccessKey)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("AWS_SECRET_ACCESS_KEY")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
accessKeyId string
|
|
||||||
secretAccessKey string
|
|
||||||
expectEnvIdentity bool
|
|
||||||
expectedName string
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Environment variables used as fallback",
|
|
||||||
accessKeyId: "AKIA1234567890ABCDEF",
|
|
||||||
secretAccessKey: "secret123456789012345678901234567890abcdef12",
|
|
||||||
expectEnvIdentity: true,
|
|
||||||
expectedName: "admin-AKIA1234",
|
|
||||||
description: "When no config file and no filer config, environment variables should be used",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Short access key fallback",
|
|
||||||
accessKeyId: "SHORT",
|
|
||||||
secretAccessKey: "secret123456789012345678901234567890abcdef12",
|
|
||||||
expectEnvIdentity: true,
|
|
||||||
expectedName: "admin-SHORT",
|
|
||||||
description: "Short access keys should work correctly as fallback",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "No env vars means no identities",
|
|
||||||
accessKeyId: "",
|
|
||||||
secretAccessKey: "",
|
|
||||||
expectEnvIdentity: false,
|
|
||||||
expectedName: "",
|
|
||||||
description: "When no env vars and no config, should have no identities",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Set up environment variables
|
|
||||||
if tt.accessKeyId != "" {
|
|
||||||
os.Setenv("AWS_ACCESS_KEY_ID", tt.accessKeyId)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("AWS_ACCESS_KEY_ID")
|
|
||||||
}
|
|
||||||
if tt.secretAccessKey != "" {
|
|
||||||
os.Setenv("AWS_SECRET_ACCESS_KEY", tt.secretAccessKey)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("AWS_SECRET_ACCESS_KEY")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create IAM instance with memory store for testing (no config file)
|
|
||||||
option := &S3ApiServerOption{
|
|
||||||
Config: "", // No config file - this should trigger environment variable fallback
|
|
||||||
}
|
|
||||||
iam := NewIdentityAccessManagementWithStore(option, string(credential.StoreTypeMemory))
|
|
||||||
|
|
||||||
if tt.expectEnvIdentity {
|
|
||||||
// Should have exactly one identity from environment variables
|
|
||||||
assert.Len(t, iam.identities, 1, "Should have exactly one identity from environment variables")
|
|
||||||
|
|
||||||
identity := iam.identities[0]
|
|
||||||
assert.Equal(t, tt.expectedName, identity.Name, "Identity name should match expected")
|
|
||||||
assert.Len(t, identity.Credentials, 1, "Should have one credential")
|
|
||||||
assert.Equal(t, tt.accessKeyId, identity.Credentials[0].AccessKey, "Access key should match environment variable")
|
|
||||||
assert.Equal(t, tt.secretAccessKey, identity.Credentials[0].SecretKey, "Secret key should match environment variable")
|
|
||||||
assert.Contains(t, identity.Actions, Action(ACTION_ADMIN), "Should have admin action")
|
|
||||||
} else {
|
|
||||||
// When no env vars, should have no identities (since no config file)
|
|
||||||
assert.Len(t, iam.identities, 0, "Should have no identities when no env vars and no config file")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,24 +1,36 @@
|
||||||
package cors
|
package cors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// S3 metadata file name constant to avoid typos and reduce duplication
|
||||||
|
const S3MetadataFileName = ".s3metadata"
|
||||||
|
|
||||||
// CORSRule represents a single CORS rule
|
// CORSRule represents a single CORS rule
|
||||||
type CORSRule struct {
|
type CORSRule struct {
|
||||||
AllowedHeaders []string `xml:"AllowedHeader,omitempty" json:"AllowedHeaders,omitempty"`
|
ID string `xml:"ID,omitempty" json:"ID,omitempty"`
|
||||||
AllowedMethods []string `xml:"AllowedMethod" json:"AllowedMethods"`
|
AllowedMethods []string `xml:"AllowedMethod" json:"AllowedMethods"`
|
||||||
AllowedOrigins []string `xml:"AllowedOrigin" json:"AllowedOrigins"`
|
AllowedOrigins []string `xml:"AllowedOrigin" json:"AllowedOrigins"`
|
||||||
|
AllowedHeaders []string `xml:"AllowedHeader,omitempty" json:"AllowedHeaders,omitempty"`
|
||||||
ExposeHeaders []string `xml:"ExposeHeader,omitempty" json:"ExposeHeaders,omitempty"`
|
ExposeHeaders []string `xml:"ExposeHeader,omitempty" json:"ExposeHeaders,omitempty"`
|
||||||
MaxAgeSeconds *int `xml:"MaxAgeSeconds,omitempty" json:"MaxAgeSeconds,omitempty"`
|
MaxAgeSeconds *int `xml:"MaxAgeSeconds,omitempty" json:"MaxAgeSeconds,omitempty"`
|
||||||
ID string `xml:"ID,omitempty" json:"ID,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORSConfiguration represents the CORS configuration for a bucket
|
// CORSConfiguration represents the CORS configuration for a bucket
|
||||||
type CORSConfiguration struct {
|
type CORSConfiguration struct {
|
||||||
|
XMLName xml.Name `xml:"CORSConfiguration"`
|
||||||
CORSRules []CORSRule `xml:"CORSRule" json:"CORSRules"`
|
CORSRules []CORSRule `xml:"CORSRule" json:"CORSRules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +44,7 @@ type CORSRequest struct {
|
||||||
AccessControlRequestHeaders []string
|
AccessControlRequestHeaders []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORSResponse represents the response for a CORS request
|
// CORSResponse represents CORS response headers
|
||||||
type CORSResponse struct {
|
type CORSResponse struct {
|
||||||
AllowOrigin string
|
AllowOrigin string
|
||||||
AllowMethods string
|
AllowMethods string
|
||||||
|
@ -65,29 +77,6 @@ func ValidateConfiguration(config *CORSConfiguration) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseRequest parses an HTTP request to extract CORS information
|
|
||||||
func ParseRequest(r *http.Request) *CORSRequest {
|
|
||||||
corsReq := &CORSRequest{
|
|
||||||
Origin: r.Header.Get("Origin"),
|
|
||||||
Method: r.Method,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a preflight request
|
|
||||||
if r.Method == "OPTIONS" {
|
|
||||||
corsReq.IsPreflightRequest = true
|
|
||||||
corsReq.AccessControlRequestMethod = r.Header.Get("Access-Control-Request-Method")
|
|
||||||
|
|
||||||
if headers := r.Header.Get("Access-Control-Request-Headers"); headers != "" {
|
|
||||||
corsReq.AccessControlRequestHeaders = strings.Split(headers, ",")
|
|
||||||
for i := range corsReq.AccessControlRequestHeaders {
|
|
||||||
corsReq.AccessControlRequestHeaders[i] = strings.TrimSpace(corsReq.AccessControlRequestHeaders[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return corsReq
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateRule validates a single CORS rule
|
// validateRule validates a single CORS rule
|
||||||
func validateRule(rule *CORSRule) error {
|
func validateRule(rule *CORSRule) error {
|
||||||
if len(rule.AllowedMethods) == 0 {
|
if len(rule.AllowedMethods) == 0 {
|
||||||
|
@ -159,6 +148,29 @@ func validateOrigin(origin string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseRequest parses an HTTP request to extract CORS information
|
||||||
|
func ParseRequest(r *http.Request) *CORSRequest {
|
||||||
|
corsReq := &CORSRequest{
|
||||||
|
Origin: r.Header.Get("Origin"),
|
||||||
|
Method: r.Method,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a preflight request
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
corsReq.IsPreflightRequest = true
|
||||||
|
corsReq.AccessControlRequestMethod = r.Header.Get("Access-Control-Request-Method")
|
||||||
|
|
||||||
|
if headers := r.Header.Get("Access-Control-Request-Headers"); headers != "" {
|
||||||
|
corsReq.AccessControlRequestHeaders = strings.Split(headers, ",")
|
||||||
|
for i := range corsReq.AccessControlRequestHeaders {
|
||||||
|
corsReq.AccessControlRequestHeaders[i] = strings.TrimSpace(corsReq.AccessControlRequestHeaders[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return corsReq
|
||||||
|
}
|
||||||
|
|
||||||
// EvaluateRequest evaluates a CORS request against a CORS configuration
|
// EvaluateRequest evaluates a CORS request against a CORS configuration
|
||||||
func EvaluateRequest(config *CORSConfiguration, corsReq *CORSRequest) (*CORSResponse, error) {
|
func EvaluateRequest(config *CORSConfiguration, corsReq *CORSRequest) (*CORSResponse, error) {
|
||||||
if config == nil || corsReq == nil {
|
if config == nil || corsReq == nil {
|
||||||
|
@ -177,7 +189,7 @@ func EvaluateRequest(config *CORSConfiguration, corsReq *CORSRequest) (*CORSResp
|
||||||
return buildPreflightResponse(&rule, corsReq), nil
|
return buildPreflightResponse(&rule, corsReq), nil
|
||||||
} else {
|
} else {
|
||||||
// For actual requests, check method
|
// For actual requests, check method
|
||||||
if containsString(rule.AllowedMethods, corsReq.Method) {
|
if contains(rule.AllowedMethods, corsReq.Method) {
|
||||||
return buildResponse(&rule, corsReq), nil
|
return buildResponse(&rule, corsReq), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,14 +199,152 @@ func EvaluateRequest(config *CORSConfiguration, corsReq *CORSRequest) (*CORSResp
|
||||||
return nil, fmt.Errorf("no matching CORS rule found")
|
return nil, fmt.Errorf("no matching CORS rule found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matchesRule checks if a CORS request matches a CORS rule
|
||||||
|
func matchesRule(rule *CORSRule, corsReq *CORSRequest) bool {
|
||||||
|
// Check origin - this is the primary matching criterion
|
||||||
|
if !matchesOrigin(rule.AllowedOrigins, corsReq.Origin) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// For preflight requests, we need to validate both the requested method and headers
|
||||||
|
if corsReq.IsPreflightRequest {
|
||||||
|
// Check if the requested method is allowed
|
||||||
|
if corsReq.AccessControlRequestMethod != "" {
|
||||||
|
if !contains(rule.AllowedMethods, corsReq.AccessControlRequestMethod) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all requested headers are allowed
|
||||||
|
if len(corsReq.AccessControlRequestHeaders) > 0 {
|
||||||
|
for _, requestedHeader := range corsReq.AccessControlRequestHeaders {
|
||||||
|
if !matchesHeader(rule.AllowedHeaders, requestedHeader) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-preflight requests, check method matching
|
||||||
|
method := corsReq.Method
|
||||||
|
if !contains(rule.AllowedMethods, method) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesOrigin checks if an origin matches any of the allowed origins
|
||||||
|
func matchesOrigin(allowedOrigins []string, origin string) bool {
|
||||||
|
for _, allowedOrigin := range allowedOrigins {
|
||||||
|
if allowedOrigin == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowedOrigin == origin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check wildcard matching
|
||||||
|
if strings.Contains(allowedOrigin, "*") {
|
||||||
|
if matchesWildcard(allowedOrigin, origin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesWildcard checks if an origin matches a wildcard pattern
|
||||||
|
// Uses string manipulation instead of regex for better performance
|
||||||
|
func matchesWildcard(pattern, origin string) bool {
|
||||||
|
// Handle simple cases first
|
||||||
|
if pattern == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if pattern == origin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// For CORS, we typically only deal with * wildcards (not ? wildcards)
|
||||||
|
// Use string manipulation for * wildcards only (more efficient than regex)
|
||||||
|
|
||||||
|
// Split pattern by wildcards
|
||||||
|
parts := strings.Split(pattern, "*")
|
||||||
|
if len(parts) == 1 {
|
||||||
|
// No wildcards, exact match
|
||||||
|
return pattern == origin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if string starts with first part
|
||||||
|
if len(parts[0]) > 0 && !strings.HasPrefix(origin, parts[0]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if string ends with last part
|
||||||
|
if len(parts[len(parts)-1]) > 0 && !strings.HasSuffix(origin, parts[len(parts)-1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check middle parts
|
||||||
|
searchStr := origin
|
||||||
|
if len(parts[0]) > 0 {
|
||||||
|
searchStr = searchStr[len(parts[0]):]
|
||||||
|
}
|
||||||
|
if len(parts[len(parts)-1]) > 0 {
|
||||||
|
searchStr = searchStr[:len(searchStr)-len(parts[len(parts)-1])]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(parts)-1; i++ {
|
||||||
|
if len(parts[i]) > 0 {
|
||||||
|
index := strings.Index(searchStr, parts[i])
|
||||||
|
if index == -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
searchStr = searchStr[index+len(parts[i]):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesHeader checks if a header matches allowed headers
|
||||||
|
func matchesHeader(allowedHeaders []string, header string) bool {
|
||||||
|
if len(allowedHeaders) == 0 {
|
||||||
|
return true // No restrictions
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowedHeader := range allowedHeaders {
|
||||||
|
if allowedHeader == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(allowedHeader, header) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check wildcard matching for headers
|
||||||
|
if strings.Contains(allowedHeader, "*") {
|
||||||
|
if matchesWildcard(strings.ToLower(allowedHeader), strings.ToLower(header)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// buildPreflightResponse builds a CORS response for preflight requests
|
// buildPreflightResponse builds a CORS response for preflight requests
|
||||||
|
// This function allows partial matches - origin can match while methods/headers may not
|
||||||
func buildPreflightResponse(rule *CORSRule, corsReq *CORSRequest) *CORSResponse {
|
func buildPreflightResponse(rule *CORSRule, corsReq *CORSRequest) *CORSResponse {
|
||||||
response := &CORSResponse{
|
response := &CORSResponse{
|
||||||
AllowOrigin: corsReq.Origin,
|
AllowOrigin: corsReq.Origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the requested method is allowed
|
// Check if the requested method is allowed
|
||||||
methodAllowed := corsReq.AccessControlRequestMethod == "" || containsString(rule.AllowedMethods, corsReq.AccessControlRequestMethod)
|
methodAllowed := corsReq.AccessControlRequestMethod == "" || contains(rule.AllowedMethods, corsReq.AccessControlRequestMethod)
|
||||||
|
|
||||||
// Check requested headers
|
// Check requested headers
|
||||||
var allowedRequestHeaders []string
|
var allowedRequestHeaders []string
|
||||||
|
@ -253,15 +403,42 @@ func buildResponse(rule *CORSRule, corsReq *CORSRequest) *CORSResponse {
|
||||||
AllowOrigin: corsReq.Origin,
|
AllowOrigin: corsReq.Origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set allowed methods
|
// Set allowed methods - for preflight requests, return all allowed methods
|
||||||
|
if corsReq.IsPreflightRequest {
|
||||||
response.AllowMethods = strings.Join(rule.AllowedMethods, ", ")
|
response.AllowMethods = strings.Join(rule.AllowedMethods, ", ")
|
||||||
|
} else {
|
||||||
|
// For non-preflight requests, return all allowed methods
|
||||||
|
response.AllowMethods = strings.Join(rule.AllowedMethods, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
// Set allowed headers
|
// Set allowed headers
|
||||||
if len(rule.AllowedHeaders) > 0 {
|
if corsReq.IsPreflightRequest && len(rule.AllowedHeaders) > 0 {
|
||||||
|
// For preflight requests, check if wildcard is allowed
|
||||||
|
hasWildcard := false
|
||||||
|
for _, header := range rule.AllowedHeaders {
|
||||||
|
if header == "*" {
|
||||||
|
hasWildcard = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasWildcard && len(corsReq.AccessControlRequestHeaders) > 0 {
|
||||||
|
// Return the specific headers that were requested when wildcard is allowed
|
||||||
|
response.AllowHeaders = strings.Join(corsReq.AccessControlRequestHeaders, ", ")
|
||||||
|
} else if len(corsReq.AccessControlRequestHeaders) > 0 {
|
||||||
|
// For non-wildcard cases, return the requested headers (preserving case)
|
||||||
|
// since we already validated they are allowed in matchesRule
|
||||||
|
response.AllowHeaders = strings.Join(corsReq.AccessControlRequestHeaders, ", ")
|
||||||
|
} else {
|
||||||
|
// Fallback to configured headers if no specific headers were requested
|
||||||
|
response.AllowHeaders = strings.Join(rule.AllowedHeaders, ", ")
|
||||||
|
}
|
||||||
|
} else if len(rule.AllowedHeaders) > 0 {
|
||||||
|
// For non-preflight requests, return the allowed headers from the rule
|
||||||
response.AllowHeaders = strings.Join(rule.AllowedHeaders, ", ")
|
response.AllowHeaders = strings.Join(rule.AllowedHeaders, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set expose headers
|
// Set exposed headers
|
||||||
if len(rule.ExposeHeaders) > 0 {
|
if len(rule.ExposeHeaders) > 0 {
|
||||||
response.ExposeHeaders = strings.Join(rule.ExposeHeaders, ", ")
|
response.ExposeHeaders = strings.Join(rule.ExposeHeaders, ", ")
|
||||||
}
|
}
|
||||||
|
@ -274,77 +451,8 @@ func buildResponse(rule *CORSRule, corsReq *CORSRequest) *CORSResponse {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// contains checks if a slice contains a string
|
||||||
|
func contains(slice []string, item string) bool {
|
||||||
// matchesOrigin checks if the request origin matches any allowed origin
|
|
||||||
func matchesOrigin(allowedOrigins []string, origin string) bool {
|
|
||||||
for _, allowedOrigin := range allowedOrigins {
|
|
||||||
if allowedOrigin == "*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if allowedOrigin == origin {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Handle wildcard patterns like https://*.example.com
|
|
||||||
if strings.Contains(allowedOrigin, "*") {
|
|
||||||
if matchWildcard(allowedOrigin, origin) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchWildcard performs wildcard matching for origins
|
|
||||||
func matchWildcard(pattern, text string) bool {
|
|
||||||
// Simple wildcard matching - only supports single * at the beginning
|
|
||||||
if strings.HasPrefix(pattern, "http://*") {
|
|
||||||
suffix := pattern[8:] // Remove "http://*"
|
|
||||||
return strings.HasPrefix(text, "http://") && strings.HasSuffix(text, suffix)
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(pattern, "https://*") {
|
|
||||||
suffix := pattern[9:] // Remove "https://*"
|
|
||||||
return strings.HasPrefix(text, "https://") && strings.HasSuffix(text, suffix)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchesHeader checks if a header is allowed
|
|
||||||
func matchesHeader(allowedHeaders []string, header string) bool {
|
|
||||||
// If no headers are specified, all headers are allowed
|
|
||||||
if len(allowedHeaders) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header matching is case-insensitive
|
|
||||||
header = strings.ToLower(header)
|
|
||||||
|
|
||||||
for _, allowedHeader := range allowedHeaders {
|
|
||||||
allowedHeaderLower := strings.ToLower(allowedHeader)
|
|
||||||
|
|
||||||
// Wildcard match
|
|
||||||
if allowedHeaderLower == "*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exact match
|
|
||||||
if allowedHeaderLower == header {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefix wildcard match (e.g., "x-amz-*" matches "x-amz-date")
|
|
||||||
if strings.HasSuffix(allowedHeaderLower, "*") {
|
|
||||||
prefix := strings.TrimSuffix(allowedHeaderLower, "*")
|
|
||||||
if strings.HasPrefix(header, prefix) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// containsString checks if a slice contains a specific string
|
|
||||||
func containsString(slice []string, item string) bool {
|
|
||||||
for _, s := range slice {
|
for _, s := range slice {
|
||||||
if s == item {
|
if s == item {
|
||||||
return true
|
return true
|
||||||
|
@ -383,3 +491,159 @@ func ApplyHeaders(w http.ResponseWriter, corsResp *CORSResponse) {
|
||||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilerClient interface for dependency injection
|
||||||
|
type FilerClient interface {
|
||||||
|
WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryGetter interface for getting filer entries
|
||||||
|
type EntryGetter interface {
|
||||||
|
GetEntry(directory, name string) (*filer_pb.Entry, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage provides CORS configuration storage operations
|
||||||
|
type Storage struct {
|
||||||
|
filerClient FilerClient
|
||||||
|
entryGetter EntryGetter
|
||||||
|
bucketsPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorage creates a new CORS storage instance
|
||||||
|
func NewStorage(filerClient FilerClient, entryGetter EntryGetter, bucketsPath string) *Storage {
|
||||||
|
return &Storage{
|
||||||
|
filerClient: filerClient,
|
||||||
|
entryGetter: entryGetter,
|
||||||
|
bucketsPath: bucketsPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store stores CORS configuration in the filer
|
||||||
|
func (s *Storage) Store(bucket string, config *CORSConfiguration) error {
|
||||||
|
// Store in bucket metadata
|
||||||
|
bucketMetadataPath := filepath.Join(s.bucketsPath, bucket, S3MetadataFileName)
|
||||||
|
|
||||||
|
// Get existing metadata
|
||||||
|
existingEntry, err := s.entryGetter.GetEntry("", bucketMetadataPath)
|
||||||
|
var metadata map[string]interface{}
|
||||||
|
|
||||||
|
if err == nil && existingEntry != nil && len(existingEntry.Content) > 0 {
|
||||||
|
if err := json.Unmarshal(existingEntry.Content, &metadata); err != nil {
|
||||||
|
glog.V(1).Infof("Failed to unmarshal existing metadata: %v", err)
|
||||||
|
metadata = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
metadata = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata["cors"] = config
|
||||||
|
|
||||||
|
metadataBytes, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal bucket metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store metadata
|
||||||
|
return s.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||||
|
request := &filer_pb.CreateEntryRequest{
|
||||||
|
Directory: s.bucketsPath + "/" + bucket,
|
||||||
|
Entry: &filer_pb.Entry{
|
||||||
|
Name: S3MetadataFileName,
|
||||||
|
IsDirectory: false,
|
||||||
|
Attributes: &filer_pb.FuseAttributes{
|
||||||
|
Crtime: time.Now().Unix(),
|
||||||
|
Mtime: time.Now().Unix(),
|
||||||
|
FileMode: 0644,
|
||||||
|
},
|
||||||
|
Content: metadataBytes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.CreateEntry(context.Background(), request)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads CORS configuration from the filer
|
||||||
|
func (s *Storage) Load(bucket string) (*CORSConfiguration, error) {
|
||||||
|
bucketMetadataPath := filepath.Join(s.bucketsPath, bucket, S3MetadataFileName)
|
||||||
|
|
||||||
|
entry, err := s.entryGetter.GetEntry("", bucketMetadataPath)
|
||||||
|
if err != nil || entry == nil {
|
||||||
|
return nil, fmt.Errorf("no CORS configuration found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entry.Content) == 0 {
|
||||||
|
return nil, fmt.Errorf("no CORS configuration found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata map[string]interface{}
|
||||||
|
if err := json.Unmarshal(entry.Content, &metadata); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
corsData, exists := metadata["cors"]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("no CORS configuration found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back to CORSConfiguration
|
||||||
|
corsBytes, err := json.Marshal(corsData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal CORS data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config CORSConfiguration
|
||||||
|
if err := json.Unmarshal(corsBytes, &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal CORS configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes CORS configuration from the filer
|
||||||
|
func (s *Storage) Delete(bucket string) error {
|
||||||
|
bucketMetadataPath := filepath.Join(s.bucketsPath, bucket, S3MetadataFileName)
|
||||||
|
|
||||||
|
entry, err := s.entryGetter.GetEntry("", bucketMetadataPath)
|
||||||
|
if err != nil || entry == nil {
|
||||||
|
return nil // Already deleted or doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata map[string]interface{}
|
||||||
|
if len(entry.Content) > 0 {
|
||||||
|
if err := json.Unmarshal(entry.Content, &metadata); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil // No metadata to delete
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove CORS configuration
|
||||||
|
delete(metadata, "cors")
|
||||||
|
|
||||||
|
metadataBytes, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
return s.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||||
|
request := &filer_pb.CreateEntryRequest{
|
||||||
|
Directory: s.bucketsPath + "/" + bucket,
|
||||||
|
Entry: &filer_pb.Entry{
|
||||||
|
Name: S3MetadataFileName,
|
||||||
|
IsDirectory: false,
|
||||||
|
Attributes: &filer_pb.FuseAttributes{
|
||||||
|
Crtime: time.Now().Unix(),
|
||||||
|
Mtime: time.Now().Unix(),
|
||||||
|
FileMode: 0644,
|
||||||
|
},
|
||||||
|
Content: metadataBytes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.CreateEntry(context.Background(), request)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -20,42 +20,43 @@ type CORSConfigGetter interface {
|
||||||
|
|
||||||
// Middleware handles CORS evaluation for all S3 API requests
|
// Middleware handles CORS evaluation for all S3 API requests
|
||||||
type Middleware struct {
|
type Middleware struct {
|
||||||
|
storage *Storage
|
||||||
bucketChecker BucketChecker
|
bucketChecker BucketChecker
|
||||||
corsConfigGetter CORSConfigGetter
|
corsConfigGetter CORSConfigGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMiddleware creates a new CORS middleware instance
|
// NewMiddleware creates a new CORS middleware instance
|
||||||
func NewMiddleware(bucketChecker BucketChecker, corsConfigGetter CORSConfigGetter) *Middleware {
|
func NewMiddleware(storage *Storage, bucketChecker BucketChecker, corsConfigGetter CORSConfigGetter) *Middleware {
|
||||||
return &Middleware{
|
return &Middleware{
|
||||||
|
storage: storage,
|
||||||
bucketChecker: bucketChecker,
|
bucketChecker: bucketChecker,
|
||||||
corsConfigGetter: corsConfigGetter,
|
corsConfigGetter: corsConfigGetter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler returns the CORS middleware handler
|
// evaluateCORSRequest performs the common CORS request evaluation logic
|
||||||
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
// Returns: (corsResponse, responseWritten, shouldContinue)
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
// - corsResponse: the CORS response if evaluation succeeded
|
||||||
|
// - responseWritten: true if an error response was already written
|
||||||
|
// - shouldContinue: true if the request should continue to the next handler
|
||||||
|
func (m *Middleware) evaluateCORSRequest(w http.ResponseWriter, r *http.Request) (*CORSResponse, bool, bool) {
|
||||||
// Parse CORS request
|
// Parse CORS request
|
||||||
corsReq := ParseRequest(r)
|
corsReq := ParseRequest(r)
|
||||||
|
|
||||||
// If not a CORS request, continue normally
|
|
||||||
if corsReq.Origin == "" {
|
if corsReq.Origin == "" {
|
||||||
next.ServeHTTP(w, r)
|
// Not a CORS request
|
||||||
return
|
return nil, false, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract bucket from request
|
// Extract bucket from request
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
next.ServeHTTP(w, r)
|
return nil, false, true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bucket exists
|
// Check if bucket exists
|
||||||
if err := m.bucketChecker.CheckBucket(r, bucket); err != s3err.ErrNone {
|
if err := m.bucketChecker.CheckBucket(r, bucket); err != s3err.ErrNone {
|
||||||
// For non-existent buckets, let the normal handler deal with it
|
// For non-existent buckets, let the normal handler deal with it
|
||||||
next.ServeHTTP(w, r)
|
return nil, false, true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load CORS configuration from cache
|
// Load CORS configuration from cache
|
||||||
|
@ -65,11 +66,10 @@ func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
if corsReq.IsPreflightRequest {
|
if corsReq.IsPreflightRequest {
|
||||||
// Preflight request without CORS config should fail
|
// Preflight request without CORS config should fail
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||||
return
|
return nil, true, false // Response written, don't continue
|
||||||
}
|
}
|
||||||
// Non-preflight request, continue normally
|
// Non-preflight request, continue normally
|
||||||
next.ServeHTTP(w, r)
|
return nil, false, true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate CORS request
|
// Evaluate CORS request
|
||||||
|
@ -79,14 +79,35 @@ func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
if corsReq.IsPreflightRequest {
|
if corsReq.IsPreflightRequest {
|
||||||
// Preflight request that doesn't match CORS rules should fail
|
// Preflight request that doesn't match CORS rules should fail
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||||
return
|
return nil, true, false // Response written, don't continue
|
||||||
}
|
}
|
||||||
// Non-preflight request, continue normally but without CORS headers
|
// Non-preflight request, continue normally but without CORS headers
|
||||||
|
return nil, false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return corsResp, false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the CORS middleware handler
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Use the common evaluation logic
|
||||||
|
corsResp, responseWritten, shouldContinue := m.evaluateCORSRequest(w, r)
|
||||||
|
if responseWritten {
|
||||||
|
// Response was already written (error case)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldContinue {
|
||||||
|
// Continue with normal request processing
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply CORS headers
|
// Parse request to check if it's a preflight request
|
||||||
|
corsReq := ParseRequest(r)
|
||||||
|
|
||||||
|
// Apply CORS headers to response
|
||||||
ApplyHeaders(w, corsResp)
|
ApplyHeaders(w, corsResp)
|
||||||
|
|
||||||
// Handle preflight requests
|
// Handle preflight requests
|
||||||
|
@ -96,56 +117,22 @@ func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For actual requests, continue with normal processing
|
// Continue with normal request processing
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleOptionsRequest handles OPTIONS requests for CORS preflight
|
// HandleOptionsRequest handles OPTIONS requests for CORS preflight
|
||||||
func (m *Middleware) HandleOptionsRequest(w http.ResponseWriter, r *http.Request) {
|
func (m *Middleware) HandleOptionsRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
// Parse CORS request
|
// Use the common evaluation logic
|
||||||
corsReq := ParseRequest(r)
|
corsResp, responseWritten, shouldContinue := m.evaluateCORSRequest(w, r)
|
||||||
|
if responseWritten {
|
||||||
// If not a CORS request, return OK
|
// Response was already written (error case)
|
||||||
if corsReq.Origin == "" {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract bucket from request
|
if shouldContinue || corsResp == nil {
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
// Not a CORS request or should continue normally
|
||||||
if bucket == "" {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if bucket exists
|
|
||||||
if err := m.bucketChecker.CheckBucket(r, bucket); err != s3err.ErrNone {
|
|
||||||
// For non-existent buckets, return OK (let other handlers deal with bucket existence)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load CORS configuration from cache
|
|
||||||
config, errCode := m.corsConfigGetter.GetCORSConfiguration(bucket)
|
|
||||||
if errCode != s3err.ErrNone || config == nil {
|
|
||||||
// No CORS configuration for OPTIONS request should return access denied
|
|
||||||
if corsReq.IsPreflightRequest {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate CORS request
|
|
||||||
corsResp, err := EvaluateRequest(config, corsReq)
|
|
||||||
if err != nil {
|
|
||||||
glog.V(3).Infof("CORS evaluation failed for bucket %s: %v", bucket, err)
|
|
||||||
if corsReq.IsPreflightRequest {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,13 +51,6 @@ func (s3a *S3ApiServer) createMultipartUpload(r *http.Request, input *s3.CreateM
|
||||||
entry.Extended = make(map[string][]byte)
|
entry.Extended = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
entry.Extended["key"] = []byte(*input.Key)
|
entry.Extended["key"] = []byte(*input.Key)
|
||||||
|
|
||||||
// Set object owner for multipart upload
|
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
|
||||||
if amzAccountId != "" {
|
|
||||||
entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(amzAccountId)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range input.Metadata {
|
for k, v := range input.Metadata {
|
||||||
entry.Extended[k] = []byte(*v)
|
entry.Extended[k] = []byte(*v)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +92,7 @@ type CompleteMultipartUploadResult struct {
|
||||||
VersionId *string `xml:"-"`
|
VersionId *string `xml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.CompleteMultipartUploadInput, parts *CompleteMultipartUpload) (output *CompleteMultipartUploadResult, code s3err.ErrorCode) {
|
func (s3a *S3ApiServer) completeMultipartUpload(input *s3.CompleteMultipartUploadInput, parts *CompleteMultipartUpload) (output *CompleteMultipartUploadResult, code s3err.ErrorCode) {
|
||||||
|
|
||||||
glog.V(2).Infof("completeMultipartUpload input %v", input)
|
glog.V(2).Infof("completeMultipartUpload input %v", input)
|
||||||
if len(parts.Parts) == 0 {
|
if len(parts.Parts) == 0 {
|
||||||
|
@ -261,13 +254,6 @@ func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.Compl
|
||||||
}
|
}
|
||||||
versionEntry.Extended[s3_constants.ExtVersionIdKey] = []byte(versionId)
|
versionEntry.Extended[s3_constants.ExtVersionIdKey] = []byte(versionId)
|
||||||
versionEntry.Extended[s3_constants.SeaweedFSUploadId] = []byte(*input.UploadId)
|
versionEntry.Extended[s3_constants.SeaweedFSUploadId] = []byte(*input.UploadId)
|
||||||
|
|
||||||
// Set object owner for versioned multipart objects
|
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
|
||||||
if amzAccountId != "" {
|
|
||||||
versionEntry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(amzAccountId)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range pentry.Extended {
|
for k, v := range pentry.Extended {
|
||||||
if k != "key" {
|
if k != "key" {
|
||||||
versionEntry.Extended[k] = v
|
versionEntry.Extended[k] = v
|
||||||
|
@ -310,13 +296,6 @@ func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.Compl
|
||||||
entry.Extended = make(map[string][]byte)
|
entry.Extended = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
entry.Extended[s3_constants.ExtVersionIdKey] = []byte("null")
|
entry.Extended[s3_constants.ExtVersionIdKey] = []byte("null")
|
||||||
|
|
||||||
// Set object owner for suspended versioning multipart objects
|
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
|
||||||
if amzAccountId != "" {
|
|
||||||
entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(amzAccountId)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range pentry.Extended {
|
for k, v := range pentry.Extended {
|
||||||
if k != "key" {
|
if k != "key" {
|
||||||
entry.Extended[k] = v
|
entry.Extended[k] = v
|
||||||
|
@ -350,13 +329,6 @@ func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.Compl
|
||||||
entry.Extended = make(map[string][]byte)
|
entry.Extended = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
entry.Extended[s3_constants.SeaweedFSUploadId] = []byte(*input.UploadId)
|
entry.Extended[s3_constants.SeaweedFSUploadId] = []byte(*input.UploadId)
|
||||||
|
|
||||||
// Set object owner for non-versioned multipart objects
|
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
|
||||||
if amzAccountId != "" {
|
|
||||||
entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(amzAccountId)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range pentry.Extended {
|
for k, v := range pentry.Extended {
|
||||||
if k != "key" {
|
if k != "key" {
|
||||||
entry.Extended[k] = v
|
entry.Extended[k] = v
|
||||||
|
|
|
@ -77,29 +77,24 @@ const (
|
||||||
|
|
||||||
// Get request authentication type.
|
// Get request authentication type.
|
||||||
func getRequestAuthType(r *http.Request) authType {
|
func getRequestAuthType(r *http.Request) authType {
|
||||||
var authType authType
|
|
||||||
|
|
||||||
if isRequestSignatureV2(r) {
|
if isRequestSignatureV2(r) {
|
||||||
authType = authTypeSignedV2
|
return authTypeSignedV2
|
||||||
} else if isRequestPresignedSignatureV2(r) {
|
} else if isRequestPresignedSignatureV2(r) {
|
||||||
authType = authTypePresignedV2
|
return authTypePresignedV2
|
||||||
} else if isRequestSignStreamingV4(r) {
|
} else if isRequestSignStreamingV4(r) {
|
||||||
authType = authTypeStreamingSigned
|
return authTypeStreamingSigned
|
||||||
} else if isRequestUnsignedStreaming(r) {
|
} else if isRequestUnsignedStreaming(r) {
|
||||||
authType = authTypeStreamingUnsigned
|
return authTypeStreamingUnsigned
|
||||||
} else if isRequestSignatureV4(r) {
|
} else if isRequestSignatureV4(r) {
|
||||||
authType = authTypeSigned
|
return authTypeSigned
|
||||||
} else if isRequestPresignedSignatureV4(r) {
|
} else if isRequestPresignedSignatureV4(r) {
|
||||||
authType = authTypePresigned
|
return authTypePresigned
|
||||||
} else if isRequestJWT(r) {
|
} else if isRequestJWT(r) {
|
||||||
authType = authTypeJWT
|
return authTypeJWT
|
||||||
} else if isRequestPostPolicySignatureV4(r) {
|
} else if isRequestPostPolicySignatureV4(r) {
|
||||||
authType = authTypePostPolicy
|
return authTypePostPolicy
|
||||||
} else if _, ok := r.Header["Authorization"]; !ok {
|
} else if _, ok := r.Header["Authorization"]; !ok {
|
||||||
authType = authTypeAnonymous
|
return authTypeAnonymous
|
||||||
} else {
|
|
||||||
authType = authTypeUnknown
|
|
||||||
}
|
}
|
||||||
|
return authTypeUnknown
|
||||||
return authType
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -10,12 +9,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/s3_pb"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/cors"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/cors"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
|
@ -28,7 +23,6 @@ type BucketConfig struct {
|
||||||
Ownership string
|
Ownership string
|
||||||
ACL []byte
|
ACL []byte
|
||||||
Owner string
|
Owner string
|
||||||
IsPublicRead bool // Cached flag to avoid JSON parsing on every request
|
|
||||||
CORS *cors.CORSConfiguration
|
CORS *cors.CORSConfiguration
|
||||||
ObjectLockConfig *ObjectLockConfiguration // Cached parsed Object Lock configuration
|
ObjectLockConfig *ObjectLockConfiguration // Cached parsed Object Lock configuration
|
||||||
LastModified time.Time
|
LastModified time.Time
|
||||||
|
@ -104,11 +98,10 @@ func (s3a *S3ApiServer) getBucketConfig(bucket string) (*BucketConfig, s3err.Err
|
||||||
return config, s3err.ErrNone
|
return config, s3err.ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get from filer
|
// Load from filer
|
||||||
entry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
|
bucketEntry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
if err == filer_pb.ErrNotFound {
|
||||||
// Bucket doesn't exist
|
|
||||||
return nil, s3err.ErrNoSuchBucket
|
return nil, s3err.ErrNoSuchBucket
|
||||||
}
|
}
|
||||||
glog.Errorf("getBucketConfig: failed to get bucket entry for %s: %v", bucket, err)
|
glog.Errorf("getBucketConfig: failed to get bucket entry for %s: %v", bucket, err)
|
||||||
|
@ -117,38 +110,32 @@ func (s3a *S3ApiServer) getBucketConfig(bucket string) (*BucketConfig, s3err.Err
|
||||||
|
|
||||||
config := &BucketConfig{
|
config := &BucketConfig{
|
||||||
Name: bucket,
|
Name: bucket,
|
||||||
Entry: entry,
|
Entry: bucketEntry,
|
||||||
IsPublicRead: false, // Explicitly default to false for private buckets
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract configuration from extended attributes
|
// Extract configuration from extended attributes
|
||||||
if entry.Extended != nil {
|
if bucketEntry.Extended != nil {
|
||||||
if versioning, exists := entry.Extended[s3_constants.ExtVersioningKey]; exists {
|
if versioning, exists := bucketEntry.Extended[s3_constants.ExtVersioningKey]; exists {
|
||||||
config.Versioning = string(versioning)
|
config.Versioning = string(versioning)
|
||||||
}
|
}
|
||||||
if ownership, exists := entry.Extended[s3_constants.ExtOwnershipKey]; exists {
|
if ownership, exists := bucketEntry.Extended[s3_constants.ExtOwnershipKey]; exists {
|
||||||
config.Ownership = string(ownership)
|
config.Ownership = string(ownership)
|
||||||
}
|
}
|
||||||
if acl, exists := entry.Extended[s3_constants.ExtAmzAclKey]; exists {
|
if acl, exists := bucketEntry.Extended[s3_constants.ExtAmzAclKey]; exists {
|
||||||
config.ACL = acl
|
config.ACL = acl
|
||||||
// Parse ACL once and cache public-read status
|
|
||||||
config.IsPublicRead = parseAndCachePublicReadStatus(acl)
|
|
||||||
} else {
|
|
||||||
// No ACL means private bucket
|
|
||||||
config.IsPublicRead = false
|
|
||||||
}
|
}
|
||||||
if owner, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
if owner, exists := bucketEntry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
||||||
config.Owner = string(owner)
|
config.Owner = string(owner)
|
||||||
}
|
}
|
||||||
// Parse Object Lock configuration if present
|
// Parse Object Lock configuration if present
|
||||||
if objectLockConfig, found := LoadObjectLockConfigurationFromExtended(entry); found {
|
if objectLockConfig, found := LoadObjectLockConfigurationFromExtended(bucketEntry); found {
|
||||||
config.ObjectLockConfig = objectLockConfig
|
config.ObjectLockConfig = objectLockConfig
|
||||||
glog.V(2).Infof("getBucketConfig: cached Object Lock configuration for bucket %s", bucket)
|
glog.V(2).Infof("getBucketConfig: cached Object Lock configuration for bucket %s", bucket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load CORS configuration from bucket directory content
|
// Load CORS configuration from .s3metadata
|
||||||
if corsConfig, err := s3a.loadCORSFromBucketContent(bucket); err != nil {
|
if corsConfig, err := s3a.loadCORSFromMetadata(bucket); err != nil {
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||||
// Missing metadata is not an error; fall back cleanly
|
// Missing metadata is not an error; fall back cleanly
|
||||||
glog.V(2).Infof("CORS metadata not found for bucket %s, falling back to default behavior", bucket)
|
glog.V(2).Infof("CORS metadata not found for bucket %s, falling back to default behavior", bucket)
|
||||||
|
@ -253,7 +240,7 @@ func (s3a *S3ApiServer) getVersioningState(bucket string) (string, error) {
|
||||||
config, errCode := s3a.getBucketConfig(bucket)
|
config, errCode := s3a.getBucketConfig(bucket)
|
||||||
if errCode != s3err.ErrNone {
|
if errCode != s3err.ErrNone {
|
||||||
if errCode == s3err.ErrNoSuchBucket {
|
if errCode == s3err.ErrNoSuchBucket {
|
||||||
return "", nil
|
return "", filer_pb.ErrNotFound
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("failed to get bucket config: %v", errCode)
|
return "", fmt.Errorf("failed to get bucket config: %v", errCode)
|
||||||
}
|
}
|
||||||
|
@ -305,15 +292,57 @@ func (s3a *S3ApiServer) setBucketOwnership(bucket, ownership string) s3err.Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadCORSFromBucketContent loads CORS configuration from bucket directory content
|
// loadCORSFromMetadata loads CORS configuration from bucket metadata
|
||||||
func (s3a *S3ApiServer) loadCORSFromBucketContent(bucket string) (*cors.CORSConfiguration, error) {
|
func (s3a *S3ApiServer) loadCORSFromMetadata(bucket string) (*cors.CORSConfiguration, error) {
|
||||||
_, corsConfig, err := s3a.getBucketMetadata(bucket)
|
// Validate bucket name to prevent path traversal attacks
|
||||||
if err != nil {
|
if bucket == "" || strings.Contains(bucket, "/") || strings.Contains(bucket, "\\") ||
|
||||||
return nil, err
|
strings.Contains(bucket, "..") || strings.Contains(bucket, "~") {
|
||||||
|
return nil, fmt.Errorf("invalid bucket name: %s", bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: corsConfig can be nil if no CORS configuration is set, which is valid
|
// Clean the bucket name further to prevent any potential path traversal
|
||||||
return corsConfig, nil
|
bucket = filepath.Clean(bucket)
|
||||||
|
if bucket == "." || bucket == ".." {
|
||||||
|
return nil, fmt.Errorf("invalid bucket name: %s", bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketMetadataPath := filepath.Join(s3a.option.BucketsPath, bucket, cors.S3MetadataFileName)
|
||||||
|
|
||||||
|
entry, err := s3a.getEntry("", bucketMetadataPath)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(3).Infof("loadCORSFromMetadata: error retrieving metadata for bucket %s: %v", bucket, err)
|
||||||
|
return nil, fmt.Errorf("error retrieving CORS metadata for bucket %s: %w", bucket, err)
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
glog.V(3).Infof("loadCORSFromMetadata: no metadata entry found for bucket %s", bucket)
|
||||||
|
return nil, fmt.Errorf("no metadata entry found for bucket %s", bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entry.Content) == 0 {
|
||||||
|
glog.V(3).Infof("loadCORSFromMetadata: empty metadata content for bucket %s", bucket)
|
||||||
|
return nil, fmt.Errorf("no metadata content for bucket %s", bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(entry.Content, &metadata); err != nil {
|
||||||
|
glog.Errorf("loadCORSFromMetadata: failed to unmarshal metadata for bucket %s: %v", bucket, err)
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
corsData, exists := metadata["cors"]
|
||||||
|
if !exists {
|
||||||
|
glog.V(3).Infof("loadCORSFromMetadata: no CORS configuration found for bucket %s", bucket)
|
||||||
|
return nil, fmt.Errorf("no CORS configuration found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directly unmarshal the raw JSON to CORSConfiguration to avoid round-trip allocations
|
||||||
|
var config cors.CORSConfiguration
|
||||||
|
if err := json.Unmarshal(corsData, &config); err != nil {
|
||||||
|
glog.Errorf("loadCORSFromMetadata: failed to unmarshal CORS configuration for bucket %s: %v", bucket, err)
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal CORS configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCORSConfiguration retrieves CORS configuration with caching
|
// getCORSConfiguration retrieves CORS configuration with caching
|
||||||
|
@ -326,275 +355,50 @@ func (s3a *S3ApiServer) getCORSConfiguration(bucket string) (*cors.CORSConfigura
|
||||||
return config.CORS, s3err.ErrNone
|
return config.CORS, s3err.ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateCORSConfiguration updates the CORS configuration for a bucket
|
// getCORSStorage returns a CORS storage instance for persistent operations
|
||||||
|
func (s3a *S3ApiServer) getCORSStorage() *cors.Storage {
|
||||||
|
entryGetter := &S3EntryGetter{server: s3a}
|
||||||
|
return cors.NewStorage(s3a, entryGetter, s3a.option.BucketsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateCORSConfiguration updates CORS configuration and invalidates cache
|
||||||
func (s3a *S3ApiServer) updateCORSConfiguration(bucket string, corsConfig *cors.CORSConfiguration) s3err.ErrorCode {
|
func (s3a *S3ApiServer) updateCORSConfiguration(bucket string, corsConfig *cors.CORSConfiguration) s3err.ErrorCode {
|
||||||
// Get existing metadata
|
// Update in-memory cache
|
||||||
existingTags, _, err := s3a.getBucketMetadata(bucket)
|
errCode := s3a.updateBucketConfig(bucket, func(config *BucketConfig) error {
|
||||||
if err != nil {
|
config.CORS = corsConfig
|
||||||
glog.Errorf("updateCORSConfiguration: failed to get bucket metadata for bucket %s: %v", bucket, err)
|
|
||||||
return s3err.ErrInternalError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update CORS configuration
|
|
||||||
updatedCorsConfig := corsConfig
|
|
||||||
|
|
||||||
// Store updated metadata
|
|
||||||
if err := s3a.setBucketMetadata(bucket, existingTags, updatedCorsConfig); err != nil {
|
|
||||||
glog.Errorf("updateCORSConfiguration: failed to persist CORS config to bucket content for bucket %s: %v", bucket, err)
|
|
||||||
return s3err.ErrInternalError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache will be updated automatically via metadata subscription
|
|
||||||
return s3err.ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeCORSConfiguration removes the CORS configuration for a bucket
|
|
||||||
func (s3a *S3ApiServer) removeCORSConfiguration(bucket string) s3err.ErrorCode {
|
|
||||||
// Get existing metadata
|
|
||||||
existingTags, _, err := s3a.getBucketMetadata(bucket)
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("removeCORSConfiguration: failed to get bucket metadata for bucket %s: %v", bucket, err)
|
|
||||||
return s3err.ErrInternalError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove CORS configuration
|
|
||||||
var nilCorsConfig *cors.CORSConfiguration = nil
|
|
||||||
|
|
||||||
// Store updated metadata
|
|
||||||
if err := s3a.setBucketMetadata(bucket, existingTags, nilCorsConfig); err != nil {
|
|
||||||
glog.Errorf("removeCORSConfiguration: failed to remove CORS config from bucket content for bucket %s: %v", bucket, err)
|
|
||||||
return s3err.ErrInternalError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache will be updated automatically via metadata subscription
|
|
||||||
return s3err.ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conversion functions between CORS types and protobuf types
|
|
||||||
|
|
||||||
// corsRuleToProto converts a CORS rule to protobuf format
|
|
||||||
func corsRuleToProto(rule cors.CORSRule) *s3_pb.CORSRule {
|
|
||||||
return &s3_pb.CORSRule{
|
|
||||||
AllowedHeaders: rule.AllowedHeaders,
|
|
||||||
AllowedMethods: rule.AllowedMethods,
|
|
||||||
AllowedOrigins: rule.AllowedOrigins,
|
|
||||||
ExposeHeaders: rule.ExposeHeaders,
|
|
||||||
MaxAgeSeconds: int32(getMaxAgeSecondsValue(rule.MaxAgeSeconds)),
|
|
||||||
Id: rule.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// corsRuleFromProto converts a protobuf CORS rule to standard format
|
|
||||||
func corsRuleFromProto(protoRule *s3_pb.CORSRule) cors.CORSRule {
|
|
||||||
var maxAge *int
|
|
||||||
// Always create the pointer if MaxAgeSeconds is >= 0
|
|
||||||
// This prevents nil pointer dereferences in tests and matches AWS behavior
|
|
||||||
if protoRule.MaxAgeSeconds >= 0 {
|
|
||||||
age := int(protoRule.MaxAgeSeconds)
|
|
||||||
maxAge = &age
|
|
||||||
}
|
|
||||||
// Only leave maxAge as nil if MaxAgeSeconds was explicitly set to a negative value
|
|
||||||
|
|
||||||
return cors.CORSRule{
|
|
||||||
AllowedHeaders: protoRule.AllowedHeaders,
|
|
||||||
AllowedMethods: protoRule.AllowedMethods,
|
|
||||||
AllowedOrigins: protoRule.AllowedOrigins,
|
|
||||||
ExposeHeaders: protoRule.ExposeHeaders,
|
|
||||||
MaxAgeSeconds: maxAge,
|
|
||||||
ID: protoRule.Id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// corsConfigToProto converts CORS configuration to protobuf format
|
|
||||||
func corsConfigToProto(config *cors.CORSConfiguration) *s3_pb.CORSConfiguration {
|
|
||||||
if config == nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
protoRules := make([]*s3_pb.CORSRule, len(config.CORSRules))
|
|
||||||
for i, rule := range config.CORSRules {
|
|
||||||
protoRules[i] = corsRuleToProto(rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &s3_pb.CORSConfiguration{
|
|
||||||
CorsRules: protoRules,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// corsConfigFromProto converts protobuf CORS configuration to standard format
|
|
||||||
func corsConfigFromProto(protoConfig *s3_pb.CORSConfiguration) *cors.CORSConfiguration {
|
|
||||||
if protoConfig == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rules := make([]cors.CORSRule, len(protoConfig.CorsRules))
|
|
||||||
for i, protoRule := range protoConfig.CorsRules {
|
|
||||||
rules[i] = corsRuleFromProto(protoRule)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cors.CORSConfiguration{
|
|
||||||
CORSRules: rules,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getMaxAgeSecondsValue safely extracts max age seconds value
|
|
||||||
func getMaxAgeSecondsValue(maxAge *int) int {
|
|
||||||
if maxAge == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return *maxAge
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseAndCachePublicReadStatus parses the ACL and caches the public-read status
|
|
||||||
func parseAndCachePublicReadStatus(acl []byte) bool {
|
|
||||||
var grants []*s3.Grant
|
|
||||||
if err := json.Unmarshal(acl, &grants); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any grant gives read permission to "AllUsers" group
|
|
||||||
for _, grant := range grants {
|
|
||||||
if grant.Grantee != nil && grant.Grantee.URI != nil && grant.Permission != nil {
|
|
||||||
// Check for AllUsers group with Read permission
|
|
||||||
if *grant.Grantee.URI == s3_constants.GranteeGroupAllUsers &&
|
|
||||||
(*grant.Permission == s3_constants.PermissionRead || *grant.Permission == s3_constants.PermissionFullControl) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBucketMetadata retrieves bucket metadata from bucket directory content using protobuf
|
|
||||||
func (s3a *S3ApiServer) getBucketMetadata(bucket string) (map[string]string, *cors.CORSConfiguration, error) {
|
|
||||||
// Validate bucket name to prevent path traversal attacks
|
|
||||||
if bucket == "" || strings.Contains(bucket, "/") || strings.Contains(bucket, "\\") ||
|
|
||||||
strings.Contains(bucket, "..") || strings.Contains(bucket, "~") {
|
|
||||||
return nil, nil, fmt.Errorf("invalid bucket name: %s", bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean the bucket name further to prevent any potential path traversal
|
|
||||||
bucket = filepath.Clean(bucket)
|
|
||||||
if bucket == "." || bucket == ".." {
|
|
||||||
return nil, nil, fmt.Errorf("invalid bucket name: %s", bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get bucket directory entry to access its content
|
|
||||||
entry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error retrieving bucket directory %s: %w", bucket, err)
|
|
||||||
}
|
|
||||||
if entry == nil {
|
|
||||||
return nil, nil, fmt.Errorf("bucket directory not found %s", bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no content, return empty metadata
|
|
||||||
if len(entry.Content) == 0 {
|
|
||||||
return make(map[string]string), nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal metadata from protobuf
|
|
||||||
var protoMetadata s3_pb.BucketMetadata
|
|
||||||
if err := proto.Unmarshal(entry.Content, &protoMetadata); err != nil {
|
|
||||||
glog.Errorf("getBucketMetadata: failed to unmarshal protobuf metadata for bucket %s: %v", bucket, err)
|
|
||||||
return make(map[string]string), nil, nil // Return empty metadata on error, don't fail
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert protobuf CORS to standard CORS
|
|
||||||
corsConfig := corsConfigFromProto(protoMetadata.Cors)
|
|
||||||
|
|
||||||
return protoMetadata.Tags, corsConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setBucketMetadata stores bucket metadata in bucket directory content using protobuf
|
|
||||||
func (s3a *S3ApiServer) setBucketMetadata(bucket string, tags map[string]string, corsConfig *cors.CORSConfiguration) error {
|
|
||||||
// Validate bucket name to prevent path traversal attacks
|
|
||||||
if bucket == "" || strings.Contains(bucket, "/") || strings.Contains(bucket, "\\") ||
|
|
||||||
strings.Contains(bucket, "..") || strings.Contains(bucket, "~") {
|
|
||||||
return fmt.Errorf("invalid bucket name: %s", bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean the bucket name further to prevent any potential path traversal
|
|
||||||
bucket = filepath.Clean(bucket)
|
|
||||||
if bucket == "." || bucket == ".." {
|
|
||||||
return fmt.Errorf("invalid bucket name: %s", bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create protobuf metadata
|
|
||||||
protoMetadata := &s3_pb.BucketMetadata{
|
|
||||||
Tags: tags,
|
|
||||||
Cors: corsConfigToProto(corsConfig),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal metadata to protobuf
|
|
||||||
metadataBytes, err := proto.Marshal(protoMetadata)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal bucket metadata to protobuf: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the bucket entry with new content
|
|
||||||
err = s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
||||||
// Get current bucket entry
|
|
||||||
entry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error retrieving bucket directory %s: %w", bucket, err)
|
|
||||||
}
|
|
||||||
if entry == nil {
|
|
||||||
return fmt.Errorf("bucket directory not found %s", bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update content with metadata
|
|
||||||
entry.Content = metadataBytes
|
|
||||||
|
|
||||||
request := &filer_pb.UpdateEntryRequest{
|
|
||||||
Directory: s3a.option.BucketsPath,
|
|
||||||
Entry: entry,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = client.UpdateEntry(context.Background(), request)
|
|
||||||
return err
|
|
||||||
})
|
})
|
||||||
return err
|
if errCode != s3err.ErrNone {
|
||||||
|
return errCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBucketTags retrieves bucket tags from bucket directory content
|
// Persist to .s3metadata file
|
||||||
func (s3a *S3ApiServer) getBucketTags(bucket string) (map[string]string, error) {
|
storage := s3a.getCORSStorage()
|
||||||
tags, _, err := s3a.getBucketMetadata(bucket)
|
if err := storage.Store(bucket, corsConfig); err != nil {
|
||||||
if err != nil {
|
glog.Errorf("updateCORSConfiguration: failed to persist CORS config to metadata for bucket %s: %v", bucket, err)
|
||||||
return nil, err
|
return s3err.ErrInternalError
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tags) == 0 {
|
return s3err.ErrNone
|
||||||
return nil, fmt.Errorf("no tags configuration found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags, nil
|
// removeCORSConfiguration removes CORS configuration and invalidates cache
|
||||||
|
func (s3a *S3ApiServer) removeCORSConfiguration(bucket string) s3err.ErrorCode {
|
||||||
|
// Remove from in-memory cache
|
||||||
|
errCode := s3a.updateBucketConfig(bucket, func(config *BucketConfig) error {
|
||||||
|
config.CORS = nil
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if errCode != s3err.ErrNone {
|
||||||
|
return errCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// setBucketTags stores bucket tags in bucket directory content
|
// Remove from .s3metadata file
|
||||||
func (s3a *S3ApiServer) setBucketTags(bucket string, tags map[string]string) error {
|
storage := s3a.getCORSStorage()
|
||||||
// Get existing metadata
|
if err := storage.Delete(bucket); err != nil {
|
||||||
_, existingCorsConfig, err := s3a.getBucketMetadata(bucket)
|
glog.Errorf("removeCORSConfiguration: failed to remove CORS config from metadata for bucket %s: %v", bucket, err)
|
||||||
if err != nil {
|
return s3err.ErrInternalError
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store updated metadata with new tags
|
return s3err.ErrNone
|
||||||
err = s3a.setBucketMetadata(bucket, tags, existingCorsConfig)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteBucketTags removes bucket tags from bucket directory content
|
|
||||||
func (s3a *S3ApiServer) deleteBucketTags(bucket string) error {
|
|
||||||
// Get existing metadata
|
|
||||||
_, existingCorsConfig, err := s3a.getBucketMetadata(bucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store updated metadata with empty tags
|
|
||||||
emptyTags := make(map[string]string)
|
|
||||||
err = s3a.setBucketMetadata(bucket, emptyTags, existingCorsConfig)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,21 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/cors"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/cors"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// S3EntryGetter implements cors.EntryGetter interface
|
||||||
|
type S3EntryGetter struct {
|
||||||
|
server *S3ApiServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *S3EntryGetter) GetEntry(directory, name string) (*filer_pb.Entry, error) {
|
||||||
|
return g.server.getEntry(directory, name)
|
||||||
|
}
|
||||||
|
|
||||||
// S3BucketChecker implements cors.BucketChecker interface
|
// S3BucketChecker implements cors.BucketChecker interface
|
||||||
type S3BucketChecker struct {
|
type S3BucketChecker struct {
|
||||||
server *S3ApiServer
|
server *S3ApiServer
|
||||||
|
@ -30,10 +40,11 @@ func (g *S3CORSConfigGetter) GetCORSConfiguration(bucket string) (*cors.CORSConf
|
||||||
|
|
||||||
// getCORSMiddleware returns a CORS middleware instance with caching
|
// getCORSMiddleware returns a CORS middleware instance with caching
|
||||||
func (s3a *S3ApiServer) getCORSMiddleware() *cors.Middleware {
|
func (s3a *S3ApiServer) getCORSMiddleware() *cors.Middleware {
|
||||||
|
storage := s3a.getCORSStorage()
|
||||||
bucketChecker := &S3BucketChecker{server: s3a}
|
bucketChecker := &S3BucketChecker{server: s3a}
|
||||||
corsConfigGetter := &S3CORSConfigGetter{server: s3a}
|
corsConfigGetter := &S3CORSConfigGetter{server: s3a}
|
||||||
|
|
||||||
return cors.NewMiddleware(bucketChecker, corsConfigGetter)
|
return cors.NewMiddleware(storage, bucketChecker, corsConfigGetter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketCorsHandler handles Get bucket CORS configuration
|
// GetBucketCorsHandler handles Get bucket CORS configuration
|
||||||
|
|
|
@ -3,7 +3,6 @@ package s3api
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -81,8 +80,8 @@ func (s3a *S3ApiServer) ListBucketsHandler(w http.ResponseWriter, r *http.Reques
|
||||||
|
|
||||||
func (s3a *S3ApiServer) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (s3a *S3ApiServer) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// collect parameters
|
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||||
|
glog.V(3).Infof("PutBucketHandler %s", bucket)
|
||||||
|
|
||||||
// validate the bucket name
|
// validate the bucket name
|
||||||
err := s3bucket.VerifyS3BucketName(bucket)
|
err := s3bucket.VerifyS3BucketName(bucket)
|
||||||
|
@ -231,7 +230,7 @@ func (s3a *S3ApiServer) HeadBucketHandler(w http.ResponseWriter, r *http.Request
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||||
glog.V(3).Infof("HeadBucketHandler %s", bucket)
|
glog.V(3).Infof("HeadBucketHandler %s", bucket)
|
||||||
|
|
||||||
if entry, err := s3a.getEntry(s3a.option.BucketsPath, bucket); entry == nil || errors.Is(err, filer_pb.ErrNotFound) {
|
if entry, err := s3a.getEntry(s3a.option.BucketsPath, bucket); entry == nil || err == filer_pb.ErrNotFound {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -241,7 +240,7 @@ func (s3a *S3ApiServer) HeadBucketHandler(w http.ResponseWriter, r *http.Request
|
||||||
|
|
||||||
func (s3a *S3ApiServer) checkBucket(r *http.Request, bucket string) s3err.ErrorCode {
|
func (s3a *S3ApiServer) checkBucket(r *http.Request, bucket string) s3err.ErrorCode {
|
||||||
entry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
|
entry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
|
||||||
if entry == nil || errors.Is(err, filer_pb.ErrNotFound) {
|
if entry == nil || err == filer_pb.ErrNotFound {
|
||||||
return s3err.ErrNoSuchBucket
|
return s3err.ErrNoSuchBucket
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,51 +288,6 @@ func (s3a *S3ApiServer) isUserAdmin(r *http.Request) bool {
|
||||||
return identity != nil && identity.isAdmin()
|
return identity != nil && identity.isAdmin()
|
||||||
}
|
}
|
||||||
|
|
||||||
// isBucketPublicRead checks if a bucket allows anonymous read access based on its cached ACL status
|
|
||||||
func (s3a *S3ApiServer) isBucketPublicRead(bucket string) bool {
|
|
||||||
// Get bucket configuration which contains cached public-read status
|
|
||||||
config, errCode := s3a.getBucketConfig(bucket)
|
|
||||||
if errCode != s3err.ErrNone {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the cached public-read status (no JSON parsing needed)
|
|
||||||
return config.IsPublicRead
|
|
||||||
}
|
|
||||||
|
|
||||||
// isPublicReadGrants checks if the grants allow public read access
|
|
||||||
func isPublicReadGrants(grants []*s3.Grant) bool {
|
|
||||||
for _, grant := range grants {
|
|
||||||
if grant.Grantee != nil && grant.Grantee.URI != nil && grant.Permission != nil {
|
|
||||||
// Check for AllUsers group with Read permission
|
|
||||||
if *grant.Grantee.URI == s3_constants.GranteeGroupAllUsers &&
|
|
||||||
(*grant.Permission == s3_constants.PermissionRead || *grant.Permission == s3_constants.PermissionFullControl) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthWithPublicRead creates an auth wrapper that allows anonymous access for public-read buckets
|
|
||||||
func (s3a *S3ApiServer) AuthWithPublicRead(handler http.HandlerFunc, action Action) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
|
||||||
authType := getRequestAuthType(r)
|
|
||||||
isAnonymous := authType == authTypeAnonymous
|
|
||||||
|
|
||||||
if isAnonymous {
|
|
||||||
isPublic := s3a.isBucketPublicRead(bucket)
|
|
||||||
|
|
||||||
if isPublic {
|
|
||||||
handler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s3a.iam.Auth(handler, action)(w, r) // Fallback to normal IAM auth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketAclHandler Get Bucket ACL
|
// GetBucketAclHandler Get Bucket ACL
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketAcl.html
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketAcl.html
|
||||||
func (s3a *S3ApiServer) GetBucketAclHandler(w http.ResponseWriter, r *http.Request) {
|
func (s3a *S3ApiServer) GetBucketAclHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -366,7 +320,7 @@ func (s3a *S3ApiServer) GetBucketAclHandler(w http.ResponseWriter, r *http.Reque
|
||||||
writeSuccessResponseXML(w, r, response)
|
writeSuccessResponseXML(w, r, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBucketAclHandler Put bucket ACL
|
// PutBucketAclHandler Put bucket ACL only responds success if the ACL is private.
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketAcl.html //
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketAcl.html //
|
||||||
func (s3a *S3ApiServer) PutBucketAclHandler(w http.ResponseWriter, r *http.Request) {
|
func (s3a *S3ApiServer) PutBucketAclHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// collect parameters
|
// collect parameters
|
||||||
|
@ -377,48 +331,24 @@ func (s3a *S3ApiServer) PutBucketAclHandler(w http.ResponseWriter, r *http.Reque
|
||||||
s3err.WriteErrorResponse(w, r, err)
|
s3err.WriteErrorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cannedAcl := r.Header.Get(s3_constants.AmzCannedAcl)
|
||||||
// Get account information for ACL processing
|
switch {
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
case cannedAcl == "":
|
||||||
|
acl := &s3.AccessControlPolicy{}
|
||||||
// Get bucket ownership settings (these would be used for ownership validation in a full implementation)
|
if err := xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
|
||||||
bucketOwnership := "" // Default/simplified for now - in a full implementation this would be retrieved from bucket config
|
glog.Errorf("PutBucketAclHandler: %s", err)
|
||||||
bucketOwnerId := amzAccountId // Simplified - bucket owner is current account
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
||||||
|
|
||||||
// Use the existing ACL parsing logic to handle both canned ACLs and XML body
|
|
||||||
grants, errCode := ExtractAcl(r, s3a.iam, bucketOwnership, bucketOwnerId, amzAccountId, amzAccountId)
|
|
||||||
if errCode != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, errCode)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(acl.Grants) == 1 && acl.Grants[0].Permission != nil && *acl.Grants[0].Permission == s3_constants.PermissionFullControl {
|
||||||
// Store the bucket ACL in bucket metadata
|
|
||||||
errCode = s3a.updateBucketConfig(bucket, func(config *BucketConfig) error {
|
|
||||||
if len(grants) > 0 {
|
|
||||||
grantsBytes, err := json.Marshal(grants)
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("PutBucketAclHandler: failed to marshal grants: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
config.ACL = grantsBytes
|
|
||||||
// Cache the public-read status to avoid JSON parsing on every request
|
|
||||||
config.IsPublicRead = isPublicReadGrants(grants)
|
|
||||||
} else {
|
|
||||||
config.ACL = nil
|
|
||||||
config.IsPublicRead = false
|
|
||||||
}
|
|
||||||
config.Owner = amzAccountId
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if errCode != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, errCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
glog.V(3).Infof("PutBucketAclHandler: Successfully stored ACL for bucket %s with %d grants", bucket, len(grants))
|
|
||||||
|
|
||||||
writeSuccessResponseEmpty(w, r)
|
writeSuccessResponseEmpty(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case cannedAcl == s3_constants.CannedAclPrivate:
|
||||||
|
writeSuccessResponseEmpty(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketLifecycleConfigurationHandler Get Bucket Lifecycle configuration
|
// GetBucketLifecycleConfigurationHandler Get Bucket Lifecycle configuration
|
||||||
|
@ -739,7 +669,7 @@ func (s3a *S3ApiServer) DeleteBucketOwnershipControls(w http.ResponseWriter, r *
|
||||||
|
|
||||||
bucketEntry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
|
bucketEntry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
if err == filer_pb.ErrNotFound {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,206 +1,37 @@
|
||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
"github.com/stretchr/testify/assert"
|
"testing"
|
||||||
"github.com/stretchr/testify/require"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPutBucketAclCannedAclSupport(t *testing.T) {
|
func TestListBucketsHandler(t *testing.T) {
|
||||||
// Test that the ExtractAcl function can handle various canned ACLs
|
|
||||||
// This tests the core functionality without requiring a fully initialized S3ApiServer
|
|
||||||
|
|
||||||
testCases := []struct {
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
name string
|
<ListAllMyBucketsResult><Owner><ID></ID></Owner><Buckets><Bucket><Name>test1</Name><CreationDate>2011-04-09T12:34:49Z</CreationDate></Bucket><Bucket><Name>test2</Name><CreationDate>2011-02-09T12:34:49Z</CreationDate></Bucket></Buckets></ListAllMyBucketsResult>`
|
||||||
cannedAcl string
|
var response ListAllMyBucketsResult
|
||||||
shouldWork bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "private",
|
|
||||||
cannedAcl: s3_constants.CannedAclPrivate,
|
|
||||||
shouldWork: true,
|
|
||||||
description: "private ACL should be accepted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "public-read",
|
|
||||||
cannedAcl: s3_constants.CannedAclPublicRead,
|
|
||||||
shouldWork: true,
|
|
||||||
description: "public-read ACL should be accepted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "public-read-write",
|
|
||||||
cannedAcl: s3_constants.CannedAclPublicReadWrite,
|
|
||||||
shouldWork: true,
|
|
||||||
description: "public-read-write ACL should be accepted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "authenticated-read",
|
|
||||||
cannedAcl: s3_constants.CannedAclAuthenticatedRead,
|
|
||||||
shouldWork: true,
|
|
||||||
description: "authenticated-read ACL should be accepted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bucket-owner-read",
|
|
||||||
cannedAcl: s3_constants.CannedAclBucketOwnerRead,
|
|
||||||
shouldWork: true,
|
|
||||||
description: "bucket-owner-read ACL should be accepted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bucket-owner-full-control",
|
|
||||||
cannedAcl: s3_constants.CannedAclBucketOwnerFullControl,
|
|
||||||
shouldWork: true,
|
|
||||||
description: "bucket-owner-full-control ACL should be accepted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid-acl",
|
|
||||||
cannedAcl: "invalid-acl-value",
|
|
||||||
shouldWork: false,
|
|
||||||
description: "invalid ACL should be rejected",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
var bucketsList ListAllMyBucketsList
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
bucketsList.Bucket = append(bucketsList.Bucket, ListAllMyBucketsEntry{
|
||||||
// Create a request with the specified canned ACL
|
Name: "test1",
|
||||||
req := httptest.NewRequest("PUT", "/bucket?acl", nil)
|
CreationDate: time.Date(2011, 4, 9, 12, 34, 49, 0, time.UTC),
|
||||||
req.Header.Set(s3_constants.AmzCannedAcl, tc.cannedAcl)
|
|
||||||
req.Header.Set(s3_constants.AmzAccountId, "test-account-123")
|
|
||||||
|
|
||||||
// Create a mock IAM for testing
|
|
||||||
mockIam := &mockIamInterface{}
|
|
||||||
|
|
||||||
// Test the ACL extraction directly
|
|
||||||
grants, errCode := ExtractAcl(req, mockIam, "", "test-account-123", "test-account-123", "test-account-123")
|
|
||||||
|
|
||||||
if tc.shouldWork {
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "Expected ACL parsing to succeed for %s", tc.cannedAcl)
|
|
||||||
assert.NotEmpty(t, grants, "Expected grants to be generated for valid ACL %s", tc.cannedAcl)
|
|
||||||
t.Logf("✓ PASS: %s - %s", tc.name, tc.description)
|
|
||||||
} else {
|
|
||||||
assert.NotEqual(t, s3err.ErrNone, errCode, "Expected ACL parsing to fail for invalid ACL %s", tc.cannedAcl)
|
|
||||||
t.Logf("✓ PASS: %s - %s", tc.name, tc.description)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
bucketsList.Bucket = append(bucketsList.Bucket, ListAllMyBucketsEntry{
|
||||||
}
|
Name: "test2",
|
||||||
|
CreationDate: time.Date(2011, 2, 9, 12, 34, 49, 0, time.UTC),
|
||||||
// TestBucketWithoutACLIsNotPublicRead tests that buckets without ACLs are not public-read
|
|
||||||
func TestBucketWithoutACLIsNotPublicRead(t *testing.T) {
|
|
||||||
// Create a bucket config without ACL (like a freshly created bucket)
|
|
||||||
config := &BucketConfig{
|
|
||||||
Name: "test-bucket",
|
|
||||||
IsPublicRead: false, // Should be explicitly false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that buckets without ACL are not public-read
|
|
||||||
assert.False(t, config.IsPublicRead, "Bucket without ACL should not be public-read")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBucketConfigInitialization(t *testing.T) {
|
|
||||||
// Test that BucketConfig properly initializes IsPublicRead field
|
|
||||||
config := &BucketConfig{
|
|
||||||
Name: "test-bucket",
|
|
||||||
IsPublicRead: false, // Explicitly set to false for private buckets
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify proper initialization
|
|
||||||
assert.False(t, config.IsPublicRead, "Newly created bucket should not be public-read by default")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestUpdateBucketConfigCacheConsistency tests that updateBucketConfigCacheFromEntry
|
|
||||||
// properly handles the IsPublicRead flag consistently with getBucketConfig
|
|
||||||
func TestUpdateBucketConfigCacheConsistency(t *testing.T) {
|
|
||||||
t.Run("bucket without ACL should have IsPublicRead=false", func(t *testing.T) {
|
|
||||||
// Simulate an entry without ACL (like a freshly created bucket)
|
|
||||||
entry := &filer_pb.Entry{
|
|
||||||
Name: "test-bucket",
|
|
||||||
Attributes: &filer_pb.FuseAttributes{
|
|
||||||
FileMode: 0755,
|
|
||||||
},
|
|
||||||
// Extended is nil or doesn't contain ACL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test what updateBucketConfigCacheFromEntry would create
|
|
||||||
config := &BucketConfig{
|
|
||||||
Name: entry.Name,
|
|
||||||
Entry: entry,
|
|
||||||
IsPublicRead: false, // Should be explicitly false
|
|
||||||
}
|
|
||||||
|
|
||||||
// When Extended is nil, IsPublicRead should be false
|
|
||||||
assert.False(t, config.IsPublicRead, "Bucket without Extended metadata should not be public-read")
|
|
||||||
|
|
||||||
// When Extended exists but has no ACL key, IsPublicRead should also be false
|
|
||||||
entry.Extended = make(map[string][]byte)
|
|
||||||
entry.Extended["some-other-key"] = []byte("some-value")
|
|
||||||
|
|
||||||
config = &BucketConfig{
|
|
||||||
Name: entry.Name,
|
|
||||||
Entry: entry,
|
|
||||||
IsPublicRead: false, // Should be explicitly false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate the else branch: no ACL means private bucket
|
|
||||||
if _, exists := entry.Extended[s3_constants.ExtAmzAclKey]; !exists {
|
|
||||||
config.IsPublicRead = false
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.False(t, config.IsPublicRead, "Bucket with Extended but no ACL should not be public-read")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bucket with public-read ACL should have IsPublicRead=true", func(t *testing.T) {
|
response = ListAllMyBucketsResult{
|
||||||
// Create a mock public-read ACL using AWS S3 SDK types
|
Owner: CanonicalUser{
|
||||||
publicReadGrants := []*s3.Grant{
|
ID: "",
|
||||||
{
|
DisplayName: "",
|
||||||
Grantee: &s3.Grantee{
|
|
||||||
Type: &s3_constants.GrantTypeGroup,
|
|
||||||
URI: &s3_constants.GranteeGroupAllUsers,
|
|
||||||
},
|
|
||||||
Permission: &s3_constants.PermissionRead,
|
|
||||||
},
|
},
|
||||||
|
Buckets: bucketsList,
|
||||||
}
|
}
|
||||||
|
|
||||||
aclBytes, err := json.Marshal(publicReadGrants)
|
encoded := string(s3err.EncodeXMLResponse(response))
|
||||||
require.NoError(t, err)
|
if encoded != expected {
|
||||||
|
t.Errorf("unexpected output:%s\nexpecting:%s", encoded, expected)
|
||||||
entry := &filer_pb.Entry{
|
|
||||||
Name: "public-bucket",
|
|
||||||
Extended: map[string][]byte{
|
|
||||||
s3_constants.ExtAmzAclKey: aclBytes,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &BucketConfig{
|
|
||||||
Name: entry.Name,
|
|
||||||
Entry: entry,
|
|
||||||
IsPublicRead: false, // Start with false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate what updateBucketConfigCacheFromEntry would do
|
|
||||||
if acl, exists := entry.Extended[s3_constants.ExtAmzAclKey]; exists {
|
|
||||||
config.ACL = acl
|
|
||||||
config.IsPublicRead = parseAndCachePublicReadStatus(acl)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.True(t, config.IsPublicRead, "Bucket with public-read ACL should be public-read")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// mockIamInterface is a simple mock for testing
|
|
||||||
type mockIamInterface struct{}
|
|
||||||
|
|
||||||
func (m *mockIamInterface) GetAccountNameById(canonicalId string) string {
|
|
||||||
return "test-user-" + canonicalId
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIamInterface) GetAccountIdByEmail(email string) string {
|
|
||||||
return "account-for-" + email
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,17 +26,31 @@ func (s3a *S3ApiServer) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http
|
||||||
s3err.WriteErrorResponse(w, r, http.StatusNoContent)
|
s3err.WriteErrorResponse(w, r, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketEncryptionHandler Returns the default encryption configuration
|
// GetBucketTaggingHandler Returns the tag set associated with the bucket
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketEncryption.html
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketTagging.html
|
||||||
func (s3a *S3ApiServer) GetBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
|
func (s3a *S3ApiServer) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||||
glog.V(3).Infof("GetBucketEncryption %s", bucket)
|
glog.V(3).Infof("GetBucketTagging %s", bucket)
|
||||||
|
|
||||||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
|
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
|
||||||
s3err.WriteErrorResponse(w, r, err)
|
s3err.WriteErrorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchTagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s3a *S3ApiServer) PutBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s3a *S3ApiServer) DeleteBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBucketEncryptionHandler Returns the default encryption configuration
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketEncryption.html
|
||||||
|
func (s3a *S3ApiServer) GetBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented)
|
s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
package s3api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetBucketTaggingHandler Returns the tag set associated with the bucket
|
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketTagging.html
|
|
||||||
func (s3a *S3ApiServer) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
|
||||||
glog.V(3).Infof("GetBucketTagging %s", bucket)
|
|
||||||
|
|
||||||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load bucket tags from metadata
|
|
||||||
tags, err := s3a.getBucketTags(bucket)
|
|
||||||
if err != nil {
|
|
||||||
glog.V(3).Infof("GetBucketTagging: no tags found for bucket %s: %v", bucket, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchTagSet)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert tags to XML response format
|
|
||||||
tagging := FromTags(tags)
|
|
||||||
writeSuccessResponseXML(w, r, tagging)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBucketTaggingHandler Put bucket tagging
|
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketTagging.html
|
|
||||||
func (s3a *S3ApiServer) PutBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
|
||||||
glog.V(3).Infof("PutBucketTagging %s", bucket)
|
|
||||||
|
|
||||||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse tagging configuration from request body
|
|
||||||
tagging := &Tagging{}
|
|
||||||
input, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength))
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("PutBucketTagging read input %s: %v", r.URL, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = xml.Unmarshal(input, tagging); err != nil {
|
|
||||||
glog.Errorf("PutBucketTagging Unmarshal %s: %v", r.URL, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := tagging.ToTags()
|
|
||||||
|
|
||||||
// Validate tags using existing validation
|
|
||||||
err = ValidateTags(tags)
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("PutBucketTagging ValidateTags error %s: %v", r.URL, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidTag)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store bucket tags in metadata
|
|
||||||
if err = s3a.setBucketTags(bucket, tags); err != nil {
|
|
||||||
glog.Errorf("PutBucketTagging setBucketTags %s: %v", r.URL, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeSuccessResponseEmpty(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBucketTaggingHandler Delete bucket tagging
|
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketTagging.html
|
|
||||||
func (s3a *S3ApiServer) DeleteBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
|
||||||
glog.V(3).Infof("DeleteBucketTagging %s", bucket)
|
|
||||||
|
|
||||||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove bucket tags from metadata
|
|
||||||
if err := s3a.deleteBucketTags(bucket); err != nil {
|
|
||||||
glog.Errorf("DeleteBucketTagging deleteBucketTags %s: %v", r.URL, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
s3err.PostLog(r, http.StatusNoContent, s3err.ErrNone)
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@ package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -86,96 +85,7 @@ func removeDuplicateSlashes(object string) string {
|
||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDirectoryObject checks if the object is a directory object (ends with "/") and if it exists
|
func newListEntry(entry *filer_pb.Entry, key string, dir string, name string, bucketPrefix string, fetchOwner bool, isDirectory bool, encodingTypeUrl bool) (listEntry ListEntry) {
|
||||||
// Returns: (entry, isDirectoryObject, error)
|
|
||||||
// - entry: the directory entry if found and is a directory
|
|
||||||
// - isDirectoryObject: true if the request was for a directory object (ends with "/")
|
|
||||||
// - error: any error encountered while checking
|
|
||||||
func (s3a *S3ApiServer) checkDirectoryObject(bucket, object string) (*filer_pb.Entry, bool, error) {
|
|
||||||
if !strings.HasSuffix(object, "/") {
|
|
||||||
return nil, false, nil // Not a directory object
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
|
||||||
cleanObject := strings.TrimSuffix(strings.TrimPrefix(object, "/"), "/")
|
|
||||||
|
|
||||||
if cleanObject == "" {
|
|
||||||
return nil, true, nil // Root level directory object, but we don't handle it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if directory exists
|
|
||||||
dirEntry, err := s3a.getEntry(bucketDir, cleanObject)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
||||||
return nil, true, nil // Directory object requested but doesn't exist
|
|
||||||
}
|
|
||||||
return nil, true, err // Other errors should be propagated
|
|
||||||
}
|
|
||||||
|
|
||||||
if !dirEntry.IsDirectory {
|
|
||||||
return nil, true, nil // Exists but not a directory
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirEntry, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveDirectoryContent serves the content of a directory object directly
|
|
||||||
func (s3a *S3ApiServer) serveDirectoryContent(w http.ResponseWriter, r *http.Request, entry *filer_pb.Entry) {
|
|
||||||
// Set content type - use stored MIME type or default
|
|
||||||
contentType := entry.Attributes.Mime
|
|
||||||
if contentType == "" {
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
|
|
||||||
// Set content length - use FileSize for accuracy, especially for large files
|
|
||||||
contentLength := int64(entry.Attributes.FileSize)
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))
|
|
||||||
|
|
||||||
// Set last modified
|
|
||||||
w.Header().Set("Last-Modified", time.Unix(entry.Attributes.Mtime, 0).UTC().Format(http.TimeFormat))
|
|
||||||
|
|
||||||
// Set ETag
|
|
||||||
w.Header().Set("ETag", "\""+filer.ETag(entry)+"\"")
|
|
||||||
|
|
||||||
// For HEAD requests, don't write body
|
|
||||||
if r.Method == http.MethodHead {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write content
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
if len(entry.Content) > 0 {
|
|
||||||
if _, err := w.Write(entry.Content); err != nil {
|
|
||||||
glog.Errorf("serveDirectoryContent: failed to write response: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleDirectoryObjectRequest is a helper function that handles directory object requests
|
|
||||||
// for both GET and HEAD operations, eliminating code duplication
|
|
||||||
func (s3a *S3ApiServer) handleDirectoryObjectRequest(w http.ResponseWriter, r *http.Request, bucket, object, handlerName string) bool {
|
|
||||||
// Check if this is a directory object and handle it directly
|
|
||||||
if dirEntry, isDirectoryObject, err := s3a.checkDirectoryObject(bucket, object); err != nil {
|
|
||||||
glog.Errorf("%s: error checking directory object %s/%s: %v", handlerName, bucket, object, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return true // Request was handled (with error)
|
|
||||||
} else if dirEntry != nil {
|
|
||||||
glog.V(2).Infof("%s: directory object %s/%s found, serving content", handlerName, bucket, object)
|
|
||||||
s3a.serveDirectoryContent(w, r, dirEntry)
|
|
||||||
return true // Request was handled successfully
|
|
||||||
} else if isDirectoryObject {
|
|
||||||
// Directory object but doesn't exist
|
|
||||||
glog.V(2).Infof("%s: directory object %s/%s not found", handlerName, bucket, object)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return true // Request was handled (with not found)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false // Not a directory object, continue with normal processing
|
|
||||||
}
|
|
||||||
|
|
||||||
func newListEntry(entry *filer_pb.Entry, key string, dir string, name string, bucketPrefix string, fetchOwner bool, isDirectory bool, encodingTypeUrl bool, iam AccountManager) (listEntry ListEntry) {
|
|
||||||
storageClass := "STANDARD"
|
storageClass := "STANDARD"
|
||||||
if v, ok := entry.Extended[s3_constants.AmzStorageClass]; ok {
|
if v, ok := entry.Extended[s3_constants.AmzStorageClass]; ok {
|
||||||
storageClass = string(v)
|
storageClass = string(v)
|
||||||
|
@ -198,30 +108,9 @@ func newListEntry(entry *filer_pb.Entry, key string, dir string, name string, bu
|
||||||
StorageClass: StorageClass(storageClass),
|
StorageClass: StorageClass(storageClass),
|
||||||
}
|
}
|
||||||
if fetchOwner {
|
if fetchOwner {
|
||||||
// Extract owner from S3 metadata (Extended attributes) instead of file system attributes
|
listEntry.Owner = CanonicalUser{
|
||||||
var ownerID, displayName string
|
ID: fmt.Sprintf("%x", entry.Attributes.Uid),
|
||||||
if entry.Extended != nil {
|
DisplayName: entry.Attributes.UserName,
|
||||||
if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
|
||||||
ownerID = string(ownerBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to anonymous if no S3 owner found
|
|
||||||
if ownerID == "" {
|
|
||||||
ownerID = s3_constants.AccountAnonymousId
|
|
||||||
displayName = "anonymous"
|
|
||||||
} else {
|
|
||||||
// Get the proper display name from IAM system
|
|
||||||
displayName = iam.GetAccountNameById(ownerID)
|
|
||||||
// Fallback to ownerID if no display name found
|
|
||||||
if displayName == "" {
|
|
||||||
displayName = ownerID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listEntry.Owner = &CanonicalUser{
|
|
||||||
ID: ownerID,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return listEntry
|
return listEntry
|
||||||
|
@ -239,9 +128,9 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
|
||||||
bucket, object := s3_constants.GetBucketAndObject(r)
|
bucket, object := s3_constants.GetBucketAndObject(r)
|
||||||
glog.V(3).Infof("GetObjectHandler %s %s", bucket, object)
|
glog.V(3).Infof("GetObjectHandler %s %s", bucket, object)
|
||||||
|
|
||||||
// Handle directory objects with shared logic
|
if strings.HasSuffix(r.URL.Path, "/") {
|
||||||
if s3a.handleDirectoryObjectRequest(w, r, bucket, object, "GetObjectHandler") {
|
s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented)
|
||||||
return // Directory object request was handled
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for specific version ID in query parameters
|
// Check for specific version ID in query parameters
|
||||||
|
@ -270,7 +159,7 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
if versionId != "" {
|
if versionId != "" {
|
||||||
// Request for specific version
|
// Request for specific version
|
||||||
glog.V(2).Infof("GetObject: requesting specific version %s for %s%s", versionId, bucket, object)
|
glog.V(2).Infof("GetObject: requesting specific version %s for %s/%s", versionId, bucket, object)
|
||||||
entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
|
entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Failed to get specific version %s: %v", versionId, err)
|
glog.Errorf("Failed to get specific version %s: %v", versionId, err)
|
||||||
|
@ -280,10 +169,10 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
|
||||||
targetVersionId = versionId
|
targetVersionId = versionId
|
||||||
} else {
|
} else {
|
||||||
// Request for latest version
|
// Request for latest version
|
||||||
glog.V(1).Infof("GetObject: requesting latest version for %s%s", bucket, object)
|
glog.V(1).Infof("GetObject: requesting latest version for %s/%s", bucket, object)
|
||||||
entry, err = s3a.getLatestObjectVersion(bucket, object)
|
entry, err = s3a.getLatestObjectVersion(bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("GetObject: Failed to get latest version for %s%s: %v", bucket, object, err)
|
glog.Errorf("GetObject: Failed to get latest version for %s/%s: %v", bucket, object, err)
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -336,11 +225,6 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
|
||||||
bucket, object := s3_constants.GetBucketAndObject(r)
|
bucket, object := s3_constants.GetBucketAndObject(r)
|
||||||
glog.V(3).Infof("HeadObjectHandler %s %s", bucket, object)
|
glog.V(3).Infof("HeadObjectHandler %s %s", bucket, object)
|
||||||
|
|
||||||
// Handle directory objects with shared logic
|
|
||||||
if s3a.handleDirectoryObjectRequest(w, r, bucket, object, "HeadObjectHandler") {
|
|
||||||
return // Directory object request was handled
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for specific version ID in query parameters
|
// Check for specific version ID in query parameters
|
||||||
versionId := r.URL.Query().Get("versionId")
|
versionId := r.URL.Query().Get("versionId")
|
||||||
|
|
||||||
|
@ -365,7 +249,7 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
|
||||||
|
|
||||||
if versionId != "" {
|
if versionId != "" {
|
||||||
// Request for specific version
|
// Request for specific version
|
||||||
glog.V(2).Infof("HeadObject: requesting specific version %s for %s%s", versionId, bucket, object)
|
glog.V(2).Infof("HeadObject: requesting specific version %s for %s/%s", versionId, bucket, object)
|
||||||
entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
|
entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Failed to get specific version %s: %v", versionId, err)
|
glog.Errorf("Failed to get specific version %s: %v", versionId, err)
|
||||||
|
@ -375,7 +259,7 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
|
||||||
targetVersionId = versionId
|
targetVersionId = versionId
|
||||||
} else {
|
} else {
|
||||||
// Request for latest version
|
// Request for latest version
|
||||||
glog.V(2).Infof("HeadObject: requesting latest version for %s%s", bucket, object)
|
glog.V(2).Infof("HeadObject: requesting latest version for %s/%s", bucket, object)
|
||||||
entry, err = s3a.getLatestObjectVersion(bucket, object)
|
entry, err = s3a.getLatestObjectVersion(bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Failed to get latest version: %v", err)
|
glog.Errorf("Failed to get latest version: %v", err)
|
||||||
|
|
|
@ -1,356 +0,0 @@
|
||||||
package s3api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetObjectAclHandler Get object ACL
|
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAcl.html
|
|
||||||
func (s3a *S3ApiServer) GetObjectAclHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// collect parameters
|
|
||||||
bucket, object := s3_constants.GetBucketAndObject(r)
|
|
||||||
glog.V(3).Infof("GetObjectAclHandler %s %s", bucket, object)
|
|
||||||
|
|
||||||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for specific version ID in query parameters
|
|
||||||
versionId := r.URL.Query().Get("versionId")
|
|
||||||
|
|
||||||
// Check if versioning is configured for the bucket (Enabled or Suspended)
|
|
||||||
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
|
|
||||||
if err != nil {
|
|
||||||
if err == filer_pb.ErrNotFound {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
glog.Errorf("GetObjectAclHandler: Error checking versioning status for bucket %s: %v", bucket, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var entry *filer_pb.Entry
|
|
||||||
|
|
||||||
if versioningConfigured {
|
|
||||||
// Handle versioned object ACL retrieval - use same logic as GetObjectHandler
|
|
||||||
if versionId != "" {
|
|
||||||
// Request for specific version
|
|
||||||
glog.V(2).Infof("GetObjectAclHandler: requesting ACL for specific version %s of %s%s", versionId, bucket, object)
|
|
||||||
entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
|
|
||||||
} else {
|
|
||||||
// Request for latest version
|
|
||||||
glog.V(2).Infof("GetObjectAclHandler: requesting ACL for latest version of %s%s", bucket, object)
|
|
||||||
entry, err = s3a.getLatestObjectVersion(bucket, object)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("GetObjectAclHandler: Failed to get object version %s for %s%s: %v", versionId, bucket, object, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a delete marker
|
|
||||||
if entry.Extended != nil {
|
|
||||||
if deleteMarker, exists := entry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle regular (non-versioned) object ACL retrieval
|
|
||||||
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
|
||||||
entry, err = s3a.getEntry(bucketDir, object)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
glog.Errorf("GetObjectAclHandler: error checking object %s/%s: %v", bucket, object, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get object owner from metadata, fallback to request account
|
|
||||||
var objectOwner string
|
|
||||||
var objectOwnerDisplayName string
|
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
|
||||||
|
|
||||||
if entry.Extended != nil {
|
|
||||||
if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
|
||||||
objectOwner = string(ownerBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to current account if no owner stored
|
|
||||||
if objectOwner == "" {
|
|
||||||
objectOwner = amzAccountId
|
|
||||||
}
|
|
||||||
|
|
||||||
objectOwnerDisplayName = s3a.iam.GetAccountNameById(objectOwner)
|
|
||||||
|
|
||||||
// Build ACL response
|
|
||||||
response := AccessControlPolicy{
|
|
||||||
Owner: CanonicalUser{
|
|
||||||
ID: objectOwner,
|
|
||||||
DisplayName: objectOwnerDisplayName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get grants from stored ACL metadata
|
|
||||||
grants := GetAcpGrants(entry.Extended)
|
|
||||||
if len(grants) > 0 {
|
|
||||||
// Convert AWS SDK grants to local Grant format
|
|
||||||
for _, grant := range grants {
|
|
||||||
localGrant := Grant{
|
|
||||||
Permission: Permission(*grant.Permission),
|
|
||||||
}
|
|
||||||
|
|
||||||
if grant.Grantee != nil {
|
|
||||||
localGrant.Grantee = Grantee{
|
|
||||||
Type: *grant.Grantee.Type,
|
|
||||||
XMLXSI: "CanonicalUser",
|
|
||||||
XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
|
|
||||||
}
|
|
||||||
|
|
||||||
if grant.Grantee.ID != nil {
|
|
||||||
localGrant.Grantee.ID = *grant.Grantee.ID
|
|
||||||
localGrant.Grantee.DisplayName = s3a.iam.GetAccountNameById(*grant.Grantee.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if grant.Grantee.URI != nil {
|
|
||||||
localGrant.Grantee.URI = *grant.Grantee.URI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.AccessControlList.Grant = append(response.AccessControlList.Grant, localGrant)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback to default full control for object owner
|
|
||||||
response.AccessControlList.Grant = append(response.AccessControlList.Grant, Grant{
|
|
||||||
Grantee: Grantee{
|
|
||||||
ID: objectOwner,
|
|
||||||
DisplayName: objectOwnerDisplayName,
|
|
||||||
Type: "CanonicalUser",
|
|
||||||
XMLXSI: "CanonicalUser",
|
|
||||||
XMLNS: "http://www.w3.org/2001/XMLSchema-instance"},
|
|
||||||
Permission: Permission(s3_constants.PermissionFullControl),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
writeSuccessResponseXML(w, r, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutObjectAclHandler Put object ACL
|
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html
|
|
||||||
func (s3a *S3ApiServer) PutObjectAclHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// collect parameters
|
|
||||||
bucket, object := s3_constants.GetBucketAndObject(r)
|
|
||||||
glog.V(3).Infof("PutObjectAclHandler %s %s", bucket, object)
|
|
||||||
|
|
||||||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for specific version ID in query parameters
|
|
||||||
versionId := r.URL.Query().Get("versionId")
|
|
||||||
|
|
||||||
// Check if versioning is configured for the bucket (Enabled or Suspended)
|
|
||||||
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
|
|
||||||
if err != nil {
|
|
||||||
if err == filer_pb.ErrNotFound {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
glog.Errorf("PutObjectAclHandler: Error checking versioning status for bucket %s: %v", bucket, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var entry *filer_pb.Entry
|
|
||||||
|
|
||||||
if versioningConfigured {
|
|
||||||
// Handle versioned object ACL modification - use same logic as GetObjectHandler
|
|
||||||
if versionId != "" {
|
|
||||||
// Request for specific version
|
|
||||||
glog.V(2).Infof("PutObjectAclHandler: modifying ACL for specific version %s of %s%s", versionId, bucket, object)
|
|
||||||
entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
|
|
||||||
} else {
|
|
||||||
// Request for latest version
|
|
||||||
glog.V(2).Infof("PutObjectAclHandler: modifying ACL for latest version of %s%s", bucket, object)
|
|
||||||
entry, err = s3a.getLatestObjectVersion(bucket, object)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("PutObjectAclHandler: Failed to get object version %s for %s%s: %v", versionId, bucket, object, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a delete marker
|
|
||||||
if entry.Extended != nil {
|
|
||||||
if deleteMarker, exists := entry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle regular (non-versioned) object ACL modification
|
|
||||||
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
|
||||||
entry, err = s3a.getEntry(bucketDir, object)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
glog.Errorf("PutObjectAclHandler: error checking object %s/%s: %v", bucket, object, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current object owner from metadata
|
|
||||||
var objectOwner string
|
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
|
||||||
|
|
||||||
if entry.Extended != nil {
|
|
||||||
if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
|
||||||
objectOwner = string(ownerBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to current account if no owner stored
|
|
||||||
if objectOwner == "" {
|
|
||||||
objectOwner = amzAccountId
|
|
||||||
}
|
|
||||||
|
|
||||||
// **PERMISSION CHECKS**
|
|
||||||
|
|
||||||
// 1. Check if user is admin (admins can modify any ACL)
|
|
||||||
if !s3a.isUserAdmin(r) {
|
|
||||||
// 2. Check object ownership - only object owner can modify ACL (unless admin)
|
|
||||||
if objectOwner != amzAccountId {
|
|
||||||
glog.V(3).Infof("PutObjectAclHandler: Access denied - user %s is not owner of object %s/%s (owner: %s)",
|
|
||||||
amzAccountId, bucket, object, objectOwner)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check object-level WRITE_ACP permission
|
|
||||||
// Create the specific action for this object
|
|
||||||
writeAcpAction := Action(fmt.Sprintf("WriteAcp:%s/%s", bucket, object))
|
|
||||||
identity, errCode := s3a.iam.authRequest(r, writeAcpAction)
|
|
||||||
if errCode != s3err.ErrNone {
|
|
||||||
glog.V(3).Infof("PutObjectAclHandler: Auth failed for WriteAcp action on %s/%s: %v", bucket, object, errCode)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Verify the authenticated identity can perform WriteAcp on this specific object
|
|
||||||
if identity == nil || !identity.canDo(writeAcpAction, bucket, object) {
|
|
||||||
glog.V(3).Infof("PutObjectAclHandler: Identity %v cannot perform WriteAcp on %s/%s", identity, bucket, object)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
glog.V(3).Infof("PutObjectAclHandler: Admin user %s granted ACL modification permission for %s/%s", amzAccountId, bucket, object)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get bucket config for ownership settings
|
|
||||||
bucketConfig, errCode := s3a.getBucketConfig(bucket)
|
|
||||||
if errCode != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, errCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketOwnership := bucketConfig.Ownership
|
|
||||||
bucketOwnerId := bucketConfig.Owner
|
|
||||||
|
|
||||||
// Extract ACL from request (either canned ACL or XML body)
|
|
||||||
// This function also validates that the owner in the request matches the object owner
|
|
||||||
grants, errCode := ExtractAcl(r, s3a.iam, bucketOwnership, bucketOwnerId, objectOwner, amzAccountId)
|
|
||||||
if errCode != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, errCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store ACL in object metadata
|
|
||||||
if errCode := AssembleEntryWithAcp(entry, objectOwner, grants); errCode != s3err.ErrNone {
|
|
||||||
glog.Errorf("PutObjectAclHandler: failed to assemble entry with ACP: %v", errCode)
|
|
||||||
s3err.WriteErrorResponse(w, r, errCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the correct directory for ACL update
|
|
||||||
var updateDirectory string
|
|
||||||
|
|
||||||
if versioningConfigured {
|
|
||||||
if versionId != "" && versionId != "null" {
|
|
||||||
// Versioned object - update the specific version file in .versions directory
|
|
||||||
updateDirectory = s3a.option.BucketsPath + "/" + bucket + "/" + object + ".versions"
|
|
||||||
} else {
|
|
||||||
// Latest version in versioned bucket - could be null version or versioned object
|
|
||||||
// Extract version ID from the entry to determine where it's stored
|
|
||||||
var actualVersionId string
|
|
||||||
if entry.Extended != nil {
|
|
||||||
if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists {
|
|
||||||
actualVersionId = string(versionIdBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if actualVersionId == "null" || actualVersionId == "" {
|
|
||||||
// Null version (pre-versioning object) - stored as regular file
|
|
||||||
updateDirectory = s3a.option.BucketsPath + "/" + bucket
|
|
||||||
} else {
|
|
||||||
// Versioned object - stored in .versions directory
|
|
||||||
updateDirectory = s3a.option.BucketsPath + "/" + bucket + "/" + object + ".versions"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Non-versioned object - stored as regular file
|
|
||||||
updateDirectory = s3a.option.BucketsPath + "/" + bucket
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the object with new ACL metadata
|
|
||||||
err = s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
||||||
request := &filer_pb.UpdateEntryRequest{
|
|
||||||
Directory: updateDirectory,
|
|
||||||
Entry: entry,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := client.UpdateEntry(context.Background(), request); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("PutObjectAclHandler: failed to update entry: %v", err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
glog.V(3).Infof("PutObjectAclHandler: Successfully updated ACL for %s/%s by user %s", bucket, object, amzAccountId)
|
|
||||||
writeSuccessResponseEmpty(w, r)
|
|
||||||
}
|
|
|
@ -32,8 +32,8 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||||
// Check for specific version ID in query parameters
|
// Check for specific version ID in query parameters
|
||||||
versionId := r.URL.Query().Get("versionId")
|
versionId := r.URL.Query().Get("versionId")
|
||||||
|
|
||||||
// Get detailed versioning state for proper handling of suspended vs enabled versioning
|
// Check if versioning is configured for the bucket (Enabled or Suspended)
|
||||||
versioningState, err := s3a.getVersioningState(bucket)
|
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == filer_pb.ErrNotFound {
|
if err == filer_pb.ErrNotFound {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
||||||
|
@ -44,19 +44,14 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
versioningEnabled := (versioningState == s3_constants.VersioningEnabled)
|
|
||||||
versioningSuspended := (versioningState == s3_constants.VersioningSuspended)
|
|
||||||
versioningConfigured := (versioningState != "")
|
|
||||||
|
|
||||||
var auditLog *s3err.AccessLog
|
var auditLog *s3err.AccessLog
|
||||||
if s3err.Logger != nil {
|
if s3err.Logger != nil {
|
||||||
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
|
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
if versioningConfigured {
|
if versioningConfigured {
|
||||||
// Handle versioned delete based on specific versioning state
|
// Handle versioned delete
|
||||||
if versionId != "" {
|
if versionId != "" {
|
||||||
// Delete specific version (same for both enabled and suspended)
|
|
||||||
// Check object lock permissions before deleting specific version
|
// Check object lock permissions before deleting specific version
|
||||||
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
||||||
if err := s3a.enforceObjectLockProtections(r, bucket, object, versionId, governanceBypassAllowed); err != nil {
|
if err := s3a.enforceObjectLockProtections(r, bucket, object, versionId, governanceBypassAllowed); err != nil {
|
||||||
|
@ -76,9 +71,7 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||||
// Set version ID in response header
|
// Set version ID in response header
|
||||||
w.Header().Set("x-amz-version-id", versionId)
|
w.Header().Set("x-amz-version-id", versionId)
|
||||||
} else {
|
} else {
|
||||||
// Delete without version ID - behavior depends on versioning state
|
// Create delete marker (logical delete)
|
||||||
if versioningEnabled {
|
|
||||||
// Enabled versioning: Create delete marker (logical delete)
|
|
||||||
// AWS S3 behavior: Delete marker creation is NOT blocked by object retention
|
// AWS S3 behavior: Delete marker creation is NOT blocked by object retention
|
||||||
// because it's a logical delete that doesn't actually remove the retained version
|
// because it's a logical delete that doesn't actually remove the retained version
|
||||||
deleteMarkerVersionId, err := s3a.createDeleteMarker(bucket, object)
|
deleteMarkerVersionId, err := s3a.createDeleteMarker(bucket, object)
|
||||||
|
@ -91,29 +84,6 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||||
// Set delete marker version ID in response header
|
// Set delete marker version ID in response header
|
||||||
w.Header().Set("x-amz-version-id", deleteMarkerVersionId)
|
w.Header().Set("x-amz-version-id", deleteMarkerVersionId)
|
||||||
w.Header().Set("x-amz-delete-marker", "true")
|
w.Header().Set("x-amz-delete-marker", "true")
|
||||||
} else if versioningSuspended {
|
|
||||||
// Suspended versioning: Actually delete the "null" version object
|
|
||||||
glog.V(2).Infof("DeleteObjectHandler: deleting null version for suspended versioning %s/%s", bucket, object)
|
|
||||||
|
|
||||||
// Check object lock permissions before deleting "null" version
|
|
||||||
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
|
||||||
if err := s3a.enforceObjectLockProtections(r, bucket, object, "null", governanceBypassAllowed); err != nil {
|
|
||||||
glog.V(2).Infof("DeleteObjectHandler: object lock check failed for %s/%s: %v", bucket, object, err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the "null" version (the regular file)
|
|
||||||
err := s3a.deleteSpecificObjectVersion(bucket, object, "null")
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("Failed to delete null version: %v", err)
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: According to AWS S3 spec, suspended versioning should NOT return version ID headers
|
|
||||||
// The object is deleted but no version information is returned
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle regular delete (non-versioned)
|
// Handle regular delete (non-versioned)
|
||||||
|
@ -233,8 +203,8 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
|
||||||
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
|
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get detailed versioning state for proper handling of suspended vs enabled versioning
|
// Check if versioning is configured for the bucket (needed for object lock checks)
|
||||||
versioningState, err := s3a.getVersioningState(bucket)
|
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == filer_pb.ErrNotFound {
|
if err == filer_pb.ErrNotFound {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
||||||
|
@ -245,10 +215,6 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
versioningEnabled := (versioningState == s3_constants.VersioningEnabled)
|
|
||||||
versioningSuspended := (versioningState == s3_constants.VersioningSuspended)
|
|
||||||
versioningConfigured := (versioningState != "")
|
|
||||||
|
|
||||||
s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||||
|
|
||||||
// delete file entries
|
// delete file entries
|
||||||
|
@ -277,9 +243,9 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
|
||||||
var isDeleteMarker bool
|
var isDeleteMarker bool
|
||||||
|
|
||||||
if versioningConfigured {
|
if versioningConfigured {
|
||||||
// Handle versioned delete based on specific versioning state
|
// Handle versioned delete
|
||||||
if object.VersionId != "" {
|
if object.VersionId != "" {
|
||||||
// Delete specific version (same for both enabled and suspended)
|
// Delete specific version
|
||||||
err := s3a.deleteSpecificObjectVersion(bucket, object.Key, object.VersionId)
|
err := s3a.deleteSpecificObjectVersion(bucket, object.Key, object.VersionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
deleteErrors = append(deleteErrors, DeleteError{
|
deleteErrors = append(deleteErrors, DeleteError{
|
||||||
|
@ -292,9 +258,7 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
|
||||||
}
|
}
|
||||||
deleteVersionId = object.VersionId
|
deleteVersionId = object.VersionId
|
||||||
} else {
|
} else {
|
||||||
// Delete without version ID - behavior depends on versioning state
|
// Create delete marker (logical delete)
|
||||||
if versioningEnabled {
|
|
||||||
// Enabled versioning: Create delete marker (logical delete)
|
|
||||||
deleteMarkerVersionId, err := s3a.createDeleteMarker(bucket, object.Key)
|
deleteMarkerVersionId, err := s3a.createDeleteMarker(bucket, object.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
deleteErrors = append(deleteErrors, DeleteError{
|
deleteErrors = append(deleteErrors, DeleteError{
|
||||||
|
@ -307,24 +271,6 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
|
||||||
}
|
}
|
||||||
deleteVersionId = deleteMarkerVersionId
|
deleteVersionId = deleteMarkerVersionId
|
||||||
isDeleteMarker = true
|
isDeleteMarker = true
|
||||||
} else if versioningSuspended {
|
|
||||||
// Suspended versioning: Actually delete the "null" version object
|
|
||||||
glog.V(2).Infof("DeleteMultipleObjectsHandler: deleting null version for suspended versioning %s/%s", bucket, object.Key)
|
|
||||||
|
|
||||||
err := s3a.deleteSpecificObjectVersion(bucket, object.Key, "null")
|
|
||||||
if err != nil {
|
|
||||||
deleteErrors = append(deleteErrors, DeleteError{
|
|
||||||
Code: "",
|
|
||||||
Message: err.Error(),
|
|
||||||
Key: object.Key,
|
|
||||||
VersionId: "null",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
deleteVersionId = "null"
|
|
||||||
// Note: For suspended versioning, we don't set isDeleteMarker=true
|
|
||||||
// because we actually deleted the object, not created a delete marker
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to successful deletions with version info
|
// Add to successful deletions with version info
|
||||||
|
|
|
@ -4,17 +4,16 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OptionalString struct {
|
type OptionalString struct {
|
||||||
|
@ -53,32 +52,18 @@ func (s3a *S3ApiServer) ListObjectsV2Handler(w http.ResponseWriter, r *http.Requ
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||||
glog.V(3).Infof("ListObjectsV2Handler %s", bucket)
|
glog.V(3).Infof("ListObjectsV2Handler %s", bucket)
|
||||||
|
|
||||||
originalPrefix, startAfter, delimiter, continuationToken, encodingTypeUrl, fetchOwner, maxKeys, allowUnordered, errCode := getListObjectsV2Args(r.URL.Query())
|
originalPrefix, startAfter, delimiter, continuationToken, encodingTypeUrl, fetchOwner, maxKeys := getListObjectsV2Args(r.URL.Query())
|
||||||
|
|
||||||
if errCode != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, errCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if maxKeys < 0 {
|
if maxKeys < 0 {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidMaxKeys)
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidMaxKeys)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// AWS S3 compatibility: allow-unordered cannot be used with delimiter
|
|
||||||
if allowUnordered && delimiter != "" {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidUnorderedWithDelimiter)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
marker := continuationToken.string
|
marker := continuationToken.string
|
||||||
if !continuationToken.set {
|
if !continuationToken.set {
|
||||||
marker = startAfter
|
marker = startAfter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust marker if it ends with delimiter to skip all entries with that prefix
|
|
||||||
marker = adjustMarkerForDelimiter(marker, delimiter)
|
|
||||||
|
|
||||||
response, err := s3a.listFilerEntries(bucket, originalPrefix, maxKeys, marker, delimiter, encodingTypeUrl, fetchOwner)
|
response, err := s3a.listFilerEntries(bucket, originalPrefix, maxKeys, marker, delimiter, encodingTypeUrl, fetchOwner)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -121,27 +106,12 @@ func (s3a *S3ApiServer) ListObjectsV1Handler(w http.ResponseWriter, r *http.Requ
|
||||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||||
glog.V(3).Infof("ListObjectsV1Handler %s", bucket)
|
glog.V(3).Infof("ListObjectsV1Handler %s", bucket)
|
||||||
|
|
||||||
originalPrefix, marker, delimiter, encodingTypeUrl, maxKeys, allowUnordered, errCode := getListObjectsV1Args(r.URL.Query())
|
originalPrefix, marker, delimiter, encodingTypeUrl, maxKeys := getListObjectsV1Args(r.URL.Query())
|
||||||
|
|
||||||
if errCode != s3err.ErrNone {
|
|
||||||
s3err.WriteErrorResponse(w, r, errCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if maxKeys < 0 {
|
if maxKeys < 0 {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidMaxKeys)
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidMaxKeys)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// AWS S3 compatibility: allow-unordered cannot be used with delimiter
|
|
||||||
if allowUnordered && delimiter != "" {
|
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidUnorderedWithDelimiter)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust marker if it ends with delimiter to skip all entries with that prefix
|
|
||||||
marker = adjustMarkerForDelimiter(marker, delimiter)
|
|
||||||
|
|
||||||
response, err := s3a.listFilerEntries(bucket, originalPrefix, uint16(maxKeys), marker, delimiter, encodingTypeUrl, true)
|
response, err := s3a.listFilerEntries(bucket, originalPrefix, uint16(maxKeys), marker, delimiter, encodingTypeUrl, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -177,84 +147,17 @@ func (s3a *S3ApiServer) listFilerEntries(bucket string, originalPrefix string, m
|
||||||
prefixEndsOnDelimiter: strings.HasSuffix(originalPrefix, "/") && len(originalMarker) == 0,
|
prefixEndsOnDelimiter: strings.HasSuffix(originalPrefix, "/") && len(originalMarker) == 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: when maxKeys = 0, return empty results immediately with IsTruncated=false
|
|
||||||
if maxKeys == 0 {
|
|
||||||
response = ListBucketResult{
|
|
||||||
Name: bucket,
|
|
||||||
Prefix: originalPrefix,
|
|
||||||
Marker: originalMarker,
|
|
||||||
NextMarker: "",
|
|
||||||
MaxKeys: int(maxKeys),
|
|
||||||
Delimiter: delimiter,
|
|
||||||
IsTruncated: false,
|
|
||||||
Contents: contents,
|
|
||||||
CommonPrefixes: commonPrefixes,
|
|
||||||
}
|
|
||||||
if encodingTypeUrl {
|
|
||||||
response.EncodingType = s3.EncodingTypeUrl
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check filer
|
// check filer
|
||||||
err = s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
err = s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||||
var lastEntryWasCommonPrefix bool
|
|
||||||
var lastCommonPrefixName string
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
empty := true
|
empty := true
|
||||||
|
|
||||||
nextMarker, doErr = s3a.doListFilerEntries(client, reqDir, prefix, cursor, marker, delimiter, false, func(dir string, entry *filer_pb.Entry) {
|
nextMarker, doErr = s3a.doListFilerEntries(client, reqDir, prefix, cursor, marker, delimiter, false, func(dir string, entry *filer_pb.Entry) {
|
||||||
empty = false
|
empty = false
|
||||||
dirName, entryName, prefixName := entryUrlEncode(dir, entry.Name, encodingTypeUrl)
|
dirName, entryName, prefixName := entryUrlEncode(dir, entry.Name, encodingTypeUrl)
|
||||||
if entry.IsDirectory {
|
if entry.IsDirectory {
|
||||||
// When delimiter is specified, apply delimiter logic to directory key objects too
|
if entry.IsDirectoryKeyObject() {
|
||||||
if delimiter != "" && entry.IsDirectoryKeyObject() {
|
contents = append(contents, newListEntry(entry, "", dirName, entryName, bucketPrefix, fetchOwner, true, false))
|
||||||
// Apply the same delimiter logic as for regular files
|
|
||||||
var delimiterFound bool
|
|
||||||
undelimitedPath := fmt.Sprintf("%s/%s/", dirName, entryName)[len(bucketPrefix):]
|
|
||||||
|
|
||||||
// take into account a prefix if supplied while delimiting.
|
|
||||||
undelimitedPath = strings.TrimPrefix(undelimitedPath, originalPrefix)
|
|
||||||
|
|
||||||
delimitedPath := strings.SplitN(undelimitedPath, delimiter, 2)
|
|
||||||
|
|
||||||
if len(delimitedPath) == 2 {
|
|
||||||
// S3 clients expect the delimited prefix to contain the delimiter and prefix.
|
|
||||||
delimitedPrefix := originalPrefix + delimitedPath[0] + delimiter
|
|
||||||
|
|
||||||
for i := range commonPrefixes {
|
|
||||||
if commonPrefixes[i].Prefix == delimitedPrefix {
|
|
||||||
delimiterFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !delimiterFound {
|
|
||||||
commonPrefixes = append(commonPrefixes, PrefixEntry{
|
|
||||||
Prefix: delimitedPrefix,
|
|
||||||
})
|
|
||||||
cursor.maxKeys--
|
cursor.maxKeys--
|
||||||
delimiterFound = true
|
|
||||||
lastEntryWasCommonPrefix = true
|
|
||||||
lastCommonPrefixName = delimitedPath[0]
|
|
||||||
} else {
|
|
||||||
// This directory object belongs to an existing CommonPrefix, skip it
|
|
||||||
delimiterFound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no delimiter found in the directory object name, treat it as a regular key
|
|
||||||
if !delimiterFound {
|
|
||||||
contents = append(contents, newListEntry(entry, "", dirName, entryName, bucketPrefix, fetchOwner, true, false, s3a.iam))
|
|
||||||
cursor.maxKeys--
|
|
||||||
lastEntryWasCommonPrefix = false
|
|
||||||
}
|
|
||||||
} else if entry.IsDirectoryKeyObject() {
|
|
||||||
// No delimiter specified, or delimiter doesn't apply - treat as regular key
|
|
||||||
contents = append(contents, newListEntry(entry, "", dirName, entryName, bucketPrefix, fetchOwner, true, false, s3a.iam))
|
|
||||||
cursor.maxKeys--
|
|
||||||
lastEntryWasCommonPrefix = false
|
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
|
||||||
} else if delimiter == "/" { // A response can contain CommonPrefixes only if you specify a delimiter.
|
} else if delimiter == "/" { // A response can contain CommonPrefixes only if you specify a delimiter.
|
||||||
commonPrefixes = append(commonPrefixes, PrefixEntry{
|
commonPrefixes = append(commonPrefixes, PrefixEntry{
|
||||||
|
@ -262,8 +165,6 @@ func (s3a *S3ApiServer) listFilerEntries(bucket string, originalPrefix string, m
|
||||||
})
|
})
|
||||||
//All of the keys (up to 1,000) rolled up into a common prefix count as a single return when calculating the number of returns.
|
//All of the keys (up to 1,000) rolled up into a common prefix count as a single return when calculating the number of returns.
|
||||||
cursor.maxKeys--
|
cursor.maxKeys--
|
||||||
lastEntryWasCommonPrefix = true
|
|
||||||
lastCommonPrefixName = entry.Name
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var delimiterFound bool
|
var delimiterFound bool
|
||||||
|
@ -294,19 +195,12 @@ func (s3a *S3ApiServer) listFilerEntries(bucket string, originalPrefix string, m
|
||||||
})
|
})
|
||||||
cursor.maxKeys--
|
cursor.maxKeys--
|
||||||
delimiterFound = true
|
delimiterFound = true
|
||||||
lastEntryWasCommonPrefix = true
|
|
||||||
lastCommonPrefixName = delimitedPath[0]
|
|
||||||
} else {
|
|
||||||
// This object belongs to an existing CommonPrefix, skip it
|
|
||||||
// but continue processing to maintain correct flow
|
|
||||||
delimiterFound = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !delimiterFound {
|
if !delimiterFound {
|
||||||
contents = append(contents, newListEntry(entry, "", dirName, entryName, bucketPrefix, fetchOwner, false, false, s3a.iam))
|
contents = append(contents, newListEntry(entry, "", dirName, entryName, bucketPrefix, fetchOwner, false, false))
|
||||||
cursor.maxKeys--
|
cursor.maxKeys--
|
||||||
lastEntryWasCommonPrefix = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -314,21 +208,10 @@ func (s3a *S3ApiServer) listFilerEntries(bucket string, originalPrefix string, m
|
||||||
return doErr
|
return doErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust nextMarker for CommonPrefixes to include trailing slash (AWS S3 compliance)
|
if cursor.isTruncated {
|
||||||
if cursor.isTruncated && lastEntryWasCommonPrefix && lastCommonPrefixName != "" {
|
|
||||||
// For CommonPrefixes, NextMarker should include the trailing slash
|
|
||||||
if requestDir != "" {
|
|
||||||
nextMarker = requestDir + "/" + lastCommonPrefixName + "/"
|
|
||||||
} else {
|
|
||||||
nextMarker = lastCommonPrefixName + "/"
|
|
||||||
}
|
|
||||||
} else if cursor.isTruncated {
|
|
||||||
if requestDir != "" {
|
if requestDir != "" {
|
||||||
nextMarker = requestDir + "/" + nextMarker
|
nextMarker = requestDir + "/" + nextMarker
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if cursor.isTruncated {
|
|
||||||
break
|
break
|
||||||
} else if empty || strings.HasSuffix(originalPrefix, "/") {
|
} else if empty || strings.HasSuffix(originalPrefix, "/") {
|
||||||
nextMarker = ""
|
nextMarker = ""
|
||||||
|
@ -434,7 +317,7 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cursor.maxKeys <= 0 {
|
if cursor.maxKeys <= 0 {
|
||||||
return // Don't set isTruncated here - let caller decide based on whether more entries exist
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(marker, "/") {
|
if strings.Contains(marker, "/") {
|
||||||
|
@ -473,9 +356,6 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track .versions directories found in this directory for later processing
|
|
||||||
var versionsDirs []string
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
resp, recvErr := stream.Recv()
|
resp, recvErr := stream.Recv()
|
||||||
if recvErr != nil {
|
if recvErr != nil {
|
||||||
|
@ -486,14 +366,11 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry := resp.Entry
|
|
||||||
|
|
||||||
if cursor.maxKeys <= 0 {
|
if cursor.maxKeys <= 0 {
|
||||||
cursor.isTruncated = true
|
cursor.isTruncated = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
entry := resp.Entry
|
||||||
// Set nextMarker only when we have quota to process this entry
|
|
||||||
nextMarker = entry.Name
|
nextMarker = entry.Name
|
||||||
if cursor.prefixEndsOnDelimiter {
|
if cursor.prefixEndsOnDelimiter {
|
||||||
if entry.Name == prefix && entry.IsDirectory {
|
if entry.Name == prefix && entry.IsDirectory {
|
||||||
|
@ -509,14 +386,6 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||||
if entry.Name == s3_constants.MultipartUploadsFolder { // FIXME no need to apply to all directories. this extra also affects maxKeys
|
if entry.Name == s3_constants.MultipartUploadsFolder { // FIXME no need to apply to all directories. this extra also affects maxKeys
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip .versions directories in regular list operations but track them for logical object creation
|
|
||||||
if strings.HasSuffix(entry.Name, ".versions") {
|
|
||||||
glog.V(4).Infof("Found .versions directory: %s", entry.Name)
|
|
||||||
versionsDirs = append(versionsDirs, entry.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if delimiter != "/" || cursor.prefixEndsOnDelimiter {
|
if delimiter != "/" || cursor.prefixEndsOnDelimiter {
|
||||||
if cursor.prefixEndsOnDelimiter {
|
if cursor.prefixEndsOnDelimiter {
|
||||||
cursor.prefixEndsOnDelimiter = false
|
cursor.prefixEndsOnDelimiter = false
|
||||||
|
@ -556,52 +425,10 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||||
cursor.prefixEndsOnDelimiter = false
|
cursor.prefixEndsOnDelimiter = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// After processing all regular entries, handle versioned objects
|
|
||||||
// Create logical entries for objects that have .versions directories
|
|
||||||
for _, versionsDir := range versionsDirs {
|
|
||||||
if cursor.maxKeys <= 0 {
|
|
||||||
cursor.isTruncated = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract object name from .versions directory name (remove .versions suffix)
|
|
||||||
baseObjectName := strings.TrimSuffix(versionsDir, ".versions")
|
|
||||||
|
|
||||||
// Construct full object path relative to bucket
|
|
||||||
// dir is something like "/buckets/sea-test-1/Veeam/Backup/vbr/Config"
|
|
||||||
// we need to get the path relative to bucket: "Veeam/Backup/vbr/Config/Owner"
|
|
||||||
bucketPath := strings.TrimPrefix(dir, s3a.option.BucketsPath+"/")
|
|
||||||
bucketName := strings.Split(bucketPath, "/")[0]
|
|
||||||
|
|
||||||
// Remove bucket name from path to get directory within bucket
|
|
||||||
bucketRelativePath := strings.Join(strings.Split(bucketPath, "/")[1:], "/")
|
|
||||||
|
|
||||||
var fullObjectPath string
|
|
||||||
if bucketRelativePath == "" {
|
|
||||||
// Object is at bucket root
|
|
||||||
fullObjectPath = baseObjectName
|
|
||||||
} else {
|
|
||||||
// Object is in subdirectory
|
|
||||||
fullObjectPath = bucketRelativePath + "/" + baseObjectName
|
|
||||||
}
|
|
||||||
|
|
||||||
glog.V(4).Infof("Processing versioned object: baseObjectName=%s, bucketRelativePath=%s, fullObjectPath=%s",
|
|
||||||
baseObjectName, bucketRelativePath, fullObjectPath)
|
|
||||||
|
|
||||||
// Get the latest version information for this object
|
|
||||||
if latestVersionEntry, latestVersionErr := s3a.getLatestVersionEntryForListOperation(bucketName, fullObjectPath); latestVersionErr == nil {
|
|
||||||
glog.V(4).Infof("Creating logical entry for versioned object: %s", fullObjectPath)
|
|
||||||
eachEntryFn(dir, latestVersionEntry)
|
|
||||||
} else {
|
|
||||||
glog.V(4).Infof("Failed to get latest version for %s: %v", fullObjectPath, latestVersionErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListObjectsV2Args(values url.Values) (prefix, startAfter, delimiter string, token OptionalString, encodingTypeUrl bool, fetchOwner bool, maxkeys uint16, allowUnordered bool, errCode s3err.ErrorCode) {
|
func getListObjectsV2Args(values url.Values) (prefix, startAfter, delimiter string, token OptionalString, encodingTypeUrl bool, fetchOwner bool, maxkeys uint16) {
|
||||||
prefix = values.Get("prefix")
|
prefix = values.Get("prefix")
|
||||||
token = OptionalString{set: values.Has("continuation-token"), string: values.Get("continuation-token")}
|
token = OptionalString{set: values.Has("continuation-token"), string: values.Get("continuation-token")}
|
||||||
startAfter = values.Get("start-after")
|
startAfter = values.Get("start-after")
|
||||||
|
@ -610,21 +437,15 @@ func getListObjectsV2Args(values url.Values) (prefix, startAfter, delimiter stri
|
||||||
if values.Get("max-keys") != "" {
|
if values.Get("max-keys") != "" {
|
||||||
if maxKeys, err := strconv.ParseUint(values.Get("max-keys"), 10, 16); err == nil {
|
if maxKeys, err := strconv.ParseUint(values.Get("max-keys"), 10, 16); err == nil {
|
||||||
maxkeys = uint16(maxKeys)
|
maxkeys = uint16(maxKeys)
|
||||||
} else {
|
|
||||||
// Invalid max-keys value (non-numeric)
|
|
||||||
errCode = s3err.ErrInvalidMaxKeys
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
maxkeys = maxObjectListSizeLimit
|
maxkeys = maxObjectListSizeLimit
|
||||||
}
|
}
|
||||||
fetchOwner = values.Get("fetch-owner") == "true"
|
fetchOwner = values.Get("fetch-owner") == "true"
|
||||||
allowUnordered = values.Get("allow-unordered") == "true"
|
|
||||||
errCode = s3err.ErrNone
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, encodingTypeUrl bool, maxkeys int16, allowUnordered bool, errCode s3err.ErrorCode) {
|
func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, encodingTypeUrl bool, maxkeys int16) {
|
||||||
prefix = values.Get("prefix")
|
prefix = values.Get("prefix")
|
||||||
marker = values.Get("marker")
|
marker = values.Get("marker")
|
||||||
delimiter = values.Get("delimiter")
|
delimiter = values.Get("delimiter")
|
||||||
|
@ -632,16 +453,10 @@ func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string,
|
||||||
if values.Get("max-keys") != "" {
|
if values.Get("max-keys") != "" {
|
||||||
if maxKeys, err := strconv.ParseInt(values.Get("max-keys"), 10, 16); err == nil {
|
if maxKeys, err := strconv.ParseInt(values.Get("max-keys"), 10, 16); err == nil {
|
||||||
maxkeys = int16(maxKeys)
|
maxkeys = int16(maxKeys)
|
||||||
} else {
|
|
||||||
// Invalid max-keys value (non-numeric)
|
|
||||||
errCode = s3err.ErrInvalidMaxKeys
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
maxkeys = maxObjectListSizeLimit
|
maxkeys = maxObjectListSizeLimit
|
||||||
}
|
}
|
||||||
allowUnordered = values.Get("allow-unordered") == "true"
|
|
||||||
errCode = s3err.ErrNone
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -698,55 +513,3 @@ func (s3a *S3ApiServer) ensureDirectoryAllEmpty(filerClient filer_pb.SeaweedFile
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLatestVersionEntryForListOperation gets the latest version of an object and creates a logical entry for list operations
|
|
||||||
// This is used to show versioned objects as logical object names in regular list operations
|
|
||||||
func (s3a *S3ApiServer) getLatestVersionEntryForListOperation(bucket, object string) (*filer_pb.Entry, error) {
|
|
||||||
// Get the latest version entry
|
|
||||||
latestVersionEntry, err := s3a.getLatestObjectVersion(bucket, object)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get latest version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a delete marker (should not be shown in regular list)
|
|
||||||
if latestVersionEntry.Extended != nil {
|
|
||||||
if deleteMarker, exists := latestVersionEntry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" {
|
|
||||||
return nil, fmt.Errorf("latest version is a delete marker")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a logical entry that appears to be stored at the object path (not the versioned path)
|
|
||||||
// This allows the list operation to show the logical object name while preserving all metadata
|
|
||||||
logicalEntry := &filer_pb.Entry{
|
|
||||||
Name: strings.TrimPrefix(object, "/"),
|
|
||||||
IsDirectory: false,
|
|
||||||
Attributes: latestVersionEntry.Attributes,
|
|
||||||
Extended: latestVersionEntry.Extended,
|
|
||||||
Chunks: latestVersionEntry.Chunks,
|
|
||||||
}
|
|
||||||
|
|
||||||
return logicalEntry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// adjustMarkerForDelimiter handles delimiter-ending markers by incrementing them to skip entries with that prefix.
|
|
||||||
// For example, when continuation token is "boo/", this returns "boo~" to skip all "boo/*" entries
|
|
||||||
// but still finds any "bop" or later entries. We add a high ASCII character rather than incrementing
|
|
||||||
// the last character to avoid skipping potential directory entries.
|
|
||||||
// This is essential for correct S3 list operations with delimiters and CommonPrefixes.
|
|
||||||
func adjustMarkerForDelimiter(marker, delimiter string) string {
|
|
||||||
if delimiter == "" || !strings.HasSuffix(marker, delimiter) {
|
|
||||||
return marker
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the trailing delimiter and append a high ASCII character
|
|
||||||
// This ensures we skip all entries under the prefix but don't skip
|
|
||||||
// potential directory entries that start with a similar prefix
|
|
||||||
prefix := strings.TrimSuffix(marker, delimiter)
|
|
||||||
if len(prefix) == 0 {
|
|
||||||
return marker
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use tilde (~) which has ASCII value 126, higher than most printable characters
|
|
||||||
// This skips "prefix/*" entries but still finds "prefix" + any higher character
|
|
||||||
return prefix + "~"
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestListObjectsHandler(t *testing.T) {
|
func TestListObjectsHandler(t *testing.T) {
|
||||||
|
@ -28,7 +26,7 @@ func TestListObjectsHandler(t *testing.T) {
|
||||||
LastModified: time.Date(2011, 4, 9, 12, 34, 49, 0, time.UTC),
|
LastModified: time.Date(2011, 4, 9, 12, 34, 49, 0, time.UTC),
|
||||||
ETag: "\"4397da7a7649e8085de9916c240e8166\"",
|
ETag: "\"4397da7a7649e8085de9916c240e8166\"",
|
||||||
Size: 1234567,
|
Size: 1234567,
|
||||||
Owner: &CanonicalUser{
|
Owner: CanonicalUser{
|
||||||
ID: "65a011niqo39cdf8ec533ec3d1ccaafsa932",
|
ID: "65a011niqo39cdf8ec533ec3d1ccaafsa932",
|
||||||
},
|
},
|
||||||
StorageClass: "STANDARD",
|
StorageClass: "STANDARD",
|
||||||
|
@ -91,207 +89,3 @@ func Test_normalizePrefixMarker(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllowUnorderedParameterValidation(t *testing.T) {
|
|
||||||
// Test getListObjectsV1Args with allow-unordered parameter
|
|
||||||
t.Run("getListObjectsV1Args with allow-unordered", func(t *testing.T) {
|
|
||||||
// Test with allow-unordered=true
|
|
||||||
values := map[string][]string{
|
|
||||||
"allow-unordered": {"true"},
|
|
||||||
"delimiter": {"/"},
|
|
||||||
}
|
|
||||||
_, _, _, _, _, allowUnordered, errCode := getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.True(t, allowUnordered, "allow-unordered should be true when set to 'true'")
|
|
||||||
|
|
||||||
// Test with allow-unordered=false
|
|
||||||
values = map[string][]string{
|
|
||||||
"allow-unordered": {"false"},
|
|
||||||
}
|
|
||||||
_, _, _, _, _, allowUnordered, errCode = getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.False(t, allowUnordered, "allow-unordered should be false when set to 'false'")
|
|
||||||
|
|
||||||
// Test without allow-unordered parameter
|
|
||||||
values = map[string][]string{}
|
|
||||||
_, _, _, _, _, allowUnordered, errCode = getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.False(t, allowUnordered, "allow-unordered should be false when not set")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test getListObjectsV2Args with allow-unordered parameter
|
|
||||||
t.Run("getListObjectsV2Args with allow-unordered", func(t *testing.T) {
|
|
||||||
// Test with allow-unordered=true
|
|
||||||
values := map[string][]string{
|
|
||||||
"allow-unordered": {"true"},
|
|
||||||
"delimiter": {"/"},
|
|
||||||
}
|
|
||||||
_, _, _, _, _, _, _, allowUnordered, errCode := getListObjectsV2Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.True(t, allowUnordered, "allow-unordered should be true when set to 'true'")
|
|
||||||
|
|
||||||
// Test with allow-unordered=false
|
|
||||||
values = map[string][]string{
|
|
||||||
"allow-unordered": {"false"},
|
|
||||||
}
|
|
||||||
_, _, _, _, _, _, _, allowUnordered, errCode = getListObjectsV2Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.False(t, allowUnordered, "allow-unordered should be false when set to 'false'")
|
|
||||||
|
|
||||||
// Test without allow-unordered parameter
|
|
||||||
values = map[string][]string{}
|
|
||||||
_, _, _, _, _, _, _, allowUnordered, errCode = getListObjectsV2Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.False(t, allowUnordered, "allow-unordered should be false when not set")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAllowUnorderedWithDelimiterValidation(t *testing.T) {
|
|
||||||
t.Run("should return error when allow-unordered=true and delimiter are both present", func(t *testing.T) {
|
|
||||||
// Create a request with both allow-unordered=true and delimiter
|
|
||||||
req := httptest.NewRequest("GET", "/bucket?allow-unordered=true&delimiter=/", nil)
|
|
||||||
|
|
||||||
// Extract query parameters like the handler would
|
|
||||||
values := req.URL.Query()
|
|
||||||
|
|
||||||
// Test ListObjectsV1Args
|
|
||||||
_, _, delimiter, _, _, allowUnordered, errCode := getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.True(t, allowUnordered, "allow-unordered should be true")
|
|
||||||
assert.Equal(t, "/", delimiter, "delimiter should be '/'")
|
|
||||||
|
|
||||||
// The validation should catch this combination
|
|
||||||
if allowUnordered && delimiter != "" {
|
|
||||||
assert.True(t, true, "Validation correctly detected invalid combination")
|
|
||||||
} else {
|
|
||||||
assert.Fail(t, "Validation should have detected invalid combination")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test ListObjectsV2Args
|
|
||||||
_, _, delimiter2, _, _, _, _, allowUnordered2, errCode2 := getListObjectsV2Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode2, "should not return error for valid parameters")
|
|
||||||
assert.True(t, allowUnordered2, "allow-unordered should be true")
|
|
||||||
assert.Equal(t, "/", delimiter2, "delimiter should be '/'")
|
|
||||||
|
|
||||||
// The validation should catch this combination
|
|
||||||
if allowUnordered2 && delimiter2 != "" {
|
|
||||||
assert.True(t, true, "Validation correctly detected invalid combination")
|
|
||||||
} else {
|
|
||||||
assert.Fail(t, "Validation should have detected invalid combination")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("should allow allow-unordered=true without delimiter", func(t *testing.T) {
|
|
||||||
// Create a request with only allow-unordered=true
|
|
||||||
req := httptest.NewRequest("GET", "/bucket?allow-unordered=true", nil)
|
|
||||||
|
|
||||||
values := req.URL.Query()
|
|
||||||
|
|
||||||
// Test ListObjectsV1Args
|
|
||||||
_, _, delimiter, _, _, allowUnordered, errCode := getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.True(t, allowUnordered, "allow-unordered should be true")
|
|
||||||
assert.Equal(t, "", delimiter, "delimiter should be empty")
|
|
||||||
|
|
||||||
// This combination should be valid
|
|
||||||
if allowUnordered && delimiter != "" {
|
|
||||||
assert.Fail(t, "This should be a valid combination")
|
|
||||||
} else {
|
|
||||||
assert.True(t, true, "Valid combination correctly allowed")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("should allow delimiter without allow-unordered", func(t *testing.T) {
|
|
||||||
// Create a request with only delimiter
|
|
||||||
req := httptest.NewRequest("GET", "/bucket?delimiter=/", nil)
|
|
||||||
|
|
||||||
values := req.URL.Query()
|
|
||||||
|
|
||||||
// Test ListObjectsV1Args
|
|
||||||
_, _, delimiter, _, _, allowUnordered, errCode := getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "should not return error for valid parameters")
|
|
||||||
assert.False(t, allowUnordered, "allow-unordered should be false")
|
|
||||||
assert.Equal(t, "/", delimiter, "delimiter should be '/'")
|
|
||||||
|
|
||||||
// This combination should be valid
|
|
||||||
if allowUnordered && delimiter != "" {
|
|
||||||
assert.Fail(t, "This should be a valid combination")
|
|
||||||
} else {
|
|
||||||
assert.True(t, true, "Valid combination correctly allowed")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMaxKeysParameterValidation tests the validation of max-keys parameter
|
|
||||||
func TestMaxKeysParameterValidation(t *testing.T) {
|
|
||||||
t.Run("valid max-keys values should work", func(t *testing.T) {
|
|
||||||
// Test valid numeric values
|
|
||||||
values := map[string][]string{
|
|
||||||
"max-keys": {"100"},
|
|
||||||
}
|
|
||||||
_, _, _, _, _, _, errCode := getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "valid max-keys should not return error")
|
|
||||||
|
|
||||||
_, _, _, _, _, _, _, _, errCode = getListObjectsV2Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "valid max-keys should not return error")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid max-keys values should return error", func(t *testing.T) {
|
|
||||||
// Test non-numeric value
|
|
||||||
values := map[string][]string{
|
|
||||||
"max-keys": {"blah"},
|
|
||||||
}
|
|
||||||
_, _, _, _, _, _, errCode := getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrInvalidMaxKeys, errCode, "non-numeric max-keys should return ErrInvalidMaxKeys")
|
|
||||||
|
|
||||||
_, _, _, _, _, _, _, _, errCode = getListObjectsV2Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrInvalidMaxKeys, errCode, "non-numeric max-keys should return ErrInvalidMaxKeys")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("empty max-keys should use default", func(t *testing.T) {
|
|
||||||
// Test empty max-keys
|
|
||||||
values := map[string][]string{}
|
|
||||||
_, _, _, _, maxkeys, _, errCode := getListObjectsV1Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "empty max-keys should not return error")
|
|
||||||
assert.Equal(t, int16(1000), maxkeys, "empty max-keys should use default value")
|
|
||||||
|
|
||||||
_, _, _, _, _, _, maxkeys2, _, errCode := getListObjectsV2Args(values)
|
|
||||||
assert.Equal(t, s3err.ErrNone, errCode, "empty max-keys should not return error")
|
|
||||||
assert.Equal(t, uint16(1000), maxkeys2, "empty max-keys should use default value")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDelimiterWithDirectoryKeyObjects tests that directory key objects (like "0/") are properly
|
|
||||||
// grouped into common prefixes when using delimiters, matching AWS S3 behavior.
|
|
||||||
//
|
|
||||||
// This test addresses the issue found in test_bucket_list_delimiter_not_skip_special where
|
|
||||||
// directory key objects were incorrectly returned as individual keys instead of being
|
|
||||||
// grouped into common prefixes when a delimiter was specified.
|
|
||||||
func TestDelimiterWithDirectoryKeyObjects(t *testing.T) {
|
|
||||||
// This test simulates the failing test scenario:
|
|
||||||
// Objects: ['0/'] + ['0/1000', '0/1001', ..., '0/1998'] + ['1999', '1999#', '1999+', '2000']
|
|
||||||
// With delimiter='/', expect:
|
|
||||||
// - Keys: ['1999', '1999#', '1999+', '2000']
|
|
||||||
// - CommonPrefixes: ['0/']
|
|
||||||
|
|
||||||
t.Run("directory key object should be grouped into common prefix with delimiter", func(t *testing.T) {
|
|
||||||
// The fix ensures that when a delimiter is specified, directory key objects
|
|
||||||
// (entries that are both directories AND have MIME types set) undergo the same
|
|
||||||
// delimiter-based grouping logic as regular files.
|
|
||||||
|
|
||||||
// Before fix: '0/' would be returned as an individual key
|
|
||||||
// After fix: '0/' is grouped with '0/xxxx' objects into common prefix '0/'
|
|
||||||
|
|
||||||
// This matches AWS S3 behavior where all objects sharing a prefix up to the
|
|
||||||
// delimiter are grouped together, regardless of whether they are directory key objects.
|
|
||||||
|
|
||||||
assert.True(t, true, "Directory key objects should be grouped into common prefixes when delimiter is used")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("directory key object without delimiter should be individual key", func(t *testing.T) {
|
|
||||||
// When no delimiter is specified, directory key objects should still be
|
|
||||||
// returned as individual keys (existing behavior maintained).
|
|
||||||
|
|
||||||
assert.True(t, true, "Directory key objects should be individual keys when no delimiter is used")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package s3api
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -23,7 +22,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxObjectListSizeLimit = 1000 // Limit number of objects in a listObjectsResponse.
|
maxObjectListSizeLimit = 10000 // Limit number of objects in a listObjectsResponse.
|
||||||
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
||||||
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
||||||
globalMaxPartID = 100000
|
globalMaxPartID = 100000
|
||||||
|
@ -42,7 +41,7 @@ func (s3a *S3ApiServer) NewMultipartUploadHandler(w http.ResponseWriter, r *http
|
||||||
// Check if versioning is enabled for the bucket (needed for object lock)
|
// Check if versioning is enabled for the bucket (needed for object lock)
|
||||||
versioningEnabled, err := s3a.isVersioningEnabled(bucket)
|
versioningEnabled, err := s3a.isVersioningEnabled(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
if err == filer_pb.ErrNotFound {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -112,7 +111,7 @@ func (s3a *S3ApiServer) CompleteMultipartUploadHandler(w http.ResponseWriter, r
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response, errCode := s3a.completeMultipartUpload(r, &s3.CompleteMultipartUploadInput{
|
response, errCode := s3a.completeMultipartUpload(&s3.CompleteMultipartUploadInput{
|
||||||
Bucket: aws.String(bucket),
|
Bucket: aws.String(bucket),
|
||||||
Key: objectKey(aws.String(object)),
|
Key: objectKey(aws.String(object)),
|
||||||
UploadId: aws.String(uploadID),
|
UploadId: aws.String(uploadID),
|
||||||
|
@ -331,8 +330,9 @@ func (s3a *S3ApiServer) genPartUploadUrl(bucket, uploadID string, partID int) st
|
||||||
|
|
||||||
// Generate uploadID hash string from object
|
// Generate uploadID hash string from object
|
||||||
func (s3a *S3ApiServer) generateUploadID(object string) string {
|
func (s3a *S3ApiServer) generateUploadID(object string) string {
|
||||||
|
if strings.HasPrefix(object, "/") {
|
||||||
object = strings.TrimPrefix(object, "/")
|
object = object[1:]
|
||||||
|
}
|
||||||
h := sha1.New()
|
h := sha1.New()
|
||||||
h.Write([]byte(object))
|
h.Write([]byte(object))
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
|
|
|
@ -90,9 +90,6 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request)
|
||||||
entry.Content, _ = io.ReadAll(r.Body)
|
entry.Content, _ = io.ReadAll(r.Body)
|
||||||
}
|
}
|
||||||
entry.Attributes.Mime = objectContentType
|
entry.Attributes.Mime = objectContentType
|
||||||
|
|
||||||
// Set object owner for directory objects (same as regular objects)
|
|
||||||
s3a.setObjectOwnerFromRequest(r, entry)
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||||
return
|
return
|
||||||
|
@ -101,7 +98,7 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request)
|
||||||
// Get detailed versioning state for the bucket
|
// Get detailed versioning state for the bucket
|
||||||
versioningState, err := s3a.getVersioningState(bucket)
|
versioningState, err := s3a.getVersioningState(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
if err == filer_pb.ErrNotFound {
|
||||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -216,14 +213,6 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, uploadUrl string, dataReader
|
||||||
proxyReq.Header.Add(header, value)
|
proxyReq.Header.Add(header, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set object owner header for filer to extract
|
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
|
||||||
if amzAccountId != "" {
|
|
||||||
proxyReq.Header.Set(s3_constants.ExtAmzOwnerKey, amzAccountId)
|
|
||||||
glog.V(2).Infof("putToFiler: setting owner header %s for object %s", amzAccountId, uploadUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure that the Authorization header is overriding any previous
|
// ensure that the Authorization header is overriding any previous
|
||||||
// Authorization header which might be already present in proxyReq
|
// Authorization header which might be already present in proxyReq
|
||||||
s3a.maybeAddFilerJwtAuthorization(proxyReq, true)
|
s3a.maybeAddFilerJwtAuthorization(proxyReq, true)
|
||||||
|
@ -255,8 +244,8 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, uploadUrl string, dataReader
|
||||||
glog.Errorf("upload to filer error: %v", ret.Error)
|
glog.Errorf("upload to filer error: %v", ret.Error)
|
||||||
return "", filerErrorToS3Error(ret.Error)
|
return "", filerErrorToS3Error(ret.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
stats_collect.RecordBucketActiveTime(bucket)
|
stats_collect.RecordBucketActiveTime(bucket)
|
||||||
|
stats_collect.S3BucketTrafficReceivedBytesCounter.WithLabelValues(bucket).Add(float64(ret.Size))
|
||||||
return etag, s3err.ErrNone
|
return etag, s3err.ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,18 +290,6 @@ func (s3a *S3ApiServer) maybeGetFilerJwtAuthorizationToken(isWrite bool) string
|
||||||
return string(encodedJwt)
|
return string(encodedJwt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setObjectOwnerFromRequest sets the object owner metadata based on the authenticated user
|
|
||||||
func (s3a *S3ApiServer) setObjectOwnerFromRequest(r *http.Request, entry *filer_pb.Entry) {
|
|
||||||
amzAccountId := r.Header.Get(s3_constants.AmzAccountId)
|
|
||||||
if amzAccountId != "" {
|
|
||||||
if entry.Extended == nil {
|
|
||||||
entry.Extended = make(map[string][]byte)
|
|
||||||
}
|
|
||||||
entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(amzAccountId)
|
|
||||||
glog.V(2).Infof("setObjectOwnerFromRequest: set object owner to %s", amzAccountId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// putVersionedObject handles PUT operations for versioned buckets using the new layout
|
// putVersionedObject handles PUT operations for versioned buckets using the new layout
|
||||||
// where all versions (including latest) are stored in the .versions directory
|
// where all versions (including latest) are stored in the .versions directory
|
||||||
func (s3a *S3ApiServer) putSuspendedVersioningObject(r *http.Request, bucket, object string, dataReader io.Reader, objectContentType string) (etag string, errCode s3err.ErrorCode) {
|
func (s3a *S3ApiServer) putSuspendedVersioningObject(r *http.Request, bucket, object string, dataReader io.Reader, objectContentType string) (etag string, errCode s3err.ErrorCode) {
|
||||||
|
@ -344,9 +321,6 @@ func (s3a *S3ApiServer) putSuspendedVersioningObject(r *http.Request, bucket, ob
|
||||||
}
|
}
|
||||||
entry.Extended[s3_constants.ExtVersionIdKey] = []byte("null")
|
entry.Extended[s3_constants.ExtVersionIdKey] = []byte("null")
|
||||||
|
|
||||||
// Set object owner for suspended versioning objects
|
|
||||||
s3a.setObjectOwnerFromRequest(r, entry)
|
|
||||||
|
|
||||||
// Extract and store object lock metadata from request headers (if any)
|
// Extract and store object lock metadata from request headers (if any)
|
||||||
if err := s3a.extractObjectLockMetadataFromRequest(r, entry); err != nil {
|
if err := s3a.extractObjectLockMetadataFromRequest(r, entry); err != nil {
|
||||||
glog.Errorf("putSuspendedVersioningObject: failed to extract object lock metadata: %v", err)
|
glog.Errorf("putSuspendedVersioningObject: failed to extract object lock metadata: %v", err)
|
||||||
|
@ -379,16 +353,17 @@ func (s3a *S3ApiServer) putSuspendedVersioningObject(r *http.Request, bucket, ob
|
||||||
// when a new "null" version becomes the latest during suspended versioning
|
// when a new "null" version becomes the latest during suspended versioning
|
||||||
func (s3a *S3ApiServer) updateIsLatestFlagsForSuspendedVersioning(bucket, object string) error {
|
func (s3a *S3ApiServer) updateIsLatestFlagsForSuspendedVersioning(bucket, object string) error {
|
||||||
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
||||||
versionsObjectPath := object + ".versions"
|
cleanObject := strings.TrimPrefix(object, "/")
|
||||||
|
versionsObjectPath := cleanObject + ".versions"
|
||||||
versionsDir := bucketDir + "/" + versionsObjectPath
|
versionsDir := bucketDir + "/" + versionsObjectPath
|
||||||
|
|
||||||
glog.V(2).Infof("updateIsLatestFlagsForSuspendedVersioning: updating flags for %s%s", bucket, object)
|
glog.V(2).Infof("updateIsLatestFlagsForSuspendedVersioning: updating flags for %s/%s", bucket, cleanObject)
|
||||||
|
|
||||||
// Check if .versions directory exists
|
// Check if .versions directory exists
|
||||||
_, err := s3a.getEntry(bucketDir, versionsObjectPath)
|
_, err := s3a.getEntry(bucketDir, versionsObjectPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// No .versions directory exists, nothing to update
|
// No .versions directory exists, nothing to update
|
||||||
glog.V(2).Infof("updateIsLatestFlagsForSuspendedVersioning: no .versions directory for %s%s", bucket, object)
|
glog.V(2).Infof("updateIsLatestFlagsForSuspendedVersioning: no .versions directory for %s/%s", bucket, cleanObject)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,7 +413,7 @@ func (s3a *S3ApiServer) updateIsLatestFlagsForSuspendedVersioning(bucket, object
|
||||||
return fmt.Errorf("failed to update .versions directory metadata: %v", err)
|
return fmt.Errorf("failed to update .versions directory metadata: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
glog.V(2).Infof("updateIsLatestFlagsForSuspendedVersioning: cleared latest version metadata for %s%s", bucket, object)
|
glog.V(2).Infof("updateIsLatestFlagsForSuspendedVersioning: cleared latest version metadata for %s/%s", bucket, cleanObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -492,9 +467,6 @@ func (s3a *S3ApiServer) putVersionedObject(r *http.Request, bucket, object strin
|
||||||
}
|
}
|
||||||
versionEntry.Extended[s3_constants.ExtETagKey] = []byte(etag)
|
versionEntry.Extended[s3_constants.ExtETagKey] = []byte(etag)
|
||||||
|
|
||||||
// Set object owner for versioned objects
|
|
||||||
s3a.setObjectOwnerFromRequest(r, versionEntry)
|
|
||||||
|
|
||||||
// Extract and store object lock metadata from request headers
|
// Extract and store object lock metadata from request headers
|
||||||
if err := s3a.extractObjectLockMetadataFromRequest(r, versionEntry); err != nil {
|
if err := s3a.extractObjectLockMetadataFromRequest(r, versionEntry); err != nil {
|
||||||
glog.Errorf("putVersionedObject: failed to extract object lock metadata: %v", err)
|
glog.Errorf("putVersionedObject: failed to extract object lock metadata: %v", err)
|
||||||
|
|
21
weed/s3api/s3api_object_handlers_skip.go
Normal file
21
weed/s3api/s3api_object_handlers_skip.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package s3api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetObjectAclHandler Get object ACL
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAcl.html
|
||||||
|
func (s3a *S3ApiServer) GetObjectAclHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectAclHandler Put object ACL
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html
|
||||||
|
func (s3a *S3ApiServer) PutObjectAclHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
|
||||||
|
}
|
|
@ -2,77 +2,10 @@ package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mockAccountManager implements AccountManager for testing
|
|
||||||
type mockAccountManager struct {
|
|
||||||
accounts map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockAccountManager) GetAccountNameById(id string) string {
|
|
||||||
if name, exists := m.accounts[id]; exists {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockAccountManager) GetAccountIdByEmail(email string) string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewListEntryOwnerDisplayName(t *testing.T) {
|
|
||||||
// Create mock IAM with test accounts
|
|
||||||
iam := &mockAccountManager{
|
|
||||||
accounts: map[string]string{
|
|
||||||
"testid": "M. Tester",
|
|
||||||
"userid123": "John Doe",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create test entry with owner metadata
|
|
||||||
entry := &filer_pb.Entry{
|
|
||||||
Name: "test-object",
|
|
||||||
Attributes: &filer_pb.FuseAttributes{
|
|
||||||
Mtime: time.Now().Unix(),
|
|
||||||
FileSize: 1024,
|
|
||||||
},
|
|
||||||
Extended: map[string][]byte{
|
|
||||||
s3_constants.ExtAmzOwnerKey: []byte("testid"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that display name is correctly looked up from IAM
|
|
||||||
listEntry := newListEntry(entry, "", "dir", "test-object", "/buckets/test/", true, false, false, iam)
|
|
||||||
|
|
||||||
assert.NotNil(t, listEntry.Owner, "Owner should be set when fetchOwner is true")
|
|
||||||
assert.Equal(t, "testid", listEntry.Owner.ID, "Owner ID should match stored owner")
|
|
||||||
assert.Equal(t, "M. Tester", listEntry.Owner.DisplayName, "Display name should be looked up from IAM")
|
|
||||||
|
|
||||||
// Test with owner that doesn't exist in IAM (should fallback to ID)
|
|
||||||
entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte("unknown-user")
|
|
||||||
listEntry = newListEntry(entry, "", "dir", "test-object", "/buckets/test/", true, false, false, iam)
|
|
||||||
|
|
||||||
assert.Equal(t, "unknown-user", listEntry.Owner.ID, "Owner ID should match stored owner")
|
|
||||||
assert.Equal(t, "unknown-user", listEntry.Owner.DisplayName, "Display name should fallback to ID when not found in IAM")
|
|
||||||
|
|
||||||
// Test with no owner metadata (should use anonymous)
|
|
||||||
entry.Extended = make(map[string][]byte)
|
|
||||||
listEntry = newListEntry(entry, "", "dir", "test-object", "/buckets/test/", true, false, false, iam)
|
|
||||||
|
|
||||||
assert.Equal(t, s3_constants.AccountAnonymousId, listEntry.Owner.ID, "Should use anonymous ID when no owner metadata")
|
|
||||||
assert.Equal(t, "anonymous", listEntry.Owner.DisplayName, "Should use anonymous display name when no owner metadata")
|
|
||||||
|
|
||||||
// Test with fetchOwner false (should not set owner)
|
|
||||||
listEntry = newListEntry(entry, "", "dir", "test-object", "/buckets/test/", false, false, false, iam)
|
|
||||||
|
|
||||||
assert.Nil(t, listEntry.Owner, "Owner should not be set when fetchOwner is false")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveDuplicateSlashes(t *testing.T) {
|
func TestRemoveDuplicateSlashes(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
@ -591,7 +591,7 @@ func (s3a *S3ApiServer) enforceObjectLockProtections(request *http.Request, buck
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If object doesn't exist, it's not under retention or legal hold - this is expected during delete operations
|
// If object doesn't exist, it's not under retention or legal hold - this is expected during delete operations
|
||||||
if errors.Is(err, filer_pb.ErrNotFound) || errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) || errors.Is(err, ErrLatestVersionNotFound) {
|
if errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrVersionNotFound) || errors.Is(err, ErrLatestVersionNotFound) {
|
||||||
// Object doesn't exist, so it can't be under retention or legal hold - this is normal
|
// Object doesn't exist, so it can't be under retention or legal hold - this is normal
|
||||||
glog.V(4).Infof("Object %s/%s (versionId: %s) not found during object lock check (expected during delete operations)", bucket, object, versionId)
|
glog.V(4).Infof("Object %s/%s (versionId: %s) not found during object lock check (expected during delete operations)", bucket, object, versionId)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -19,31 +19,18 @@ import (
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
)
|
)
|
||||||
|
|
||||||
// S3ListObjectVersionsResult - Custom struct for S3 list-object-versions response
|
// ObjectVersion represents a version of an S3 object
|
||||||
// This avoids conflicts with the XSD generated ListVersionsResult struct
|
type ObjectVersion struct {
|
||||||
// and ensures proper separation of versions and delete markers into arrays
|
VersionId string
|
||||||
type S3ListObjectVersionsResult struct {
|
IsLatest bool
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult"`
|
IsDeleteMarker bool
|
||||||
|
LastModified time.Time
|
||||||
Name string `xml:"Name"`
|
ETag string
|
||||||
Prefix string `xml:"Prefix,omitempty"`
|
Size int64
|
||||||
KeyMarker string `xml:"KeyMarker,omitempty"`
|
Entry *filer_pb.Entry
|
||||||
VersionIdMarker string `xml:"VersionIdMarker,omitempty"`
|
|
||||||
NextKeyMarker string `xml:"NextKeyMarker,omitempty"`
|
|
||||||
NextVersionIdMarker string `xml:"NextVersionIdMarker,omitempty"`
|
|
||||||
MaxKeys int `xml:"MaxKeys"`
|
|
||||||
Delimiter string `xml:"Delimiter,omitempty"`
|
|
||||||
IsTruncated bool `xml:"IsTruncated"`
|
|
||||||
|
|
||||||
// These are the critical fields - arrays instead of single elements
|
|
||||||
Versions []VersionEntry `xml:"Version,omitempty"` // Array for versions
|
|
||||||
DeleteMarkers []DeleteMarkerEntry `xml:"DeleteMarker,omitempty"` // Array for delete markers
|
|
||||||
|
|
||||||
CommonPrefixes []PrefixEntry `xml:"CommonPrefixes,omitempty"`
|
|
||||||
EncodingType string `xml:"EncodingType,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original struct - keeping for compatibility but will use S3ListObjectVersionsResult for XML response
|
// ListObjectVersionsResult represents the response for ListObjectVersions
|
||||||
type ListObjectVersionsResult struct {
|
type ListObjectVersionsResult struct {
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult"`
|
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult"`
|
||||||
Name string `xml:"Name"`
|
Name string `xml:"Name"`
|
||||||
|
@ -60,17 +47,6 @@ type ListObjectVersionsResult struct {
|
||||||
CommonPrefixes []PrefixEntry `xml:"CommonPrefixes,omitempty"`
|
CommonPrefixes []PrefixEntry `xml:"CommonPrefixes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectVersion represents a version of an S3 object
|
|
||||||
type ObjectVersion struct {
|
|
||||||
VersionId string
|
|
||||||
IsLatest bool
|
|
||||||
IsDeleteMarker bool
|
|
||||||
LastModified time.Time
|
|
||||||
ETag string
|
|
||||||
Size int64
|
|
||||||
Entry *filer_pb.Entry
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateVersionId creates a unique version ID that preserves chronological order
|
// generateVersionId creates a unique version ID that preserves chronological order
|
||||||
func generateVersionId() string {
|
func generateVersionId() string {
|
||||||
// Use nanosecond timestamp to ensure chronological ordering
|
// Use nanosecond timestamp to ensure chronological ordering
|
||||||
|
@ -148,7 +124,7 @@ func (s3a *S3ApiServer) createDeleteMarker(bucket, object string) (string, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// listObjectVersions lists all versions of an object
|
// listObjectVersions lists all versions of an object
|
||||||
func (s3a *S3ApiServer) listObjectVersions(bucket, prefix, keyMarker, versionIdMarker, delimiter string, maxKeys int) (*S3ListObjectVersionsResult, error) {
|
func (s3a *S3ApiServer) listObjectVersions(bucket, prefix, keyMarker, versionIdMarker, delimiter string, maxKeys int) (*ListObjectVersionsResult, error) {
|
||||||
var allVersions []interface{} // Can contain VersionEntry or DeleteMarkerEntry
|
var allVersions []interface{} // Can contain VersionEntry or DeleteMarkerEntry
|
||||||
|
|
||||||
// Track objects that have been processed to avoid duplicates
|
// Track objects that have been processed to avoid duplicates
|
||||||
|
@ -208,8 +184,8 @@ func (s3a *S3ApiServer) listObjectVersions(bucket, prefix, keyMarker, versionIdM
|
||||||
return versionIdI > versionIdJ
|
return versionIdI > versionIdJ
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build result using S3ListObjectVersionsResult to avoid conflicts with XSD structs
|
// Build result
|
||||||
result := &S3ListObjectVersionsResult{
|
result := &ListObjectVersionsResult{
|
||||||
Name: bucket,
|
Name: bucket,
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
KeyMarker: keyMarker,
|
KeyMarker: keyMarker,
|
||||||
|
@ -263,25 +239,9 @@ func (s3a *S3ApiServer) findVersionsRecursively(currentPath, relativePath string
|
||||||
entryPath := path.Join(relativePath, entry.Name)
|
entryPath := path.Join(relativePath, entry.Name)
|
||||||
|
|
||||||
// Skip if this doesn't match the prefix filter
|
// Skip if this doesn't match the prefix filter
|
||||||
if normalizedPrefix := strings.TrimPrefix(prefix, "/"); normalizedPrefix != "" {
|
if prefix != "" && !strings.HasPrefix(entryPath, strings.TrimPrefix(prefix, "/")) {
|
||||||
// An entry is a candidate if:
|
|
||||||
// 1. Its path is a match for the prefix.
|
|
||||||
// 2. It is a directory that is an ancestor of the prefix path, so we must descend into it.
|
|
||||||
|
|
||||||
// Condition 1: The entry's path starts with the prefix.
|
|
||||||
isMatch := strings.HasPrefix(entryPath, normalizedPrefix)
|
|
||||||
if !isMatch && entry.IsDirectory {
|
|
||||||
// Also check if a directory entry matches a directory-style prefix (e.g., prefix "a/", entry "a").
|
|
||||||
isMatch = strings.HasPrefix(entryPath+"/", normalizedPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Condition 2: The prefix path starts with the entry's path (and it's a directory).
|
|
||||||
canDescend := entry.IsDirectory && strings.HasPrefix(normalizedPrefix, entryPath)
|
|
||||||
|
|
||||||
if !isMatch && !canDescend {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if entry.IsDirectory {
|
if entry.IsDirectory {
|
||||||
// Skip .uploads directory (multipart upload temporary files)
|
// Skip .uploads directory (multipart upload temporary files)
|
||||||
|
@ -318,7 +278,7 @@ func (s3a *S3ApiServer) findVersionsRecursively(currentPath, relativePath string
|
||||||
VersionId: version.VersionId,
|
VersionId: version.VersionId,
|
||||||
IsLatest: version.IsLatest,
|
IsLatest: version.IsLatest,
|
||||||
LastModified: version.LastModified,
|
LastModified: version.LastModified,
|
||||||
Owner: s3a.getObjectOwnerFromVersion(version, bucket, objectKey),
|
Owner: CanonicalUser{ID: "unknown", DisplayName: "unknown"},
|
||||||
}
|
}
|
||||||
*allVersions = append(*allVersions, deleteMarker)
|
*allVersions = append(*allVersions, deleteMarker)
|
||||||
} else {
|
} else {
|
||||||
|
@ -329,42 +289,14 @@ func (s3a *S3ApiServer) findVersionsRecursively(currentPath, relativePath string
|
||||||
LastModified: version.LastModified,
|
LastModified: version.LastModified,
|
||||||
ETag: version.ETag,
|
ETag: version.ETag,
|
||||||
Size: version.Size,
|
Size: version.Size,
|
||||||
Owner: s3a.getObjectOwnerFromVersion(version, bucket, objectKey),
|
Owner: CanonicalUser{ID: "unknown", DisplayName: "unknown"},
|
||||||
StorageClass: "STANDARD",
|
StorageClass: "STANDARD",
|
||||||
}
|
}
|
||||||
*allVersions = append(*allVersions, versionEntry)
|
*allVersions = append(*allVersions, versionEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This is a regular directory - check if it's an explicit S3 directory object
|
// Recursively search subdirectories
|
||||||
// Only include directories that were explicitly created via S3 API (have FolderMimeType)
|
|
||||||
// This excludes implicit directories created when uploading files like "test1/a"
|
|
||||||
if entry.Attributes.Mime == s3_constants.FolderMimeType {
|
|
||||||
directoryKey := entryPath
|
|
||||||
if !strings.HasSuffix(directoryKey, "/") {
|
|
||||||
directoryKey += "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add directory as a version entry with VersionId "null" (following S3/Minio behavior)
|
|
||||||
glog.V(2).Infof("findVersionsRecursively: found explicit S3 directory %s", directoryKey)
|
|
||||||
|
|
||||||
// Calculate ETag for empty directory
|
|
||||||
directoryETag := "\"d41d8cd98f00b204e9800998ecf8427e\""
|
|
||||||
|
|
||||||
versionEntry := &VersionEntry{
|
|
||||||
Key: directoryKey,
|
|
||||||
VersionId: "null",
|
|
||||||
IsLatest: true,
|
|
||||||
LastModified: time.Unix(entry.Attributes.Mtime, 0),
|
|
||||||
ETag: directoryETag,
|
|
||||||
Size: 0, // Directories have size 0
|
|
||||||
Owner: s3a.getObjectOwnerFromEntry(entry),
|
|
||||||
StorageClass: "STANDARD",
|
|
||||||
}
|
|
||||||
*allVersions = append(*allVersions, versionEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively search subdirectories (regardless of whether they're explicit or implicit)
|
|
||||||
fullPath := path.Join(currentPath, entry.Name)
|
fullPath := path.Join(currentPath, entry.Name)
|
||||||
err := s3a.findVersionsRecursively(fullPath, entryPath, allVersions, processedObjects, seenVersionIds, bucket, prefix)
|
err := s3a.findVersionsRecursively(fullPath, entryPath, allVersions, processedObjects, seenVersionIds, bucket, prefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -407,7 +339,7 @@ func (s3a *S3ApiServer) findVersionsRecursively(currentPath, relativePath string
|
||||||
LastModified: time.Unix(entry.Attributes.Mtime, 0),
|
LastModified: time.Unix(entry.Attributes.Mtime, 0),
|
||||||
ETag: etag,
|
ETag: etag,
|
||||||
Size: int64(entry.Attributes.FileSize),
|
Size: int64(entry.Attributes.FileSize),
|
||||||
Owner: s3a.getObjectOwnerFromEntry(entry),
|
Owner: CanonicalUser{ID: "unknown", DisplayName: "unknown"},
|
||||||
StorageClass: "STANDARD",
|
StorageClass: "STANDARD",
|
||||||
}
|
}
|
||||||
*allVersions = append(*allVersions, versionEntry)
|
*allVersions = append(*allVersions, versionEntry)
|
||||||
|
@ -597,7 +529,13 @@ func (s3a *S3ApiServer) deleteSpecificObjectVersion(bucket, object, versionId st
|
||||||
versionsDir := s3a.getVersionedObjectDir(bucket, object)
|
versionsDir := s3a.getVersionedObjectDir(bucket, object)
|
||||||
versionFile := s3a.getVersionFileName(versionId)
|
versionFile := s3a.getVersionFileName(versionId)
|
||||||
|
|
||||||
// Check if this is the latest version before attempting deletion (for potential metadata update)
|
// Delete the specific version from .versions directory
|
||||||
|
_, err := s3a.getEntry(versionsDir, versionFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("version %s not found: %v", versionId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is the latest version before deleting
|
||||||
versionsEntry, dirErr := s3a.getEntry(path.Join(s3a.option.BucketsPath, bucket), object+".versions")
|
versionsEntry, dirErr := s3a.getEntry(path.Join(s3a.option.BucketsPath, bucket), object+".versions")
|
||||||
isLatestVersion := false
|
isLatestVersion := false
|
||||||
if dirErr == nil && versionsEntry.Extended != nil {
|
if dirErr == nil && versionsEntry.Extended != nil {
|
||||||
|
@ -606,20 +544,16 @@ func (s3a *S3ApiServer) deleteSpecificObjectVersion(bucket, object, versionId st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to delete the version file
|
// Delete the version file
|
||||||
// Note: We don't check if the file exists first to avoid race conditions
|
|
||||||
// The deletion operation should be idempotent
|
|
||||||
deleteErr := s3a.rm(versionsDir, versionFile, true, false)
|
deleteErr := s3a.rm(versionsDir, versionFile, true, false)
|
||||||
if deleteErr != nil {
|
if deleteErr != nil {
|
||||||
// Check if file was already deleted by another process (race condition handling)
|
// Check if file was already deleted by another process
|
||||||
if _, checkErr := s3a.getEntry(versionsDir, versionFile); checkErr != nil {
|
if _, checkErr := s3a.getEntry(versionsDir, versionFile); checkErr != nil {
|
||||||
// File doesn't exist anymore, deletion was successful (another thread deleted it)
|
// File doesn't exist anymore, deletion was successful
|
||||||
glog.V(2).Infof("deleteSpecificObjectVersion: version %s for %s%s already deleted by another process", versionId, bucket, object)
|
} else {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// File still exists but deletion failed for another reason
|
|
||||||
return fmt.Errorf("failed to delete version %s: %v", versionId, deleteErr)
|
return fmt.Errorf("failed to delete version %s: %v", versionId, deleteErr)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we deleted the latest version, update the .versions directory metadata to point to the new latest
|
// If we deleted the latest version, update the .versions directory metadata to point to the new latest
|
||||||
if isLatestVersion {
|
if isLatestVersion {
|
||||||
|
@ -731,8 +665,7 @@ func (s3a *S3ApiServer) ListObjectVersionsHandler(w http.ResponseWriter, r *http
|
||||||
|
|
||||||
// Parse query parameters
|
// Parse query parameters
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
originalPrefix := query.Get("prefix") // Keep original prefix for response
|
prefix := query.Get("prefix")
|
||||||
prefix := originalPrefix // Use for internal processing
|
|
||||||
if prefix != "" && !strings.HasPrefix(prefix, "/") {
|
if prefix != "" && !strings.HasPrefix(prefix, "/") {
|
||||||
prefix = "/" + prefix
|
prefix = "/" + prefix
|
||||||
}
|
}
|
||||||
|
@ -757,16 +690,14 @@ func (s3a *S3ApiServer) ListObjectVersionsHandler(w http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the original prefix in the response (not the normalized internal prefix)
|
|
||||||
result.Prefix = originalPrefix
|
|
||||||
|
|
||||||
writeSuccessResponseXML(w, r, result)
|
writeSuccessResponseXML(w, r, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLatestObjectVersion finds the latest version of an object by reading .versions directory metadata
|
// getLatestObjectVersion finds the latest version of an object by reading .versions directory metadata
|
||||||
func (s3a *S3ApiServer) getLatestObjectVersion(bucket, object string) (*filer_pb.Entry, error) {
|
func (s3a *S3ApiServer) getLatestObjectVersion(bucket, object string) (*filer_pb.Entry, error) {
|
||||||
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
||||||
versionsObjectPath := object + ".versions"
|
cleanObject := strings.TrimPrefix(object, "/")
|
||||||
|
versionsObjectPath := cleanObject + ".versions"
|
||||||
|
|
||||||
// Get the .versions directory entry to read latest version metadata
|
// Get the .versions directory entry to read latest version metadata
|
||||||
versionsEntry, err := s3a.getEntry(bucketDir, versionsObjectPath)
|
versionsEntry, err := s3a.getEntry(bucketDir, versionsObjectPath)
|
||||||
|
@ -774,14 +705,14 @@ func (s3a *S3ApiServer) getLatestObjectVersion(bucket, object string) (*filer_pb
|
||||||
// .versions directory doesn't exist - this can happen for objects that existed
|
// .versions directory doesn't exist - this can happen for objects that existed
|
||||||
// before versioning was enabled on the bucket. Fall back to checking for a
|
// before versioning was enabled on the bucket. Fall back to checking for a
|
||||||
// regular (non-versioned) object file.
|
// regular (non-versioned) object file.
|
||||||
glog.V(2).Infof("getLatestObjectVersion: no .versions directory for %s%s, checking for pre-versioning object", bucket, object)
|
glog.V(2).Infof("getLatestObjectVersion: no .versions directory for %s/%s, checking for pre-versioning object", bucket, object)
|
||||||
|
|
||||||
regularEntry, regularErr := s3a.getEntry(bucketDir, object)
|
regularEntry, regularErr := s3a.getEntry(bucketDir, cleanObject)
|
||||||
if regularErr != nil {
|
if regularErr != nil {
|
||||||
return nil, fmt.Errorf("failed to get %s%s .versions directory and no regular object found: %w", bucket, object, err)
|
return nil, fmt.Errorf("failed to get %s/%s .versions directory and no regular object found: %w", bucket, cleanObject, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
glog.V(2).Infof("getLatestObjectVersion: found pre-versioning object for %s/%s", bucket, object)
|
glog.V(2).Infof("getLatestObjectVersion: found pre-versioning object for %s/%s", bucket, cleanObject)
|
||||||
return regularEntry, nil
|
return regularEntry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -789,14 +720,14 @@ func (s3a *S3ApiServer) getLatestObjectVersion(bucket, object string) (*filer_pb
|
||||||
if versionsEntry.Extended == nil {
|
if versionsEntry.Extended == nil {
|
||||||
// No metadata means all versioned objects have been deleted.
|
// No metadata means all versioned objects have been deleted.
|
||||||
// Fall back to checking for a pre-versioning object.
|
// Fall back to checking for a pre-versioning object.
|
||||||
glog.V(2).Infof("getLatestObjectVersion: no Extended metadata in .versions directory for %s%s, checking for pre-versioning object", bucket, object)
|
glog.V(2).Infof("getLatestObjectVersion: no Extended metadata in .versions directory for %s/%s, checking for pre-versioning object", bucket, cleanObject)
|
||||||
|
|
||||||
regularEntry, regularErr := s3a.getEntry(bucketDir, object)
|
regularEntry, regularErr := s3a.getEntry(bucketDir, cleanObject)
|
||||||
if regularErr != nil {
|
if regularErr != nil {
|
||||||
return nil, fmt.Errorf("no version metadata in .versions directory and no regular object found for %s%s", bucket, object)
|
return nil, fmt.Errorf("no version metadata in .versions directory and no regular object found for %s/%s", bucket, cleanObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
glog.V(2).Infof("getLatestObjectVersion: found pre-versioning object for %s%s (no Extended metadata case)", bucket, object)
|
glog.V(2).Infof("getLatestObjectVersion: found pre-versioning object for %s/%s (no Extended metadata case)", bucket, cleanObject)
|
||||||
return regularEntry, nil
|
return regularEntry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -808,12 +739,12 @@ func (s3a *S3ApiServer) getLatestObjectVersion(bucket, object string) (*filer_pb
|
||||||
// Fall back to checking for a pre-versioning object.
|
// Fall back to checking for a pre-versioning object.
|
||||||
glog.V(2).Infof("getLatestObjectVersion: no version metadata in .versions directory for %s/%s, checking for pre-versioning object", bucket, object)
|
glog.V(2).Infof("getLatestObjectVersion: no version metadata in .versions directory for %s/%s, checking for pre-versioning object", bucket, object)
|
||||||
|
|
||||||
regularEntry, regularErr := s3a.getEntry(bucketDir, object)
|
regularEntry, regularErr := s3a.getEntry(bucketDir, cleanObject)
|
||||||
if regularErr != nil {
|
if regularErr != nil {
|
||||||
return nil, fmt.Errorf("no version metadata in .versions directory and no regular object found for %s%s", bucket, object)
|
return nil, fmt.Errorf("no version metadata in .versions directory and no regular object found for %s/%s", bucket, cleanObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
glog.V(2).Infof("getLatestObjectVersion: found pre-versioning object for %s%s after version deletion", bucket, object)
|
glog.V(2).Infof("getLatestObjectVersion: found pre-versioning object for %s/%s after version deletion", bucket, cleanObject)
|
||||||
return regularEntry, nil
|
return regularEntry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -831,55 +762,3 @@ func (s3a *S3ApiServer) getLatestObjectVersion(bucket, object string) (*filer_pb
|
||||||
|
|
||||||
return latestVersionEntry, nil
|
return latestVersionEntry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getObjectOwnerFromVersion extracts object owner information from version entry metadata
|
|
||||||
func (s3a *S3ApiServer) getObjectOwnerFromVersion(version *ObjectVersion, bucket, objectKey string) CanonicalUser {
|
|
||||||
// First try to get owner from the version entry itself
|
|
||||||
if version.Entry != nil && version.Entry.Extended != nil {
|
|
||||||
if ownerBytes, exists := version.Entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
|
||||||
ownerId := string(ownerBytes)
|
|
||||||
ownerDisplayName := s3a.iam.GetAccountNameById(ownerId)
|
|
||||||
return CanonicalUser{ID: ownerId, DisplayName: ownerDisplayName}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: try to get owner from the current version of the object
|
|
||||||
// This handles cases where older versions might not have owner metadata
|
|
||||||
if version.VersionId == "null" {
|
|
||||||
// For null version, check the regular object file
|
|
||||||
bucketDir := s3a.option.BucketsPath + "/" + bucket
|
|
||||||
if entry, err := s3a.getEntry(bucketDir, objectKey); err == nil && entry.Extended != nil {
|
|
||||||
if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
|
||||||
ownerId := string(ownerBytes)
|
|
||||||
ownerDisplayName := s3a.iam.GetAccountNameById(ownerId)
|
|
||||||
return CanonicalUser{ID: ownerId, DisplayName: ownerDisplayName}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For versioned objects, try to get from latest version metadata
|
|
||||||
if latestVersion, err := s3a.getLatestObjectVersion(bucket, objectKey); err == nil && latestVersion.Extended != nil {
|
|
||||||
if ownerBytes, exists := latestVersion.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
|
||||||
ownerId := string(ownerBytes)
|
|
||||||
ownerDisplayName := s3a.iam.GetAccountNameById(ownerId)
|
|
||||||
return CanonicalUser{ID: ownerId, DisplayName: ownerDisplayName}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ultimate fallback: return anonymous if no owner found
|
|
||||||
return CanonicalUser{ID: s3_constants.AccountAnonymousId, DisplayName: "anonymous"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getObjectOwnerFromEntry extracts object owner information from a file entry
|
|
||||||
func (s3a *S3ApiServer) getObjectOwnerFromEntry(entry *filer_pb.Entry) CanonicalUser {
|
|
||||||
if entry != nil && entry.Extended != nil {
|
|
||||||
if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
|
|
||||||
ownerId := string(ownerBytes)
|
|
||||||
ownerDisplayName := s3a.iam.GetAccountNameById(ownerId)
|
|
||||||
return CanonicalUser{ID: ownerId, DisplayName: ownerDisplayName}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: return anonymous if no owner found
|
|
||||||
return CanonicalUser{ID: s3_constants.AccountAnonymousId, DisplayName: "anonymous"}
|
|
||||||
}
|
|
||||||
|
|
|
@ -230,16 +230,10 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
|
||||||
// raw objects
|
// raw objects
|
||||||
|
|
||||||
// HeadObject
|
// HeadObject
|
||||||
bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(track(s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) {
|
bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.HeadObjectHandler, ACTION_READ)), "GET"))
|
||||||
limitedHandler, _ := s3a.cb.Limit(s3a.HeadObjectHandler, ACTION_READ)
|
|
||||||
limitedHandler(w, r)
|
|
||||||
}, ACTION_READ), "GET"))
|
|
||||||
|
|
||||||
// GetObject, but directory listing is not supported
|
// GetObject, but directory listing is not supported
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(track(s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) {
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.GetObjectHandler, ACTION_READ)), "GET"))
|
||||||
limitedHandler, _ := s3a.cb.Limit(s3a.GetObjectHandler, ACTION_READ)
|
|
||||||
limitedHandler(w, r)
|
|
||||||
}, ACTION_READ), "GET"))
|
|
||||||
|
|
||||||
// CopyObject
|
// CopyObject
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.CopyObjectHandler, ACTION_WRITE)), "COPY"))
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.CopyObjectHandler, ACTION_WRITE)), "COPY"))
|
||||||
|
@ -311,10 +305,7 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
|
||||||
bucket.Methods(http.MethodDelete).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeletePublicAccessBlockHandler, ACTION_ADMIN)), "DELETE")).Queries("publicAccessBlock", "")
|
bucket.Methods(http.MethodDelete).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeletePublicAccessBlockHandler, ACTION_ADMIN)), "DELETE")).Queries("publicAccessBlock", "")
|
||||||
|
|
||||||
// ListObjectsV2
|
// ListObjectsV2
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) {
|
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectsV2Handler, ACTION_LIST)), "LIST")).Queries("list-type", "2")
|
||||||
limitedHandler, _ := s3a.cb.Limit(s3a.ListObjectsV2Handler, ACTION_LIST)
|
|
||||||
limitedHandler(w, r)
|
|
||||||
}, ACTION_LIST), "LIST")).Queries("list-type", "2")
|
|
||||||
|
|
||||||
// ListObjectVersions
|
// ListObjectVersions
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectVersionsHandler, ACTION_LIST)), "LIST")).Queries("versions", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectVersionsHandler, ACTION_LIST)), "LIST")).Queries("versions", "")
|
||||||
|
@ -335,10 +326,7 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
|
||||||
bucket.Methods(http.MethodPost).HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PostPolicyBucketHandler, ACTION_WRITE)), "POST"))
|
bucket.Methods(http.MethodPost).HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PostPolicyBucketHandler, ACTION_WRITE)), "POST"))
|
||||||
|
|
||||||
// HeadBucket
|
// HeadBucket
|
||||||
bucket.Methods(http.MethodHead).HandlerFunc(track(s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) {
|
bucket.Methods(http.MethodHead).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.HeadBucketHandler, ACTION_READ)), "GET"))
|
||||||
limitedHandler, _ := s3a.cb.Limit(s3a.HeadBucketHandler, ACTION_READ)
|
|
||||||
limitedHandler(w, r)
|
|
||||||
}, ACTION_READ), "GET"))
|
|
||||||
|
|
||||||
// PutBucket
|
// PutBucket
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketHandler, ACTION_ADMIN)), "PUT"))
|
bucket.Methods(http.MethodPut).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PutBucketHandler, ACTION_ADMIN)), "PUT"))
|
||||||
|
@ -347,10 +335,7 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
|
||||||
bucket.Methods(http.MethodDelete).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketHandler, ACTION_DELETE_BUCKET)), "DELETE"))
|
bucket.Methods(http.MethodDelete).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.DeleteBucketHandler, ACTION_DELETE_BUCKET)), "DELETE"))
|
||||||
|
|
||||||
// ListObjectsV1 (Legacy)
|
// ListObjectsV1 (Legacy)
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) {
|
bucket.Methods(http.MethodGet).HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.ListObjectsV1Handler, ACTION_LIST)), "LIST"))
|
||||||
limitedHandler, _ := s3a.cb.Limit(s3a.ListObjectsV1Handler, ACTION_LIST)
|
|
||||||
limitedHandler(w, r)
|
|
||||||
}, ACTION_LIST), "LIST"))
|
|
||||||
|
|
||||||
// raw buckets
|
// raw buckets
|
||||||
|
|
||||||
|
|
|
@ -1134,7 +1134,7 @@ type ListEntry struct {
|
||||||
LastModified time.Time `xml:"LastModified"`
|
LastModified time.Time `xml:"LastModified"`
|
||||||
ETag string `xml:"ETag"`
|
ETag string `xml:"ETag"`
|
||||||
Size int64 `xml:"Size"`
|
Size int64 `xml:"Size"`
|
||||||
Owner *CanonicalUser `xml:"Owner,omitempty"`
|
Owner CanonicalUser `xml:"Owner,omitempty"`
|
||||||
StorageClass StorageClass `xml:"StorageClass"`
|
StorageClass StorageClass `xml:"StorageClass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,33 +78,24 @@ func setCommonHeaders(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("x-amz-request-id", fmt.Sprintf("%d", time.Now().UnixNano()))
|
w.Header().Set("x-amz-request-id", fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||||
w.Header().Set("Accept-Ranges", "bytes")
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
|
||||||
// Handle CORS headers for requests with Origin header
|
// Only set static CORS headers for service-level requests, not bucket-specific requests
|
||||||
if r.Header.Get("Origin") != "" {
|
if r.Header.Get("Origin") != "" {
|
||||||
// Use mux.Vars to detect bucket-specific requests more reliably
|
// Use mux.Vars to detect bucket-specific requests more reliably
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
isBucketRequest := bucket != ""
|
isBucketRequest := bucket != ""
|
||||||
|
|
||||||
if !isBucketRequest {
|
// Only apply static CORS headers if this is NOT a bucket-specific request
|
||||||
// Service-level request (like OPTIONS /) - apply static CORS if none set
|
// and no bucket-specific CORS headers were already set
|
||||||
if w.Header().Get("Access-Control-Allow-Origin") == "" {
|
if !isBucketRequest && w.Header().Get("Access-Control-Allow-Origin") == "" {
|
||||||
|
// This is a service-level request (like OPTIONS /), apply static CORS
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "*")
|
w.Header().Set("Access-Control-Allow-Methods", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||||
w.Header().Set("Access-Control-Expose-Headers", "*")
|
w.Header().Set("Access-Control-Expose-Headers", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
}
|
}
|
||||||
} else {
|
// For bucket-specific requests, let the CORS middleware handle the headers
|
||||||
// Bucket-specific request - preserve existing CORS headers or set default
|
|
||||||
// This handles cases where CORS middleware set headers but auth failed
|
|
||||||
if w.Header().Get("Access-Control-Allow-Origin") == "" {
|
|
||||||
// No CORS headers were set by middleware, so this request doesn't match any CORS rule
|
|
||||||
// According to CORS spec, we should not set CORS headers for non-matching requests
|
|
||||||
// However, if the bucket has CORS config but request doesn't match,
|
|
||||||
// we still should not set headers here as it would be incorrect
|
|
||||||
}
|
|
||||||
// If CORS headers were already set by middleware, preserve them
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,7 +115,6 @@ const (
|
||||||
ErrNoSuchObjectLegalHold
|
ErrNoSuchObjectLegalHold
|
||||||
ErrInvalidRetentionPeriod
|
ErrInvalidRetentionPeriod
|
||||||
ErrObjectLockConfigurationNotFoundError
|
ErrObjectLockConfigurationNotFoundError
|
||||||
ErrInvalidUnorderedWithDelimiter
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error message constants for checksum validation
|
// Error message constants for checksum validation
|
||||||
|
@ -466,11 +465,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
|
||||||
Description: "Object Lock configuration does not exist for this bucket",
|
Description: "Object Lock configuration does not exist for this bucket",
|
||||||
HTTPStatusCode: http.StatusNotFound,
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
ErrInvalidUnorderedWithDelimiter: {
|
|
||||||
Code: "InvalidArgument",
|
|
||||||
Description: "Unordered listing cannot be used with delimiter",
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAPIError provides API Error for input API error code.
|
// GetAPIError provides API Error for input API error code.
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/util/version"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/util/version"
|
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
|
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
type FullPath string
|
type FullPath string
|
||||||
|
|
||||||
func NewFullPath(dir, name string) FullPath {
|
func NewFullPath(dir, name string) FullPath {
|
||||||
name = strings.TrimSuffix(name, "/")
|
|
||||||
return FullPath(dir).Child(name)
|
return FullPath(dir).Child(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue