mod clam; mod dav; mod imaginary; mod kaspersky; mod ldap; mod mail; mod objectstore; mod oc; mod office; mod onlyoffice; mod push; mod sftp; // mod sharding; mod redis; mod sharded; mod smb; use crate::cloud::CloudOptions; use crate::config::{HazeConfig, Preset, ProxyConfig}; pub use crate::service::clam::{Clam, ClamIcap, ClamIcapTls, ClamSocket}; use crate::service::dav::Dav; use crate::service::imaginary::Imaginary; use crate::service::kaspersky::{Kaspersky, KasperskyIcap}; pub use crate::service::ldap::{Ldap, LdapAdmin}; use crate::service::mail::Mail; pub use crate::service::objectstore::ObjectStore; use crate::service::oc::Oc; pub use crate::service::office::Office; pub use crate::service::onlyoffice::OnlyOffice; pub use crate::service::push::NotifyPush; use crate::service::redis::Redis; use crate::service::sftp::Sftp; use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard}; use crate::service::smb::Smb; use bollard::models::ContainerState; use bollard::Docker; use enum_dispatch::enum_dispatch; use miette::{IntoDiagnostic, Report, Result, WrapErr}; use serde_json::Value; use std::collections::HashMap; use std::iter::empty; use std::net::IpAddr; use std::str::FromStr; use std::time::Duration; use strum::{Display, EnumIter, EnumMessage, EnumString, IntoStaticStr}; use tokio::time::{sleep, timeout}; #[async_trait::async_trait] #[enum_dispatch(Service)] pub trait ServiceTrait { fn name(&self) -> &str; fn env(&self) -> &[&str] { &[] } async fn spawn( &self, _docker: &Docker, _cloud_id: &str, _network: &str, _config: &HazeConfig, _options: &CloudOptions, ) -> Result> { Ok(Vec::new()) } async fn is_healthy( &self, _docker: &Docker, _cloud_id: &str, _options: &CloudOptions, ) -> Result { Ok(true) } fn container_name(&self, _cloud_id: &str) -> Option { None } async fn start_message( &self, _docker: &Docker, _cloud_id: &str, _proxy: &ProxyConfig, ) -> Result> { Ok(None) } fn apps(&self) -> &'static [&'static str] { &[] } fn config( &self, _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, ) -> Result> { Ok(HashMap::default()) } fn pre_setup( &self, _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, ) -> Result>> { Ok(Vec::new()) } async fn post_setup( &self, _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, ) -> Result> { Ok(Vec::new()) } async fn is_running(&self, docker: &Docker, cloud_id: &str) -> Result { let Some(container) = self.container_name(cloud_id) else { return Ok(true); }; let info = docker .inspect_container(&container, None) .await .into_diagnostic()?; Ok(matches!( info.state, Some(ContainerState { running: Some(true), .. }) )) } async fn wait_for_running(&self, docker: &Docker, cloud_id: &str) -> Result<()> { timeout(Duration::from_secs(30), async { while !self.is_running(docker, cloud_id).await? { sleep(Duration::from_millis(100)).await } Ok(()) }) .await .into_diagnostic() .wrap_err("Timeout after 30 seconds")? } async fn get_ips( &self, docker: &Docker, cloud_id: &str, ) -> Result>> { let Some(container) = self.container_name(cloud_id) else { return Ok(Box::new(empty())); }; docker .start_container(&container, None) .await .into_diagnostic()?; self.wait_for_running(docker, cloud_id).await?; sleep(Duration::from_millis(100)).await; let info = docker .inspect_container(&container, None) .await .into_diagnostic()?; if matches!( info.state, Some(ContainerState { running: Some(true), .. }) ) { let ips: Vec<_> = info .network_settings .unwrap() .networks .unwrap() .values() .filter_map(|network| network.ip_address.clone()) .filter_map(|address| IpAddr::from_str(&address).ok()) .collect(); Ok(Box::new(ips.into_iter())) } else { Err(Report::msg("service not started")) } } fn proxy_port(&self) -> u16 { 80 } } #[derive(Clone, Eq, PartialEq, Debug)] pub struct RedisTls; impl ServiceTrait for RedisTls { fn name(&self) -> &str { "redis-tls" } fn env(&self) -> &[&str] { &["REDIS_TLS=1"] } } #[derive(Clone, Eq, PartialEq, Debug)] pub struct FrankenPhp; impl ServiceTrait for FrankenPhp { fn name(&self) -> &str { "franken-php" } fn env(&self) -> &[&str] { &["FRANKENPHP=1"] } } #[derive( Copy, Clone, Debug, PartialEq, EnumString, EnumMessage, EnumIter, IntoStaticStr, Display, )] #[strum(serialize_all = "kebab-case")] pub enum ServiceType { /// S3 Primary storage and external storage S3, /// S3 Primary storage with TLS S3s, /// S3 multi-object store Primary storage and external storage S3m, /// S3 multi-bucket Primary storage and external storage S3mb, /// Azure Primary storage and external storage Azure, /// Ldap user backend Ldap, /// Ldap admin interface LdapAdmin, /// OnlyOffice OnlyOffice, /// Libre office online Office, /// notify_push Push, /// Smb external storage Smb, /// Database sharding #[strum(serialize = "sharding", serialize = "sharded")] Sharding, /// Database sharding migration #[strum(serialize = "sharding-migrate", serialize = "sharded-migrate")] ShardingMigrate, /// Database sharding migration, with the shards unset #[strum( serialize = "sharding-migrate-unset", serialize = "sharded-migrate-unset" )] ShardingMigrateUnset, /// Database sharding with a single shard SingleShard, /// WebDav external storage Dav, /// Sftp external storage Sftp, /// ownCloud instance for migration Oc, /// Imaginary for preview generation Imaginary, /// Kaspersky antivirus in http mode Kaspersky, /// Kaspersky antivirus in icap mode KasperskyIcap, /// Kaspersky antivirus in local binary #[strum(serialize = "clamav", serialize = "clam")] ClamAv, /// Kaspersky antivirus in external socket mode #[strum(serialize = "clamav-external", serialize = "clam-external")] ClamAvExternal, /// Kaspersky antivirus in local socket mode #[strum(serialize = "clamav-socket", serialize = "clam-socket")] ClamAvSocket, /// Kaspersky antivirus in icap mode #[strum(serialize = "clamav-icap", serialize = "clam-icap")] ClamAvIcap, /// Kaspersky antivirus in icap mode with TLS #[strum(serialize = "clamav-icap-tls", serialize = "clam-icap-tls")] ClamAvIcapTls, /// Mail server Mail, /// External redis instance Redis, /// External redis instance with TLS RedisTls, /// Use FrankenPHP instead of PHP-FPM FrankenPhp, } #[enum_dispatch] #[derive(Clone, Eq, PartialEq, Debug)] pub enum Service { ObjectStore(ObjectStore), Ldap(Ldap), LdapAdmin(LdapAdmin), OnlyOffice(OnlyOffice), Office(Office), Push(NotifyPush), Smb(Smb), Dav(Dav), Sharding(Sharding), SingleShard(SingleShard), ShardingMigrate(ShardingMigrate), ShardingMigrateUnset(ShardingMigrateUnset), Sftp(Sftp), Kaspersky(Kaspersky), KasperskyIcap(KasperskyIcap), Clam(Clam), ClamSocket(ClamSocket), ClamIcap(ClamIcap), ClamIcapTls(ClamIcapTls), Oc(Oc), Imaginary(Imaginary), Mail(Mail), Redis(Redis), RedisTls(RedisTls), FrankenPhp(FrankenPhp), Preset(PresetService), } impl Service { pub fn from_type(presets: &[Preset], ty: &str) -> Option> { if let Ok(ty) = ServiceType::from_str(ty) { match ty { ServiceType::S3 => Some(vec![Service::ObjectStore(ObjectStore::S3)]), ServiceType::S3s => Some(vec![Service::ObjectStore(ObjectStore::S3s)]), ServiceType::S3m => Some(vec![Service::ObjectStore(ObjectStore::S3m)]), ServiceType::S3mb => Some(vec![Service::ObjectStore(ObjectStore::S3mb)]), ServiceType::Azure => Some(vec![Service::ObjectStore(ObjectStore::Azure)]), ServiceType::Ldap => Some(vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)]), ServiceType::LdapAdmin => { Some(vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)]) } ServiceType::OnlyOffice => Some(vec![Service::OnlyOffice(OnlyOffice)]), ServiceType::Office => Some(vec![Service::Office(Office)]), ServiceType::Push => Some(vec![Service::Push(NotifyPush)]), ServiceType::Smb => Some(vec![Service::Smb(Smb)]), ServiceType::Sharding => Some(vec![Service::Sharding(Sharding)]), ServiceType::SingleShard => Some(vec![Service::SingleShard(SingleShard)]), ServiceType::ShardingMigrate => { Some(vec![Service::ShardingMigrate(ShardingMigrate)]) } ServiceType::ShardingMigrateUnset => { Some(vec![Service::ShardingMigrateUnset(ShardingMigrateUnset)]) } ServiceType::Dav => Some(vec![Service::Dav(Dav)]), ServiceType::Sftp => Some(vec![Service::Sftp(Sftp)]), ServiceType::Oc => Some(vec![Service::Oc(Oc)]), ServiceType::Imaginary => Some(vec![Service::Imaginary(Imaginary)]), ServiceType::Kaspersky => Some(vec![Service::Kaspersky(Kaspersky)]), ServiceType::KasperskyIcap => Some(vec![Service::KasperskyIcap(KasperskyIcap)]), ServiceType::ClamAv => Some(vec![Service::Clam(Clam)]), ServiceType::ClamAvExternal => Some(vec![Service::ClamSocket(ClamSocket)]), ServiceType::ClamAvSocket => Some(vec![Service::ClamSocket(ClamSocket)]), ServiceType::ClamAvIcap => Some(vec![Service::ClamIcap(ClamIcap)]), ServiceType::ClamAvIcapTls => Some(vec![Service::ClamIcapTls(ClamIcapTls)]), ServiceType::Mail => Some(vec![Service::Mail(Mail)]), ServiceType::Redis => Some(vec![Service::Redis(Redis)]), ServiceType::RedisTls => Some(vec![Service::RedisTls(RedisTls)]), ServiceType::FrankenPhp => Some(vec![Service::FrankenPhp(FrankenPhp)]), } } else { presets .iter() .find_map(|preset| (preset.name == ty).then(|| PresetService(preset.name.clone()))) .map(Service::Preset) .map(|service| vec![service]) } } pub async fn wait_for_start( &self, docker: &Docker, cloud_id: &str, options: &CloudOptions, ) -> Result<()> { timeout(Duration::from_secs(30), async { while !self.is_healthy(docker, cloud_id, options).await? { sleep(Duration::from_millis(100)).await } Ok(()) }) .await .into_diagnostic() .wrap_err("Timeout after 30 seconds")? } } fn get_preset<'a>(presets: &'a [Preset], name: &str) -> Option<&'a Preset> { presets.iter().find(|preset| preset.name == name) } #[derive(Clone, Eq, PartialEq, Debug)] pub struct PresetService(pub String); #[async_trait::async_trait] impl ServiceTrait for PresetService { fn name(&self) -> &str { self.0.as_str() } fn config( &self, _docker: &Docker, _cloud_id: &str, config: &HazeConfig, ) -> Result> { let preset = get_preset(&config.preset, &self.0).ok_or_else(|| Report::msg("invalid preset"))?; let config = preset .config .iter() .map(|(k, v)| Ok((k.clone(), serde_json::to_value(v).into_diagnostic()?))) .collect::>>()?; Ok(config) } async fn post_setup( &self, _docker: &Docker, _cloud_id: &str, config: &HazeConfig, ) -> Result> { let preset = get_preset(&config.preset, &self.0).ok_or_else(|| Report::msg("invalid preset"))?; let mut commands: Vec<_> = preset .apps .iter() .map(|app| format!("occ app:enable {app} --force")) .collect(); commands.extend_from_slice(&preset.commands); Ok(commands) } }