Garage v0.9 #473
33 changed files with 2057 additions and 877 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -2641,9 +2641,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-hack"
|
name = "proc-macro-hack"
|
||||||
version = "0.5.19"
|
version = "0.5.20+deprecated"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
|
|
12
Cargo.nix
12
Cargo.nix
|
@ -32,7 +32,7 @@ args@{
|
||||||
ignoreLockHash,
|
ignoreLockHash,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
nixifiedLockHash = "30497a698042332c229ed062aa3c4bc7d17c3e927deb3cf9d4dc12d8a0492515";
|
nixifiedLockHash = "5214dc0a8455fa16df874d9a8efdaa0b99cb3b6ae4cf2ea973137c1b8a1d39d6";
|
||||||
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
|
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
|
||||||
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
|
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
|
||||||
lockHashIgnored = if ignoreLockHash
|
lockHashIgnored = if ignoreLockHash
|
||||||
|
@ -1985,7 +1985,7 @@ in
|
||||||
src = fetchCratesIo { inherit name version; sha256 = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899"; };
|
src = fetchCratesIo { inherit name version; sha256 = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899"; };
|
||||||
dependencies = {
|
dependencies = {
|
||||||
git_version_macro = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version-macro."0.3.5" { profileName = "__noProfile"; }).out;
|
git_version_macro = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".git-version-macro."0.3.5" { profileName = "__noProfile"; }).out;
|
||||||
proc_macro_hack = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-hack."0.5.19" { profileName = "__noProfile"; }).out;
|
proc_macro_hack = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-hack."0.5.20+deprecated" { profileName = "__noProfile"; }).out;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1995,7 +1995,7 @@ in
|
||||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||||
src = fetchCratesIo { inherit name version; sha256 = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f"; };
|
src = fetchCratesIo { inherit name version; sha256 = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f"; };
|
||||||
dependencies = {
|
dependencies = {
|
||||||
proc_macro_hack = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-hack."0.5.19" { profileName = "__noProfile"; }).out;
|
proc_macro_hack = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro-hack."0.5.20+deprecated" { profileName = "__noProfile"; }).out;
|
||||||
proc_macro2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.56" { inherit profileName; }).out;
|
proc_macro2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.56" { inherit profileName; }).out;
|
||||||
quote = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.26" { inherit profileName; }).out;
|
quote = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.26" { inherit profileName; }).out;
|
||||||
syn = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }).out;
|
syn = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."1.0.94" { inherit profileName; }).out;
|
||||||
|
@ -3695,11 +3695,11 @@ in
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
"registry+https://github.com/rust-lang/crates.io-index".proc-macro-hack."0.5.19" = overridableMkRustCrate (profileName: rec {
|
"registry+https://github.com/rust-lang/crates.io-index".proc-macro-hack."0.5.20+deprecated" = overridableMkRustCrate (profileName: rec {
|
||||||
name = "proc-macro-hack";
|
name = "proc-macro-hack";
|
||||||
version = "0.5.19";
|
version = "0.5.20+deprecated";
|
||||||
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
registry = "registry+https://github.com/rust-lang/crates.io-index";
|
||||||
src = fetchCratesIo { inherit name version; sha256 = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"; };
|
src = fetchCratesIo { inherit name version; sha256 = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"; };
|
||||||
});
|
});
|
||||||
|
|
||||||
"registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.56" = overridableMkRustCrate (profileName: rec {
|
"registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.56" = overridableMkRustCrate (profileName: rec {
|
||||||
|
|
|
@ -535,7 +535,10 @@ Example response:
|
||||||
],
|
],
|
||||||
"objects": 14827,
|
"objects": 14827,
|
||||||
"bytes": 13189855625,
|
"bytes": 13189855625,
|
||||||
"unfinshedUploads": 0,
|
"unfinishedUploads": 1,
|
||||||
|
"unfinishedMultipartUploads": 1,
|
||||||
|
"unfinishedMultipartUploadParts": 11,
|
||||||
|
"unfinishedMultipartUploadBytes": 41943040,
|
||||||
"quotas": {
|
"quotas": {
|
||||||
"maxSize": null,
|
"maxSize": null,
|
||||||
"maxObjects": null
|
"maxObjects": null
|
||||||
|
|
138
script/test-renumbering.sh
Normal file
138
script/test-renumbering.sh
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
: '
|
||||||
|
This script tests part renumbering on an S3 remote (here configured for Minio).
|
||||||
|
|
||||||
|
On Minio:
|
||||||
|
|
||||||
|
The results confirm that if I upload parts with number 1, 4, 5 and 6,
|
||||||
|
they are renumbered to 1, 2, 3 and 4 after CompleteMultipartUpload.
|
||||||
|
Thus, specifying partNumber=4 on a GetObject/HeadObject should return
|
||||||
|
information on the part I originally uploaded with part number
|
||||||
|
|
||||||
|
On S3: not tested
|
||||||
|
|
||||||
|
Sample output (on Minio):
|
||||||
|
|
||||||
|
f07e1404cc527d494242824ded3a616b part1
|
||||||
|
78974cd4d0f622eb3426ea7cd22f5a1c part4
|
||||||
|
f9cc379f8baa61645558d9ba7e6351fa part5
|
||||||
|
1bd2383eebbac1f8e7143575ba5b1f4a part6
|
||||||
|
Upload ID: 6838b813-d0ca-400b-9d28-ec8b2b5cd004
|
||||||
|
PART 1 ETag: "f07e1404cc527d494242824ded3a616b"
|
||||||
|
PART 4 ETag: "78974cd4d0f622eb3426ea7cd22f5a1c"
|
||||||
|
PART 5 ETag: "f9cc379f8baa61645558d9ba7e6351fa"
|
||||||
|
PART 6 ETag: "1bd2383eebbac1f8e7143575ba5b1f4a"
|
||||||
|
======================================== LIST ====
|
||||||
|
{
|
||||||
|
"Parts": [
|
||||||
|
{
|
||||||
|
"PartNumber": 1,
|
||||||
|
"LastModified": "2023-04-25T10:21:54.350000+00:00",
|
||||||
|
"ETag": "\"f07e1404cc527d494242824ded3a616b\"",
|
||||||
|
"Size": 20971520
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"PartNumber": 4,
|
||||||
|
"LastModified": "2023-04-25T10:21:54.350000+00:00",
|
||||||
|
"ETag": "\"78974cd4d0f622eb3426ea7cd22f5a1c\"",
|
||||||
|
"Size": 20971520
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"PartNumber": 5,
|
||||||
|
"LastModified": "2023-04-25T10:21:54.350000+00:00",
|
||||||
|
"ETag": "\"f9cc379f8baa61645558d9ba7e6351fa\"",
|
||||||
|
"Size": 20971520
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"PartNumber": 6,
|
||||||
|
"LastModified": "2023-04-25T10:21:54.350000+00:00",
|
||||||
|
"ETag": "\"1bd2383eebbac1f8e7143575ba5b1f4a\"",
|
||||||
|
"Size": 20971520
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ChecksumAlgorithm": "",
|
||||||
|
"Initiator": {
|
||||||
|
"ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4",
|
||||||
|
"DisplayName": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
|
||||||
|
},
|
||||||
|
"Owner": {
|
||||||
|
"DisplayName": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4",
|
||||||
|
"ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
|
||||||
|
},
|
||||||
|
"StorageClass": "STANDARD"
|
||||||
|
}
|
||||||
|
======================================== COMPLETE ====
|
||||||
|
{
|
||||||
|
"Location": "http://localhost:9000/test/upload",
|
||||||
|
"Bucket": "test",
|
||||||
|
"Key": "upload",
|
||||||
|
"ETag": "\"8e817c8ccd442f9a79c77b58fe808c43-4\""
|
||||||
|
}
|
||||||
|
======================================== LIST ====
|
||||||
|
|
||||||
|
An error occurred (NoSuchUpload) when calling the ListParts operation: The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.
|
||||||
|
======================================== GET PART 4 ====
|
||||||
|
{
|
||||||
|
"AcceptRanges": "bytes",
|
||||||
|
"LastModified": "2023-04-25T10:21:59+00:00",
|
||||||
|
"ContentLength": 20971520,
|
||||||
|
"ETag": "\"8e817c8ccd442f9a79c77b58fe808c43-4\"",
|
||||||
|
"ContentRange": "bytes 62914560-83886079/83886080",
|
||||||
|
"ContentType": "binary/octet-stream",
|
||||||
|
"Metadata": {},
|
||||||
|
"PartsCount": 4
|
||||||
|
}
|
||||||
|
1bd2383eebbac1f8e7143575ba5b1f4a get-part4
|
||||||
|
|
||||||
|
|
||||||
|
Conclusions:
|
||||||
|
|
||||||
|
- Parts are indeed renumbered with consecutive numbers
|
||||||
|
- ListParts only applies to multipart uploads in progress,
|
||||||
|
it cannot be used once the multipart upload has been completed
|
||||||
|
'
|
||||||
|
|
||||||
|
export AWS_ACCESS_KEY_ID=1D8Pk2k4oQSoh1BU
|
||||||
|
export AWS_SECRET_ACCESS_KEY=4B46SR8U7FUgY0raB8Zuxg1NLyLTvbNV
|
||||||
|
|
||||||
|
function aws { command aws --endpoint-url http://localhost:9000 $@ ; }
|
||||||
|
|
||||||
|
aws --version
|
||||||
|
|
||||||
|
aws s3 mb s3://test
|
||||||
|
|
||||||
|
for NUM in 1 4 5 6; do
|
||||||
|
dd if=/dev/urandom of=part$NUM bs=1M count=10
|
||||||
|
done
|
||||||
|
md5sum part*
|
||||||
|
|
||||||
|
UPLOAD=$(aws s3api create-multipart-upload --bucket test --key 'upload' | jq -r ".UploadId")
|
||||||
|
echo "Upload ID: $UPLOAD"
|
||||||
|
|
||||||
|
PARTS=""
|
||||||
|
|
||||||
|
for NUM in 1 4 5 6; do
|
||||||
|
ETAG=$(aws s3api upload-part --bucket test --key 'upload' --part-number $NUM \
|
||||||
|
--body "part$NUM" --upload-id "$UPLOAD" | jq -r ".ETag")
|
||||||
|
echo "PART $NUM ETag: $ETAG"
|
||||||
|
if [ -n "$PARTS" ]; then
|
||||||
|
PARTS="$PARTS,"
|
||||||
|
fi
|
||||||
|
PARTS="$PARTS {\"ETag\":$ETAG,\"PartNumber\":$NUM}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "======================================== LIST ===="
|
||||||
|
aws s3api list-parts --bucket test --key upload --upload-id "$UPLOAD" | jq
|
||||||
|
|
||||||
|
echo "======================================== COMPLETE ===="
|
||||||
|
echo "{\"Parts\":[$PARTS]}" > mpu
|
||||||
|
aws s3api complete-multipart-upload --multipart-upload file://mpu \
|
||||||
|
--bucket test --key 'upload' --upload-id "$UPLOAD"
|
||||||
|
|
||||||
|
echo "======================================== LIST ===="
|
||||||
|
aws s3api list-parts --bucket test --key upload --upload-id "$UPLOAD" | jq
|
||||||
|
|
||||||
|
echo "======================================== GET PART 4 ===="
|
||||||
|
aws s3api get-object --bucket test --key upload --part-number 4 get-part4
|
||||||
|
md5sum get-part4
|
103
script/test-skip-part.sh
Normal file
103
script/test-skip-part.sh
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
: '
|
||||||
|
This script tests whether uploaded parts can be skipped in a
|
||||||
|
CompleteMultipartUpoad
|
||||||
|
|
||||||
|
On Minio: yes, parts can be skipped
|
||||||
|
|
||||||
|
On S3: not tested
|
||||||
|
|
||||||
|
Sample output (on Minio):
|
||||||
|
|
||||||
|
f23911bcd1230f5ebe8887cbf5bc396e part1
|
||||||
|
a2657143167eaf647c40473e78a091dc part4
|
||||||
|
72f72c02c5163bc81024b28ac818c5e0 part5
|
||||||
|
e29cf500d20498218904b8df8806caa2 part6
|
||||||
|
Upload ID: e8fe7b83-9800-46fb-ae90-9d7ccd42fe76
|
||||||
|
PART 1 ETag: "f23911bcd1230f5ebe8887cbf5bc396e"
|
||||||
|
PART 4 ETag: "a2657143167eaf647c40473e78a091dc"
|
||||||
|
PART 5 ETag: "72f72c02c5163bc81024b28ac818c5e0"
|
||||||
|
PART 6 ETag: "e29cf500d20498218904b8df8806caa2"
|
||||||
|
======================================== COMPLETE ====
|
||||||
|
{
|
||||||
|
"Location": "http://localhost:9000/test/upload",
|
||||||
|
"Bucket": "test",
|
||||||
|
"Key": "upload",
|
||||||
|
"ETag": "\"48246e44d4b38bdc2f3c10ee25b1af17-3\""
|
||||||
|
}
|
||||||
|
======================================== GET FULL ====
|
||||||
|
{
|
||||||
|
"AcceptRanges": "bytes",
|
||||||
|
"LastModified": "2023-04-25T10:54:35+00:00",
|
||||||
|
"ContentLength": 31457280,
|
||||||
|
"ETag": "\"48246e44d4b38bdc2f3c10ee25b1af17-3\"",
|
||||||
|
"ContentType": "binary/octet-stream",
|
||||||
|
"Metadata": {}
|
||||||
|
}
|
||||||
|
97fb904da7ad310699a6afab0eb6e061 get-full
|
||||||
|
97fb904da7ad310699a6afab0eb6e061 -
|
||||||
|
======================================== GET PART 3 ====
|
||||||
|
{
|
||||||
|
"AcceptRanges": "bytes",
|
||||||
|
"LastModified": "2023-04-25T10:54:35+00:00",
|
||||||
|
"ContentLength": 10485760,
|
||||||
|
"ETag": "\"48246e44d4b38bdc2f3c10ee25b1af17-3\"",
|
||||||
|
"ContentRange": "bytes 20971520-31457279/31457280",
|
||||||
|
"ContentType": "binary/octet-stream",
|
||||||
|
"Metadata": {},
|
||||||
|
"PartsCount": 3
|
||||||
|
}
|
||||||
|
e29cf500d20498218904b8df8806caa2 get-part3
|
||||||
|
|
||||||
|
Conclusions:
|
||||||
|
|
||||||
|
- Skipping a part in a CompleteMultipartUpoad call is OK
|
||||||
|
- The part is simply not included in the stored object
|
||||||
|
- Sequential part renumbering counts only non-skipped parts
|
||||||
|
'
|
||||||
|
|
||||||
|
export AWS_ACCESS_KEY_ID=1D8Pk2k4oQSoh1BU
|
||||||
|
export AWS_SECRET_ACCESS_KEY=4B46SR8U7FUgY0raB8Zuxg1NLyLTvbNV
|
||||||
|
|
||||||
|
function aws { command aws --endpoint-url http://localhost:9000 $@ ; }
|
||||||
|
|
||||||
|
aws --version
|
||||||
|
|
||||||
|
aws s3 mb s3://test
|
||||||
|
|
||||||
|
for NUM in 1 4 5 6; do
|
||||||
|
dd if=/dev/urandom of=part$NUM bs=1M count=10
|
||||||
|
done
|
||||||
|
md5sum part*
|
||||||
|
|
||||||
|
UPLOAD=$(aws s3api create-multipart-upload --bucket test --key 'upload' | jq -r ".UploadId")
|
||||||
|
echo "Upload ID: $UPLOAD"
|
||||||
|
|
||||||
|
PARTS=""
|
||||||
|
|
||||||
|
for NUM in 1 4 5 6; do
|
||||||
|
ETAG=$(aws s3api upload-part --bucket test --key 'upload' --part-number $NUM \
|
||||||
|
--body "part$NUM" --upload-id "$UPLOAD" | jq -r ".ETag")
|
||||||
|
echo "PART $NUM ETag: $ETAG"
|
||||||
|
if [ "$NUM" != "5" ]; then
|
||||||
|
if [ -n "$PARTS" ]; then
|
||||||
|
PARTS="$PARTS,"
|
||||||
|
fi
|
||||||
|
PARTS="$PARTS {\"ETag\":$ETAG,\"PartNumber\":$NUM}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "======================================== COMPLETE ===="
|
||||||
|
echo "{\"Parts\":[$PARTS]}" > mpu
|
||||||
|
aws s3api complete-multipart-upload --multipart-upload file://mpu \
|
||||||
|
--bucket test --key 'upload' --upload-id "$UPLOAD"
|
||||||
|
|
||||||
|
echo "======================================== GET FULL ===="
|
||||||
|
aws s3api get-object --bucket test --key upload get-full
|
||||||
|
md5sum get-full
|
||||||
|
cat part1 part4 part6 | md5sum
|
||||||
|
|
||||||
|
echo "======================================== GET PART 3 ===="
|
||||||
|
aws s3api get-object --bucket test --key upload --part-number 3 get-part3
|
||||||
|
md5sum get-part3
|
|
@ -31,6 +31,11 @@ dd if=/dev/urandom of=/tmp/garage.1.rnd bs=1k count=2 # No multipart, inline sto
|
||||||
dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5 # No multipart but file will be chunked
|
dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5 # No multipart but file will be chunked
|
||||||
dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10 # by default, AWS starts using multipart at 8MB
|
dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10 # by default, AWS starts using multipart at 8MB
|
||||||
|
|
||||||
|
dd if=/dev/urandom of=/tmp/garage.part1.rnd bs=1M count=5
|
||||||
|
dd if=/dev/urandom of=/tmp/garage.part2.rnd bs=1M count=5
|
||||||
|
dd if=/dev/urandom of=/tmp/garage.part3.rnd bs=1M count=5
|
||||||
|
dd if=/dev/urandom of=/tmp/garage.part4.rnd bs=1M count=5
|
||||||
|
|
||||||
# data of lower entropy, to test compression
|
# data of lower entropy, to test compression
|
||||||
dd if=/dev/urandom bs=1k count=2 | base64 -w0 > /tmp/garage.1.b64
|
dd if=/dev/urandom bs=1k count=2 | base64 -w0 > /tmp/garage.1.b64
|
||||||
dd if=/dev/urandom bs=1M count=5 | base64 -w0 > /tmp/garage.2.b64
|
dd if=/dev/urandom bs=1M count=5 | base64 -w0 > /tmp/garage.2.b64
|
||||||
|
@ -40,7 +45,7 @@ echo "🧪 S3 API testing..."
|
||||||
|
|
||||||
# AWS
|
# AWS
|
||||||
if [ -z "$SKIP_AWS" ]; then
|
if [ -z "$SKIP_AWS" ]; then
|
||||||
echo "🛠️ Testing with awscli"
|
echo "🛠️ Testing with awscli (aws s3)"
|
||||||
source ${SCRIPT_FOLDER}/dev-env-aws.sh
|
source ${SCRIPT_FOLDER}/dev-env-aws.sh
|
||||||
aws s3 ls
|
aws s3 ls
|
||||||
for idx in {1..3}.{rnd,b64}; do
|
for idx in {1..3}.{rnd,b64}; do
|
||||||
|
@ -51,8 +56,36 @@ if [ -z "$SKIP_AWS" ]; then
|
||||||
rm /tmp/garage.$idx.dl
|
rm /tmp/garage.$idx.dl
|
||||||
aws s3 rm "s3://eprouvette/&+-é\"/garage.$idx.aws"
|
aws s3 rm "s3://eprouvette/&+-é\"/garage.$idx.aws"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
echo "🛠️ Testing multipart uploads with awscli (aws s3api)"
|
||||||
|
UPLOAD=$(aws s3api create-multipart-upload --bucket eprouvette --key 'upload' | jq -r ".UploadId")
|
||||||
|
echo "Upload ID: $UPLOAD"
|
||||||
|
ETAG3=$(aws s3api upload-part --bucket eprouvette --key 'upload' \
|
||||||
|
--part-number 3 --body "/tmp/garage.part1.rnd" --upload-id "$UPLOAD" \
|
||||||
|
| jq -r ".ETag")
|
||||||
|
ETAG2=$(aws s3api upload-part --bucket eprouvette --key 'upload' \
|
||||||
|
--part-number 2 --body "/tmp/garage.part2.rnd" --upload-id "$UPLOAD" \
|
||||||
|
| jq -r ".ETag")
|
||||||
|
ETAG3=$(aws s3api upload-part --bucket eprouvette --key 'upload' \
|
||||||
|
--part-number 3 --body "/tmp/garage.part3.rnd" --upload-id "$UPLOAD" \
|
||||||
|
| jq -r ".ETag")
|
||||||
|
ETAG6=$(aws s3api upload-part --bucket eprouvette --key 'upload' \
|
||||||
|
--part-number 6 --body "/tmp/garage.part4.rnd" --upload-id "$UPLOAD" \
|
||||||
|
| jq -r ".ETag")
|
||||||
|
MPU="{\"Parts\":[{\"PartNumber\":2,\"ETag\":$ETAG2}, {\"PartNumber\":3,\"ETag\":$ETAG3}, {\"PartNumber\":6,\"ETag\":$ETAG6}]}"
|
||||||
|
echo $MPU > /tmp/garage.mpu.json
|
||||||
|
aws s3api complete-multipart-upload --multipart-upload file:///tmp/garage.mpu.json \
|
||||||
|
--bucket eprouvette --key 'upload' --upload-id "$UPLOAD"
|
||||||
|
aws s3api get-object --bucket eprouvette --key upload /tmp/garage.mpu.get
|
||||||
|
if [ "$(md5sum /tmp/garage.mpu.get | cut -d ' ' -f 1)" != "$(cat /tmp/garage.part{2,3,4}.rnd | md5sum | cut -d ' ' -f 1)" ]; then
|
||||||
|
echo "Invalid multipart upload"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "OK!!"
|
||||||
|
exit 0
|
||||||
|
|
||||||
# S3CMD
|
# S3CMD
|
||||||
if [ -z "$SKIP_S3CMD" ]; then
|
if [ -z "$SKIP_S3CMD" ]; then
|
||||||
echo "🛠️ Testing with s3cmd"
|
echo "🛠️ Testing with s3cmd"
|
||||||
|
@ -141,6 +174,7 @@ rm eprouvette/winscp
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rm /tmp/garage.part{1..4}.rnd
|
||||||
rm /tmp/garage.{1..3}.{rnd,b64}
|
rm /tmp/garage.{1..3}.{rnd,b64}
|
||||||
|
|
||||||
echo "🏁 Teardown"
|
echo "🏁 Teardown"
|
||||||
|
|
|
@ -14,6 +14,7 @@ use garage_model::bucket_alias_table::*;
|
||||||
use garage_model::bucket_table::*;
|
use garage_model::bucket_table::*;
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_model::permission::*;
|
use garage_model::permission::*;
|
||||||
|
use garage_model::s3::mpu_table;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
|
|
||||||
use crate::admin::error::*;
|
use crate::admin::error::*;
|
||||||
|
@ -124,6 +125,14 @@ async fn bucket_info_results(
|
||||||
.map(|x| x.filtered_values(&garage.system.ring.borrow()))
|
.map(|x| x.filtered_values(&garage.system.ring.borrow()))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mpu_counters = garage
|
||||||
|
.mpu_counter_table
|
||||||
|
.table
|
||||||
|
.get(&bucket_id, &EmptyKey)
|
||||||
|
.await?
|
||||||
|
.map(|x| x.filtered_values(&garage.system.ring.borrow()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut relevant_keys = HashMap::new();
|
let mut relevant_keys = HashMap::new();
|
||||||
for (k, _) in bucket
|
for (k, _) in bucket
|
||||||
.state
|
.state
|
||||||
|
@ -183,8 +192,8 @@ async fn bucket_info_results(
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
keys: relevant_keys
|
keys: relevant_keys
|
||||||
.into_iter()
|
.into_values()
|
||||||
.map(|(_, key)| {
|
.map(|key| {
|
||||||
let p = key.state.as_option().unwrap();
|
let p = key.state.as_option().unwrap();
|
||||||
GetBucketInfoKey {
|
GetBucketInfoKey {
|
||||||
access_key_id: key.key_id,
|
access_key_id: key.key_id,
|
||||||
|
@ -208,12 +217,12 @@ async fn bucket_info_results(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
objects: counters.get(OBJECTS).cloned().unwrap_or_default(),
|
objects: *counters.get(OBJECTS).unwrap_or(&0),
|
||||||
bytes: counters.get(BYTES).cloned().unwrap_or_default(),
|
bytes: *counters.get(BYTES).unwrap_or(&0),
|
||||||
unfinished_uploads: counters
|
unfinished_uploads: *counters.get(UNFINISHED_UPLOADS).unwrap_or(&0),
|
||||||
.get(UNFINISHED_UPLOADS)
|
unfinished_multipart_uploads: *mpu_counters.get(mpu_table::UPLOADS).unwrap_or(&0),
|
||||||
.cloned()
|
unfinished_multipart_upload_parts: *mpu_counters.get(mpu_table::PARTS).unwrap_or(&0),
|
||||||
.unwrap_or_default(),
|
unfinished_multipart_upload_bytes: *mpu_counters.get(mpu_table::BYTES).unwrap_or(&0),
|
||||||
quotas: ApiBucketQuotas {
|
quotas: ApiBucketQuotas {
|
||||||
max_size: quotas.max_size,
|
max_size: quotas.max_size,
|
||||||
max_objects: quotas.max_objects,
|
max_objects: quotas.max_objects,
|
||||||
|
@ -235,6 +244,9 @@ struct GetBucketInfoResult {
|
||||||
objects: i64,
|
objects: i64,
|
||||||
bytes: i64,
|
bytes: i64,
|
||||||
unfinished_uploads: i64,
|
unfinished_uploads: i64,
|
||||||
|
unfinished_multipart_uploads: i64,
|
||||||
|
unfinished_multipart_upload_parts: i64,
|
||||||
|
unfinished_multipart_upload_bytes: i64,
|
||||||
quotas: ApiBucketQuotas,
|
quotas: ApiBucketQuotas,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -183,8 +183,8 @@ async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Bod
|
||||||
create_bucket: *key_state.allow_create_bucket.get(),
|
create_bucket: *key_state.allow_create_bucket.get(),
|
||||||
},
|
},
|
||||||
buckets: relevant_buckets
|
buckets: relevant_buckets
|
||||||
.into_iter()
|
.into_values()
|
||||||
.map(|(_, bucket)| {
|
.map(|bucket| {
|
||||||
let state = bucket.state.as_option().unwrap();
|
let state = bucket.state.as_option().unwrap();
|
||||||
KeyInfoBucketResult {
|
KeyInfoBucketResult {
|
||||||
id: hex::encode(bucket.id),
|
id: hex::encode(bucket.id),
|
||||||
|
|
|
@ -27,6 +27,7 @@ use crate::s3::cors::*;
|
||||||
use crate::s3::delete::*;
|
use crate::s3::delete::*;
|
||||||
use crate::s3::get::*;
|
use crate::s3::get::*;
|
||||||
use crate::s3::list::*;
|
use crate::s3::list::*;
|
||||||
|
use crate::s3::multipart::*;
|
||||||
use crate::s3::post_object::handle_post_object;
|
use crate::s3::post_object::handle_post_object;
|
||||||
use crate::s3::put::*;
|
use crate::s3::put::*;
|
||||||
use crate::s3::router::Endpoint;
|
use crate::s3::router::Endpoint;
|
||||||
|
@ -256,7 +257,7 @@ impl ApiHandler for S3ApiServer {
|
||||||
bucket_name,
|
bucket_name,
|
||||||
bucket_id,
|
bucket_id,
|
||||||
delimiter: delimiter.map(|d| d.to_string()),
|
delimiter: delimiter.map(|d| d.to_string()),
|
||||||
page_size: max_keys.map(|p| p.clamp(1, 1000)).unwrap_or(1000),
|
page_size: max_keys.unwrap_or(1000).clamp(1, 1000),
|
||||||
prefix: prefix.unwrap_or_default(),
|
prefix: prefix.unwrap_or_default(),
|
||||||
urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false),
|
urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false),
|
||||||
},
|
},
|
||||||
|
@ -286,7 +287,7 @@ impl ApiHandler for S3ApiServer {
|
||||||
bucket_name,
|
bucket_name,
|
||||||
bucket_id,
|
bucket_id,
|
||||||
delimiter: delimiter.map(|d| d.to_string()),
|
delimiter: delimiter.map(|d| d.to_string()),
|
||||||
page_size: max_keys.map(|p| p.clamp(1, 1000)).unwrap_or(1000),
|
page_size: max_keys.unwrap_or(1000).clamp(1, 1000),
|
||||||
urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false),
|
urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false),
|
||||||
prefix: prefix.unwrap_or_default(),
|
prefix: prefix.unwrap_or_default(),
|
||||||
},
|
},
|
||||||
|
@ -319,7 +320,7 @@ impl ApiHandler for S3ApiServer {
|
||||||
bucket_name,
|
bucket_name,
|
||||||
bucket_id,
|
bucket_id,
|
||||||
delimiter: delimiter.map(|d| d.to_string()),
|
delimiter: delimiter.map(|d| d.to_string()),
|
||||||
page_size: max_uploads.map(|p| p.clamp(1, 1000)).unwrap_or(1000),
|
page_size: max_uploads.unwrap_or(1000).clamp(1, 1000),
|
||||||
prefix: prefix.unwrap_or_default(),
|
prefix: prefix.unwrap_or_default(),
|
||||||
urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false),
|
urlencode_resp: encoding_type.map(|e| e == "url").unwrap_or(false),
|
||||||
},
|
},
|
||||||
|
@ -343,7 +344,7 @@ impl ApiHandler for S3ApiServer {
|
||||||
key,
|
key,
|
||||||
upload_id,
|
upload_id,
|
||||||
part_number_marker: part_number_marker.map(|p| p.clamp(1, 10000)),
|
part_number_marker: part_number_marker.map(|p| p.clamp(1, 10000)),
|
||||||
max_parts: max_parts.map(|p| p.clamp(1, 1000)).unwrap_or(1000),
|
max_parts: max_parts.unwrap_or(1000).clamp(1, 1000),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use futures::{stream, stream::Stream, StreamExt, TryFutureExt};
|
use futures::{stream, stream::Stream, StreamExt};
|
||||||
use md5::{Digest as Md5Digest, Md5};
|
use md5::{Digest as Md5Digest, Md5};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
@ -18,12 +18,14 @@ use garage_util::time::*;
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_model::key_table::Key;
|
use garage_model::key_table::Key;
|
||||||
use garage_model::s3::block_ref_table::*;
|
use garage_model::s3::block_ref_table::*;
|
||||||
|
use garage_model::s3::mpu_table::*;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::*;
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
use crate::helpers::parse_bucket_key;
|
use crate::helpers::parse_bucket_key;
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::put::{decode_upload_id, get_headers};
|
use crate::s3::multipart;
|
||||||
|
use crate::s3::put::get_headers;
|
||||||
use crate::s3::xml::{self as s3_xml, xmlns_tag};
|
use crate::s3::xml::{self as s3_xml, xmlns_tag};
|
||||||
|
|
||||||
pub async fn handle_copy(
|
pub async fn handle_copy(
|
||||||
|
@ -92,7 +94,10 @@ pub async fn handle_copy(
|
||||||
let tmp_dest_object_version = ObjectVersion {
|
let tmp_dest_object_version = ObjectVersion {
|
||||||
uuid: new_uuid,
|
uuid: new_uuid,
|
||||||
timestamp: new_timestamp,
|
timestamp: new_timestamp,
|
||||||
state: ObjectVersionState::Uploading(new_meta.headers.clone()),
|
state: ObjectVersionState::Uploading {
|
||||||
|
headers: new_meta.headers.clone(),
|
||||||
|
multipart: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let tmp_dest_object = Object::new(
|
let tmp_dest_object = Object::new(
|
||||||
dest_bucket_id,
|
dest_bucket_id,
|
||||||
|
@ -105,8 +110,14 @@ pub async fn handle_copy(
|
||||||
// this means that the BlockRef entries linked to this version cannot be
|
// this means that the BlockRef entries linked to this version cannot be
|
||||||
// marked as deleted (they are marked as deleted only if the Version
|
// marked as deleted (they are marked as deleted only if the Version
|
||||||
// doesn't exist or is marked as deleted).
|
// doesn't exist or is marked as deleted).
|
||||||
let mut dest_version =
|
let mut dest_version = Version::new(
|
||||||
Version::new(new_uuid, dest_bucket_id, dest_key.to_string(), false);
|
new_uuid,
|
||||||
|
VersionBacklink::Object {
|
||||||
|
bucket_id: dest_bucket_id,
|
||||||
|
key: dest_key.to_string(),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
garage.version_table.insert(&dest_version).await?;
|
garage.version_table.insert(&dest_version).await?;
|
||||||
|
|
||||||
// Fill in block list for version and insert block refs
|
// Fill in block list for version and insert block refs
|
||||||
|
@ -179,17 +190,13 @@ pub async fn handle_upload_part_copy(
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<Body>, Error> {
|
||||||
let copy_precondition = CopyPreconditionHeaders::parse(req)?;
|
let copy_precondition = CopyPreconditionHeaders::parse(req)?;
|
||||||
|
|
||||||
let dest_version_uuid = decode_upload_id(upload_id)?;
|
let dest_upload_id = multipart::decode_upload_id(upload_id)?;
|
||||||
|
|
||||||
let dest_key = dest_key.to_string();
|
let dest_key = dest_key.to_string();
|
||||||
let (source_object, dest_object) = futures::try_join!(
|
let (source_object, (_, _, mut dest_mpu)) = futures::try_join!(
|
||||||
get_copy_source(&garage, api_key, req),
|
get_copy_source(&garage, api_key, req),
|
||||||
garage
|
multipart::get_upload(&garage, &dest_bucket_id, &dest_key, &dest_upload_id)
|
||||||
.object_table
|
|
||||||
.get(&dest_bucket_id, &dest_key)
|
|
||||||
.map_err(Error::from),
|
|
||||||
)?;
|
)?;
|
||||||
let dest_object = dest_object.ok_or(Error::NoSuchKey)?;
|
|
||||||
|
|
||||||
let (source_object_version, source_version_data, source_version_meta) =
|
let (source_object_version, source_version_data, source_version_meta) =
|
||||||
extract_source_info(&source_object)?;
|
extract_source_info(&source_object)?;
|
||||||
|
@ -217,15 +224,6 @@ pub async fn handle_upload_part_copy(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check destination version is indeed in uploading state
|
|
||||||
if !dest_object
|
|
||||||
.versions()
|
|
||||||
.iter()
|
|
||||||
.any(|v| v.uuid == dest_version_uuid && v.is_uploading())
|
|
||||||
{
|
|
||||||
return Err(Error::NoSuchUpload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check source version is not inlined
|
// Check source version is not inlined
|
||||||
match source_version_data {
|
match source_version_data {
|
||||||
ObjectVersionData::DeleteMarker => unreachable!(),
|
ObjectVersionData::DeleteMarker => unreachable!(),
|
||||||
|
@ -242,23 +240,11 @@ pub async fn handle_upload_part_copy(
|
||||||
|
|
||||||
// Fetch source versin with its block list,
|
// Fetch source versin with its block list,
|
||||||
// and destination version to check part hasn't yet been uploaded
|
// and destination version to check part hasn't yet been uploaded
|
||||||
let (source_version, dest_version) = futures::try_join!(
|
let source_version = garage
|
||||||
garage
|
.version_table
|
||||||
.version_table
|
.get(&source_object_version.uuid, &EmptyKey)
|
||||||
.get(&source_object_version.uuid, &EmptyKey),
|
.await?
|
||||||
garage.version_table.get(&dest_version_uuid, &EmptyKey),
|
.ok_or(Error::NoSuchKey)?;
|
||||||
)?;
|
|
||||||
let source_version = source_version.ok_or(Error::NoSuchKey)?;
|
|
||||||
|
|
||||||
// Check this part number hasn't yet been uploaded
|
|
||||||
if let Some(dv) = dest_version {
|
|
||||||
if dv.has_part_number(part_number) {
|
|
||||||
return Err(Error::bad_request(format!(
|
|
||||||
"Part number {} has already been uploaded",
|
|
||||||
part_number
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to reuse blocks from the source version as much as possible.
|
// We want to reuse blocks from the source version as much as possible.
|
||||||
// However, we still need to get the data from these blocks
|
// However, we still need to get the data from these blocks
|
||||||
|
@ -299,6 +285,33 @@ pub async fn handle_upload_part_copy(
|
||||||
current_offset = block_end;
|
current_offset = block_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate the identity of destination part: timestamp, version id
|
||||||
|
let dest_version_id = gen_uuid();
|
||||||
|
let dest_mpu_part_key = MpuPartKey {
|
||||||
|
part_number,
|
||||||
|
timestamp: dest_mpu.next_timestamp(part_number),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the uploaded part
|
||||||
|
dest_mpu.parts.clear();
|
||||||
|
dest_mpu.parts.put(
|
||||||
|
dest_mpu_part_key,
|
||||||
|
MpuPart {
|
||||||
|
version: dest_version_id,
|
||||||
|
etag: None,
|
||||||
|
size: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
garage.mpu_table.insert(&dest_mpu).await?;
|
||||||
|
|
||||||
|
let mut dest_version = Version::new(
|
||||||
|
dest_version_id,
|
||||||
|
VersionBacklink::MultipartUpload {
|
||||||
|
upload_id: dest_upload_id,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
// Now, actually copy the blocks
|
// Now, actually copy the blocks
|
||||||
let mut md5hasher = Md5::new();
|
let mut md5hasher = Md5::new();
|
||||||
|
|
||||||
|
@ -348,8 +361,8 @@ pub async fn handle_upload_part_copy(
|
||||||
let must_upload = existing_block_hash.is_none();
|
let must_upload = existing_block_hash.is_none();
|
||||||
let final_hash = existing_block_hash.unwrap_or_else(|| blake2sum(&data[..]));
|
let final_hash = existing_block_hash.unwrap_or_else(|| blake2sum(&data[..]));
|
||||||
|
|
||||||
let mut version = Version::new(dest_version_uuid, dest_bucket_id, dest_key.clone(), false);
|
dest_version.blocks.clear();
|
||||||
version.blocks.put(
|
dest_version.blocks.put(
|
||||||
VersionBlockKey {
|
VersionBlockKey {
|
||||||
part_number,
|
part_number,
|
||||||
offset: current_offset,
|
offset: current_offset,
|
||||||
|
@ -363,7 +376,7 @@ pub async fn handle_upload_part_copy(
|
||||||
|
|
||||||
let block_ref = BlockRef {
|
let block_ref = BlockRef {
|
||||||
block: final_hash,
|
block: final_hash,
|
||||||
version: dest_version_uuid,
|
version: dest_version_id,
|
||||||
deleted: false.into(),
|
deleted: false.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -378,23 +391,33 @@ pub async fn handle_upload_part_copy(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Thing 2: we need to insert the block in the version
|
async {
|
||||||
garage.version_table.insert(&version),
|
// Thing 2: we need to insert the block in the version
|
||||||
// Thing 3: we need to add a block reference
|
garage.version_table.insert(&dest_version).await?;
|
||||||
garage.block_ref_table.insert(&block_ref),
|
// Thing 3: we need to add a block reference
|
||||||
|
garage.block_ref_table.insert(&block_ref).await
|
||||||
|
},
|
||||||
// Thing 4: we need to prefetch the next block
|
// Thing 4: we need to prefetch the next block
|
||||||
defragmenter.next(),
|
defragmenter.next(),
|
||||||
)?;
|
)?;
|
||||||
next_block = res.3;
|
next_block = res.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_eq!(current_offset, source_range.length);
|
||||||
|
|
||||||
let data_md5sum = md5hasher.finalize();
|
let data_md5sum = md5hasher.finalize();
|
||||||
let etag = hex::encode(data_md5sum);
|
let etag = hex::encode(data_md5sum);
|
||||||
|
|
||||||
// Put the part's ETag in the Versiontable
|
// Put the part's ETag in the Versiontable
|
||||||
let mut version = Version::new(dest_version_uuid, dest_bucket_id, dest_key.clone(), false);
|
dest_mpu.parts.put(
|
||||||
version.parts_etags.put(part_number, etag.clone());
|
dest_mpu_part_key,
|
||||||
garage.version_table.insert(&version).await?;
|
MpuPart {
|
||||||
|
version: dest_version_id,
|
||||||
|
etag: Some(etag.clone()),
|
||||||
|
size: Some(current_offset),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
garage.mpu_table.insert(&dest_mpu).await?;
|
||||||
|
|
||||||
// LGTM
|
// LGTM
|
||||||
let resp_xml = s3_xml::to_xml_with_header(&CopyPartResult {
|
let resp_xml = s3_xml::to_xml_with_header(&CopyPartResult {
|
||||||
|
|
|
@ -149,7 +149,6 @@ pub async fn handle_head(
|
||||||
|
|
||||||
let (part_offset, part_end) =
|
let (part_offset, part_end) =
|
||||||
calculate_part_bounds(&version, pn).ok_or(Error::InvalidPart)?;
|
calculate_part_bounds(&version, pn).ok_or(Error::InvalidPart)?;
|
||||||
let n_parts = version.parts_etags.items().len();
|
|
||||||
|
|
||||||
Ok(object_headers(object_version, version_meta)
|
Ok(object_headers(object_version, version_meta)
|
||||||
.header(CONTENT_LENGTH, format!("{}", part_end - part_offset))
|
.header(CONTENT_LENGTH, format!("{}", part_end - part_offset))
|
||||||
|
@ -162,7 +161,7 @@ pub async fn handle_head(
|
||||||
version_meta.size
|
version_meta.size
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.header(X_AMZ_MP_PARTS_COUNT, format!("{}", n_parts))
|
.header(X_AMZ_MP_PARTS_COUNT, format!("{}", version.n_parts()?))
|
||||||
.status(StatusCode::PARTIAL_CONTENT)
|
.status(StatusCode::PARTIAL_CONTENT)
|
||||||
.body(Body::empty())?)
|
.body(Body::empty())?)
|
||||||
}
|
}
|
||||||
|
@ -376,7 +375,6 @@ async fn handle_get_part(
|
||||||
|
|
||||||
let (begin, end) =
|
let (begin, end) =
|
||||||
calculate_part_bounds(&version, part_number).ok_or(Error::InvalidPart)?;
|
calculate_part_bounds(&version, part_number).ok_or(Error::InvalidPart)?;
|
||||||
let n_parts = version.parts_etags.items().len();
|
|
||||||
|
|
||||||
let body = body_from_blocks_range(garage, version.blocks.items(), begin, end);
|
let body = body_from_blocks_range(garage, version.blocks.items(), begin, end);
|
||||||
|
|
||||||
|
@ -386,7 +384,7 @@ async fn handle_get_part(
|
||||||
CONTENT_RANGE,
|
CONTENT_RANGE,
|
||||||
format!("bytes {}-{}/{}", begin, end - 1, version_meta.size),
|
format!("bytes {}-{}/{}", begin, end - 1, version_meta.size),
|
||||||
)
|
)
|
||||||
.header(X_AMZ_MP_PARTS_COUNT, format!("{}", n_parts))
|
.header(X_AMZ_MP_PARTS_COUNT, format!("{}", version.n_parts()?))
|
||||||
.body(body)?)
|
.body(body)?)
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
@ -443,7 +441,7 @@ fn body_from_blocks_range(
|
||||||
// block.part_number, which is not the same in the case of a multipart upload)
|
// block.part_number, which is not the same in the case of a multipart upload)
|
||||||
let mut blocks: Vec<(VersionBlock, u64)> = Vec::with_capacity(std::cmp::min(
|
let mut blocks: Vec<(VersionBlock, u64)> = Vec::with_capacity(std::cmp::min(
|
||||||
all_blocks.len(),
|
all_blocks.len(),
|
||||||
4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size as u64, 1024)) as usize,
|
4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size, 1024)) as usize,
|
||||||
));
|
));
|
||||||
let mut block_offset: u64 = 0;
|
let mut block_offset: u64 = 0;
|
||||||
for (_, b) in all_blocks.iter() {
|
for (_, b) in all_blocks.iter() {
|
||||||
|
@ -454,7 +452,7 @@ fn body_from_blocks_range(
|
||||||
if block_offset < end && block_offset + b.size > begin {
|
if block_offset < end && block_offset + b.size > begin {
|
||||||
blocks.push((*b, block_offset));
|
blocks.push((*b, block_offset));
|
||||||
}
|
}
|
||||||
block_offset += b.size as u64;
|
block_offset += b.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
let order_stream = OrderTag::stream();
|
let order_stream = OrderTag::stream();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use std::cmp::Ordering;
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::iter::{Iterator, Peekable};
|
use std::iter::{Iterator, Peekable};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -11,15 +10,15 @@ use garage_util::error::Error as GarageError;
|
||||||
use garage_util::time::*;
|
use garage_util::time::*;
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
|
use garage_model::s3::mpu_table::*;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::Version;
|
|
||||||
|
|
||||||
use garage_table::{EmptyKey, EnumerationOrder};
|
use garage_table::EnumerationOrder;
|
||||||
|
|
||||||
use crate::encoding::*;
|
use crate::encoding::*;
|
||||||
use crate::helpers::key_after_prefix;
|
use crate::helpers::key_after_prefix;
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::put as s3_put;
|
use crate::s3::multipart as s3_multipart;
|
||||||
use crate::s3::xml as s3_xml;
|
use crate::s3::xml as s3_xml;
|
||||||
|
|
||||||
const DUMMY_NAME: &str = "Dummy Key";
|
const DUMMY_NAME: &str = "Dummy Key";
|
||||||
|
@ -176,7 +175,9 @@ pub async fn handle_list_multipart_upload(
|
||||||
t.get_range(
|
t.get_range(
|
||||||
&bucket,
|
&bucket,
|
||||||
key,
|
key,
|
||||||
Some(ObjectFilter::IsUploading),
|
Some(ObjectFilter::IsUploading {
|
||||||
|
check_multipart: Some(true),
|
||||||
|
}),
|
||||||
count,
|
count,
|
||||||
EnumerationOrder::Forward,
|
EnumerationOrder::Forward,
|
||||||
)
|
)
|
||||||
|
@ -272,24 +273,26 @@ pub async fn handle_list_parts(
|
||||||
) -> Result<Response<Body>, Error> {
|
) -> Result<Response<Body>, Error> {
|
||||||
debug!("ListParts {:?}", query);
|
debug!("ListParts {:?}", query);
|
||||||
|
|
||||||
let upload_id = s3_put::decode_upload_id(&query.upload_id)?;
|
let upload_id = s3_multipart::decode_upload_id(&query.upload_id)?;
|
||||||
|
|
||||||
let (object, version) = futures::try_join!(
|
let (_, _, mpu) =
|
||||||
garage.object_table.get(&query.bucket_id, &query.key),
|
s3_multipart::get_upload(&garage, &query.bucket_id, &query.key, &upload_id).await?;
|
||||||
garage.version_table.get(&upload_id, &EmptyKey),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let (info, next) = fetch_part_info(query, object, version, upload_id)?;
|
let (info, next) = fetch_part_info(query, &mpu)?;
|
||||||
|
|
||||||
let result = s3_xml::ListPartsResult {
|
let result = s3_xml::ListPartsResult {
|
||||||
xmlns: (),
|
xmlns: (),
|
||||||
|
|
||||||
|
// Query parameters
|
||||||
bucket: s3_xml::Value(query.bucket_name.to_string()),
|
bucket: s3_xml::Value(query.bucket_name.to_string()),
|
||||||
key: s3_xml::Value(query.key.to_string()),
|
key: s3_xml::Value(query.key.to_string()),
|
||||||
upload_id: s3_xml::Value(query.upload_id.to_string()),
|
upload_id: s3_xml::Value(query.upload_id.to_string()),
|
||||||
part_number_marker: query.part_number_marker.map(|e| s3_xml::IntValue(e as i64)),
|
part_number_marker: query.part_number_marker.map(|e| s3_xml::IntValue(e as i64)),
|
||||||
next_part_number_marker: next.map(|e| s3_xml::IntValue(e as i64)),
|
|
||||||
max_parts: s3_xml::IntValue(query.max_parts as i64),
|
max_parts: s3_xml::IntValue(query.max_parts as i64),
|
||||||
is_truncated: s3_xml::Value(next.map(|_| "true").unwrap_or("false").to_string()),
|
|
||||||
|
// Result values
|
||||||
|
next_part_number_marker: next.map(|e| s3_xml::IntValue(e as i64)),
|
||||||
|
is_truncated: s3_xml::Value(format!("{}", next.is_some())),
|
||||||
parts: info
|
parts: info
|
||||||
.iter()
|
.iter()
|
||||||
.map(|part| s3_xml::PartItem {
|
.map(|part| s3_xml::PartItem {
|
||||||
|
@ -299,6 +302,8 @@ pub async fn handle_list_parts(
|
||||||
size: s3_xml::IntValue(part.size as i64),
|
size: s3_xml::IntValue(part.size as i64),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
||||||
|
// Dummy result values (unsupported features)
|
||||||
initiator: s3_xml::Initiator {
|
initiator: s3_xml::Initiator {
|
||||||
display_name: s3_xml::Value(DUMMY_NAME.to_string()),
|
display_name: s3_xml::Value(DUMMY_NAME.to_string()),
|
||||||
id: s3_xml::Value(DUMMY_KEY.to_string()),
|
id: s3_xml::Value(DUMMY_KEY.to_string()),
|
||||||
|
@ -335,8 +340,8 @@ struct UploadInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
struct PartInfo {
|
struct PartInfo<'a> {
|
||||||
etag: String,
|
etag: &'a str,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
part_number: u64,
|
part_number: u64,
|
||||||
size: u64,
|
size: u64,
|
||||||
|
@ -456,107 +461,51 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_part_info(
|
fn fetch_part_info<'a>(
|
||||||
query: &ListPartsQuery,
|
query: &ListPartsQuery,
|
||||||
object: Option<Object>,
|
mpu: &'a MultipartUpload,
|
||||||
version: Option<Version>,
|
) -> Result<(Vec<PartInfo<'a>>, Option<u64>), Error> {
|
||||||
upload_id: Uuid,
|
assert!((1..=1000).contains(&query.max_parts)); // see s3/api_server.rs
|
||||||
) -> Result<(Vec<PartInfo>, Option<u64>), Error> {
|
|
||||||
// Check results
|
|
||||||
let object = object.ok_or(Error::NoSuchKey)?;
|
|
||||||
|
|
||||||
let obj_version = object
|
// Parse multipart upload part list, removing parts not yet finished
|
||||||
.versions()
|
// and failed part uploads that were overwritten
|
||||||
.iter()
|
let mut parts: Vec<PartInfo<'a>> = Vec::with_capacity(mpu.parts.items().len());
|
||||||
.find(|v| v.uuid == upload_id && v.is_uploading())
|
for (pk, p) in mpu.parts.items().iter() {
|
||||||
.ok_or(Error::NoSuchUpload)?;
|
if let (Some(etag), Some(size)) = (&p.etag, p.size) {
|
||||||
|
let part_info = PartInfo {
|
||||||
let version = version.ok_or(Error::NoSuchKey)?;
|
part_number: pk.part_number,
|
||||||
|
timestamp: pk.timestamp,
|
||||||
// Cut the beginning of our 2 vectors if required
|
etag,
|
||||||
let (etags, blocks) = match &query.part_number_marker {
|
size,
|
||||||
Some(marker) => {
|
};
|
||||||
let next = marker + 1;
|
match parts.last_mut() {
|
||||||
|
Some(lastpart) if lastpart.part_number == pk.part_number => {
|
||||||
let part_idx = into_ok_or_err(
|
*lastpart = part_info;
|
||||||
version
|
|
||||||
.parts_etags
|
|
||||||
.items()
|
|
||||||
.binary_search_by(|(part_num, _)| part_num.cmp(&next)),
|
|
||||||
);
|
|
||||||
let parts = &version.parts_etags.items()[part_idx..];
|
|
||||||
|
|
||||||
let block_idx = into_ok_or_err(
|
|
||||||
version
|
|
||||||
.blocks
|
|
||||||
.items()
|
|
||||||
.binary_search_by(|(vkey, _)| vkey.part_number.cmp(&next)),
|
|
||||||
);
|
|
||||||
let blocks = &version.blocks.items()[block_idx..];
|
|
||||||
|
|
||||||
(parts, blocks)
|
|
||||||
}
|
|
||||||
None => (version.parts_etags.items(), version.blocks.items()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the block vector to compute a (part_number, size) vector
|
|
||||||
let mut size = Vec::<(u64, u64)>::new();
|
|
||||||
blocks.iter().for_each(|(key, val)| {
|
|
||||||
let mut new_size = val.size;
|
|
||||||
match size.pop() {
|
|
||||||
Some((part_number, size)) if part_number == key.part_number => new_size += size,
|
|
||||||
Some(v) => size.push(v),
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
size.push((key.part_number, new_size))
|
|
||||||
});
|
|
||||||
|
|
||||||
// Merge the etag vector and size vector to build a PartInfo vector
|
|
||||||
let max_parts = query.max_parts as usize;
|
|
||||||
let (mut etag_iter, mut size_iter) = (etags.iter().peekable(), size.iter().peekable());
|
|
||||||
|
|
||||||
let mut info = Vec::<PartInfo>::with_capacity(max_parts);
|
|
||||||
|
|
||||||
while info.len() < max_parts {
|
|
||||||
match (etag_iter.peek(), size_iter.peek()) {
|
|
||||||
(Some((ep, etag)), Some((sp, size))) => match ep.cmp(sp) {
|
|
||||||
Ordering::Less => {
|
|
||||||
debug!("ETag information ignored due to missing corresponding block information. Query: {:?}", query);
|
|
||||||
etag_iter.next();
|
|
||||||
}
|
}
|
||||||
Ordering::Equal => {
|
_ => {
|
||||||
info.push(PartInfo {
|
parts.push(part_info);
|
||||||
etag: etag.to_string(),
|
|
||||||
timestamp: obj_version.timestamp,
|
|
||||||
part_number: *ep,
|
|
||||||
size: *size,
|
|
||||||
});
|
|
||||||
etag_iter.next();
|
|
||||||
size_iter.next();
|
|
||||||
}
|
}
|
||||||
Ordering::Greater => {
|
|
||||||
debug!("Block information ignored due to missing corresponding ETag information. Query: {:?}", query);
|
|
||||||
size_iter.next();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(None, None) => return Ok((info, None)),
|
|
||||||
_ => {
|
|
||||||
debug!(
|
|
||||||
"Additional block or ETag information ignored. Query: {:?}",
|
|
||||||
query
|
|
||||||
);
|
|
||||||
return Ok((info, None));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match info.last() {
|
// Cut the beginning if we have a marker
|
||||||
Some(part_info) => {
|
if let Some(marker) = &query.part_number_marker {
|
||||||
let pagination = Some(part_info.part_number);
|
let next = marker + 1;
|
||||||
Ok((info, pagination))
|
let part_idx = parts
|
||||||
}
|
.binary_search_by(|part| part.part_number.cmp(&next))
|
||||||
None => Ok((info, None)),
|
.unwrap_or_else(|x| x);
|
||||||
|
parts = parts.split_off(part_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cut the end if we have too many parts
|
||||||
|
if parts.len() > query.max_parts as usize {
|
||||||
|
parts.truncate(query.max_parts as usize);
|
||||||
|
let pagination = Some(parts.last().unwrap().part_number);
|
||||||
|
return Ok((parts, pagination));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((parts, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -651,7 +600,7 @@ impl ListMultipartUploadsQuery {
|
||||||
}),
|
}),
|
||||||
uuid => Ok(RangeBegin::AfterUpload {
|
uuid => Ok(RangeBegin::AfterUpload {
|
||||||
key: key_marker.to_string(),
|
key: key_marker.to_string(),
|
||||||
upload: s3_put::decode_upload_id(uuid)?,
|
upload: s3_multipart::decode_upload_id(uuid)?,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -843,7 +792,7 @@ impl ExtractAccumulator for UploadAccumulator {
|
||||||
let mut uploads_for_key = object
|
let mut uploads_for_key = object
|
||||||
.versions()
|
.versions()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| x.is_uploading())
|
.filter(|x| x.is_uploading(Some(true)))
|
||||||
.collect::<Vec<&ObjectVersion>>();
|
.collect::<Vec<&ObjectVersion>>();
|
||||||
|
|
||||||
// S3 logic requires lexicographically sorted upload ids.
|
// S3 logic requires lexicographically sorted upload ids.
|
||||||
|
@ -918,14 +867,6 @@ impl ExtractAccumulator for UploadAccumulator {
|
||||||
* Utility functions
|
* Utility functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/// This is a stub for Result::into_ok_or_err that is not yet in Rust stable
|
|
||||||
fn into_ok_or_err<T>(r: Result<T, T>) -> T {
|
|
||||||
match r {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(r) => r,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the common prefix of the object given the query prefix and delimiter
|
/// Returns the common prefix of the object given the query prefix and delimiter
|
||||||
fn common_prefix<'a>(object: &'a Object, query: &ListQueryCommon) -> Option<&'a str> {
|
fn common_prefix<'a>(object: &'a Object, query: &ListQueryCommon) -> Option<&'a str> {
|
||||||
match &query.delimiter {
|
match &query.delimiter {
|
||||||
|
@ -951,7 +892,6 @@ fn uriencode_maybe(s: &str, yes: bool) -> s3_xml::Value {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use garage_model::s3::version_table::*;
|
|
||||||
use garage_util::*;
|
use garage_util::*;
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
@ -991,10 +931,13 @@ mod tests {
|
||||||
ObjectVersion {
|
ObjectVersion {
|
||||||
uuid: Uuid::from(uuid),
|
uuid: Uuid::from(uuid),
|
||||||
timestamp: TS,
|
timestamp: TS,
|
||||||
state: ObjectVersionState::Uploading(ObjectVersionHeaders {
|
state: ObjectVersionState::Uploading {
|
||||||
content_type: "text/plain".to_string(),
|
multipart: true,
|
||||||
other: BTreeMap::<String, String>::new(),
|
headers: ObjectVersionHeaders {
|
||||||
}),
|
content_type: "text/plain".to_string(),
|
||||||
|
other: BTreeMap::<String, String>::new(),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1169,83 +1112,76 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn version() -> Version {
|
fn mpu() -> MultipartUpload {
|
||||||
let uuid = Uuid::from([0x08; 32]);
|
let uuid = Uuid::from([0x08; 32]);
|
||||||
|
|
||||||
let blocks = vec![
|
let parts = vec![
|
||||||
(
|
(
|
||||||
VersionBlockKey {
|
MpuPartKey {
|
||||||
part_number: 1,
|
part_number: 1,
|
||||||
offset: 1,
|
timestamp: TS,
|
||||||
},
|
},
|
||||||
VersionBlock {
|
MpuPart {
|
||||||
hash: uuid,
|
version: uuid,
|
||||||
size: 3,
|
size: Some(3),
|
||||||
|
etag: Some("etag1".into()),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
VersionBlockKey {
|
MpuPartKey {
|
||||||
part_number: 1,
|
|
||||||
offset: 2,
|
|
||||||
},
|
|
||||||
VersionBlock {
|
|
||||||
hash: uuid,
|
|
||||||
size: 2,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
VersionBlockKey {
|
|
||||||
part_number: 2,
|
part_number: 2,
|
||||||
offset: 1,
|
timestamp: TS,
|
||||||
},
|
},
|
||||||
VersionBlock {
|
MpuPart {
|
||||||
hash: uuid,
|
version: uuid,
|
||||||
size: 8,
|
size: None,
|
||||||
|
etag: None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
VersionBlockKey {
|
MpuPartKey {
|
||||||
|
part_number: 3,
|
||||||
|
timestamp: TS,
|
||||||
|
},
|
||||||
|
MpuPart {
|
||||||
|
version: uuid,
|
||||||
|
size: Some(10),
|
||||||
|
etag: Some("etag2".into()),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
MpuPartKey {
|
||||||
part_number: 5,
|
part_number: 5,
|
||||||
offset: 1,
|
timestamp: TS,
|
||||||
},
|
},
|
||||||
VersionBlock {
|
MpuPart {
|
||||||
hash: uuid,
|
version: uuid,
|
||||||
size: 7,
|
size: Some(7),
|
||||||
|
etag: Some("etag3".into()),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
VersionBlockKey {
|
MpuPartKey {
|
||||||
part_number: 8,
|
part_number: 8,
|
||||||
offset: 1,
|
timestamp: TS,
|
||||||
},
|
},
|
||||||
VersionBlock {
|
MpuPart {
|
||||||
hash: uuid,
|
version: uuid,
|
||||||
size: 5,
|
size: Some(5),
|
||||||
|
etag: Some("etag4".into()),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
let etags = vec![
|
|
||||||
(1, "etag1".to_string()),
|
|
||||||
(3, "etag2".to_string()),
|
|
||||||
(5, "etag3".to_string()),
|
|
||||||
(8, "etag4".to_string()),
|
|
||||||
(9, "etag5".to_string()),
|
|
||||||
];
|
|
||||||
|
|
||||||
Version {
|
MultipartUpload {
|
||||||
bucket_id: uuid,
|
upload_id: uuid,
|
||||||
key: "a".to_string(),
|
|
||||||
uuid,
|
|
||||||
deleted: false.into(),
|
deleted: false.into(),
|
||||||
blocks: crdt::Map::<VersionBlockKey, VersionBlock>::from_iter(blocks),
|
parts: crdt::Map::<MpuPartKey, MpuPart>::from_iter(parts),
|
||||||
parts_etags: crdt::Map::<u64, String>::from_iter(etags),
|
bucket_id: uuid,
|
||||||
|
key: "a".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn obj() -> Object {
|
|
||||||
Object::new(bucket(), "d".to_string(), vec![objup_version([0x08; 32])])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fetch_part_info() -> Result<(), Error> {
|
fn test_fetch_part_info() -> Result<(), Error> {
|
||||||
let uuid = Uuid::from([0x08; 32]);
|
let uuid = Uuid::from([0x08; 32]);
|
||||||
|
@ -1258,82 +1194,85 @@ mod tests {
|
||||||
max_parts: 2,
|
max_parts: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(
|
let mpu = mpu();
|
||||||
fetch_part_info(&query, None, None, uuid).is_err(),
|
|
||||||
"No object and version should fail"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
fetch_part_info(&query, Some(obj()), None, uuid).is_err(),
|
|
||||||
"No version should faild"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
fetch_part_info(&query, None, Some(version()), uuid).is_err(),
|
|
||||||
"No object should fail"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start from the beginning but with limited size to trigger pagination
|
// Start from the beginning but with limited size to trigger pagination
|
||||||
let (info, pagination) = fetch_part_info(&query, Some(obj()), Some(version()), uuid)?;
|
let (info, pagination) = fetch_part_info(&query, &mpu)?;
|
||||||
assert_eq!(pagination.unwrap(), 5);
|
assert_eq!(pagination.unwrap(), 3);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
info,
|
info,
|
||||||
vec![
|
vec![
|
||||||
PartInfo {
|
PartInfo {
|
||||||
etag: "etag1".to_string(),
|
etag: "etag1",
|
||||||
timestamp: TS,
|
timestamp: TS,
|
||||||
part_number: 1,
|
part_number: 1,
|
||||||
size: 5
|
size: 3
|
||||||
},
|
},
|
||||||
PartInfo {
|
PartInfo {
|
||||||
etag: "etag3".to_string(),
|
etag: "etag2",
|
||||||
timestamp: TS,
|
timestamp: TS,
|
||||||
part_number: 5,
|
part_number: 3,
|
||||||
size: 7
|
size: 10
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use previous pagination to make a new request
|
// Use previous pagination to make a new request
|
||||||
query.part_number_marker = Some(pagination.unwrap());
|
query.part_number_marker = Some(pagination.unwrap());
|
||||||
let (info, pagination) = fetch_part_info(&query, Some(obj()), Some(version()), uuid)?;
|
let (info, pagination) = fetch_part_info(&query, &mpu)?;
|
||||||
assert!(pagination.is_none());
|
assert!(pagination.is_none());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
info,
|
info,
|
||||||
vec![PartInfo {
|
vec![
|
||||||
etag: "etag4".to_string(),
|
PartInfo {
|
||||||
timestamp: TS,
|
etag: "etag3",
|
||||||
part_number: 8,
|
timestamp: TS,
|
||||||
size: 5
|
part_number: 5,
|
||||||
},]
|
size: 7
|
||||||
|
},
|
||||||
|
PartInfo {
|
||||||
|
etag: "etag4",
|
||||||
|
timestamp: TS,
|
||||||
|
part_number: 8,
|
||||||
|
size: 5
|
||||||
|
},
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trying to access a part that is way larger than registered ones
|
// Trying to access a part that is way larger than registered ones
|
||||||
query.part_number_marker = Some(9999);
|
query.part_number_marker = Some(9999);
|
||||||
let (info, pagination) = fetch_part_info(&query, Some(obj()), Some(version()), uuid)?;
|
let (info, pagination) = fetch_part_info(&query, &mpu)?;
|
||||||
assert!(pagination.is_none());
|
assert!(pagination.is_none());
|
||||||
assert_eq!(info, vec![]);
|
assert_eq!(info, vec![]);
|
||||||
|
|
||||||
// Try without any limitation
|
// Try without any limitation
|
||||||
query.max_parts = 1000;
|
query.max_parts = 1000;
|
||||||
query.part_number_marker = None;
|
query.part_number_marker = None;
|
||||||
let (info, pagination) = fetch_part_info(&query, Some(obj()), Some(version()), uuid)?;
|
let (info, pagination) = fetch_part_info(&query, &mpu)?;
|
||||||
assert!(pagination.is_none());
|
assert!(pagination.is_none());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
info,
|
info,
|
||||||
vec![
|
vec![
|
||||||
PartInfo {
|
PartInfo {
|
||||||
etag: "etag1".to_string(),
|
etag: "etag1",
|
||||||
timestamp: TS,
|
timestamp: TS,
|
||||||
part_number: 1,
|
part_number: 1,
|
||||||
size: 5
|
size: 3
|
||||||
},
|
},
|
||||||
PartInfo {
|
PartInfo {
|
||||||
etag: "etag3".to_string(),
|
etag: "etag2",
|
||||||
|
timestamp: TS,
|
||||||
|
part_number: 3,
|
||||||
|
size: 10
|
||||||
|
},
|
||||||
|
PartInfo {
|
||||||
|
etag: "etag3",
|
||||||
timestamp: TS,
|
timestamp: TS,
|
||||||
part_number: 5,
|
part_number: 5,
|
||||||
size: 7
|
size: 7
|
||||||
},
|
},
|
||||||
PartInfo {
|
PartInfo {
|
||||||
etag: "etag4".to_string(),
|
etag: "etag4",
|
||||||
timestamp: TS,
|
timestamp: TS,
|
||||||
part_number: 8,
|
part_number: 8,
|
||||||
size: 5
|
size: 5
|
||||||
|
|
|
@ -7,6 +7,7 @@ pub mod cors;
|
||||||
mod delete;
|
mod delete;
|
||||||
pub mod get;
|
pub mod get;
|
||||||
mod list;
|
mod list;
|
||||||
|
mod multipart;
|
||||||
mod post_object;
|
mod post_object;
|
||||||
mod put;
|
mod put;
|
||||||
mod website;
|
mod website;
|
||||||
|
|
464
src/api/s3/multipart.rs
Normal file
464
src/api/s3/multipart.rs
Normal file
|
@ -0,0 +1,464 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use futures::prelude::*;
|
||||||
|
use hyper::body::Body;
|
||||||
|
use hyper::{Request, Response};
|
||||||
|
use md5::{Digest as Md5Digest, Md5};
|
||||||
|
|
||||||
|
use garage_table::*;
|
||||||
|
use garage_util::async_hash::*;
|
||||||
|
use garage_util::data::*;
|
||||||
|
use garage_util::time::*;
|
||||||
|
|
||||||
|
use garage_model::bucket_table::Bucket;
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
use garage_model::s3::block_ref_table::*;
|
||||||
|
use garage_model::s3::mpu_table::*;
|
||||||
|
use garage_model::s3::object_table::*;
|
||||||
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
|
use crate::s3::error::*;
|
||||||
|
use crate::s3::put::*;
|
||||||
|
use crate::s3::xml as s3_xml;
|
||||||
|
use crate::signature::verify_signed_content;
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
|
pub async fn handle_create_multipart_upload(
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
req: &Request<Body>,
|
||||||
|
bucket_name: &str,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
key: &str,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let upload_id = gen_uuid();
|
||||||
|
let headers = get_headers(req.headers())?;
|
||||||
|
|
||||||
|
// Create object in object table
|
||||||
|
let object_version = ObjectVersion {
|
||||||
|
uuid: upload_id,
|
||||||
|
timestamp: now_msec(),
|
||||||
|
state: ObjectVersionState::Uploading {
|
||||||
|
multipart: true,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let object = Object::new(bucket_id, key.to_string(), vec![object_version]);
|
||||||
|
garage.object_table.insert(&object).await?;
|
||||||
|
|
||||||
|
// Create multipart upload in mpu table
|
||||||
|
// This multipart upload will hold references to uploaded parts
|
||||||
|
// (which are entries in the Version table)
|
||||||
|
let mpu = MultipartUpload::new(upload_id, bucket_id, key.into(), false);
|
||||||
|
garage.mpu_table.insert(&mpu).await?;
|
||||||
|
|
||||||
|
// Send success response
|
||||||
|
let result = s3_xml::InitiateMultipartUploadResult {
|
||||||
|
xmlns: (),
|
||||||
|
bucket: s3_xml::Value(bucket_name.to_string()),
|
||||||
|
key: s3_xml::Value(key.to_string()),
|
||||||
|
upload_id: s3_xml::Value(hex::encode(upload_id)),
|
||||||
|
};
|
||||||
|
let xml = s3_xml::to_xml_with_header(&result)?;
|
||||||
|
|
||||||
|
Ok(Response::new(Body::from(xml.into_bytes())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_put_part(
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
key: &str,
|
||||||
|
part_number: u64,
|
||||||
|
upload_id: &str,
|
||||||
|
content_sha256: Option<Hash>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let upload_id = decode_upload_id(upload_id)?;
|
||||||
|
|
||||||
|
let content_md5 = match req.headers().get("content-md5") {
|
||||||
|
Some(x) => Some(x.to_str()?.to_string()),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read first chuck, and at the same time try to get object to see if it exists
|
||||||
|
let key = key.to_string();
|
||||||
|
|
||||||
|
let body = req.into_body().map_err(Error::from);
|
||||||
|
let mut chunker = StreamChunker::new(body, garage.config.block_size);
|
||||||
|
|
||||||
|
let ((_, _, mut mpu), first_block) = futures::try_join!(
|
||||||
|
get_upload(&garage, &bucket_id, &key, &upload_id),
|
||||||
|
chunker.next(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Check object is valid and part can be accepted
|
||||||
|
let first_block = first_block.ok_or_bad_request("Empty body")?;
|
||||||
|
|
||||||
|
// Calculate part identity: timestamp, version id
|
||||||
|
let version_uuid = gen_uuid();
|
||||||
|
let mpu_part_key = MpuPartKey {
|
||||||
|
part_number,
|
||||||
|
timestamp: mpu.next_timestamp(part_number),
|
||||||
|
};
|
||||||
|
|
||||||
|
// The following consists in many steps that can each fail.
|
||||||
|
// Keep track that some cleanup will be needed if things fail
|
||||||
|
// before everything is finished (cleanup is done using the Drop trait).
|
||||||
|
let mut interrupted_cleanup = InterruptedCleanup(Some(InterruptedCleanupInner {
|
||||||
|
garage: garage.clone(),
|
||||||
|
upload_id,
|
||||||
|
version_uuid,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create version and link version from MPU
|
||||||
|
mpu.parts.clear();
|
||||||
|
mpu.parts.put(
|
||||||
|
mpu_part_key,
|
||||||
|
MpuPart {
|
||||||
|
version: version_uuid,
|
||||||
|
etag: None,
|
||||||
|
size: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
garage.mpu_table.insert(&mpu).await?;
|
||||||
|
|
||||||
|
let version = Version::new(
|
||||||
|
version_uuid,
|
||||||
|
VersionBacklink::MultipartUpload { upload_id },
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
garage.version_table.insert(&version).await?;
|
||||||
|
|
||||||
|
// Copy data to version
|
||||||
|
let first_block_hash = async_blake2sum(first_block.clone()).await;
|
||||||
|
|
||||||
|
let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks(
|
||||||
|
&garage,
|
||||||
|
&version,
|
||||||
|
part_number,
|
||||||
|
first_block,
|
||||||
|
first_block_hash,
|
||||||
|
&mut chunker,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Verify that checksums map
|
||||||
|
ensure_checksum_matches(
|
||||||
|
data_md5sum.as_slice(),
|
||||||
|
data_sha256sum,
|
||||||
|
content_md5.as_deref(),
|
||||||
|
content_sha256,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Store part etag in version
|
||||||
|
let data_md5sum_hex = hex::encode(data_md5sum);
|
||||||
|
mpu.parts.put(
|
||||||
|
mpu_part_key,
|
||||||
|
MpuPart {
|
||||||
|
version: version_uuid,
|
||||||
|
etag: Some(data_md5sum_hex.clone()),
|
||||||
|
size: Some(total_size),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
garage.mpu_table.insert(&mpu).await?;
|
||||||
|
|
||||||
|
// We were not interrupted, everything went fine.
|
||||||
|
// We won't have to clean up on drop.
|
||||||
|
interrupted_cleanup.cancel();
|
||||||
|
|
||||||
|
let response = Response::builder()
|
||||||
|
.header("ETag", format!("\"{}\"", data_md5sum_hex))
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InterruptedCleanup(Option<InterruptedCleanupInner>);
|
||||||
|
struct InterruptedCleanupInner {
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
upload_id: Uuid,
|
||||||
|
version_uuid: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterruptedCleanup {
|
||||||
|
fn cancel(&mut self) {
|
||||||
|
drop(self.0.take());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for InterruptedCleanup {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(info) = self.0.take() {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let version = Version::new(
|
||||||
|
info.version_uuid,
|
||||||
|
VersionBacklink::MultipartUpload {
|
||||||
|
upload_id: info.upload_id,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if let Err(e) = info.garage.version_table.insert(&version).await {
|
||||||
|
warn!("Cannot cleanup after aborted UploadPart: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_complete_multipart_upload(
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
req: Request<Body>,
|
||||||
|
bucket_name: &str,
|
||||||
|
bucket: &Bucket,
|
||||||
|
key: &str,
|
||||||
|
upload_id: &str,
|
||||||
|
content_sha256: Option<Hash>,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||||
|
|
||||||
|
if let Some(content_sha256) = content_sha256 {
|
||||||
|
verify_signed_content(content_sha256, &body[..])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
|
||||||
|
let body_list_of_parts = parse_complete_multipart_upload_body(&body_xml)
|
||||||
|
.ok_or_bad_request("Invalid CompleteMultipartUpload XML")?;
|
||||||
|
debug!(
|
||||||
|
"CompleteMultipartUpload list of parts: {:?}",
|
||||||
|
body_list_of_parts
|
||||||
|
);
|
||||||
|
|
||||||
|
let upload_id = decode_upload_id(upload_id)?;
|
||||||
|
|
||||||
|
// Get object and multipart upload
|
||||||
|
let key = key.to_string();
|
||||||
|
let (_, mut object_version, mpu) = get_upload(&garage, &bucket.id, &key, &upload_id).await?;
|
||||||
|
|
||||||
|
if mpu.parts.is_empty() {
|
||||||
|
return Err(Error::bad_request("No data was uploaded"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers = match object_version.state {
|
||||||
|
ObjectVersionState::Uploading { headers, .. } => headers,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check that part numbers are an increasing sequence.
|
||||||
|
// (it doesn't need to start at 1 nor to be a continuous sequence,
|
||||||
|
// see discussion in #192)
|
||||||
|
if body_list_of_parts.is_empty() {
|
||||||
|
return Err(Error::EntityTooSmall);
|
||||||
|
}
|
||||||
|
if !body_list_of_parts
|
||||||
|
.iter()
|
||||||
|
.zip(body_list_of_parts.iter().skip(1))
|
||||||
|
.all(|(p1, p2)| p1.part_number < p2.part_number)
|
||||||
|
{
|
||||||
|
return Err(Error::InvalidPartOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the list of parts they gave us corresponds to parts we have here
|
||||||
|
debug!("Parts stored in multipart upload: {:?}", mpu.parts.items());
|
||||||
|
let mut have_parts = HashMap::new();
|
||||||
|
for (pk, pv) in mpu.parts.items().iter() {
|
||||||
|
have_parts.insert(pk.part_number, pv);
|
||||||
|
}
|
||||||
|
let mut parts = vec![];
|
||||||
|
for req_part in body_list_of_parts.iter() {
|
||||||
|
match have_parts.get(&req_part.part_number) {
|
||||||
|
Some(part) if part.etag.as_ref() == Some(&req_part.etag) && part.size.is_some() => {
|
||||||
|
parts.push(*part)
|
||||||
|
}
|
||||||
|
_ => return Err(Error::InvalidPart),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let grg = &garage;
|
||||||
|
let parts_versions = futures::future::try_join_all(parts.iter().map(|p| async move {
|
||||||
|
grg.version_table
|
||||||
|
.get(&p.version, &EmptyKey)
|
||||||
|
.await?
|
||||||
|
.ok_or_internal_error("Part version missing from version table")
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create final version and block refs
|
||||||
|
let mut final_version = Version::new(
|
||||||
|
upload_id,
|
||||||
|
VersionBacklink::Object {
|
||||||
|
bucket_id: bucket.id,
|
||||||
|
key: key.to_string(),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
for (part_number, part_version) in parts_versions.iter().enumerate() {
|
||||||
|
if part_version.deleted.get() {
|
||||||
|
return Err(Error::InvalidPart);
|
||||||
|
}
|
||||||
|
for (vbk, vb) in part_version.blocks.items().iter() {
|
||||||
|
final_version.blocks.put(
|
||||||
|
VersionBlockKey {
|
||||||
|
part_number: (part_number + 1) as u64,
|
||||||
|
offset: vbk.offset,
|
||||||
|
},
|
||||||
|
*vb,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
garage.version_table.insert(&final_version).await?;
|
||||||
|
|
||||||
|
let block_refs = final_version.blocks.items().iter().map(|(_, b)| BlockRef {
|
||||||
|
block: b.hash,
|
||||||
|
version: upload_id,
|
||||||
|
deleted: false.into(),
|
||||||
|
});
|
||||||
|
garage.block_ref_table.insert_many(block_refs).await?;
|
||||||
|
|
||||||
|
// Calculate etag of final object
|
||||||
|
// To understand how etags are calculated, read more here:
|
||||||
|
// https://teppen.io/2018/06/23/aws_s3_etags/
|
||||||
|
let mut etag_md5_hasher = Md5::new();
|
||||||
|
for part in parts.iter() {
|
||||||
|
etag_md5_hasher.update(part.etag.as_ref().unwrap().as_bytes());
|
||||||
|
}
|
||||||
|
let etag = format!(
|
||||||
|
"{}-{}",
|
||||||
|
hex::encode(etag_md5_hasher.finalize()),
|
||||||
|
parts.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate total size of final object
|
||||||
|
let total_size = parts.iter().map(|x| x.size.unwrap()).sum();
|
||||||
|
|
||||||
|
if let Err(e) = check_quotas(&garage, bucket, &key, total_size).await {
|
||||||
|
object_version.state = ObjectVersionState::Aborted;
|
||||||
|
let final_object = Object::new(bucket.id, key.clone(), vec![object_version]);
|
||||||
|
garage.object_table.insert(&final_object).await?;
|
||||||
|
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write final object version
|
||||||
|
object_version.state = ObjectVersionState::Complete(ObjectVersionData::FirstBlock(
|
||||||
|
ObjectVersionMeta {
|
||||||
|
headers,
|
||||||
|
size: total_size,
|
||||||
|
etag: etag.clone(),
|
||||||
|
},
|
||||||
|
final_version.blocks.items()[0].1.hash,
|
||||||
|
));
|
||||||
|
|
||||||
|
let final_object = Object::new(bucket.id, key.clone(), vec![object_version]);
|
||||||
|
garage.object_table.insert(&final_object).await?;
|
||||||
|
|
||||||
|
// Send response saying ok we're done
|
||||||
|
let result = s3_xml::CompleteMultipartUploadResult {
|
||||||
|
xmlns: (),
|
||||||
|
location: None,
|
||||||
|
bucket: s3_xml::Value(bucket_name.to_string()),
|
||||||
|
key: s3_xml::Value(key),
|
||||||
|
etag: s3_xml::Value(format!("\"{}\"", etag)),
|
||||||
|
};
|
||||||
|
let xml = s3_xml::to_xml_with_header(&result)?;
|
||||||
|
|
||||||
|
Ok(Response::new(Body::from(xml.into_bytes())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_abort_multipart_upload(
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
key: &str,
|
||||||
|
upload_id: &str,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let upload_id = decode_upload_id(upload_id)?;
|
||||||
|
|
||||||
|
let (_, mut object_version, _) =
|
||||||
|
get_upload(&garage, &bucket_id, &key.to_string(), &upload_id).await?;
|
||||||
|
|
||||||
|
object_version.state = ObjectVersionState::Aborted;
|
||||||
|
let final_object = Object::new(bucket_id, key.to_string(), vec![object_version]);
|
||||||
|
garage.object_table.insert(&final_object).await?;
|
||||||
|
|
||||||
|
Ok(Response::new(Body::from(vec![])))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======== helpers ============
|
||||||
|
|
||||||
|
#[allow(clippy::ptr_arg)]
|
||||||
|
pub(crate) async fn get_upload(
|
||||||
|
garage: &Garage,
|
||||||
|
bucket_id: &Uuid,
|
||||||
|
key: &String,
|
||||||
|
upload_id: &Uuid,
|
||||||
|
) -> Result<(Object, ObjectVersion, MultipartUpload), Error> {
|
||||||
|
let (object, mpu) = futures::try_join!(
|
||||||
|
garage.object_table.get(bucket_id, key).map_err(Error::from),
|
||||||
|
garage
|
||||||
|
.mpu_table
|
||||||
|
.get(upload_id, &EmptyKey)
|
||||||
|
.map_err(Error::from),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let object = object.ok_or(Error::NoSuchUpload)?;
|
||||||
|
let mpu = mpu.ok_or(Error::NoSuchUpload)?;
|
||||||
|
|
||||||
|
let object_version = object
|
||||||
|
.versions()
|
||||||
|
.iter()
|
||||||
|
.find(|v| v.uuid == *upload_id && v.is_uploading(Some(true)))
|
||||||
|
.ok_or(Error::NoSuchUpload)?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
Ok((object, object_version, mpu))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_upload_id(id: &str) -> Result<Uuid, Error> {
|
||||||
|
let id_bin = hex::decode(id).map_err(|_| Error::NoSuchUpload)?;
|
||||||
|
if id_bin.len() != 32 {
|
||||||
|
return Err(Error::NoSuchUpload);
|
||||||
|
}
|
||||||
|
let mut uuid = [0u8; 32];
|
||||||
|
uuid.copy_from_slice(&id_bin[..]);
|
||||||
|
Ok(Uuid::from(uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CompleteMultipartUploadPart {
|
||||||
|
etag: String,
|
||||||
|
part_number: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_complete_multipart_upload_body(
|
||||||
|
xml: &roxmltree::Document,
|
||||||
|
) -> Option<Vec<CompleteMultipartUploadPart>> {
|
||||||
|
let mut parts = vec![];
|
||||||
|
|
||||||
|
let root = xml.root();
|
||||||
|
let cmu = root.first_child()?;
|
||||||
|
if !cmu.has_tag_name("CompleteMultipartUpload") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in cmu.children() {
|
||||||
|
// Only parse <Part> nodes
|
||||||
|
if !item.is_element() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.has_tag_name("Part") {
|
||||||
|
let etag = item.children().find(|e| e.has_tag_name("ETag"))?.text()?;
|
||||||
|
let part_number = item
|
||||||
|
.children()
|
||||||
|
.find(|e| e.has_tag_name("PartNumber"))?
|
||||||
|
.text()?;
|
||||||
|
parts.push(CompleteMultipartUploadPart {
|
||||||
|
etag: etag.trim_matches('"').to_string(),
|
||||||
|
part_number: part_number.parse().ok()?,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(parts)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
|
@ -30,8 +30,6 @@ use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::*;
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
use crate::s3::xml as s3_xml;
|
|
||||||
use crate::signature::verify_signed_content;
|
|
||||||
|
|
||||||
pub async fn handle_put(
|
pub async fn handle_put(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
|
@ -123,20 +121,23 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||||
// The following consists in many steps that can each fail.
|
// The following consists in many steps that can each fail.
|
||||||
// Keep track that some cleanup will be needed if things fail
|
// Keep track that some cleanup will be needed if things fail
|
||||||
// before everything is finished (cleanup is done using the Drop trait).
|
// before everything is finished (cleanup is done using the Drop trait).
|
||||||
let mut interrupted_cleanup = InterruptedCleanup(Some((
|
let mut interrupted_cleanup = InterruptedCleanup(Some(InterruptedCleanupInner {
|
||||||
garage.clone(),
|
garage: garage.clone(),
|
||||||
bucket.id,
|
bucket_id: bucket.id,
|
||||||
key.into(),
|
key: key.into(),
|
||||||
version_uuid,
|
version_uuid,
|
||||||
version_timestamp,
|
version_timestamp,
|
||||||
)));
|
}));
|
||||||
|
|
||||||
// Write version identifier in object table so that we have a trace
|
// Write version identifier in object table so that we have a trace
|
||||||
// that we are uploading something
|
// that we are uploading something
|
||||||
let mut object_version = ObjectVersion {
|
let mut object_version = ObjectVersion {
|
||||||
uuid: version_uuid,
|
uuid: version_uuid,
|
||||||
timestamp: version_timestamp,
|
timestamp: version_timestamp,
|
||||||
state: ObjectVersionState::Uploading(headers.clone()),
|
state: ObjectVersionState::Uploading {
|
||||||
|
headers: headers.clone(),
|
||||||
|
multipart: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let object = Object::new(bucket.id, key.into(), vec![object_version.clone()]);
|
let object = Object::new(bucket.id, key.into(), vec![object_version.clone()]);
|
||||||
garage.object_table.insert(&object).await?;
|
garage.object_table.insert(&object).await?;
|
||||||
|
@ -145,7 +146,14 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||||
// Write this entry now, even with empty block list,
|
// Write this entry now, even with empty block list,
|
||||||
// to prevent block_ref entries from being deleted (they can be deleted
|
// to prevent block_ref entries from being deleted (they can be deleted
|
||||||
// if the reference a version that isn't found in the version table)
|
// if the reference a version that isn't found in the version table)
|
||||||
let version = Version::new(version_uuid, bucket.id, key.into(), false);
|
let version = Version::new(
|
||||||
|
version_uuid,
|
||||||
|
VersionBacklink::Object {
|
||||||
|
bucket_id: bucket.id,
|
||||||
|
key: key.into(),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
garage.version_table.insert(&version).await?;
|
garage.version_table.insert(&version).await?;
|
||||||
|
|
||||||
// Transfer data and verify checksum
|
// Transfer data and verify checksum
|
||||||
|
@ -192,7 +200,7 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||||
|
|
||||||
/// Validate MD5 sum against content-md5 header
|
/// Validate MD5 sum against content-md5 header
|
||||||
/// and sha256sum against signed content-sha256
|
/// and sha256sum against signed content-sha256
|
||||||
fn ensure_checksum_matches(
|
pub(crate) fn ensure_checksum_matches(
|
||||||
data_md5sum: &[u8],
|
data_md5sum: &[u8],
|
||||||
data_sha256sum: garage_util::data::FixedBytes32,
|
data_sha256sum: garage_util::data::FixedBytes32,
|
||||||
content_md5: Option<&str>,
|
content_md5: Option<&str>,
|
||||||
|
@ -218,7 +226,7 @@ fn ensure_checksum_matches(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that inserting this object with this size doesn't exceed bucket quotas
|
/// Check that inserting this object with this size doesn't exceed bucket quotas
|
||||||
async fn check_quotas(
|
pub(crate) async fn check_quotas(
|
||||||
garage: &Arc<Garage>,
|
garage: &Arc<Garage>,
|
||||||
bucket: &Bucket,
|
bucket: &Bucket,
|
||||||
key: &str,
|
key: &str,
|
||||||
|
@ -275,7 +283,7 @@ async fn check_quotas(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
pub(crate) async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||||
garage: &Garage,
|
garage: &Garage,
|
||||||
version: &Version,
|
version: &Version,
|
||||||
part_number: u64,
|
part_number: u64,
|
||||||
|
@ -381,7 +389,7 @@ async fn put_block_meta(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StreamChunker<S: Stream<Item = Result<Bytes, Error>>> {
|
pub(crate) struct StreamChunker<S: Stream<Item = Result<Bytes, Error>>> {
|
||||||
stream: S,
|
stream: S,
|
||||||
read_all: bool,
|
read_all: bool,
|
||||||
block_size: usize,
|
block_size: usize,
|
||||||
|
@ -389,7 +397,7 @@ struct StreamChunker<S: Stream<Item = Result<Bytes, Error>>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
|
impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
|
||||||
fn new(stream: S, block_size: usize) -> Self {
|
pub(crate) fn new(stream: S, block_size: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stream,
|
stream,
|
||||||
read_all: false,
|
read_all: false,
|
||||||
|
@ -398,7 +406,7 @@ impl<S: Stream<Item = Result<Bytes, Error>> + Unpin> StreamChunker<S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn next(&mut self) -> Result<Option<Bytes>, Error> {
|
pub(crate) async fn next(&mut self) -> Result<Option<Bytes>, Error> {
|
||||||
while !self.read_all && self.buf.len() < self.block_size {
|
while !self.read_all && self.buf.len() < self.block_size {
|
||||||
if let Some(block) = self.stream.next().await {
|
if let Some(block) = self.stream.next().await {
|
||||||
let bytes = block?;
|
let bytes = block?;
|
||||||
|
@ -425,7 +433,14 @@ pub fn put_response(version_uuid: Uuid, md5sum_hex: String) -> Response<Body> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InterruptedCleanup(Option<(Arc<Garage>, Uuid, String, Uuid, u64)>);
|
struct InterruptedCleanup(Option<InterruptedCleanupInner>);
|
||||||
|
struct InterruptedCleanupInner {
|
||||||
|
garage: Arc<Garage>,
|
||||||
|
bucket_id: Uuid,
|
||||||
|
key: String,
|
||||||
|
version_uuid: Uuid,
|
||||||
|
version_timestamp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
impl InterruptedCleanup {
|
impl InterruptedCleanup {
|
||||||
fn cancel(&mut self) {
|
fn cancel(&mut self) {
|
||||||
|
@ -434,15 +449,15 @@ impl InterruptedCleanup {
|
||||||
}
|
}
|
||||||
impl Drop for InterruptedCleanup {
|
impl Drop for InterruptedCleanup {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some((garage, bucket_id, key, version_uuid, version_ts)) = self.0.take() {
|
if let Some(info) = self.0.take() {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let object_version = ObjectVersion {
|
let object_version = ObjectVersion {
|
||||||
uuid: version_uuid,
|
uuid: info.version_uuid,
|
||||||
timestamp: version_ts,
|
timestamp: info.version_timestamp,
|
||||||
state: ObjectVersionState::Aborted,
|
state: ObjectVersionState::Aborted,
|
||||||
};
|
};
|
||||||
let object = Object::new(bucket_id, key, vec![object_version]);
|
let object = Object::new(info.bucket_id, info.key, vec![object_version]);
|
||||||
if let Err(e) = garage.object_table.insert(&object).await {
|
if let Err(e) = info.garage.object_table.insert(&object).await {
|
||||||
warn!("Cannot cleanup after aborted PutObject: {}", e);
|
warn!("Cannot cleanup after aborted PutObject: {}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -450,326 +465,9 @@ impl Drop for InterruptedCleanup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----
|
// ============ helpers ============
|
||||||
|
|
||||||
pub async fn handle_create_multipart_upload(
|
pub(crate) fn get_mime_type(headers: &HeaderMap<HeaderValue>) -> Result<String, Error> {
|
||||||
garage: Arc<Garage>,
|
|
||||||
req: &Request<Body>,
|
|
||||||
bucket_name: &str,
|
|
||||||
bucket_id: Uuid,
|
|
||||||
key: &str,
|
|
||||||
) -> Result<Response<Body>, Error> {
|
|
||||||
let version_uuid = gen_uuid();
|
|
||||||
let headers = get_headers(req.headers())?;
|
|
||||||
|
|
||||||
// Create object in object table
|
|
||||||
let object_version = ObjectVersion {
|
|
||||||
uuid: version_uuid,
|
|
||||||
timestamp: now_msec(),
|
|
||||||
state: ObjectVersionState::Uploading(headers),
|
|
||||||
};
|
|
||||||
let object = Object::new(bucket_id, key.to_string(), vec![object_version]);
|
|
||||||
garage.object_table.insert(&object).await?;
|
|
||||||
|
|
||||||
// Insert empty version so that block_ref entries refer to something
|
|
||||||
// (they are inserted concurrently with blocks in the version table, so
|
|
||||||
// there is the possibility that they are inserted before the version table
|
|
||||||
// is created, in which case it is allowed to delete them, e.g. in repair_*)
|
|
||||||
let version = Version::new(version_uuid, bucket_id, key.into(), false);
|
|
||||||
garage.version_table.insert(&version).await?;
|
|
||||||
|
|
||||||
// Send success response
|
|
||||||
let result = s3_xml::InitiateMultipartUploadResult {
|
|
||||||
xmlns: (),
|
|
||||||
bucket: s3_xml::Value(bucket_name.to_string()),
|
|
||||||
key: s3_xml::Value(key.to_string()),
|
|
||||||
upload_id: s3_xml::Value(hex::encode(version_uuid)),
|
|
||||||
};
|
|
||||||
let xml = s3_xml::to_xml_with_header(&result)?;
|
|
||||||
|
|
||||||
Ok(Response::new(Body::from(xml.into_bytes())))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_put_part(
|
|
||||||
garage: Arc<Garage>,
|
|
||||||
req: Request<Body>,
|
|
||||||
bucket_id: Uuid,
|
|
||||||
key: &str,
|
|
||||||
part_number: u64,
|
|
||||||
upload_id: &str,
|
|
||||||
content_sha256: Option<Hash>,
|
|
||||||
) -> Result<Response<Body>, Error> {
|
|
||||||
let version_uuid = decode_upload_id(upload_id)?;
|
|
||||||
|
|
||||||
let content_md5 = match req.headers().get("content-md5") {
|
|
||||||
Some(x) => Some(x.to_str()?.to_string()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read first chuck, and at the same time try to get object to see if it exists
|
|
||||||
let key = key.to_string();
|
|
||||||
|
|
||||||
let body = req.into_body().map_err(Error::from);
|
|
||||||
let mut chunker = StreamChunker::new(body, garage.config.block_size);
|
|
||||||
|
|
||||||
let (object, version, first_block) = futures::try_join!(
|
|
||||||
garage
|
|
||||||
.object_table
|
|
||||||
.get(&bucket_id, &key)
|
|
||||||
.map_err(Error::from),
|
|
||||||
garage
|
|
||||||
.version_table
|
|
||||||
.get(&version_uuid, &EmptyKey)
|
|
||||||
.map_err(Error::from),
|
|
||||||
chunker.next(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Check object is valid and multipart block can be accepted
|
|
||||||
let first_block = first_block.ok_or_bad_request("Empty body")?;
|
|
||||||
let object = object.ok_or_bad_request("Object not found")?;
|
|
||||||
|
|
||||||
if !object
|
|
||||||
.versions()
|
|
||||||
.iter()
|
|
||||||
.any(|v| v.uuid == version_uuid && v.is_uploading())
|
|
||||||
{
|
|
||||||
return Err(Error::NoSuchUpload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check part hasn't already been uploaded
|
|
||||||
if let Some(v) = version {
|
|
||||||
if v.has_part_number(part_number) {
|
|
||||||
return Err(Error::bad_request(format!(
|
|
||||||
"Part number {} has already been uploaded",
|
|
||||||
part_number
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy block to store
|
|
||||||
let version = Version::new(version_uuid, bucket_id, key, false);
|
|
||||||
|
|
||||||
let first_block_hash = async_blake2sum(first_block.clone()).await;
|
|
||||||
|
|
||||||
let (_, data_md5sum, data_sha256sum) = read_and_put_blocks(
|
|
||||||
&garage,
|
|
||||||
&version,
|
|
||||||
part_number,
|
|
||||||
first_block,
|
|
||||||
first_block_hash,
|
|
||||||
&mut chunker,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Verify that checksums map
|
|
||||||
ensure_checksum_matches(
|
|
||||||
data_md5sum.as_slice(),
|
|
||||||
data_sha256sum,
|
|
||||||
content_md5.as_deref(),
|
|
||||||
content_sha256,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Store part etag in version
|
|
||||||
let data_md5sum_hex = hex::encode(data_md5sum);
|
|
||||||
let mut version = version;
|
|
||||||
version
|
|
||||||
.parts_etags
|
|
||||||
.put(part_number, data_md5sum_hex.clone());
|
|
||||||
garage.version_table.insert(&version).await?;
|
|
||||||
|
|
||||||
let response = Response::builder()
|
|
||||||
.header("ETag", format!("\"{}\"", data_md5sum_hex))
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_complete_multipart_upload(
|
|
||||||
garage: Arc<Garage>,
|
|
||||||
req: Request<Body>,
|
|
||||||
bucket_name: &str,
|
|
||||||
bucket: &Bucket,
|
|
||||||
key: &str,
|
|
||||||
upload_id: &str,
|
|
||||||
content_sha256: Option<Hash>,
|
|
||||||
) -> Result<Response<Body>, Error> {
|
|
||||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
|
||||||
|
|
||||||
if let Some(content_sha256) = content_sha256 {
|
|
||||||
verify_signed_content(content_sha256, &body[..])?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let body_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
|
|
||||||
let body_list_of_parts = parse_complete_multipart_upload_body(&body_xml)
|
|
||||||
.ok_or_bad_request("Invalid CompleteMultipartUpload XML")?;
|
|
||||||
debug!(
|
|
||||||
"CompleteMultipartUpload list of parts: {:?}",
|
|
||||||
body_list_of_parts
|
|
||||||
);
|
|
||||||
|
|
||||||
let version_uuid = decode_upload_id(upload_id)?;
|
|
||||||
|
|
||||||
// Get object and version
|
|
||||||
let key = key.to_string();
|
|
||||||
let (object, version) = futures::try_join!(
|
|
||||||
garage.object_table.get(&bucket.id, &key),
|
|
||||||
garage.version_table.get(&version_uuid, &EmptyKey),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let object = object.ok_or(Error::NoSuchKey)?;
|
|
||||||
let mut object_version = object
|
|
||||||
.versions()
|
|
||||||
.iter()
|
|
||||||
.find(|v| v.uuid == version_uuid && v.is_uploading())
|
|
||||||
.cloned()
|
|
||||||
.ok_or(Error::NoSuchUpload)?;
|
|
||||||
|
|
||||||
let version = version.ok_or(Error::NoSuchKey)?;
|
|
||||||
if version.blocks.is_empty() {
|
|
||||||
return Err(Error::bad_request("No data was uploaded"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let headers = match object_version.state {
|
|
||||||
ObjectVersionState::Uploading(headers) => headers,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check that part numbers are an increasing sequence.
|
|
||||||
// (it doesn't need to start at 1 nor to be a continuous sequence,
|
|
||||||
// see discussion in #192)
|
|
||||||
if body_list_of_parts.is_empty() {
|
|
||||||
return Err(Error::EntityTooSmall);
|
|
||||||
}
|
|
||||||
if !body_list_of_parts
|
|
||||||
.iter()
|
|
||||||
.zip(body_list_of_parts.iter().skip(1))
|
|
||||||
.all(|(p1, p2)| p1.part_number < p2.part_number)
|
|
||||||
{
|
|
||||||
return Err(Error::InvalidPartOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Garage-specific restriction, see #204: part numbers must be
|
|
||||||
// consecutive starting at 1
|
|
||||||
if body_list_of_parts[0].part_number != 1
|
|
||||||
|| !body_list_of_parts
|
|
||||||
.iter()
|
|
||||||
.zip(body_list_of_parts.iter().skip(1))
|
|
||||||
.all(|(p1, p2)| p1.part_number + 1 == p2.part_number)
|
|
||||||
{
|
|
||||||
return Err(Error::NotImplemented("Garage does not support completing a Multipart upload with non-consecutive part numbers. This is a restriction of Garage's data model, which might be fixed in a future release. See issue #204 for more information on this topic.".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the list of parts they gave us corresponds to the parts we have here
|
|
||||||
debug!("Expected parts from request: {:?}", body_list_of_parts);
|
|
||||||
debug!("Parts stored in version: {:?}", version.parts_etags.items());
|
|
||||||
let parts = version
|
|
||||||
.parts_etags
|
|
||||||
.items()
|
|
||||||
.iter()
|
|
||||||
.map(|pair| (&pair.0, &pair.1));
|
|
||||||
let same_parts = body_list_of_parts
|
|
||||||
.iter()
|
|
||||||
.map(|x| (&x.part_number, &x.etag))
|
|
||||||
.eq(parts);
|
|
||||||
if !same_parts {
|
|
||||||
return Err(Error::InvalidPart);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that all blocks belong to one of the parts
|
|
||||||
let block_parts = version
|
|
||||||
.blocks
|
|
||||||
.items()
|
|
||||||
.iter()
|
|
||||||
.map(|(bk, _)| bk.part_number)
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let same_parts = body_list_of_parts
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.part_number)
|
|
||||||
.eq(block_parts.into_iter());
|
|
||||||
if !same_parts {
|
|
||||||
return Err(Error::bad_request(
|
|
||||||
"Part numbers in block list and part list do not match. This can happen if a part was partially uploaded. Please abort the multipart upload and try again."
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate etag of final object
|
|
||||||
// To understand how etags are calculated, read more here:
|
|
||||||
// https://teppen.io/2018/06/23/aws_s3_etags/
|
|
||||||
let num_parts = body_list_of_parts.len();
|
|
||||||
let mut etag_md5_hasher = Md5::new();
|
|
||||||
for (_, etag) in version.parts_etags.items().iter() {
|
|
||||||
etag_md5_hasher.update(etag.as_bytes());
|
|
||||||
}
|
|
||||||
let etag = format!("{}-{}", hex::encode(etag_md5_hasher.finalize()), num_parts);
|
|
||||||
|
|
||||||
// Calculate total size of final object
|
|
||||||
let total_size = version.blocks.items().iter().map(|x| x.1.size).sum();
|
|
||||||
|
|
||||||
if let Err(e) = check_quotas(&garage, bucket, &key, total_size).await {
|
|
||||||
object_version.state = ObjectVersionState::Aborted;
|
|
||||||
let final_object = Object::new(bucket.id, key.clone(), vec![object_version]);
|
|
||||||
garage.object_table.insert(&final_object).await?;
|
|
||||||
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write final object version
|
|
||||||
object_version.state = ObjectVersionState::Complete(ObjectVersionData::FirstBlock(
|
|
||||||
ObjectVersionMeta {
|
|
||||||
headers,
|
|
||||||
size: total_size,
|
|
||||||
etag: etag.clone(),
|
|
||||||
},
|
|
||||||
version.blocks.items()[0].1.hash,
|
|
||||||
));
|
|
||||||
|
|
||||||
let final_object = Object::new(bucket.id, key.clone(), vec![object_version]);
|
|
||||||
garage.object_table.insert(&final_object).await?;
|
|
||||||
|
|
||||||
// Send response saying ok we're done
|
|
||||||
let result = s3_xml::CompleteMultipartUploadResult {
|
|
||||||
xmlns: (),
|
|
||||||
location: None,
|
|
||||||
bucket: s3_xml::Value(bucket_name.to_string()),
|
|
||||||
key: s3_xml::Value(key),
|
|
||||||
etag: s3_xml::Value(format!("\"{}\"", etag)),
|
|
||||||
};
|
|
||||||
let xml = s3_xml::to_xml_with_header(&result)?;
|
|
||||||
|
|
||||||
Ok(Response::new(Body::from(xml.into_bytes())))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_abort_multipart_upload(
|
|
||||||
garage: Arc<Garage>,
|
|
||||||
bucket_id: Uuid,
|
|
||||||
key: &str,
|
|
||||||
upload_id: &str,
|
|
||||||
) -> Result<Response<Body>, Error> {
|
|
||||||
let version_uuid = decode_upload_id(upload_id)?;
|
|
||||||
|
|
||||||
let object = garage
|
|
||||||
.object_table
|
|
||||||
.get(&bucket_id, &key.to_string())
|
|
||||||
.await?;
|
|
||||||
let object = object.ok_or(Error::NoSuchKey)?;
|
|
||||||
|
|
||||||
let object_version = object
|
|
||||||
.versions()
|
|
||||||
.iter()
|
|
||||||
.find(|v| v.uuid == version_uuid && v.is_uploading());
|
|
||||||
let mut object_version = match object_version {
|
|
||||||
None => return Err(Error::NoSuchUpload),
|
|
||||||
Some(x) => x.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
object_version.state = ObjectVersionState::Aborted;
|
|
||||||
let final_object = Object::new(bucket_id, key.to_string(), vec![object_version]);
|
|
||||||
garage.object_table.insert(&final_object).await?;
|
|
||||||
|
|
||||||
Ok(Response::new(Body::from(vec![])))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mime_type(headers: &HeaderMap<HeaderValue>) -> Result<String, Error> {
|
|
||||||
Ok(headers
|
Ok(headers
|
||||||
.get(hyper::header::CONTENT_TYPE)
|
.get(hyper::header::CONTENT_TYPE)
|
||||||
.map(|x| x.to_str())
|
.map(|x| x.to_str())
|
||||||
|
@ -821,54 +519,3 @@ pub(crate) fn get_headers(headers: &HeaderMap<HeaderValue>) -> Result<ObjectVers
|
||||||
other,
|
other,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_upload_id(id: &str) -> Result<Uuid, Error> {
|
|
||||||
let id_bin = hex::decode(id).map_err(|_| Error::NoSuchUpload)?;
|
|
||||||
if id_bin.len() != 32 {
|
|
||||||
return Err(Error::NoSuchUpload);
|
|
||||||
}
|
|
||||||
let mut uuid = [0u8; 32];
|
|
||||||
uuid.copy_from_slice(&id_bin[..]);
|
|
||||||
Ok(Uuid::from(uuid))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct CompleteMultipartUploadPart {
|
|
||||||
etag: String,
|
|
||||||
part_number: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_complete_multipart_upload_body(
|
|
||||||
xml: &roxmltree::Document,
|
|
||||||
) -> Option<Vec<CompleteMultipartUploadPart>> {
|
|
||||||
let mut parts = vec![];
|
|
||||||
|
|
||||||
let root = xml.root();
|
|
||||||
let cmu = root.first_child()?;
|
|
||||||
if !cmu.has_tag_name("CompleteMultipartUpload") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
for item in cmu.children() {
|
|
||||||
// Only parse <Part> nodes
|
|
||||||
if !item.is_element() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.has_tag_name("Part") {
|
|
||||||
let etag = item.children().find(|e| e.has_tag_name("ETag"))?.text()?;
|
|
||||||
let part_number = item
|
|
||||||
.children()
|
|
||||||
.find(|e| e.has_tag_name("PartNumber"))?
|
|
||||||
.text()?;
|
|
||||||
parts.push(CompleteMultipartUploadPart {
|
|
||||||
etag: etag.trim_matches('"').to_string(),
|
|
||||||
part_number: part_number.parse().ok()?,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(parts)
|
|
||||||
}
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ impl AdminRpcHandler {
|
||||||
.get_range(&hash, None, None, 10000, Default::default())
|
.get_range(&hash, None, None, 10000, Default::default())
|
||||||
.await?;
|
.await?;
|
||||||
let mut versions = vec![];
|
let mut versions = vec![];
|
||||||
|
let mut uploads = vec![];
|
||||||
for br in block_refs {
|
for br in block_refs {
|
||||||
if let Some(v) = self
|
if let Some(v) = self
|
||||||
.garage
|
.garage
|
||||||
|
@ -41,6 +42,11 @@ impl AdminRpcHandler {
|
||||||
.get(&br.version, &EmptyKey)
|
.get(&br.version, &EmptyKey)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
|
if let VersionBacklink::MultipartUpload { upload_id } = &v.backlink {
|
||||||
|
if let Some(u) = self.garage.mpu_table.get(upload_id, &EmptyKey).await? {
|
||||||
|
uploads.push(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
versions.push(Ok(v));
|
versions.push(Ok(v));
|
||||||
} else {
|
} else {
|
||||||
versions.push(Err(br.version));
|
versions.push(Err(br.version));
|
||||||
|
@ -50,6 +56,7 @@ impl AdminRpcHandler {
|
||||||
hash,
|
hash,
|
||||||
refcount,
|
refcount,
|
||||||
versions,
|
versions,
|
||||||
|
uploads,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +100,7 @@ impl AdminRpcHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut obj_dels = 0;
|
let mut obj_dels = 0;
|
||||||
|
let mut mpu_dels = 0;
|
||||||
let mut ver_dels = 0;
|
let mut ver_dels = 0;
|
||||||
|
|
||||||
for hash in blocks {
|
for hash in blocks {
|
||||||
|
@ -105,56 +113,80 @@ impl AdminRpcHandler {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for br in block_refs {
|
for br in block_refs {
|
||||||
let version = match self
|
if let Some(version) = self
|
||||||
.garage
|
.garage
|
||||||
.version_table
|
.version_table
|
||||||
.get(&br.version, &EmptyKey)
|
.get(&br.version, &EmptyKey)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Some(v) => v,
|
self.handle_block_purge_version_backlink(
|
||||||
None => continue,
|
&version,
|
||||||
};
|
&mut obj_dels,
|
||||||
|
&mut mpu_dels,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if let Some(object) = self
|
if !version.deleted.get() {
|
||||||
.garage
|
let deleted_version = Version::new(version.uuid, version.backlink, true);
|
||||||
.object_table
|
self.garage.version_table.insert(&deleted_version).await?;
|
||||||
.get(&version.bucket_id, &version.key)
|
ver_dels += 1;
|
||||||
.await?
|
|
||||||
{
|
|
||||||
let ov = object.versions().iter().rev().find(|v| v.is_complete());
|
|
||||||
if let Some(ov) = ov {
|
|
||||||
if ov.uuid == br.version {
|
|
||||||
let del_uuid = gen_uuid();
|
|
||||||
let deleted_object = Object::new(
|
|
||||||
version.bucket_id,
|
|
||||||
version.key.clone(),
|
|
||||||
vec![ObjectVersion {
|
|
||||||
uuid: del_uuid,
|
|
||||||
timestamp: ov.timestamp + 1,
|
|
||||||
state: ObjectVersionState::Complete(
|
|
||||||
ObjectVersionData::DeleteMarker,
|
|
||||||
),
|
|
||||||
}],
|
|
||||||
);
|
|
||||||
self.garage.object_table.insert(&deleted_object).await?;
|
|
||||||
obj_dels += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !version.deleted.get() {
|
|
||||||
let deleted_version =
|
|
||||||
Version::new(version.uuid, version.bucket_id, version.key.clone(), true);
|
|
||||||
self.garage.version_table.insert(&deleted_version).await?;
|
|
||||||
ver_dels += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AdminRpc::Ok(format!(
|
Ok(AdminRpc::Ok(format!(
|
||||||
"{} blocks were purged: {} object deletion markers added, {} versions marked deleted",
|
"Purged {} blocks, {} versions, {} objects, {} multipart uploads",
|
||||||
blocks.len(),
|
blocks.len(),
|
||||||
|
ver_dels,
|
||||||
obj_dels,
|
obj_dels,
|
||||||
ver_dels
|
mpu_dels,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_block_purge_version_backlink(
|
||||||
|
&self,
|
||||||
|
version: &Version,
|
||||||
|
obj_dels: &mut usize,
|
||||||
|
mpu_dels: &mut usize,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let (bucket_id, key, ov_id) = match &version.backlink {
|
||||||
|
VersionBacklink::Object { bucket_id, key } => (*bucket_id, key.clone(), version.uuid),
|
||||||
|
VersionBacklink::MultipartUpload { upload_id } => {
|
||||||
|
if let Some(mut mpu) = self.garage.mpu_table.get(upload_id, &EmptyKey).await? {
|
||||||
|
if !mpu.deleted.get() {
|
||||||
|
mpu.parts.clear();
|
||||||
|
mpu.deleted.set();
|
||||||
|
self.garage.mpu_table.insert(&mpu).await?;
|
||||||
|
*mpu_dels += 1;
|
||||||
|
}
|
||||||
|
(mpu.bucket_id, mpu.key.clone(), *upload_id)
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(object) = self.garage.object_table.get(&bucket_id, &key).await? {
|
||||||
|
let ov = object.versions().iter().rev().find(|v| v.is_complete());
|
||||||
|
if let Some(ov) = ov {
|
||||||
|
if ov.uuid == ov_id {
|
||||||
|
let del_uuid = gen_uuid();
|
||||||
|
let deleted_object = Object::new(
|
||||||
|
bucket_id,
|
||||||
|
key,
|
||||||
|
vec![ObjectVersion {
|
||||||
|
uuid: del_uuid,
|
||||||
|
timestamp: ov.timestamp + 1,
|
||||||
|
state: ObjectVersionState::Complete(ObjectVersionData::DeleteMarker),
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
self.garage.object_table.insert(&deleted_object).await?;
|
||||||
|
*obj_dels += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,15 @@ impl AdminRpcHandler {
|
||||||
.map(|x| x.filtered_values(&self.garage.system.ring.borrow()))
|
.map(|x| x.filtered_values(&self.garage.system.ring.borrow()))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mpu_counters = self
|
||||||
|
.garage
|
||||||
|
.mpu_counter_table
|
||||||
|
.table
|
||||||
|
.get(&bucket_id, &EmptyKey)
|
||||||
|
.await?
|
||||||
|
.map(|x| x.filtered_values(&self.garage.system.ring.borrow()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut relevant_keys = HashMap::new();
|
let mut relevant_keys = HashMap::new();
|
||||||
for (k, _) in bucket
|
for (k, _) in bucket
|
||||||
.state
|
.state
|
||||||
|
@ -112,6 +121,7 @@ impl AdminRpcHandler {
|
||||||
bucket,
|
bucket,
|
||||||
relevant_keys,
|
relevant_keys,
|
||||||
counters,
|
counters,
|
||||||
|
mpu_counters,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ use garage_model::garage::Garage;
|
||||||
use garage_model::helper::error::{Error, OkOrBadRequest};
|
use garage_model::helper::error::{Error, OkOrBadRequest};
|
||||||
use garage_model::key_table::*;
|
use garage_model::key_table::*;
|
||||||
use garage_model::migrate::Migrate;
|
use garage_model::migrate::Migrate;
|
||||||
|
use garage_model::s3::mpu_table::MultipartUpload;
|
||||||
use garage_model::s3::version_table::Version;
|
use garage_model::s3::version_table::Version;
|
||||||
|
|
||||||
use crate::cli::*;
|
use crate::cli::*;
|
||||||
|
@ -52,6 +53,7 @@ pub enum AdminRpc {
|
||||||
bucket: Bucket,
|
bucket: Bucket,
|
||||||
relevant_keys: HashMap<String, Key>,
|
relevant_keys: HashMap<String, Key>,
|
||||||
counters: HashMap<String, i64>,
|
counters: HashMap<String, i64>,
|
||||||
|
mpu_counters: HashMap<String, i64>,
|
||||||
},
|
},
|
||||||
KeyList(Vec<(String, String)>),
|
KeyList(Vec<(String, String)>),
|
||||||
KeyInfo(Key, HashMap<Uuid, Bucket>),
|
KeyInfo(Key, HashMap<Uuid, Bucket>),
|
||||||
|
@ -66,6 +68,7 @@ pub enum AdminRpc {
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
refcount: u64,
|
refcount: u64,
|
||||||
versions: Vec<Result<Version, Uuid>>,
|
versions: Vec<Result<Version, Uuid>>,
|
||||||
|
uploads: Vec<MultipartUpload>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -190,8 +190,9 @@ pub async fn cmd_admin(
|
||||||
bucket,
|
bucket,
|
||||||
relevant_keys,
|
relevant_keys,
|
||||||
counters,
|
counters,
|
||||||
|
mpu_counters,
|
||||||
} => {
|
} => {
|
||||||
print_bucket_info(&bucket, &relevant_keys, &counters);
|
print_bucket_info(&bucket, &relevant_keys, &counters, &mpu_counters);
|
||||||
}
|
}
|
||||||
AdminRpc::KeyList(kl) => {
|
AdminRpc::KeyList(kl) => {
|
||||||
print_key_list(kl);
|
print_key_list(kl);
|
||||||
|
@ -215,8 +216,9 @@ pub async fn cmd_admin(
|
||||||
hash,
|
hash,
|
||||||
refcount,
|
refcount,
|
||||||
versions,
|
versions,
|
||||||
|
uploads,
|
||||||
} => {
|
} => {
|
||||||
print_block_info(hash, refcount, versions);
|
print_block_info(hash, refcount, versions, uploads);
|
||||||
}
|
}
|
||||||
r => {
|
r => {
|
||||||
error!("Unexpected response: {:?}", r);
|
error!("Unexpected response: {:?}", r);
|
||||||
|
|
|
@ -443,19 +443,22 @@ pub struct RepairOpt {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
#[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||||
pub enum RepairWhat {
|
pub enum RepairWhat {
|
||||||
/// Only do a full sync of metadata tables
|
/// Do a full sync of metadata tables
|
||||||
#[structopt(name = "tables", version = garage_version())]
|
#[structopt(name = "tables", version = garage_version())]
|
||||||
Tables,
|
Tables,
|
||||||
/// Only repair (resync/rebalance) the set of stored blocks
|
/// Repair (resync/rebalance) the set of stored blocks
|
||||||
#[structopt(name = "blocks", version = garage_version())]
|
#[structopt(name = "blocks", version = garage_version())]
|
||||||
Blocks,
|
Blocks,
|
||||||
/// Only redo the propagation of object deletions to the version table (slow)
|
/// Repropagate object deletions to the version table
|
||||||
#[structopt(name = "versions", version = garage_version())]
|
#[structopt(name = "versions", version = garage_version())]
|
||||||
Versions,
|
Versions,
|
||||||
/// Only redo the propagation of version deletions to the block ref table (extremely slow)
|
/// Repropagate object deletions to the multipart upload table
|
||||||
|
#[structopt(name = "mpu", version = garage_version())]
|
||||||
|
MultipartUploads,
|
||||||
|
/// Repropagate version deletions to the block ref table
|
||||||
#[structopt(name = "block_refs", version = garage_version())]
|
#[structopt(name = "block_refs", version = garage_version())]
|
||||||
BlockRefs,
|
BlockRefs,
|
||||||
/// Verify integrity of all blocks on disc (extremely slow, i/o intensive)
|
/// Verify integrity of all blocks on disc
|
||||||
#[structopt(name = "scrub", version = garage_version())]
|
#[structopt(name = "scrub", version = garage_version())]
|
||||||
Scrub {
|
Scrub {
|
||||||
#[structopt(subcommand)]
|
#[structopt(subcommand)]
|
||||||
|
|
|
@ -12,8 +12,9 @@ use garage_block::manager::BlockResyncErrorInfo;
|
||||||
|
|
||||||
use garage_model::bucket_table::*;
|
use garage_model::bucket_table::*;
|
||||||
use garage_model::key_table::*;
|
use garage_model::key_table::*;
|
||||||
use garage_model::s3::object_table::{BYTES, OBJECTS, UNFINISHED_UPLOADS};
|
use garage_model::s3::mpu_table::{self, MultipartUpload};
|
||||||
use garage_model::s3::version_table::Version;
|
use garage_model::s3::object_table;
|
||||||
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
use crate::cli::structs::WorkerListOpt;
|
use crate::cli::structs::WorkerListOpt;
|
||||||
|
|
||||||
|
@ -135,6 +136,7 @@ pub fn print_bucket_info(
|
||||||
bucket: &Bucket,
|
bucket: &Bucket,
|
||||||
relevant_keys: &HashMap<String, Key>,
|
relevant_keys: &HashMap<String, Key>,
|
||||||
counters: &HashMap<String, i64>,
|
counters: &HashMap<String, i64>,
|
||||||
|
mpu_counters: &HashMap<String, i64>,
|
||||||
) {
|
) {
|
||||||
let key_name = |k| {
|
let key_name = |k| {
|
||||||
relevant_keys
|
relevant_keys
|
||||||
|
@ -148,7 +150,7 @@ pub fn print_bucket_info(
|
||||||
Deletable::Deleted => println!("Bucket is deleted."),
|
Deletable::Deleted => println!("Bucket is deleted."),
|
||||||
Deletable::Present(p) => {
|
Deletable::Present(p) => {
|
||||||
let size =
|
let size =
|
||||||
bytesize::ByteSize::b(counters.get(BYTES).cloned().unwrap_or_default() as u64);
|
bytesize::ByteSize::b(*counters.get(object_table::BYTES).unwrap_or(&0) as u64);
|
||||||
println!(
|
println!(
|
||||||
"\nSize: {} ({})",
|
"\nSize: {} ({})",
|
||||||
size.to_string_as(true),
|
size.to_string_as(true),
|
||||||
|
@ -156,14 +158,22 @@ pub fn print_bucket_info(
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"Objects: {}",
|
"Objects: {}",
|
||||||
counters.get(OBJECTS).cloned().unwrap_or_default()
|
*counters.get(object_table::OBJECTS).unwrap_or(&0)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Unfinished uploads (multipart and non-multipart): {}",
|
||||||
|
*counters.get(object_table::UNFINISHED_UPLOADS).unwrap_or(&0)
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"Unfinished multipart uploads: {}",
|
"Unfinished multipart uploads: {}",
|
||||||
counters
|
*mpu_counters.get(mpu_table::UPLOADS).unwrap_or(&0)
|
||||||
.get(UNFINISHED_UPLOADS)
|
);
|
||||||
.cloned()
|
let mpu_size =
|
||||||
.unwrap_or_default()
|
bytesize::ByteSize::b(*mpu_counters.get(mpu_table::BYTES).unwrap_or(&0) as u64);
|
||||||
|
println!(
|
||||||
|
"Size of unfinished multipart uploads: {} ({})",
|
||||||
|
mpu_size.to_string_as(true),
|
||||||
|
mpu_size.to_string_as(false),
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("\nWebsite access: {}", p.website_config.get().is_some());
|
println!("\nWebsite access: {}", p.website_config.get().is_some());
|
||||||
|
@ -385,29 +395,49 @@ pub fn print_block_error_list(el: Vec<BlockResyncErrorInfo>) {
|
||||||
format_table(table);
|
format_table(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_block_info(hash: Hash, refcount: u64, versions: Vec<Result<Version, Uuid>>) {
|
pub fn print_block_info(
|
||||||
|
hash: Hash,
|
||||||
|
refcount: u64,
|
||||||
|
versions: Vec<Result<Version, Uuid>>,
|
||||||
|
uploads: Vec<MultipartUpload>,
|
||||||
|
) {
|
||||||
println!("Block hash: {}", hex::encode(hash.as_slice()));
|
println!("Block hash: {}", hex::encode(hash.as_slice()));
|
||||||
println!("Refcount: {}", refcount);
|
println!("Refcount: {}", refcount);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let mut table = vec!["Version\tBucket\tKey\tDeleted".into()];
|
let mut table = vec!["Version\tBucket\tKey\tMPU\tDeleted".into()];
|
||||||
let mut nondeleted_count = 0;
|
let mut nondeleted_count = 0;
|
||||||
for v in versions.iter() {
|
for v in versions.iter() {
|
||||||
match v {
|
match v {
|
||||||
Ok(ver) => {
|
Ok(ver) => {
|
||||||
table.push(format!(
|
match &ver.backlink {
|
||||||
"{:?}\t{:?}\t{}\t{:?}",
|
VersionBacklink::Object { bucket_id, key } => {
|
||||||
ver.uuid,
|
table.push(format!(
|
||||||
ver.bucket_id,
|
"{:?}\t{:?}\t{}\t\t{:?}",
|
||||||
ver.key,
|
ver.uuid,
|
||||||
ver.deleted.get()
|
bucket_id,
|
||||||
));
|
key,
|
||||||
|
ver.deleted.get()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
VersionBacklink::MultipartUpload { upload_id } => {
|
||||||
|
let upload = uploads.iter().find(|x| x.upload_id == *upload_id);
|
||||||
|
table.push(format!(
|
||||||
|
"{:?}\t{:?}\t{}\t{:?}\t{:?}",
|
||||||
|
ver.uuid,
|
||||||
|
upload.map(|u| u.bucket_id).unwrap_or_default(),
|
||||||
|
upload.map(|u| u.key.as_str()).unwrap_or_default(),
|
||||||
|
upload_id,
|
||||||
|
ver.deleted.get()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
if !ver.deleted.get() {
|
if !ver.deleted.get() {
|
||||||
nondeleted_count += 1;
|
nondeleted_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(vh) => {
|
Err(vh) => {
|
||||||
table.push(format!("{:?}\t\t\tyes", vh));
|
table.push(format!("{:?}\t\t\t\tyes", vh));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,16 @@ use async_trait::async_trait;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
|
|
||||||
use garage_block::repair::ScrubWorkerCommand;
|
use garage_block::repair::ScrubWorkerCommand;
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_model::s3::block_ref_table::*;
|
use garage_model::s3::block_ref_table::*;
|
||||||
|
use garage_model::s3::mpu_table::*;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::*;
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
|
use garage_table::replication::*;
|
||||||
use garage_table::*;
|
use garage_table::*;
|
||||||
|
|
||||||
use garage_util::background::*;
|
use garage_util::background::*;
|
||||||
use garage_util::error::Error;
|
use garage_util::error::Error;
|
||||||
use garage_util::migrate::Migrate;
|
use garage_util::migrate::Migrate;
|
||||||
|
@ -32,11 +37,15 @@ pub async fn launch_online_repair(
|
||||||
}
|
}
|
||||||
RepairWhat::Versions => {
|
RepairWhat::Versions => {
|
||||||
info!("Repairing the versions table");
|
info!("Repairing the versions table");
|
||||||
bg.spawn_worker(RepairVersionsWorker::new(garage.clone()));
|
bg.spawn_worker(TableRepairWorker::new(garage.clone(), RepairVersions));
|
||||||
|
}
|
||||||
|
RepairWhat::MultipartUploads => {
|
||||||
|
info!("Repairing the multipart uploads table");
|
||||||
|
bg.spawn_worker(TableRepairWorker::new(garage.clone(), RepairMpu));
|
||||||
}
|
}
|
||||||
RepairWhat::BlockRefs => {
|
RepairWhat::BlockRefs => {
|
||||||
info!("Repairing the block refs table");
|
info!("Repairing the block refs table");
|
||||||
bg.spawn_worker(RepairBlockrefsWorker::new(garage.clone()));
|
bg.spawn_worker(TableRepairWorker::new(garage.clone(), RepairBlockRefs));
|
||||||
}
|
}
|
||||||
RepairWhat::Blocks => {
|
RepairWhat::Blocks => {
|
||||||
info!("Repairing the stored blocks");
|
info!("Repairing the stored blocks");
|
||||||
|
@ -67,70 +76,70 @@ pub async fn launch_online_repair(
|
||||||
|
|
||||||
// ----
|
// ----
|
||||||
|
|
||||||
struct RepairVersionsWorker {
|
#[async_trait]
|
||||||
|
trait TableRepair: Send + Sync + 'static {
|
||||||
|
type T: TableSchema;
|
||||||
|
|
||||||
|
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication>;
|
||||||
|
|
||||||
|
async fn process(
|
||||||
|
&mut self,
|
||||||
|
garage: &Garage,
|
||||||
|
entry: <<Self as TableRepair>::T as TableSchema>::E,
|
||||||
|
) -> Result<bool, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TableRepairWorker<T: TableRepair> {
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
pos: Vec<u8>,
|
pos: Vec<u8>,
|
||||||
counter: usize,
|
counter: usize,
|
||||||
|
repairs: usize,
|
||||||
|
inner: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepairVersionsWorker {
|
impl<R: TableRepair> TableRepairWorker<R> {
|
||||||
fn new(garage: Arc<Garage>) -> Self {
|
fn new(garage: Arc<Garage>, inner: R) -> Self {
|
||||||
Self {
|
Self {
|
||||||
garage,
|
garage,
|
||||||
|
inner,
|
||||||
pos: vec![],
|
pos: vec![],
|
||||||
counter: 0,
|
counter: 0,
|
||||||
|
repairs: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Worker for RepairVersionsWorker {
|
impl<R: TableRepair> Worker for TableRepairWorker<R> {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"Version repair worker".into()
|
format!("{} repair worker", R::T::TABLE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status(&self) -> WorkerStatus {
|
fn status(&self) -> WorkerStatus {
|
||||||
WorkerStatus {
|
WorkerStatus {
|
||||||
progress: Some(self.counter.to_string()),
|
progress: Some(format!("{} ({})", self.counter, self.repairs)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
|
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
|
||||||
let (item_bytes, next_pos) = match self.garage.version_table.data.store.get_gt(&self.pos)? {
|
let (item_bytes, next_pos) = match R::table(&self.garage).data.store.get_gt(&self.pos)? {
|
||||||
Some((k, v)) => (v, k),
|
Some((k, v)) => (v, k),
|
||||||
None => {
|
None => {
|
||||||
info!("repair_versions: finished, done {}", self.counter);
|
info!(
|
||||||
|
"{}: finished, done {}, fixed {}",
|
||||||
|
self.name(),
|
||||||
|
self.counter,
|
||||||
|
self.repairs
|
||||||
|
);
|
||||||
return Ok(WorkerState::Done);
|
return Ok(WorkerState::Done);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let version = Version::decode(&item_bytes).ok_or_message("Cannot decode Version")?;
|
let entry = <R::T as TableSchema>::E::decode(&item_bytes)
|
||||||
if !version.deleted.get() {
|
.ok_or_message("Cannot decode table entry")?;
|
||||||
let object = self
|
if self.inner.process(&self.garage, entry).await? {
|
||||||
.garage
|
self.repairs += 1;
|
||||||
.object_table
|
|
||||||
.get(&version.bucket_id, &version.key)
|
|
||||||
.await?;
|
|
||||||
let version_exists = match object {
|
|
||||||
Some(o) => o
|
|
||||||
.versions()
|
|
||||||
.iter()
|
|
||||||
.any(|x| x.uuid == version.uuid && x.state != ObjectVersionState::Aborted),
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
if !version_exists {
|
|
||||||
info!("Repair versions: marking version as deleted: {:?}", version);
|
|
||||||
self.garage
|
|
||||||
.version_table
|
|
||||||
.insert(&Version::new(
|
|
||||||
version.uuid,
|
|
||||||
version.bucket_id,
|
|
||||||
version.key,
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.counter += 1;
|
self.counter += 1;
|
||||||
|
@ -146,77 +155,124 @@ impl Worker for RepairVersionsWorker {
|
||||||
|
|
||||||
// ----
|
// ----
|
||||||
|
|
||||||
struct RepairBlockrefsWorker {
|
struct RepairVersions;
|
||||||
garage: Arc<Garage>,
|
|
||||||
pos: Vec<u8>,
|
|
||||||
counter: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepairBlockrefsWorker {
|
|
||||||
fn new(garage: Arc<Garage>) -> Self {
|
|
||||||
Self {
|
|
||||||
garage,
|
|
||||||
pos: vec![],
|
|
||||||
counter: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Worker for RepairBlockrefsWorker {
|
impl TableRepair for RepairVersions {
|
||||||
fn name(&self) -> String {
|
type T = VersionTable;
|
||||||
"Block refs repair worker".into()
|
|
||||||
|
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication> {
|
||||||
|
&garage.version_table
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status(&self) -> WorkerStatus {
|
async fn process(&mut self, garage: &Garage, version: Version) -> Result<bool, Error> {
|
||||||
WorkerStatus {
|
if !version.deleted.get() {
|
||||||
progress: Some(self.counter.to_string()),
|
let ref_exists = match &version.backlink {
|
||||||
..Default::default()
|
VersionBacklink::Object { bucket_id, key } => garage
|
||||||
}
|
.object_table
|
||||||
}
|
.get(bucket_id, key)
|
||||||
|
.await?
|
||||||
async fn work(&mut self, _must_exit: &mut watch::Receiver<bool>) -> Result<WorkerState, Error> {
|
.map(|o| {
|
||||||
let (item_bytes, next_pos) =
|
o.versions().iter().any(|x| {
|
||||||
match self.garage.block_ref_table.data.store.get_gt(&self.pos)? {
|
x.uuid == version.uuid && x.state != ObjectVersionState::Aborted
|
||||||
Some((k, v)) => (v, k),
|
})
|
||||||
None => {
|
})
|
||||||
info!("repair_block_ref: finished, done {}", self.counter);
|
.unwrap_or(false),
|
||||||
return Ok(WorkerState::Done);
|
VersionBacklink::MultipartUpload { upload_id } => garage
|
||||||
}
|
.mpu_table
|
||||||
|
.get(upload_id, &EmptyKey)
|
||||||
|
.await?
|
||||||
|
.map(|u| !u.deleted.get())
|
||||||
|
.unwrap_or(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let block_ref = BlockRef::decode(&item_bytes).ok_or_message("Cannot decode BlockRef")?;
|
if !ref_exists {
|
||||||
|
info!("Repair versions: marking version as deleted: {:?}", version);
|
||||||
|
garage
|
||||||
|
.version_table
|
||||||
|
.insert(&Version::new(version.uuid, version.backlink, true))
|
||||||
|
.await?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
|
struct RepairBlockRefs;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TableRepair for RepairBlockRefs {
|
||||||
|
type T = BlockRefTable;
|
||||||
|
|
||||||
|
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication> {
|
||||||
|
&garage.block_ref_table
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process(&mut self, garage: &Garage, mut block_ref: BlockRef) -> Result<bool, Error> {
|
||||||
if !block_ref.deleted.get() {
|
if !block_ref.deleted.get() {
|
||||||
let version = self
|
let ref_exists = garage
|
||||||
.garage
|
|
||||||
.version_table
|
.version_table
|
||||||
.get(&block_ref.version, &EmptyKey)
|
.get(&block_ref.version, &EmptyKey)
|
||||||
.await?;
|
.await?
|
||||||
// The version might not exist if it has been GC'ed
|
.map(|v| !v.deleted.get())
|
||||||
let ref_exists = version.map(|v| !v.deleted.get()).unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if !ref_exists {
|
if !ref_exists {
|
||||||
info!(
|
info!(
|
||||||
"Repair block ref: marking block_ref as deleted: {:?}",
|
"Repair block ref: marking block_ref as deleted: {:?}",
|
||||||
block_ref
|
block_ref
|
||||||
);
|
);
|
||||||
self.garage
|
block_ref.deleted.set();
|
||||||
.block_ref_table
|
garage.block_ref_table.insert(&block_ref).await?;
|
||||||
.insert(&BlockRef {
|
return Ok(true);
|
||||||
block: block_ref.block,
|
|
||||||
version: block_ref.version,
|
|
||||||
deleted: true.into(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.counter += 1;
|
Ok(false)
|
||||||
self.pos = next_pos;
|
}
|
||||||
|
}
|
||||||
Ok(WorkerState::Busy)
|
|
||||||
}
|
// ----
|
||||||
|
|
||||||
async fn wait_for_work(&mut self) -> WorkerState {
|
struct RepairMpu;
|
||||||
unreachable!()
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TableRepair for RepairMpu {
|
||||||
|
type T = MultipartUploadTable;
|
||||||
|
|
||||||
|
fn table(garage: &Garage) -> &Table<Self::T, TableShardedReplication> {
|
||||||
|
&garage.mpu_table
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process(&mut self, garage: &Garage, mut mpu: MultipartUpload) -> Result<bool, Error> {
|
||||||
|
if !mpu.deleted.get() {
|
||||||
|
let ref_exists = garage
|
||||||
|
.object_table
|
||||||
|
.get(&mpu.bucket_id, &mpu.key)
|
||||||
|
.await?
|
||||||
|
.map(|o| {
|
||||||
|
o.versions()
|
||||||
|
.iter()
|
||||||
|
.any(|x| x.uuid == mpu.upload_id && x.is_uploading(Some(true)))
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !ref_exists {
|
||||||
|
info!(
|
||||||
|
"Repair multipart uploads: marking mpu as deleted: {:?}",
|
||||||
|
mpu
|
||||||
|
);
|
||||||
|
mpu.parts.clear();
|
||||||
|
mpu.deleted.set();
|
||||||
|
garage.mpu_table.insert(&mpu).await?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,190 @@ use aws_sdk_s3::types::ByteStream;
|
||||||
const SZ_5MB: usize = 5 * 1024 * 1024;
|
const SZ_5MB: usize = 5 * 1024 * 1024;
|
||||||
const SZ_10MB: usize = 10 * 1024 * 1024;
|
const SZ_10MB: usize = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_multipart_upload() {
|
||||||
|
let ctx = common::context();
|
||||||
|
let bucket = ctx.create_bucket("testmpu");
|
||||||
|
|
||||||
|
let u1 = vec![0x11; SZ_5MB];
|
||||||
|
let u2 = vec![0x22; SZ_5MB];
|
||||||
|
let u3 = vec![0x33; SZ_5MB];
|
||||||
|
let u4 = vec![0x44; SZ_5MB];
|
||||||
|
let u5 = vec![0x55; SZ_5MB];
|
||||||
|
|
||||||
|
let up = ctx
|
||||||
|
.client
|
||||||
|
.create_multipart_upload()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(up.upload_id.is_some());
|
||||||
|
|
||||||
|
let uid = up.upload_id.as_ref().unwrap();
|
||||||
|
|
||||||
|
let p3 = ctx
|
||||||
|
.client
|
||||||
|
.upload_part()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.upload_id(uid)
|
||||||
|
.part_number(3)
|
||||||
|
.body(ByteStream::from(u3.clone()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let _p1 = ctx
|
||||||
|
.client
|
||||||
|
.upload_part()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.upload_id(uid)
|
||||||
|
.part_number(1)
|
||||||
|
.body(ByteStream::from(u1))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let _p4 = ctx
|
||||||
|
.client
|
||||||
|
.upload_part()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.upload_id(uid)
|
||||||
|
.part_number(4)
|
||||||
|
.body(ByteStream::from(u4))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let p1bis = ctx
|
||||||
|
.client
|
||||||
|
.upload_part()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.upload_id(uid)
|
||||||
|
.part_number(1)
|
||||||
|
.body(ByteStream::from(u2.clone()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let p6 = ctx
|
||||||
|
.client
|
||||||
|
.upload_part()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.upload_id(uid)
|
||||||
|
.part_number(6)
|
||||||
|
.body(ByteStream::from(u5.clone()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let r = ctx
|
||||||
|
.client
|
||||||
|
.list_parts()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.upload_id(uid)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(r.parts.unwrap().len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmp = CompletedMultipartUpload::builder()
|
||||||
|
.parts(
|
||||||
|
CompletedPart::builder()
|
||||||
|
.part_number(1)
|
||||||
|
.e_tag(p1bis.e_tag.unwrap())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.parts(
|
||||||
|
CompletedPart::builder()
|
||||||
|
.part_number(3)
|
||||||
|
.e_tag(p3.e_tag.unwrap())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.parts(
|
||||||
|
CompletedPart::builder()
|
||||||
|
.part_number(6)
|
||||||
|
.e_tag(p6.e_tag.unwrap())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ctx.client
|
||||||
|
.complete_multipart_upload()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.upload_id(uid)
|
||||||
|
.multipart_upload(cmp)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The multipart upload must not appear anymore
|
||||||
|
assert!(ctx
|
||||||
|
.client
|
||||||
|
.list_parts()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.upload_id(uid)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
{
|
||||||
|
// The object must appear as a regular object
|
||||||
|
let r = ctx
|
||||||
|
.client
|
||||||
|
.head_object()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(r.content_length, (SZ_5MB * 3) as i64);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let o = ctx
|
||||||
|
.client
|
||||||
|
.get_object()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_bytes_eq!(o.body, &[&u2[..], &u3[..], &u5[..]].concat());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
for (part_number, data) in [(1, &u2), (2, &u3), (3, &u5)] {
|
||||||
|
let o = ctx
|
||||||
|
.client
|
||||||
|
.get_object()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key("a")
|
||||||
|
.part_number(part_number)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
eprintln!("get_object with part_number = {}", part_number);
|
||||||
|
assert_eq!(o.content_length, SZ_5MB as i64);
|
||||||
|
assert_bytes_eq!(o.body, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_uploadlistpart() {
|
async fn test_uploadlistpart() {
|
||||||
let ctx = common::context();
|
let ctx = common::context();
|
||||||
|
@ -65,7 +249,8 @@ async fn test_uploadlistpart() {
|
||||||
|
|
||||||
let ps = r.parts.unwrap();
|
let ps = r.parts.unwrap();
|
||||||
assert_eq!(ps.len(), 1);
|
assert_eq!(ps.len(), 1);
|
||||||
let fp = ps.iter().find(|x| x.part_number == 2).unwrap();
|
assert_eq!(ps[0].part_number, 2);
|
||||||
|
let fp = &ps[0];
|
||||||
assert!(fp.last_modified.is_some());
|
assert!(fp.last_modified.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fp.e_tag.as_ref().unwrap(),
|
fp.e_tag.as_ref().unwrap(),
|
||||||
|
@ -100,13 +285,24 @@ async fn test_uploadlistpart() {
|
||||||
|
|
||||||
let ps = r.parts.unwrap();
|
let ps = r.parts.unwrap();
|
||||||
assert_eq!(ps.len(), 2);
|
assert_eq!(ps.len(), 2);
|
||||||
let fp = ps.iter().find(|x| x.part_number == 1).unwrap();
|
|
||||||
|
assert_eq!(ps[0].part_number, 1);
|
||||||
|
let fp = &ps[0];
|
||||||
assert!(fp.last_modified.is_some());
|
assert!(fp.last_modified.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fp.e_tag.as_ref().unwrap(),
|
fp.e_tag.as_ref().unwrap(),
|
||||||
"\"3c484266f9315485694556e6c693bfa2\""
|
"\"3c484266f9315485694556e6c693bfa2\""
|
||||||
);
|
);
|
||||||
assert_eq!(fp.size, SZ_5MB as i64);
|
assert_eq!(fp.size, SZ_5MB as i64);
|
||||||
|
|
||||||
|
assert_eq!(ps[1].part_number, 2);
|
||||||
|
let sp = &ps[1];
|
||||||
|
assert!(sp.last_modified.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
sp.e_tag.as_ref().unwrap(),
|
||||||
|
"\"3366bb9dcf710d6801b5926467d02e19\""
|
||||||
|
);
|
||||||
|
assert_eq!(sp.size, SZ_5MB as i64);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -123,12 +319,19 @@ async fn test_uploadlistpart() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(r.part_number_marker.is_none());
|
assert!(r.part_number_marker.is_none());
|
||||||
assert!(r.next_part_number_marker.is_some());
|
assert_eq!(r.next_part_number_marker.as_deref(), Some("1"));
|
||||||
assert_eq!(r.max_parts, 1_i32);
|
assert_eq!(r.max_parts, 1_i32);
|
||||||
assert!(r.is_truncated);
|
assert!(r.is_truncated);
|
||||||
assert_eq!(r.key.unwrap(), "a");
|
assert_eq!(r.key.unwrap(), "a");
|
||||||
assert_eq!(r.upload_id.unwrap().as_str(), uid.as_str());
|
assert_eq!(r.upload_id.unwrap().as_str(), uid.as_str());
|
||||||
assert_eq!(r.parts.unwrap().len(), 1);
|
let parts = r.parts.unwrap();
|
||||||
|
assert_eq!(parts.len(), 1);
|
||||||
|
let fp = &parts[0];
|
||||||
|
assert_eq!(fp.part_number, 1);
|
||||||
|
assert_eq!(
|
||||||
|
fp.e_tag.as_ref().unwrap(),
|
||||||
|
"\"3c484266f9315485694556e6c693bfa2\""
|
||||||
|
);
|
||||||
|
|
||||||
let r2 = ctx
|
let r2 = ctx
|
||||||
.client
|
.client
|
||||||
|
@ -147,10 +350,18 @@ async fn test_uploadlistpart() {
|
||||||
r.next_part_number_marker.as_ref().unwrap()
|
r.next_part_number_marker.as_ref().unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(r2.max_parts, 1_i32);
|
assert_eq!(r2.max_parts, 1_i32);
|
||||||
assert!(r2.is_truncated);
|
|
||||||
assert_eq!(r2.key.unwrap(), "a");
|
assert_eq!(r2.key.unwrap(), "a");
|
||||||
assert_eq!(r2.upload_id.unwrap().as_str(), uid.as_str());
|
assert_eq!(r2.upload_id.unwrap().as_str(), uid.as_str());
|
||||||
assert_eq!(r2.parts.unwrap().len(), 1);
|
let parts = r2.parts.unwrap();
|
||||||
|
assert_eq!(parts.len(), 1);
|
||||||
|
let fp = &parts[0];
|
||||||
|
assert_eq!(fp.part_number, 2);
|
||||||
|
assert_eq!(
|
||||||
|
fp.e_tag.as_ref().unwrap(),
|
||||||
|
"\"3366bb9dcf710d6801b5926467d02e19\""
|
||||||
|
);
|
||||||
|
//assert!(r2.is_truncated); // WHY? (this was the test before)
|
||||||
|
assert!(!r2.is_truncated);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmp = CompletedMultipartUpload::builder()
|
let cmp = CompletedMultipartUpload::builder()
|
||||||
|
|
|
@ -17,6 +17,7 @@ use garage_table::replication::TableShardedReplication;
|
||||||
use garage_table::*;
|
use garage_table::*;
|
||||||
|
|
||||||
use crate::s3::block_ref_table::*;
|
use crate::s3::block_ref_table::*;
|
||||||
|
use crate::s3::mpu_table::*;
|
||||||
use crate::s3::object_table::*;
|
use crate::s3::object_table::*;
|
||||||
use crate::s3::version_table::*;
|
use crate::s3::version_table::*;
|
||||||
|
|
||||||
|
@ -57,6 +58,10 @@ pub struct Garage {
|
||||||
pub object_table: Arc<Table<ObjectTable, TableShardedReplication>>,
|
pub object_table: Arc<Table<ObjectTable, TableShardedReplication>>,
|
||||||
/// Counting table containing object counters
|
/// Counting table containing object counters
|
||||||
pub object_counter_table: Arc<IndexCounter<Object>>,
|
pub object_counter_table: Arc<IndexCounter<Object>>,
|
||||||
|
/// Table containing S3 multipart uploads
|
||||||
|
pub mpu_table: Arc<Table<MultipartUploadTable, TableShardedReplication>>,
|
||||||
|
/// Counting table containing multipart object counters
|
||||||
|
pub mpu_counter_table: Arc<IndexCounter<MultipartUpload>>,
|
||||||
/// Table containing S3 object versions
|
/// Table containing S3 object versions
|
||||||
pub version_table: Arc<Table<VersionTable, TableShardedReplication>>,
|
pub version_table: Arc<Table<VersionTable, TableShardedReplication>>,
|
||||||
/// Table containing S3 block references (not blocks themselves)
|
/// Table containing S3 block references (not blocks themselves)
|
||||||
|
@ -261,6 +266,20 @@ impl Garage {
|
||||||
&db,
|
&db,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
info!("Initialize multipart upload counter table...");
|
||||||
|
let mpu_counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), &db);
|
||||||
|
|
||||||
|
info!("Initialize multipart upload table...");
|
||||||
|
let mpu_table = Table::new(
|
||||||
|
MultipartUploadTable {
|
||||||
|
version_table: version_table.clone(),
|
||||||
|
mpu_counter_table: mpu_counter_table.clone(),
|
||||||
|
},
|
||||||
|
meta_rep_param.clone(),
|
||||||
|
system.clone(),
|
||||||
|
&db,
|
||||||
|
);
|
||||||
|
|
||||||
info!("Initialize object counter table...");
|
info!("Initialize object counter table...");
|
||||||
let object_counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), &db);
|
let object_counter_table = IndexCounter::new(system.clone(), meta_rep_param.clone(), &db);
|
||||||
|
|
||||||
|
@ -269,6 +288,7 @@ impl Garage {
|
||||||
let object_table = Table::new(
|
let object_table = Table::new(
|
||||||
ObjectTable {
|
ObjectTable {
|
||||||
version_table: version_table.clone(),
|
version_table: version_table.clone(),
|
||||||
|
mpu_table: mpu_table.clone(),
|
||||||
object_counter_table: object_counter_table.clone(),
|
object_counter_table: object_counter_table.clone(),
|
||||||
},
|
},
|
||||||
meta_rep_param.clone(),
|
meta_rep_param.clone(),
|
||||||
|
@ -297,6 +317,8 @@ impl Garage {
|
||||||
key_table,
|
key_table,
|
||||||
object_table,
|
object_table,
|
||||||
object_counter_table,
|
object_counter_table,
|
||||||
|
mpu_table,
|
||||||
|
mpu_counter_table,
|
||||||
version_table,
|
version_table,
|
||||||
block_ref_table,
|
block_ref_table,
|
||||||
#[cfg(feature = "k2v")]
|
#[cfg(feature = "k2v")]
|
||||||
|
@ -313,6 +335,8 @@ impl Garage {
|
||||||
|
|
||||||
self.object_table.spawn_workers(bg);
|
self.object_table.spawn_workers(bg);
|
||||||
self.object_counter_table.spawn_workers(bg);
|
self.object_counter_table.spawn_workers(bg);
|
||||||
|
self.mpu_table.spawn_workers(bg);
|
||||||
|
self.mpu_counter_table.spawn_workers(bg);
|
||||||
self.version_table.spawn_workers(bg);
|
self.version_table.spawn_workers(bg);
|
||||||
self.block_ref_table.spawn_workers(bg);
|
self.block_ref_table.spawn_workers(bg);
|
||||||
|
|
||||||
|
|
|
@ -478,7 +478,9 @@ impl<'a> BucketHelper<'a> {
|
||||||
// ----
|
// ----
|
||||||
|
|
||||||
/// Deletes all incomplete multipart uploads that are older than a certain time.
|
/// Deletes all incomplete multipart uploads that are older than a certain time.
|
||||||
/// Returns the number of uploads aborted
|
/// Returns the number of uploads aborted.
|
||||||
|
/// This will also include non-multipart uploads, which may be lingering
|
||||||
|
/// after a node crash
|
||||||
pub async fn cleanup_incomplete_uploads(
|
pub async fn cleanup_incomplete_uploads(
|
||||||
&self,
|
&self,
|
||||||
bucket_id: &Uuid,
|
bucket_id: &Uuid,
|
||||||
|
@ -496,7 +498,9 @@ impl<'a> BucketHelper<'a> {
|
||||||
.get_range(
|
.get_range(
|
||||||
bucket_id,
|
bucket_id,
|
||||||
start,
|
start,
|
||||||
Some(ObjectFilter::IsUploading),
|
Some(ObjectFilter::IsUploading {
|
||||||
|
check_multipart: None,
|
||||||
|
}),
|
||||||
1000,
|
1000,
|
||||||
EnumerationOrder::Forward,
|
EnumerationOrder::Forward,
|
||||||
)
|
)
|
||||||
|
@ -508,7 +512,7 @@ impl<'a> BucketHelper<'a> {
|
||||||
let aborted_versions = object
|
let aborted_versions = object
|
||||||
.versions()
|
.versions()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| v.is_uploading() && v.timestamp < older_than)
|
.filter(|v| v.is_uploading(None) && v.timestamp < older_than)
|
||||||
.map(|v| ObjectVersion {
|
.map(|v| ObjectVersion {
|
||||||
state: ObjectVersionState::Aborted,
|
state: ObjectVersionState::Aborted,
|
||||||
uuid: v.uuid,
|
uuid: v.uuid,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod block_ref_table;
|
pub mod block_ref_table;
|
||||||
|
pub mod mpu_table;
|
||||||
pub mod object_table;
|
pub mod object_table;
|
||||||
pub mod version_table;
|
pub mod version_table;
|
||||||
|
|
245
src/model/s3/mpu_table.rs
Normal file
245
src/model/s3/mpu_table.rs
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use garage_db as db;
|
||||||
|
|
||||||
|
use garage_util::crdt::Crdt;
|
||||||
|
use garage_util::data::*;
|
||||||
|
use garage_util::time::*;
|
||||||
|
|
||||||
|
use garage_table::replication::TableShardedReplication;
|
||||||
|
use garage_table::*;
|
||||||
|
|
||||||
|
use crate::index_counter::*;
|
||||||
|
use crate::s3::version_table::*;
|
||||||
|
|
||||||
|
pub const UPLOADS: &str = "uploads";
|
||||||
|
pub const PARTS: &str = "parts";
|
||||||
|
pub const BYTES: &str = "bytes";
|
||||||
|
|
||||||
|
mod v09 {
|
||||||
|
use garage_util::crdt;
|
||||||
|
use garage_util::data::Uuid;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A part of a multipart upload
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct MultipartUpload {
|
||||||
|
/// Partition key = Upload id = UUID of the object version
|
||||||
|
pub upload_id: Uuid,
|
||||||
|
|
||||||
|
/// Is this multipart upload deleted
|
||||||
|
/// The MultipartUpload is marked as deleted as soon as the
|
||||||
|
/// multipart upload is either completed or aborted
|
||||||
|
pub deleted: crdt::Bool,
|
||||||
|
/// List of uploaded parts, key = (part number, timestamp)
|
||||||
|
/// In case of retries, all versions for each part are kept
|
||||||
|
/// Everything is cleaned up only once the MultipartUpload is marked deleted
|
||||||
|
pub parts: crdt::Map<MpuPartKey, MpuPart>,
|
||||||
|
|
||||||
|
// Back link to bucket+key so that we can find the object this mpu
|
||||||
|
// belongs to and check whether it is still valid
|
||||||
|
/// Bucket in which the related object is stored
|
||||||
|
pub bucket_id: Uuid,
|
||||||
|
/// Key in which the related object is stored
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct MpuPartKey {
|
||||||
|
/// Number of the part
|
||||||
|
pub part_number: u64,
|
||||||
|
/// Timestamp of part upload
|
||||||
|
pub timestamp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The version of an uploaded part
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct MpuPart {
|
||||||
|
/// Links to a Version in VersionTable
|
||||||
|
pub version: Uuid,
|
||||||
|
/// ETag of the content of this part (known only once done uploading)
|
||||||
|
pub etag: Option<String>,
|
||||||
|
/// Size of this part (known only once done uploading)
|
||||||
|
pub size: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl garage_util::migrate::InitialFormat for MultipartUpload {
|
||||||
|
const VERSION_MARKER: &'static [u8] = b"G09s3mpu";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use v09::*;
|
||||||
|
|
||||||
|
impl Ord for MpuPartKey {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.part_number
|
||||||
|
.cmp(&other.part_number)
|
||||||
|
.then(self.timestamp.cmp(&other.timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for MpuPartKey {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultipartUpload {
|
||||||
|
pub fn new(upload_id: Uuid, bucket_id: Uuid, key: String, deleted: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
upload_id,
|
||||||
|
deleted: crdt::Bool::new(deleted),
|
||||||
|
parts: crdt::Map::new(),
|
||||||
|
bucket_id,
|
||||||
|
key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_timestamp(&self, part_number: u64) -> u64 {
|
||||||
|
std::cmp::max(
|
||||||
|
now_msec(),
|
||||||
|
1 + self
|
||||||
|
.parts
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.filter(|(x, _)| x.part_number == part_number)
|
||||||
|
.map(|(x, _)| x.timestamp)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry<Uuid, EmptyKey> for MultipartUpload {
|
||||||
|
fn partition_key(&self) -> &Uuid {
|
||||||
|
&self.upload_id
|
||||||
|
}
|
||||||
|
fn sort_key(&self) -> &EmptyKey {
|
||||||
|
&EmptyKey
|
||||||
|
}
|
||||||
|
fn is_tombstone(&self) -> bool {
|
||||||
|
self.deleted.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crdt for MultipartUpload {
|
||||||
|
fn merge(&mut self, other: &Self) {
|
||||||
|
self.deleted.merge(&other.deleted);
|
||||||
|
|
||||||
|
if self.deleted.get() {
|
||||||
|
self.parts.clear();
|
||||||
|
} else {
|
||||||
|
self.parts.merge(&other.parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crdt for MpuPart {
|
||||||
|
fn merge(&mut self, other: &Self) {
|
||||||
|
self.etag = match (self.etag.take(), &other.etag) {
|
||||||
|
(None, Some(_)) => other.etag.clone(),
|
||||||
|
(Some(x), Some(y)) if x < *y => other.etag.clone(),
|
||||||
|
(x, _) => x,
|
||||||
|
};
|
||||||
|
self.size = match (self.size, other.size) {
|
||||||
|
(None, Some(_)) => other.size,
|
||||||
|
(Some(x), Some(y)) if x < y => other.size,
|
||||||
|
(x, _) => x,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MultipartUploadTable {
|
||||||
|
pub version_table: Arc<Table<VersionTable, TableShardedReplication>>,
|
||||||
|
pub mpu_counter_table: Arc<IndexCounter<MultipartUpload>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableSchema for MultipartUploadTable {
|
||||||
|
const TABLE_NAME: &'static str = "multipart_upload";
|
||||||
|
|
||||||
|
type P = Uuid;
|
||||||
|
type S = EmptyKey;
|
||||||
|
type E = MultipartUpload;
|
||||||
|
type Filter = DeletedFilter;
|
||||||
|
|
||||||
|
fn updated(
|
||||||
|
&self,
|
||||||
|
tx: &mut db::Transaction,
|
||||||
|
old: Option<&Self::E>,
|
||||||
|
new: Option<&Self::E>,
|
||||||
|
) -> db::TxOpResult<()> {
|
||||||
|
// 1. Count
|
||||||
|
let counter_res = self.mpu_counter_table.count(tx, old, new);
|
||||||
|
if let Err(e) = db::unabort(counter_res)? {
|
||||||
|
error!(
|
||||||
|
"Unable to update multipart object part counter: {}. Index values will be wrong!",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Propagate deletions to version table
|
||||||
|
if let (Some(old_mpu), Some(new_mpu)) = (old, new) {
|
||||||
|
if new_mpu.deleted.get() && !old_mpu.deleted.get() {
|
||||||
|
let deleted_versions = old_mpu.parts.items().iter().map(|(_k, p)| {
|
||||||
|
Version::new(
|
||||||
|
p.version,
|
||||||
|
VersionBacklink::MultipartUpload {
|
||||||
|
upload_id: old_mpu.upload_id,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
for version in deleted_versions {
|
||||||
|
let res = self.version_table.queue_insert(tx, &version);
|
||||||
|
if let Err(e) = db::unabort(res)? {
|
||||||
|
error!("Unable to enqueue version deletion propagation: {}. A repair will be needed.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool {
|
||||||
|
filter.apply(entry.is_tombstone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CountedItem for MultipartUpload {
|
||||||
|
const COUNTER_TABLE_NAME: &'static str = "bucket_mpu_counter";
|
||||||
|
|
||||||
|
// Partition key = bucket id
|
||||||
|
type CP = Uuid;
|
||||||
|
// Sort key = nothing
|
||||||
|
type CS = EmptyKey;
|
||||||
|
|
||||||
|
fn counter_partition_key(&self) -> &Uuid {
|
||||||
|
&self.bucket_id
|
||||||
|
}
|
||||||
|
fn counter_sort_key(&self) -> &EmptyKey {
|
||||||
|
&EmptyKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fn counts(&self) -> Vec<(&'static str, i64)> {
|
||||||
|
let uploads = if self.deleted.get() { 0 } else { 1 };
|
||||||
|
let mut parts = self
|
||||||
|
.parts
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.map(|(k, _)| k.part_number)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
parts.dedup();
|
||||||
|
let bytes = self
|
||||||
|
.parts
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, p)| p.size.unwrap_or(0))
|
||||||
|
.sum::<u64>();
|
||||||
|
vec![
|
||||||
|
(UPLOADS, uploads),
|
||||||
|
(PARTS, parts.len() as i64),
|
||||||
|
(BYTES, bytes as i64),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ use garage_table::replication::TableShardedReplication;
|
||||||
use garage_table::*;
|
use garage_table::*;
|
||||||
|
|
||||||
use crate::index_counter::*;
|
use crate::index_counter::*;
|
||||||
|
use crate::s3::mpu_table::*;
|
||||||
use crate::s3::version_table::*;
|
use crate::s3::version_table::*;
|
||||||
|
|
||||||
pub const OBJECTS: &str = "objects";
|
pub const OBJECTS: &str = "objects";
|
||||||
|
@ -130,7 +131,86 @@ mod v08 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use v08::*;
|
mod v09 {
|
||||||
|
use garage_util::data::Uuid;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::v08;
|
||||||
|
|
||||||
|
pub use v08::{ObjectVersionData, ObjectVersionHeaders, ObjectVersionMeta};
|
||||||
|
|
||||||
|
/// An object
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Object {
|
||||||
|
/// The bucket in which the object is stored, used as partition key
|
||||||
|
pub bucket_id: Uuid,
|
||||||
|
|
||||||
|
/// The key at which the object is stored in its bucket, used as sorting key
|
||||||
|
pub key: String,
|
||||||
|
|
||||||
|
/// The list of currenty stored versions of the object
|
||||||
|
pub(super) versions: Vec<ObjectVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Informations about a version of an object
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ObjectVersion {
|
||||||
|
/// Id of the version
|
||||||
|
pub uuid: Uuid,
|
||||||
|
/// Timestamp of when the object was created
|
||||||
|
pub timestamp: u64,
|
||||||
|
/// State of the version
|
||||||
|
pub state: ObjectVersionState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State of an object version
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ObjectVersionState {
|
||||||
|
/// The version is being received
|
||||||
|
Uploading {
|
||||||
|
/// Indicates whether this is a multipart upload
|
||||||
|
multipart: bool,
|
||||||
|
/// Headers to be included in the final object
|
||||||
|
headers: ObjectVersionHeaders,
|
||||||
|
},
|
||||||
|
/// The version is fully received
|
||||||
|
Complete(ObjectVersionData),
|
||||||
|
/// The version uploaded containded errors or the upload was explicitly aborted
|
||||||
|
Aborted,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl garage_util::migrate::Migrate for Object {
|
||||||
|
const VERSION_MARKER: &'static [u8] = b"G09s3o";
|
||||||
|
|
||||||
|
type Previous = v08::Object;
|
||||||
|
|
||||||
|
fn migrate(old: v08::Object) -> Object {
|
||||||
|
let versions = old
|
||||||
|
.versions
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| ObjectVersion {
|
||||||
|
uuid: x.uuid,
|
||||||
|
timestamp: x.timestamp,
|
||||||
|
state: match x.state {
|
||||||
|
v08::ObjectVersionState::Uploading(h) => ObjectVersionState::Uploading {
|
||||||
|
multipart: false,
|
||||||
|
headers: h,
|
||||||
|
},
|
||||||
|
v08::ObjectVersionState::Complete(d) => ObjectVersionState::Complete(d),
|
||||||
|
v08::ObjectVersionState::Aborted => ObjectVersionState::Aborted,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Object {
|
||||||
|
bucket_id: old.bucket_id,
|
||||||
|
key: old.key,
|
||||||
|
versions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use v09::*;
|
||||||
|
|
||||||
impl Object {
|
impl Object {
|
||||||
/// Initialize an Object struct from parts
|
/// Initialize an Object struct from parts
|
||||||
|
@ -180,11 +260,11 @@ impl Crdt for ObjectVersionState {
|
||||||
Complete(a) => {
|
Complete(a) => {
|
||||||
a.merge(b);
|
a.merge(b);
|
||||||
}
|
}
|
||||||
Uploading(_) => {
|
Uploading { .. } => {
|
||||||
*self = Complete(b.clone());
|
*self = Complete(b.clone());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Uploading(_) => {}
|
Uploading { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,8 +279,17 @@ impl ObjectVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the object version currently being uploaded
|
/// Is the object version currently being uploaded
|
||||||
pub fn is_uploading(&self) -> bool {
|
///
|
||||||
matches!(self.state, ObjectVersionState::Uploading(_))
|
/// matches only multipart uploads if check_multipart is Some(true)
|
||||||
|
/// matches only non-multipart uploads if check_multipart is Some(false)
|
||||||
|
/// matches both if check_multipart is None
|
||||||
|
pub fn is_uploading(&self, check_multipart: Option<bool>) -> bool {
|
||||||
|
match &self.state {
|
||||||
|
ObjectVersionState::Uploading { multipart, .. } => {
|
||||||
|
check_multipart.map(|x| x == *multipart).unwrap_or(true)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the object version completely received
|
/// Is the object version completely received
|
||||||
|
@ -267,13 +356,20 @@ impl Crdt for Object {
|
||||||
|
|
||||||
pub struct ObjectTable {
|
pub struct ObjectTable {
|
||||||
pub version_table: Arc<Table<VersionTable, TableShardedReplication>>,
|
pub version_table: Arc<Table<VersionTable, TableShardedReplication>>,
|
||||||
|
pub mpu_table: Arc<Table<MultipartUploadTable, TableShardedReplication>>,
|
||||||
pub object_counter_table: Arc<IndexCounter<Object>>,
|
pub object_counter_table: Arc<IndexCounter<Object>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
pub enum ObjectFilter {
|
pub enum ObjectFilter {
|
||||||
|
/// Is the object version available (received and not a tombstone)
|
||||||
IsData,
|
IsData,
|
||||||
IsUploading,
|
/// Is the object version currently being uploaded
|
||||||
|
///
|
||||||
|
/// matches only multipart uploads if check_multipart is Some(true)
|
||||||
|
/// matches only non-multipart uploads if check_multipart is Some(false)
|
||||||
|
/// matches both if check_multipart is None
|
||||||
|
IsUploading { check_multipart: Option<bool> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableSchema for ObjectTable {
|
impl TableSchema for ObjectTable {
|
||||||
|
@ -301,21 +397,28 @@ impl TableSchema for ObjectTable {
|
||||||
|
|
||||||
// 2. Enqueue propagation deletions to version table
|
// 2. Enqueue propagation deletions to version table
|
||||||
if let (Some(old_v), Some(new_v)) = (old, new) {
|
if let (Some(old_v), Some(new_v)) = (old, new) {
|
||||||
// Propagate deletion of old versions
|
|
||||||
for v in old_v.versions.iter() {
|
for v in old_v.versions.iter() {
|
||||||
let newly_deleted = match new_v
|
let new_v_id = new_v
|
||||||
.versions
|
.versions
|
||||||
.binary_search_by(|nv| nv.cmp_key().cmp(&v.cmp_key()))
|
.binary_search_by(|nv| nv.cmp_key().cmp(&v.cmp_key()));
|
||||||
{
|
|
||||||
|
// Propagate deletion of old versions to the Version table
|
||||||
|
let delete_version = match new_v_id {
|
||||||
Err(_) => true,
|
Err(_) => true,
|
||||||
Ok(i) => {
|
Ok(i) => {
|
||||||
new_v.versions[i].state == ObjectVersionState::Aborted
|
new_v.versions[i].state == ObjectVersionState::Aborted
|
||||||
&& v.state != ObjectVersionState::Aborted
|
&& v.state != ObjectVersionState::Aborted
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if newly_deleted {
|
if delete_version {
|
||||||
let deleted_version =
|
let deleted_version = Version::new(
|
||||||
Version::new(v.uuid, old_v.bucket_id, old_v.key.clone(), true);
|
v.uuid,
|
||||||
|
VersionBacklink::Object {
|
||||||
|
bucket_id: old_v.bucket_id,
|
||||||
|
key: old_v.key.clone(),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
let res = self.version_table.queue_insert(tx, &deleted_version);
|
let res = self.version_table.queue_insert(tx, &deleted_version);
|
||||||
if let Err(e) = db::unabort(res)? {
|
if let Err(e) = db::unabort(res)? {
|
||||||
error!(
|
error!(
|
||||||
|
@ -324,6 +427,34 @@ impl TableSchema for ObjectTable {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After abortion or completion of multipart uploads, delete MPU table entry
|
||||||
|
if matches!(
|
||||||
|
v.state,
|
||||||
|
ObjectVersionState::Uploading {
|
||||||
|
multipart: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
let delete_mpu = match new_v_id {
|
||||||
|
Err(_) => true,
|
||||||
|
Ok(i) => !matches!(
|
||||||
|
new_v.versions[i].state,
|
||||||
|
ObjectVersionState::Uploading { .. }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if delete_mpu {
|
||||||
|
let deleted_mpu =
|
||||||
|
MultipartUpload::new(v.uuid, old_v.bucket_id, old_v.key.clone(), true);
|
||||||
|
let res = self.mpu_table.queue_insert(tx, &deleted_mpu);
|
||||||
|
if let Err(e) = db::unabort(res)? {
|
||||||
|
error!(
|
||||||
|
"Unable to enqueue multipart upload deletion propagation: {}. A repair will be needed.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,7 +464,10 @@ impl TableSchema for ObjectTable {
|
||||||
fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool {
|
fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool {
|
||||||
match filter {
|
match filter {
|
||||||
ObjectFilter::IsData => entry.versions.iter().any(|v| v.is_data()),
|
ObjectFilter::IsData => entry.versions.iter().any(|v| v.is_data()),
|
||||||
ObjectFilter::IsUploading => entry.versions.iter().any(|v| v.is_uploading()),
|
ObjectFilter::IsUploading { check_multipart } => entry
|
||||||
|
.versions
|
||||||
|
.iter()
|
||||||
|
.any(|v| v.is_uploading(*check_multipart)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -360,10 +494,7 @@ impl CountedItem for Object {
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
let n_unfinished_uploads = versions
|
let n_unfinished_uploads = versions.iter().filter(|v| v.is_uploading(None)).count();
|
||||||
.iter()
|
|
||||||
.filter(|v| matches!(v.state, ObjectVersionState::Uploading(_)))
|
|
||||||
.count();
|
|
||||||
let n_bytes = versions
|
let n_bytes = versions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| match &v.state {
|
.map(|v| match &v.state {
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||||
use garage_db as db;
|
use garage_db as db;
|
||||||
|
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
use garage_util::error::*;
|
||||||
|
|
||||||
use garage_table::crdt::*;
|
use garage_table::crdt::*;
|
||||||
use garage_table::replication::TableShardedReplication;
|
use garage_table::replication::TableShardedReplication;
|
||||||
|
@ -66,6 +67,8 @@ mod v08 {
|
||||||
|
|
||||||
use super::v05;
|
use super::v05;
|
||||||
|
|
||||||
|
pub use v05::{VersionBlock, VersionBlockKey};
|
||||||
|
|
||||||
/// A version of an object
|
/// A version of an object
|
||||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Version {
|
pub struct Version {
|
||||||
|
@ -90,8 +93,6 @@ mod v08 {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use v05::{VersionBlock, VersionBlockKey};
|
|
||||||
|
|
||||||
impl garage_util::migrate::Migrate for Version {
|
impl garage_util::migrate::Migrate for Version {
|
||||||
type Previous = v05::Version;
|
type Previous = v05::Version;
|
||||||
|
|
||||||
|
@ -110,32 +111,94 @@ mod v08 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use v08::*;
|
pub(crate) mod v09 {
|
||||||
|
use garage_util::crdt;
|
||||||
|
use garage_util::data::Uuid;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::v08;
|
||||||
|
|
||||||
|
pub use v08::{VersionBlock, VersionBlockKey};
|
||||||
|
|
||||||
|
/// A version of an object
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Version {
|
||||||
|
/// UUID of the version, used as partition key
|
||||||
|
pub uuid: Uuid,
|
||||||
|
|
||||||
|
// Actual data: the blocks for this version
|
||||||
|
// In the case of a multipart upload, also store the etags
|
||||||
|
// of individual parts and check them when doing CompleteMultipartUpload
|
||||||
|
/// Is this version deleted
|
||||||
|
pub deleted: crdt::Bool,
|
||||||
|
/// list of blocks of data composing the version
|
||||||
|
pub blocks: crdt::Map<VersionBlockKey, VersionBlock>,
|
||||||
|
|
||||||
|
// Back link to owner of this version (either an object or a multipart
|
||||||
|
// upload), used to find whether it has been deleted and this version
|
||||||
|
// should in turn be deleted (see versions repair procedure)
|
||||||
|
pub backlink: VersionBacklink,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum VersionBacklink {
|
||||||
|
Object {
|
||||||
|
/// Bucket in which the related object is stored
|
||||||
|
bucket_id: Uuid,
|
||||||
|
/// Key in which the related object is stored
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
|
MultipartUpload {
|
||||||
|
upload_id: Uuid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl garage_util::migrate::Migrate for Version {
|
||||||
|
const VERSION_MARKER: &'static [u8] = b"G09s3v";
|
||||||
|
|
||||||
|
type Previous = v08::Version;
|
||||||
|
|
||||||
|
fn migrate(old: v08::Version) -> Version {
|
||||||
|
Version {
|
||||||
|
uuid: old.uuid,
|
||||||
|
deleted: old.deleted,
|
||||||
|
blocks: old.blocks,
|
||||||
|
backlink: VersionBacklink::Object {
|
||||||
|
bucket_id: old.bucket_id,
|
||||||
|
key: old.key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use v09::*;
|
||||||
|
|
||||||
impl Version {
|
impl Version {
|
||||||
pub fn new(uuid: Uuid, bucket_id: Uuid, key: String, deleted: bool) -> Self {
|
pub fn new(uuid: Uuid, backlink: VersionBacklink, deleted: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uuid,
|
uuid,
|
||||||
deleted: deleted.into(),
|
deleted: deleted.into(),
|
||||||
blocks: crdt::Map::new(),
|
blocks: crdt::Map::new(),
|
||||||
parts_etags: crdt::Map::new(),
|
backlink,
|
||||||
bucket_id,
|
|
||||||
key,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_part_number(&self, part_number: u64) -> bool {
|
pub fn has_part_number(&self, part_number: u64) -> bool {
|
||||||
let case1 = self
|
self.blocks
|
||||||
.parts_etags
|
|
||||||
.items()
|
|
||||||
.binary_search_by(|(k, _)| k.cmp(&part_number))
|
|
||||||
.is_ok();
|
|
||||||
let case2 = self
|
|
||||||
.blocks
|
|
||||||
.items()
|
.items()
|
||||||
.binary_search_by(|(k, _)| k.part_number.cmp(&part_number))
|
.binary_search_by(|(k, _)| k.part_number.cmp(&part_number))
|
||||||
.is_ok();
|
.is_ok()
|
||||||
case1 || case2
|
}
|
||||||
|
|
||||||
|
pub fn n_parts(&self) -> Result<u64, Error> {
|
||||||
|
Ok(self
|
||||||
|
.blocks
|
||||||
|
.items()
|
||||||
|
.last()
|
||||||
|
.ok_or_message("version has no parts")?
|
||||||
|
.0
|
||||||
|
.part_number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,10 +238,8 @@ impl Crdt for Version {
|
||||||
|
|
||||||
if self.deleted.get() {
|
if self.deleted.get() {
|
||||||
self.blocks.clear();
|
self.blocks.clear();
|
||||||
self.parts_etags.clear();
|
|
||||||
} else {
|
} else {
|
||||||
self.blocks.merge(&other.blocks);
|
self.blocks.merge(&other.blocks);
|
||||||
self.parts_etags.merge(&other.parts_etags);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ mod v09 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl garage_util::migrate::Migrate for ClusterLayout {
|
impl garage_util::migrate::Migrate for ClusterLayout {
|
||||||
const VERSION_MARKER: &'static [u8] = b"Glayout09";
|
const VERSION_MARKER: &'static [u8] = b"G09layout";
|
||||||
|
|
||||||
type Previous = v08::ClusterLayout;
|
type Previous = v08::ClusterLayout;
|
||||||
|
|
||||||
|
|
|
@ -347,9 +347,7 @@ impl<F: TableSchema, R: TableReplication> TableData<F, R> {
|
||||||
// ---- Utility functions ----
|
// ---- Utility functions ----
|
||||||
|
|
||||||
pub fn tree_key(&self, p: &F::P, s: &F::S) -> Vec<u8> {
|
pub fn tree_key(&self, p: &F::P, s: &F::S) -> Vec<u8> {
|
||||||
let mut ret = p.hash().to_vec();
|
[p.hash().as_slice(), s.sort_key()].concat()
|
||||||
ret.extend(s.sort_key());
|
|
||||||
ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_entry(&self, bytes: &[u8]) -> Result<F::E, Error> {
|
pub fn decode_entry(&self, bytes: &[u8]) -> Result<F::E, Error> {
|
||||||
|
|
|
@ -6,6 +6,8 @@ use garage_util::migrate::Migrate;
|
||||||
|
|
||||||
use crate::crdt::Crdt;
|
use crate::crdt::Crdt;
|
||||||
|
|
||||||
|
// =================================== PARTITION KEYS
|
||||||
|
|
||||||
/// Trait for field used to partition data
|
/// Trait for field used to partition data
|
||||||
pub trait PartitionKey:
|
pub trait PartitionKey:
|
||||||
Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static
|
Clone + PartialEq + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static
|
||||||
|
@ -29,6 +31,8 @@ impl PartitionKey for FixedBytes32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =================================== SORT KEYS
|
||||||
|
|
||||||
/// Trait for field used to sort data
|
/// Trait for field used to sort data
|
||||||
pub trait SortKey: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static {
|
pub trait SortKey: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static {
|
||||||
/// Get the key used to sort
|
/// Get the key used to sort
|
||||||
|
@ -47,6 +51,8 @@ impl SortKey for FixedBytes32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =================================== SCHEMA
|
||||||
|
|
||||||
/// Trait for an entry in a table. It must be sortable and partitionnable.
|
/// Trait for an entry in a table. It must be sortable and partitionnable.
|
||||||
pub trait Entry<P: PartitionKey, S: SortKey>:
|
pub trait Entry<P: PartitionKey, S: SortKey>:
|
||||||
Crdt + PartialEq + Clone + Migrate + Send + Sync + 'static
|
Crdt + PartialEq + Clone + Migrate + Send + Sync + 'static
|
||||||
|
|
|
@ -106,7 +106,7 @@ impl WebServer {
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
) -> Result<Response<Body>, Infallible> {
|
) -> Result<Response<Body>, Infallible> {
|
||||||
if let Ok(forwarded_for_ip_addr) =
|
if let Ok(forwarded_for_ip_addr) =
|
||||||
forwarded_headers::handle_forwarded_for_headers(&req.headers())
|
forwarded_headers::handle_forwarded_for_headers(req.headers())
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"{} (via {}) {} {}",
|
"{} (via {}) {} {}",
|
||||||
|
|
Loading…
Reference in a new issue