2019-05-24 12:16:36 +00:00
|
|
|
// Copyright (C) 2017 Author: Arun Raghavan <arun@arunraghavan.net>
|
|
|
|
//
|
2022-01-15 18:40:12 +00:00
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
|
|
|
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
|
|
|
// <https://mozilla.org/MPL/2.0/>.
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
2019-05-24 12:16:36 +00:00
|
|
|
|
2023-03-31 08:43:36 +00:00
|
|
|
use aws_sdk_s3::config::Region;
|
2019-07-24 10:05:55 +00:00
|
|
|
use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};
|
2019-05-24 12:16:36 +00:00
|
|
|
use url::Url;
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct GstS3Url {
|
|
|
|
pub region: Region,
|
|
|
|
pub bucket: String,
|
|
|
|
pub object: String,
|
|
|
|
pub version: Option<String>,
|
|
|
|
}
|
|
|
|
|
2019-07-24 10:05:55 +00:00
|
|
|
// FIXME: Copied from the url crate, see https://github.com/servo/rust-url/issues/529
|
|
|
|
// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
|
|
|
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
|
|
|
|
// https://url.spec.whatwg.org/#path-percent-encode-set
|
|
|
|
const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
|
|
|
|
const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
|
|
|
|
|
2024-05-02 15:22:50 +00:00
|
|
|
impl std::fmt::Display for GstS3Url {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
write!(
|
|
|
|
f,
|
2019-05-24 12:16:36 +00:00
|
|
|
"s3://{}/{}/{}{}",
|
2022-05-14 05:01:35 +00:00
|
|
|
self.region,
|
2019-05-24 12:16:36 +00:00
|
|
|
self.bucket,
|
2019-07-24 10:05:55 +00:00
|
|
|
percent_encode(self.object.as_bytes(), PATH_SEGMENT),
|
2019-05-24 12:16:36 +00:00
|
|
|
if self.version.is_some() {
|
|
|
|
format!("?version={}", self.version.clone().unwrap())
|
|
|
|
} else {
|
|
|
|
"".to_string()
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn parse_s3_url(url_str: &str) -> Result<GstS3Url, String> {
|
2023-01-25 08:23:46 +00:00
|
|
|
let url = Url::parse(url_str).map_err(|err| format!("Parse error: {err}"))?;
|
2019-05-24 12:16:36 +00:00
|
|
|
|
|
|
|
if url.scheme() != "s3" {
|
|
|
|
return Err(format!("Unsupported URI '{}'", url.scheme()));
|
|
|
|
}
|
|
|
|
|
|
|
|
if !url.has_host() {
|
2023-01-25 08:23:46 +00:00
|
|
|
return Err(format!("Invalid host in uri '{url}'"));
|
2019-05-24 12:16:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let host = url.host_str().unwrap();
|
2022-05-14 05:01:35 +00:00
|
|
|
|
|
|
|
let region_str = host
|
|
|
|
.parse()
|
2021-09-27 16:16:26 +00:00
|
|
|
.or_else(|_| {
|
|
|
|
let (name, endpoint) = host.split_once('+').ok_or(())?;
|
|
|
|
let name =
|
2024-05-17 06:38:49 +00:00
|
|
|
base32::decode(base32::Alphabet::Rfc4648 { padding: true }, name).ok_or(())?;
|
2021-09-27 16:16:26 +00:00
|
|
|
let endpoint =
|
2024-05-17 06:38:49 +00:00
|
|
|
base32::decode(base32::Alphabet::Rfc4648 { padding: true }, endpoint).ok_or(())?;
|
2021-09-27 16:16:26 +00:00
|
|
|
let name = String::from_utf8(name).map_err(|_| ())?;
|
|
|
|
let endpoint = String::from_utf8(endpoint).map_err(|_| ())?;
|
2023-01-25 08:23:46 +00:00
|
|
|
Ok(format!("{name}{endpoint}"))
|
2021-09-27 16:16:26 +00:00
|
|
|
})
|
2023-01-25 08:23:46 +00:00
|
|
|
.map_err(|_: ()| format!("Invalid region '{host}'"))?;
|
2019-05-24 12:16:36 +00:00
|
|
|
|
2022-05-14 05:01:35 +00:00
|
|
|
// Note that aws_sdk_s3::Region does not provide any error/validation
|
|
|
|
// methods to check the region argument being passed to it.
|
|
|
|
// See https://docs.rs/aws-sdk-s3/latest/aws_sdk_s3/struct.Region.html
|
|
|
|
let region = Region::new(region_str);
|
|
|
|
|
2019-05-24 12:16:36 +00:00
|
|
|
let mut path = url
|
|
|
|
.path_segments()
|
2023-01-25 08:23:46 +00:00
|
|
|
.ok_or_else(|| format!("Invalid uri '{url}'"))?;
|
2019-05-24 12:16:36 +00:00
|
|
|
|
|
|
|
let bucket = path.next().unwrap().to_string();
|
|
|
|
|
|
|
|
let o = path
|
|
|
|
.next()
|
2023-01-25 08:23:46 +00:00
|
|
|
.ok_or_else(|| format!("Invalid empty object/bucket '{url}'"))?;
|
2019-05-24 12:16:36 +00:00
|
|
|
|
2023-09-21 18:21:16 +00:00
|
|
|
if o.is_empty() {
|
|
|
|
return Err(format!("Invalid empty object/bucket '{url}'"));
|
|
|
|
}
|
|
|
|
|
2019-05-24 12:16:36 +00:00
|
|
|
let mut object = percent_decode(o.as_bytes())
|
|
|
|
.decode_utf8()
|
|
|
|
.unwrap()
|
|
|
|
.to_string();
|
|
|
|
|
2023-09-21 18:21:16 +00:00
|
|
|
object = path.fold(object, |o, p| {
|
|
|
|
format!(
|
|
|
|
"{o}/{}",
|
|
|
|
percent_decode(p.as_bytes()).decode_utf8().unwrap()
|
|
|
|
)
|
|
|
|
});
|
2019-05-24 12:16:36 +00:00
|
|
|
|
|
|
|
let mut q = url.query_pairs();
|
|
|
|
let v = q.next();
|
|
|
|
|
2022-01-12 17:51:08 +00:00
|
|
|
let version = match v {
|
|
|
|
Some((ref k, ref v)) if k == "version" => Some((*v).to_string()),
|
|
|
|
None => None,
|
2019-05-24 12:16:36 +00:00
|
|
|
Some(_) => return Err("Bad query, only 'version' is supported".to_owned()),
|
2022-01-12 17:51:08 +00:00
|
|
|
};
|
2019-05-24 12:16:36 +00:00
|
|
|
|
2022-11-01 08:27:48 +00:00
|
|
|
if q.next().is_some() {
|
2019-05-24 12:16:36 +00:00
|
|
|
return Err("Extra query terms, only 'version' is supported".to_owned());
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(GstS3Url {
|
2019-07-04 15:30:26 +00:00
|
|
|
region,
|
|
|
|
bucket,
|
|
|
|
object,
|
|
|
|
version,
|
2019-05-24 12:16:36 +00:00
|
|
|
})
|
|
|
|
}
|