use net_backend_protocol::storage::{DEFAULT_MAX_OBJECTS_PER_USER, DEFAULT_MAX_OBJECT_BYTES, MAX_BATCH_BYTES};
use serde::Deserialize;
use crate::error::Error;
pub const DEFAULT_MAX_BYTES_PER_USER: u64 = MAX_BATCH_BYTES as u64;
#[derive(Clone, Debug, Deserialize)]
#[serde(default, deny_unknown_fields)]
#[non_exhaustive]
pub struct StorageConfig {
pub max_object_bytes: usize,
pub max_objects_per_user: u32,
pub max_bytes_per_user: u64,
pub write_rate: u32,
pub write_rate_window_secs: u32,
pub server_collections: Vec<String>,
pub admin_in_openapi: bool,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
max_object_bytes: DEFAULT_MAX_OBJECT_BYTES,
max_objects_per_user: DEFAULT_MAX_OBJECTS_PER_USER,
max_bytes_per_user: DEFAULT_MAX_BYTES_PER_USER,
write_rate: 60,
write_rate_window_secs: 60,
server_collections: vec!["server".into()],
admin_in_openapi: false,
}
}
}
impl StorageConfig {
pub fn validate(&self) -> Result<(), Error> {
let mut problems = Vec::new();
if !(1..=MAX_BATCH_BYTES).contains(&self.max_object_bytes) {
problems.push(format!("modules.storage.max_object_bytes must be between 1 and {MAX_BATCH_BYTES}"));
}
if !(1..=100_000_000).contains(&self.max_objects_per_user) {
problems.push("modules.storage.max_objects_per_user must be between 1 and 100000000".to_string());
}
if !(1..=(1u64 << 40)).contains(&self.max_bytes_per_user) {
problems.push("modules.storage.max_bytes_per_user must be between 1 and 1099511627776 (1 TiB)".to_string());
}
if self.write_rate > 100_000 {
problems.push("modules.storage.write_rate must be at most 100000 (0 = no limit)".to_string());
}
if !(1..=86_400).contains(&self.write_rate_window_secs) {
problems.push("modules.storage.write_rate_window_secs must be between 1 and 86400".to_string());
}
for entry in &self.server_collections {
if !net_backend_protocol::storage::is_valid_name(entry) {
problems.push(format!("modules.storage.server_collections: `{entry}` is not a collection name"));
}
}
if problems.is_empty() {
Ok(())
} else {
Err(Error::Config(problems))
}
}
pub fn is_server_collection(&self, collection: &str) -> bool {
self.server_collections.iter().any(|entry| collection == entry || collection.strip_prefix(entry.as_str()).is_some_and(|rest| rest.starts_with('.')))
}
pub fn put_body_limit(&self) -> usize {
net_backend_protocol::storage::PUT_BODY_LIMIT_BYTES.max(self.max_object_bytes.saturating_add(16 * 1024))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_and_ranges() {
let config = StorageConfig::default();
assert!(config.validate().is_ok());
assert_eq!(config.put_body_limit(), net_backend_protocol::storage::PUT_BODY_LIMIT_BYTES);
let mut big = StorageConfig { max_object_bytes: MAX_BATCH_BYTES, ..StorageConfig::default() };
assert!(big.validate().is_ok());
assert_eq!(big.put_body_limit(), MAX_BATCH_BYTES + 16 * 1024);
big.max_object_bytes = MAX_BATCH_BYTES + 1;
big.max_objects_per_user = 0;
big.max_bytes_per_user = 0;
big.write_rate_window_secs = 0;
big.server_collections = vec!["../x".into()];
let error = big.validate().err().map(|e| e.to_string()).unwrap_or_default();
for part in ["max_object_bytes", "max_objects_per_user", "max_bytes_per_user", "write_rate_window_secs", "`../x`"] {
assert!(error.contains(part), "{part}: {error}");
}
let config = StorageConfig { server_collections: vec!["server".into(), "wallet".into()], ..StorageConfig::default() };
for (collection, reserved) in
[("server", true), ("server.gold", true), ("wallet", true), ("wallet.x.y", true), ("servers", false), ("walletx", false), ("saves", false)]
{
assert_eq!(config.is_server_collection(collection), reserved, "{collection}");
}
}
}