Parse and add tags to list objects output type (#77)

This commit is contained in:
Aditya Manthramurthy 2024-04-02 18:06:53 -07:00 committed by GitHub
parent c672e7528b
commit 3f160cb6c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 93 additions and 3 deletions

View File

@ -29,6 +29,7 @@ lazy_static = "1.4.0"
md5 = "0.7.0" md5 = "0.7.0"
multimap = "0.10.0" multimap = "0.10.0"
os_info = "3.7.0" os_info = "3.7.0"
percent-encoding = "2.3.0"
rand = "0.8.5" rand = "0.8.5"
regex = "1.9.4" regex = "1.9.4"
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }

View File

@ -110,6 +110,7 @@ pub enum Error {
PostPolicyError(String), PostPolicyError(String),
InvalidObjectLockConfig(String), InvalidObjectLockConfig(String),
NoClientProvided, NoClientProvided,
TagDecodingError(String, String),
} }
impl std::error::Error for Error {} impl std::error::Error for Error {}
@ -214,6 +215,7 @@ impl fmt::Display for Error {
Error::PostPolicyError(m) => write!(f, "{}", m), Error::PostPolicyError(m) => write!(f, "{}", m),
Error::InvalidObjectLockConfig(m) => write!(f, "{}", m), Error::InvalidObjectLockConfig(m) => write!(f, "{}", m),
Error::NoClientProvided => write!(f, "no client provided"), Error::NoClientProvided => write!(f, "no client provided"),
Error::TagDecodingError(input, error_message) => write!(f, "tag decoding failed: {} on input '{}'", error_message, input),
} }
} }
} }

View File

