From 43af36441aa6263fdb72204a858e73f6bbb09241 Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Fri, 26 Apr 2024 10:06:27 -0700 Subject: [PATCH] fix: missing URL encoding for object names (#86) URL encoding for S3 API requires that we do not encode '/' in addition to the standard characters (`_-~.` and alphanumerics). Also fixes a bug in error response parsing (bucket name was not parsed correctly). Also adds another put-object example accepting CLI args. --- Cargo.toml | 1 + examples/put-object.rs | 77 ++++++++++++++++++++++++++++++++++++++++++ src/s3/error.rs | 2 +- src/s3/http.rs | 5 +-- src/s3/utils.rs | 11 ++++++ 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 examples/put-object.rs diff --git a/Cargo.toml b/Cargo.toml index 30cd744..f28416c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ features = ["native-tls", "blocking", "rustls-tls", "stream"] [dev-dependencies] async-std = { version = "1.12.0", features = ["attributes", "tokio1"] } +clap = { version = "4.5.4", features = ["derive"] } quickcheck = "1.0.3" [[example]] diff --git a/examples/put-object.rs b/examples/put-object.rs new file mode 100644 index 0000000..9ad0dc2 --- /dev/null +++ b/examples/put-object.rs @@ -0,0 +1,77 @@ +// MinIO Rust Library for Amazon S3 Compatible Cloud Storage +// Copyright 2024 MinIO, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::PathBuf; + +use clap::Parser; +use log::info; +use minio::s3::{ + args::{BucketExistsArgs, MakeBucketArgs}, + builders::ObjectContent, + client::ClientBuilder, + creds::StaticProvider, +}; + +/// Upload a file to the given bucket and object path on the MinIO Play server. +#[derive(Parser)] +struct Cli { + /// Bucket to upload the file to (will be created if it doesn't exist) + bucket: String, + /// Object path to upload the file to. + object: String, + /// File to upload. + file: PathBuf, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Cli::parse(); + + let static_provider = StaticProvider::new( + "Q3AM3UQ867SPQQA43P2F", + "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", + None, + ); + + let client = ClientBuilder::new("https://play.min.io".parse()?) + .provider(Some(Box::new(static_provider))) + .build()?; + + let exists: bool = client + .bucket_exists(&BucketExistsArgs::new(&args.bucket).unwrap()) + .await + .unwrap(); + + if !exists { + client + .make_bucket(&MakeBucketArgs::new(&args.bucket).unwrap()) + .await + .unwrap(); + } + + let content = ObjectContent::from(args.file.as_path()); + // Put an object + client + .put_object_content(&args.bucket, &args.object, content) + .send() + .await?; + + info!( + "Uploaded file at {:?} to {}/{}", + args.file, args.bucket, args.object + ); + + Ok(()) +} diff --git a/src/s3/error.rs b/src/s3/error.rs index 01612dd..b3a10c9 100644 --- a/src/s3/error.rs +++ b/src/s3/error.rs @@ -46,7 +46,7 @@ impl ErrorResponse { resource: get_default_text(&root, "Resource"), request_id: get_default_text(&root, "RequestId"), host_id: get_default_text(&root, "HostId"), - bucket_name: get_default_text(&root, "bucketName"), + bucket_name: get_default_text(&root, "BucketName"), object_name: get_default_text(&root, "Key"), }) } diff --git a/src/s3/http.rs b/src/s3/http.rs index 966ca1b..8b55746 100644 --- a/src/s3/http.rs +++ b/src/s3/http.rs @@ -26,6 +26,8 @@ use regex::Regex; use std::fmt; use std::str::FromStr; +use super::utils::urlencode_object_key; + const AWS_S3_PREFIX: &str = r"^(((bucket\.|accesspoint\.)vpce(-[a-z_\d]+)+\.s3\.)|([a-z_\d-]{1,63}\.)s3-control(-[a-z_\d]+)*\.|(s3(-[a-z_\d]+)*\.))"; lazy_static! { @@ -455,8 +457,7 @@ impl BaseUrl { if !v.starts_with('/') { path.push('/'); } - // FIXME: urlencode path - path.push_str(v); + path.push_str(&urlencode_object_key(v)); } url.host = host; diff --git a/src/s3/utils.rs b/src/s3/utils.rs index d4e2895..6b33155 100644 --- a/src/s3/utils.rs +++ b/src/s3/utils.rs @@ -150,6 +150,17 @@ pub fn from_iso8601utc(s: &str) -> Result { )) } +const OBJECT_KEY_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'-') + .remove(b'_') + .remove(b'.') + .remove(b'~') + .remove(b'/'); + +pub fn urlencode_object_key(key: &str) -> String { + utf8_percent_encode(key, OBJECT_KEY_ENCODE_SET).collect() +} + pub mod aws_date_format { use super::{from_iso8601utc, to_iso8601utc, UtcTime}; use serde::{Deserialize, Deserializer, Serializer};