From b7ea4e9760fc05d307bdff33e40efaec197cfa82 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 5 Mar 2026 23:27:54 +0100 Subject: [PATCH] allow configuring additional app directories and add a writable app directory fixes #15 --- README.md | 1 + nix/image/configs/nc/config.php | 12 +++++- src/args.rs | 1 + src/cloud.rs | 69 ++++++++++++++++++++++++--------- src/config.rs | 16 +++++++- src/mapping.rs | 65 +++++++++++++++++++++++++------ 6 files changed, 130 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e5ba037..5eea607 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,7 @@ options ```toml sources_root = "/path/to/sources" # path of the nextcloud sources. required +app_directories = ["/path/to/sources/more_app"] # paths to additional app directories. work_dir = "/path/to/temp/dir" # path to temporary directory. optional, defaults to "/tmp/haze" [auto_setup] # optional diff --git a/nix/image/configs/nc/config.php b/nix/image/configs/nc/config.php index e8dc0af..49a135f 100644 --- a/nix/image/configs/nc/config.php +++ b/nix/image/configs/nc/config.php @@ -1,4 +1,12 @@ - true, 'appstoreenabled' => false, 'memcache.local' => '\\OC\\Memcache\\APCu', @@ -9,4 +17,4 @@ 'profiling.secret' => 'haze', 'profiling.path' => '/tmp/profiling', //PLACEHOLDER -]; +]); diff --git a/src/args.rs b/src/args.rs index f5434ca..e4e2b69 100644 --- a/src/args.rs +++ b/src/args.rs @@ -505,6 +505,7 @@ impl SubCommand for HazeCommand { fn test_arg_parse() { let config = HazeConfig { sources_root: Default::default(), + app_directories: Default::default(), work_dir: Default::default(), auto_setup: Default::default(), volume: vec![], diff --git a/src/cloud.rs b/src/cloud.rs index d0b645c..c4de8de 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -1,7 +1,7 @@ use crate::config::{HazeConfig, HazeVolumeConfig}; use crate::database::Database; use crate::exec::{exec, exec_io, exec_tty, ExitCode}; -use crate::mapping::{default_mappings, Mapping}; +use crate::mapping::{for_config, Mapping}; use crate::php::{PhpVersion, PHP_MEMORY_LIMIT}; use crate::service::Service; use crate::service::ServiceTrait; @@ -15,14 +15,14 @@ use flate2::read::GzDecoder; use futures_util::future::try_join_all; use miette::{IntoDiagnostic, Report, Result, WrapErr}; use petname::petname; -use serde_json::Value; +use serde_json::{Map, Value}; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Display; use std::fs; -use std::fs::read_to_string; +use std::fs::{read_to_string, write}; use std::io::{stdout, Cursor, Read, Stdout, Write}; -use std::iter::Peekable; +use std::iter::{once, Peekable}; use std::net::IpAddr; use std::os::unix::fs::MetadataExt; use std::str::FromStr; @@ -286,11 +286,8 @@ impl Cloud { }) }) .collect::>>()?; - let mappings = config - .volume - .iter() - .map(Mapping::from) - .chain(default_mappings()) + + let mappings = for_config(config) .chain(app_volumes.iter().map(Mapping::from)) .collect::>(); for mapping in &mappings { @@ -300,6 +297,48 @@ impl Cloud { .wrap_err_with(|| format!("Failed to setup work directory {}", mapping.source))?; } + let mut nc_config = Value::Object(Map::new()); + nc_config["apps_paths"] = Value::Array( + once("apps") + .chain( + config + .app_directories + .iter() + .filter_map(|dir| dir.file_name()), + ) + .map(|name| { + [ + ( + String::from("path"), + Value::from(format!("/var/www/html/{}", name)), + ), + (String::from("url"), Value::from(format!("/{}", name))), + (String::from("writable"), Value::from(false)), + ] + .into_iter() + .collect() + }) + .chain(once( + [ + ( + String::from("path"), + Value::from("/var/www/html/store_apps"), + ), + (String::from("url"), Value::from("/store_apps")), + (String::from("writable"), Value::from(true)), + ] + .into_iter() + .collect(), + )) + .collect(), + ); + write( + workdir.join("config/nextcloud.json"), + serde_json::to_string_pretty(&nc_config).unwrap(), + ) + .into_diagnostic() + .wrap_err("Failed to write config json")?; + let network = docker .create_network(NetworkCreateRequest { name: id.clone(), @@ -500,10 +539,7 @@ impl Cloud { pub async fn destroy(self, docker: &Docker) -> Result<()> { for container in self.containers { docker - .kill_container( - container.trim_start_matches('/'), - None, - ) + .kill_container(container.trim_start_matches('/'), None) .await .into_diagnostic() .wrap_err("Failed to kill container")?; @@ -802,12 +838,7 @@ impl Cloud { format!("/var/www/html/{path}").into() }; - let mut mappings = config - .volume - .iter() - .map(Mapping::from) - .chain(default_mappings()) - .collect::>(); + let mut mappings = for_config(config).collect::>(); mappings.sort_by_key(|mapping| usize::MAX - mapping.target.as_str().len()); for mapping in mappings { diff --git a/src/config.rs b/src/config.rs index 07cfcef..fad1d0a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use miette::{IntoDiagnostic, Report, Result, WrapErr}; use serde::Deserialize; use std::collections::HashMap; use std::convert::TryFrom; -use std::env::var; +use std::env::home_dir; use std::fs::read_to_string; use std::net::IpAddr; use toml::Value; @@ -13,6 +13,7 @@ use toml::Value; #[serde(from = "RawHazeConfig")] pub struct HazeConfig { pub sources_root: Utf8PathBuf, + pub app_directories: Vec, pub work_dir: Utf8PathBuf, pub auto_setup: HazeAutoSetupConfig, pub volume: Vec, @@ -27,6 +28,8 @@ pub struct RawHazeConfig { #[serde(default = "default_work_dir")] pub work_dir: Utf8PathBuf, #[serde(default)] + pub app_directories: Vec, + #[serde(default)] pub auto_setup: HazeAutoSetupConfig, #[serde(default)] pub volume: Vec, @@ -42,7 +45,11 @@ impl From for HazeConfig { fn from(raw: RawHazeConfig) -> Self { fn normalize_path(path: Utf8PathBuf) -> Utf8PathBuf { if path.starts_with("~") { - let home = var("HOME").expect("HOME not set"); + let home = home_dir().expect("can't detect home directory"); + let home = home + .into_os_string() + .into_string() + .expect("non-utf8 home directory"); format!("{}{}", home, &path.as_str()[1..]).into() } else { path @@ -51,6 +58,11 @@ impl From for HazeConfig { HazeConfig { sources_root: normalize_path(raw.sources_root), + app_directories: raw + .app_directories + .into_iter() + .map(normalize_path) + .collect(), work_dir: normalize_path(raw.work_dir), auto_setup: raw.auto_setup, volume: raw.volume, diff --git a/src/mapping.rs b/src/mapping.rs index 4061100..36587ad 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -1,13 +1,14 @@ use crate::config::{HazeConfig, HazeVolumeConfig}; use camino::{Utf8Path, Utf8PathBuf}; use miette::{IntoDiagnostic, Result}; +use std::borrow::Cow; use tokio::fs::{create_dir_all, write}; #[derive(Debug)] pub struct Mapping<'a> { source_type: MappingSourceType, - pub source: &'a Utf8Path, - pub target: &'a Utf8Path, + pub source: Cow<'a, Utf8Path>, + pub target: Cow<'a, Utf8Path>, mapping_type: MappingType, read_only: bool, map: bool, @@ -23,6 +24,26 @@ impl<'a> Mapping<'a> { where Target: Into<&'a Utf8Path>, Source: Into<&'a Utf8Path>, + { + Mapping { + source_type, + source: Cow::Borrowed(source.into()), + target: Cow::Borrowed(target.into()), + mapping_type: MappingType::Folder, + read_only: false, + map: true, + create: true, + } + } + + pub fn owned( + source_type: MappingSourceType, + source: Source, + target: Target, + ) -> Self + where + Target: Into>, + Source: Into>, { Mapping { source_type, @@ -65,10 +86,10 @@ impl<'a> Mapping<'a> { return Ok(()); } let source = match self.source_type { - MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source), - MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source), + MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source.as_ref()), + MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source.as_ref()), MappingSourceType::Sources => return Ok(()), - MappingSourceType::Absolute => self.source.into(), + MappingSourceType::Absolute => self.source.as_ref().into(), }; match self.mapping_type { MappingType::Folder => create_dir_all(source).await.into_diagnostic()?, @@ -80,10 +101,10 @@ impl<'a> Mapping<'a> { pub fn source(&self, id: &str, config: &HazeConfig, source_root: &Utf8Path) -> Utf8PathBuf { match self.source_type { - MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source), - MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source), - MappingSourceType::Sources => source_root.join(self.source), - MappingSourceType::Absolute => self.source.into(), + MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source.as_ref()), + MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source.as_ref()), + MappingSourceType::Sources => source_root.join(self.source.as_ref()), + MappingSourceType::Absolute => self.source.as_ref().into(), } } @@ -112,6 +133,7 @@ pub fn default_mappings<'a>() -> impl IntoIterator> { Mapping::new(Sources, "", "/var/www/html"), Mapping::new(WorkDir, "data", "/var/www/html/data"), Mapping::new(WorkDir, "config", "/var/www/html/config"), + Mapping::new(WorkDir, "store_app", "/var/www/html/store_app"), Mapping::new(WorkDir, "data-autotest", "/var/www/html/data-autotest"), Mapping::new(WorkDir, "skeleton", "/var/www/html/core/skeleton"), Mapping::new( @@ -168,9 +190,30 @@ pub fn default_mappings<'a>() -> impl IntoIterator> { Mapping::new(WorkDir, "profiling", "/tmp/profiling"), Mapping::new(WorkDir, "php-config", "/config"), ]; + IntoIterator::into_iter(mappings) } +pub fn for_config<'a>(config: &'a HazeConfig) -> impl Iterator> { + let app_dir_mappings = config.app_directories.iter().map(|dir| { + Mapping::owned( + MappingSourceType::Absolute, + dir.as_path(), + Cow::Owned(Utf8PathBuf::from(format!( + "/var/www/html/{}", + dir.file_name().unwrap() + ))), + ) + }); + + config + .volume + .iter() + .map(Mapping::from) + .chain(app_dir_mappings) + .chain(default_mappings()) +} + #[derive(Debug, Copy, Clone)] pub enum MappingSourceType { Sources, @@ -194,8 +237,8 @@ impl<'a> From<&'a HazeVolumeConfig> for Mapping<'a> { }; Mapping { source_type: MappingSourceType::Absolute, - source: config.source.as_path(), - target: config.target.as_path(), + source: Cow::Borrowed(config.source.as_path()), + target: Cow::Borrowed(config.target.as_path()), mapping_type: ty, read_only: config.read_only, map: true,