@ -22,7 +22,7 @@ use crate::s3::{
error::Error, error::Error,
types::{FromS3Response, ListEntry, S3Request}, types::{FromS3Response, ListEntry, S3Request},
utils::{ utils::{
from_iso8601utc, urldecode, from_iso8601utc, parse_tags, urldecode,
xml::{Element, MergeXmlElements}, xml::{Element, MergeXmlElements},
}, },
}; };
@ -125,6 +125,11 @@ fn parse_list_objects_contents(
}) })
.collect::<HashMap<String, String>>() .collect::<HashMap<String, String>>()
}); });
let user_tags = content
.get_child_text("UserTags")
.as_ref()
.map(|x| parse_tags(x))
.transpose()?;
let is_delete_marker = content.name() == "DeleteMarker"; let is_delete_marker = content.name() == "DeleteMarker";
contents.push(ListEntry { contents.push(ListEntry {
@ -138,6 +143,7 @@ fn parse_list_objects_contents(
is_latest, is_latest,
version_id, version_id,
user_metadata, user_metadata,
user_tags,
is_prefix: false, is_prefix: false,
is_delete_marker, is_delete_marker,
encoding_type: etype, encoding_type: etype,
@ -168,6 +174,7 @@ fn parse_list_objects_common_prefixes(
is_latest: false, is_latest: false,
version_id: None, version_id: None,
user_metadata: None, user_metadata: None,
user_tags: None,
is_prefix: true, is_prefix: true,
is_delete_marker: false, is_delete_marker: false,
encoding_type: encoding_type.as_ref().cloned(), encoding_type: encoding_type.as_ref().cloned(),

View File

@ -159,6 +159,7 @@ pub struct ListEntry {
pub is_latest: bool, // except ListObjects V1/V2 pub is_latest: bool, // except ListObjects V1/V2
pub version_id: Option<String>, // except ListObjects V1/V2 pub version_id: Option<String>, // except ListObjects V1/V2
pub user_metadata: Option<HashMap<String, String>>, pub user_metadata: Option<HashMap<String, String>>,
pub user_tags: Option<HashMap<String, String>>,
pub is_prefix: bool, pub is_prefix: bool,
pub is_delete_marker: bool, pub is_delete_marker: bool,
pub encoding_type: Option<String>, pub encoding_type: Option<String>,

View File

@ -15,7 +15,8 @@
//! Various utility and helper functions //! Various utility and helper functions
use crate::s3::error::Error; use std::collections::{BTreeMap, HashMap};
use base64::engine::general_purpose::STANDARD as BASE64; use base64::engine::general_purpose::STANDARD as BASE64;
use base64::engine::Engine as _; use base64::engine::Engine as _;
use byteorder::{BigEndian, ReadBytesExt}; use byteorder::{BigEndian, ReadBytesExt};
@ -24,13 +25,15 @@ use crc::{Crc, CRC_32_ISO_HDLC};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use md5::compute as md5compute; use md5::compute as md5compute;
use multimap::MultiMap; use multimap::MultiMap;
use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use regex::Regex; use regex::Regex;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
pub use urlencoding::decode as urldecode; pub use urlencoding::decode as urldecode;
pub use urlencoding::encode as urlencode; pub use urlencoding::encode as urlencode;
use xmltree::Element; use xmltree::Element;
use crate::s3::error::Error;
/// Date and time with UTC timezone /// Date and time with UTC timezone
pub type UtcTime = DateTime<Utc>; pub type UtcTime = DateTime<Utc>;
@ -392,11 +395,86 @@ pub fn copy_slice(dst: &mut [u8], src: &[u8]) -> usize {
c c
} }
// Characters to escape in query strings. Based on RFC 3986 and the golang
// net/url implementation used in the MinIO server.
//
// https://tools.ietf.org/html/rfc3986
//
// 1. All non-ascii characters are escaped always.
// 2. All reserved characters are escaped.
// 3. Any other characters are not escaped.
//
// Unreserved characters in addition to alphanumeric characters are: '-', '_',
// '.', '~' (§2.3 Unreserved characters (mark))
//
// Reserved characters for query strings: '$', '&', '+', ',', '/', ':', ';',
// '=', '?', '@' (§3.4)
//
// NON_ALPHANUMERIC already escapes everything non-alphanumeric (it includes all
// the reserved characters). So we only remove the unreserved characters from
// this set.
const QUERY_ESCAPE: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'~');
fn unescape(s: &str) -> Result<String, Error> {
percent_decode_str(s)
.decode_utf8()
.map_err(|e| Error::TagDecodingError(s.to_string(), e.to_string()))
.map(|s| s.to_string())
}
fn escape(s: &str) -> String {
utf8_percent_encode(s, QUERY_ESCAPE).collect()
}
// TODO: use this while adding API to set tags.
//
// Handles escaping same as MinIO server - needed for ensuring compatibility.
pub fn encode_tags(h: &HashMap<String, String>) -> String {
let mut tags = Vec::new();
for (k, v) in h {
tags.push(format!("{}={}", escape(k), escape(v)));
}
tags.join("&")
}
pub fn parse_tags(s: &str) -> Result<HashMap<String, String>, Error> {
let mut tags = HashMap::new();
for tag in s.split('&') {
let mut kv = tag.split('=');
let k = match kv.next() {
Some(v) => unescape(v)?,
None => {
return Err(Error::TagDecodingError(
s.to_string(),
"tag key was empty".to_string(),
))
}
};
let v = match kv.next() {
Some(v) => unescape(v)?,
None => "".to_owned(),
};
if kv.next().is_some() {
return Err(Error::TagDecodingError(
s.to_string(),
"tag had too many values for a key".to_string(),
));
}
tags.insert(k, v);
}
Ok(tags)
}
pub mod xml { pub mod xml {
use std::collections::HashMap; use std::collections::HashMap;
use crate::s3::error::Error; use crate::s3::error::Error;
#[derive(Debug, Clone)]
struct XmlElementIndex { struct XmlElementIndex {
children: HashMap<String, Vec<usize>>, children: HashMap<String, Vec<usize>>,
} }
@ -432,6 +510,7 @@ pub mod xml {
} }
} }
#[derive(Debug, Clone)]
pub struct Element<'a> { pub struct Element<'a> {
inner: &'a xmltree::Element, inner: &'a xmltree::Element,
child_element_index: XmlElementIndex, child_element_index: XmlElementIndex,