Registered Users
@@ -43,7 +43,7 @@
{{#if ../sso_enabled}}
- {{sso_identifier}}
+ {{sso_identifier}}
|
{{/if}}
diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs
index 230ac2e7..477cdd34 100644
--- a/src/static/templates/scss/vaultwarden.scss.hbs
+++ b/src/static/templates/scss/vaultwarden.scss.hbs
@@ -137,6 +137,14 @@ bit-nav-logo bit-nav-item .bwi-shield {
app-user-layout app-danger-zone button:nth-child(1) {
@extend %vw-hide;
}
+
+/* Hide unsupported Forwarding email alias options */
+ng-dropdown-panel div.ng-dropdown-panel-items div:has(> [title="Firefox Relay"]) {
+ @extend %vw-hide;
+}
+ng-dropdown-panel div.ng-dropdown-panel-items div:has(> [title="DuckDuckGo"]) {
+ @extend %vw-hide;
+}
/**** END Static Vaultwarden Changes ****/
/**** START Dynamic Vaultwarden Changes ****/
{{#if signup_disabled}}
diff --git a/src/storage.rs b/src/storage.rs
new file mode 100644
index 00000000..ada2a951
--- /dev/null
+++ b/src/storage.rs
@@ -0,0 +1,297 @@
+use std::sync::LazyLock;
+
+pub(crate) fn join_path(base: &str, child: &str) -> String {
+ #[cfg(s3)]
+ if s3::is_uri(base) {
+ return s3::join_path(base, child);
+ }
+
+ let base = base.trim_end_matches('/');
+ let child = child.trim_start_matches('/');
+ if base.is_empty() {
+ child.to_string()
+ } else if child.is_empty() {
+ base.to_string()
+ } else {
+ format!("{base}/{child}")
+ }
+}
+
+pub(crate) fn with_extension(path: &str, extension: &str) -> String {
+ let extension = extension.trim_start_matches('.');
+
+ #[cfg(s3)]
+ if s3::is_uri(path) {
+ return s3::with_extension(path, extension);
+ }
+
+ format!("{path}.{extension}")
+}
+
+pub(crate) fn parent(path: &str) -> Option {
+ #[cfg(s3)]
+ if s3::is_uri(path) {
+ return s3::parent(path);
+ }
+
+ std::path::Path::new(path).parent()?.to_str().map(ToString::to_string)
+}
+
+pub(crate) fn file_name(path: &str) -> Option {
+ #[cfg(s3)]
+ if s3::is_uri(path) {
+ return s3::file_name(path);
+ }
+
+ std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string)
+}
+
+pub(crate) fn is_fs_operator(operator: &opendal::Operator) -> bool {
+ operator.info().scheme() == opendal::services::FS_SCHEME
+}
+
+pub(crate) fn operator_for_path(path: &str) -> Result {
+ // Cache of previously built operators by path
+ static OPERATORS_BY_PATH: LazyLock> =
+ LazyLock::new(dashmap::DashMap::new);
+
+ if let Some(operator) = OPERATORS_BY_PATH.get(path) {
+ return Ok(operator.clone());
+ }
+
+ let operator = if path.starts_with("s3://") {
+ #[cfg(not(s3))]
+ return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into());
+
+ #[cfg(s3)]
+ s3::operator_for_path(path)?
+ } else {
+ let builder = opendal::services::Fs::default().root(path);
+ opendal::Operator::new(builder)?.finish()
+ };
+
+ OPERATORS_BY_PATH.insert(path.to_string(), operator.clone());
+
+ Ok(operator)
+}
+
+#[cfg(s3)]
+mod s3 {
+ use reqwest::Url;
+
+ use crate::error::Error;
+
+ pub(super) fn is_uri(path: &str) -> bool {
+ path.starts_with("s3://")
+ }
+
+ pub(super) fn join_path(base: &str, child: &str) -> String {
+ if let Ok(mut url) = Url::parse(base) {
+ let mut segments = path_segments(&url);
+ segments.extend(child.split('/').filter(|segment| !segment.is_empty()).map(ToString::to_string));
+ set_path_segments(&mut url, &segments);
+ return url.to_string();
+ }
+
+ let base = base.trim_end_matches('/');
+ let child = child.trim_start_matches('/');
+ if base.is_empty() {
+ child.to_string()
+ } else if child.is_empty() {
+ base.to_string()
+ } else {
+ format!("{base}/{child}")
+ }
+ }
+
+ pub(super) fn with_extension(path: &str, extension: &str) -> String {
+ if let Ok(mut url) = Url::parse(path) {
+ let mut segments = path_segments(&url);
+ if let Some(file_name) = segments.last_mut() {
+ file_name.push('.');
+ file_name.push_str(extension);
+ set_path_segments(&mut url, &segments);
+ return url.to_string();
+ }
+ }
+
+ format!("{path}.{extension}")
+ }
+
+ pub(super) fn parent(path: &str) -> Option {
+ if let Ok(mut url) = Url::parse(path) {
+ let mut segments = path_segments(&url);
+ segments.pop()?;
+ set_path_segments(&mut url, &segments);
+ return Some(url.to_string());
+ }
+
+ std::path::Path::new(path).parent()?.to_str().map(ToString::to_string)
+ }
+
+ pub(super) fn file_name(path: &str) -> Option {
+ if let Ok(url) = Url::parse(path) {
+ return path_segments(&url).pop();
+ }
+
+ std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string)
+ }
+
+ fn path_segments(url: &Url) -> Vec {
+ url.path_segments()
+ .map(|segments| segments.filter(|segment| !segment.is_empty()).map(ToString::to_string).collect())
+ .unwrap_or_default()
+ }
+
+ fn set_path_segments(url: &mut Url, segments: &[String]) {
+ if segments.is_empty() {
+ url.set_path("");
+ } else {
+ url.set_path(&format!("/{}", segments.join("/")));
+ }
+ }
+
+ pub(super) fn operator_for_path(path: &str) -> Result {
+ use crate::http_client::aws::AwsReqwestConnector;
+ use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};
+ use opendal::Configurator;
+ use reqsign_aws_v4::Credential;
+ use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain};
+
+ // This is a custom AWS credential loader that uses the official AWS Rust
+ // SDK config crate to load credentials. This ensures maximum compatibility
+ // with AWS credential configurations. For example, OpenDAL doesn't support
+ // AWS SSO temporary credentials yet.
+ #[derive(Debug)]
+ struct OpenDALS3CredentialProvider;
+
+ impl ProvideCredential for OpenDALS3CredentialProvider {
+ type Credential = Credential;
+
+ async fn provide_credential(&self, _ctx: &Context) -> reqsign_core::Result |