[WIP] paths

This commit is contained in:
Alex-Emmet 2026-07-24 01:36:14 +02:00
commit 3bc5cc959a
20 changed files with 817 additions and 241 deletions

5
Cargo.lock generated
View file

@ -2222,6 +2222,7 @@ dependencies = [
"dashmap",
"iota-cli",
"iota-logger",
"iota-paths",
"iota-state",
"iota-storage",
"iota-terms",
@ -2250,6 +2251,7 @@ dependencies = [
"iota-paths",
"iota-state",
"iota-storage",
"iota-util",
"omikron-connector",
"tokio",
"tokio-util",
@ -2301,6 +2303,7 @@ dependencies = [
name = "iota-logger"
version = "0.1.0"
dependencies = [
"iota-paths",
"iota-state",
"iota-util",
"json",
@ -2345,6 +2348,7 @@ dependencies = [
"hex",
"hkdf 0.12.4",
"iota-logger",
"iota-paths",
"iota-state",
"iota-util",
"json",
@ -2390,6 +2394,7 @@ dependencies = [
"hex",
"hkdf 0.12.4",
"iota-logger",
"iota-paths",
"json",
"mtp",
"once_cell",

View file

@ -108,22 +108,26 @@
cfg = config.services.iota;
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.iota-daemon or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}");
configFormat = pkgs.formats.yaml {};
configFile =
if cfg.settingsFile != null
then cfg.settingsFile
else pkgs.writeText "iota-config.json" (builtins.toJSON cfg.settings);
else configFormat.generate "iota-config.yaml" cfg.settings;
descriptionText = "Tensamin Iota daemon";
in {
options.services.iota = {
enable = lib.mkEnableOption "Enable the Iota service.";
dataDir = lib.mkOption {
stateDir = lib.mkOption {
type = lib.types.str;
default = cfg.package.passthru.dataDir or "/var/lib/iota";
defaultText = lib.literalExpression ''config.services.iota.package.passthru.dataDir or "/var/lib/iota"'';
description = "Directory where Iota stores its data, config, and certificates.";
default = "/var/lib/iota";
description = "Persistent mutable Iota state.";
};
cacheDir = lib.mkOption { type = lib.types.str; default = "/var/cache/iota"; };
runtimeDir = lib.mkOption { type = lib.types.str; default = "/run/iota"; };
logDir = lib.mkOption { type = lib.types.str; default = "/var/log/iota"; };
assetDir = lib.mkOption { type = lib.types.str; default = "${cfg.package}/share/iota/web"; };
certFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
@ -164,13 +168,13 @@
settings = lib.mkOption {
type = lib.types.attrs;
default = {};
description = "Configuration attributes for Iota, written to config.json.";
description = "Configuration attributes for Iota, written to YAML.";
};
settingsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Path to an existing config.json file to use instead of generating from settings.";
description = "Path to an existing YAML file to use instead of generating from settings.";
};
};
@ -178,7 +182,7 @@
users.users.iota = {
isSystemUser = true;
group = "iota";
home = cfg.dataDir;
home = cfg.stateDir;
createHome = true;
description = "Iota service user";
shell = pkgs.bash;
@ -194,6 +198,7 @@
SocketMode = "0660";
SocketUser = "iota";
SocketGroup = "iota";
DirectoryMode = "0750";
Backlog = 5;
RemoveOnStop = "true";
NonBlocking = true;
@ -210,28 +215,18 @@
Type = "simple";
User = "iota";
Group = "iota";
WorkingDirectory = cfg.dataDir;
ExecStart = "${cfg.package}/bin/iota-daemon";
ExecStartPre = [
("+"
+ pkgs.writeShellScript "iota-setup" ''
mkdir -p ${cfg.dataDir}/certs
${lib.optionalString (cfg.certFile != null) "ln -sf ${cfg.certFile} ${cfg.dataDir}/certs/cert.pem"}
${lib.optionalString (cfg.keyFile != null) "ln -sf ${cfg.keyFile} ${cfg.dataDir}/certs/cert.key"}
install -m 644 ${configFile} ${cfg.dataDir}/config.json
chown -R iota:iota ${cfg.dataDir}
'')
];
Restart = "on-failure";
RestartSec = "5s";
RuntimeDirectory = "iota";
RuntimeDirectoryMode = "0750";
StateDirectory = "iota";
StateDirectoryMode = "0750";
CacheDirectory = "iota";
CacheDirectoryMode = "0750";
LogsDirectory = "iota";
LogsDirectoryMode = "0750";
# Exit code 75 = restart requested
RestartPreventExitStatus = "0";
@ -248,7 +243,8 @@
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
ReadWritePaths = [cfg.dataDir];
ReadWritePaths = [cfg.stateDir cfg.cacheDir cfg.runtimeDir cfg.logDir];
ReadOnlyPaths = [configFile cfg.assetDir];
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
@ -259,7 +255,14 @@
Environment = [
"BIND_ADDRESS=${cfg.bindAddress}"
"IOTA_SOCKET=/run/iota/iota.sock"
"IOTA_DATA_DIR=${cfg.dataDir}"
"IOTA_CONFIG_FILE=${configFile}"
"IOTA_STATE_DIR=${cfg.stateDir}"
"IOTA_CACHE_DIR=${cfg.cacheDir}"
"IOTA_RUNTIME_DIR=${cfg.runtimeDir}"
"IOTA_LOG_DIR=${cfg.logDir}"
"IOTA_ASSET_DIR=${cfg.assetDir}"
"IOTA_DEPLOYMENT_MODE=system_socket_activated"
"IOTA_SUPERVISOR=systemd"
];
}
// lib.optionalAttrs (cfg.environmentFiles != []) {

View file

@ -387,8 +387,7 @@ impl IpcClient {
};
match result {
Ok(message) => self.apply(message).await,
Err(error) => {
eprintln!("IPC reader for generation {generation} stopped: {error}");
Err(_) => {
self.mark_disconnected(generation).await;
break;
}

View file

@ -12,6 +12,7 @@ iota-storage = { path = "../iota-storage" }
iota-terms = { path = "../iota-terms" }
iota-updater = { path = "../iota-updater" }
iota-util = { path = "../iota-util" }
iota-paths = { path = "../iota-paths" }
omikron-connector = { path = "../omikron-connector" }
web-server = { path = "../web-server" }
web-ui = { path = "../web-ui" }

View file

@ -23,7 +23,16 @@ async fn main() {
*state.reload.write().await = false;
*state.shutdown.write().await = false;
let ipc = IpcClient::connect("/run/iota/iota.sock")
let socket = match iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.expect("resolve Iota user paths")
.ipc_endpoint
{
iota_paths::IpcEndpoint::UnixSocket(path) => path,
iota_paths::IpcEndpoint::WindowsPipe(_) => {
panic!("Windows IPC client transport is not implemented yet")
}
};
let ipc = IpcClient::connect(socket)
.await
.expect("iota-daemon must be running before starting iota-core");
let session = start_tui(ipc).expect("interactive terminal initialization failed");

View file

@ -44,10 +44,22 @@ impl IpcServer {
let listener = match activated_listener()? {
Some(listener) => listener,
None => {
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
let parent = path.parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"IPC socket has no parent directory",
)
})?;
if !parent.is_dir() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("IPC runtime directory does not exist: {}", parent.display()),
));
}
let lock_path = path.with_extension("sock.lock");
let lock_path = path
.parent()
.unwrap_or_else(|| Path::new("/tmp"))
.join("daemon.lock");
let lock = File::options()
.create(true)
.mode(0o600)

View file

@ -10,6 +10,7 @@ iota-logger = { path = "../iota-logger" }
iota-state = { path = "../iota-state" }
iota-paths = { path = "../iota-paths" }
iota-storage = { path = "../iota-storage" }
iota-util = { path = "../iota-util" }
omikron-connector = { path = "../omikron-connector" }
web-server = { path = "../web-server" }
tokio = { version = "1.50.0", features = ["full"] }

View file

@ -10,8 +10,33 @@ use std::time::Duration;
use tokio::sync::{broadcast, watch};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> ExitCode {
logger::startup();
iota_storage::util::config_util::load_config();
let scope = match std::env::var("IOTA_DEPLOYMENT_MODE").ok().as_deref() {
Some("system_socket_activated") | Some("system_always_on") => iota_paths::Scope::System,
_ => iota_paths::Scope::User,
};
let paths = match iota_paths::IotaPaths::resolve(scope) {
Ok(paths) => paths,
Err(error) => {
eprintln!("Cannot resolve Iota paths: {error}");
return ExitCode::FAILURE;
}
};
if let Err(error) = paths.migrate_legacy_layout() {
eprintln!("Cannot migrate legacy Iota layout: {error}");
return ExitCode::FAILURE;
}
if let Err(error) = paths.prepare_writable_directories() {
eprintln!("Cannot prepare Iota directories: {error}");
return ExitCode::FAILURE;
}
iota_util::file_util::configure_storage_directory(paths.storage_dir.clone());
iota_storage::util::config_util::configure_config_path(paths.config_file.clone());
iota_storage::util::config_util::load_config_from(&paths.config_file);
omikron_connector::omikron_connection::configure_identity_path(paths.keyring_file());
match paths.scope {
iota_paths::Scope::User => logger::startup_with_log_dir(Some(paths.log_dir.clone())),
iota_paths::Scope::System => logger::startup_with_log_dir(None),
}
let runtime = Arc::new(DaemonRuntime::new());
// --- IPC infrastructure ---
@ -36,7 +61,13 @@ async fn main() -> ExitCode {
// Bind before migration and service startup: a successful bind is the
// readiness boundary visible to clients and socket activation.
let socket = iota_paths::socket_path(iota_paths::SocketScope::User);
let socket = match &paths.ipc_endpoint {
iota_paths::IpcEndpoint::UnixSocket(path) => path.clone(),
iota_paths::IpcEndpoint::WindowsPipe(_) => {
eprintln!("Windows named-pipe daemon transport is not implemented yet");
return ExitCode::FAILURE;
}
};
let omikron = match omikron_connector::omikron_connection::connect_initial(
runtime.cancellation.clone(),
runtime.state.active_tasks.clone(),
@ -65,18 +96,22 @@ async fn main() -> ExitCode {
}
};
let services = DaemonServices::new(omikron);
let ipc_server =
match IpcServer::bind(socket, runtime.clone(), services, log_tx.clone(), state_rx).await {
let ipc_server = match IpcServer::bind(
socket.clone(),
runtime.clone(),
services,
log_tx.clone(),
state_rx,
)
.await
{
Ok(server) => server,
Err(error) => {
eprintln!("Cannot bind daemon IPC socket: {error}");
return ExitCode::FAILURE;
}
};
eprintln!(
"iota-daemon IPC listener ready at {}",
iota_paths::socket_path(iota_paths::SocketScope::User).display()
);
eprintln!("iota-daemon IPC listener ready at {}", socket.display());
runtime.set_component_healthy(iota_ipc::ComponentId::Ipc, None);
let listener_runtime = runtime.clone();
runtime
@ -128,13 +163,17 @@ async fn main() -> ExitCode {
.parse()
.unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
port: web.port,
asset_dir: std::path::PathBuf::from(web.asset_dir),
asset_dir: resolve_config_path(&paths.config_file, &web.asset_dir, &paths.asset_dir),
tls: web
.certificate
.zip(web.key)
.map(|(certificate, key)| web_server::TlsConfig {
certificate: certificate.into(),
key: key.into(),
certificate: resolve_config_path(
&paths.config_file,
&certificate,
&paths.config_dir,
),
key: resolve_config_path(&paths.config_file, &key, &paths.config_dir),
}),
required: web.required,
};
@ -196,3 +235,22 @@ async fn main() -> ExitCode {
log!("iota-daemon exited (code: {})", exit_code);
ExitCode::from(exit_code as u8)
}
fn resolve_config_path(
config_file: &std::path::Path,
value: &str,
default: &std::path::Path,
) -> std::path::PathBuf {
if value.is_empty() {
return default.to_path_buf();
}
let path = std::path::PathBuf::from(value);
if path.is_absolute() {
path
} else {
config_file
.parent()
.expect("absolute configuration file has a parent")
.join(path)
}
}

View file

@ -81,7 +81,7 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
] {
install(
&staging.path().join("systemd").join(unit),
&format!("/etc/systemd/system/{unit}"),
&format!("/usr/local/lib/systemd/system/{unit}"),
"0644",
)?;
}
@ -114,7 +114,7 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
"{}/current/bin/iota-daemon",
iota_paths::install_root().display()
),
"/usr/local/lib/iota/iota-daemon",
"/usr/local/libexec/iota/iota-daemon",
],
)?;
run("systemd-sysusers", &[])?;
@ -130,8 +130,12 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
run("systemctl", &["enable", "--now", "iota-daemon.socket"])?;
run("systemctl", &["is-active", "iota-daemon.socket"])?;
run("systemctl", &["is-enabled", "iota-daemon.socket"])?;
if !Path::new("/run/iota/iota.sock").exists() {
bail!("systemd socket is active but /run/iota/iota.sock was not created");
let socket = iota_paths::socket_path(iota_paths::Scope::System);
if !socket.exists() {
bail!(
"systemd socket is active but {} was not created",
socket.display()
);
}
Ok(())
}
@ -175,7 +179,7 @@ mod tests {
let service = include_str!("../../systemd/iota-daemon.service");
let socket = include_str!("../../systemd/iota-daemon.socket");
let sysusers = include_str!("../../systemd/sysusers.d/iota.conf");
assert!(service.contains("ExecStart=/usr/local/lib/iota/iota-daemon"));
assert!(service.contains("ExecStart=/usr/local/libexec/iota/iota-daemon"));
assert!(service.contains("User=iota"));
assert!(service.contains("Group=iota"));
assert!(socket.contains("SocketUser=iota"));

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
iota-paths = { path = "../iota-paths" }
iota-state = { path = "../iota-state" }
iota-util = { path = "../iota-util" }

View file

@ -1,7 +1,6 @@
use std::{
fs::{self, OpenOptions},
io::Write,
path::Path,
sync::{OnceLock, atomic::Ordering, mpsc},
thread,
time::{SystemTime, UNIX_EPOCH},
@ -56,6 +55,15 @@ struct LogMessage {
/* The logger owns file persistence while consumers receive rendered entries
* through a process-local broadcast subscription. */
pub fn startup() {
startup_with_log_dir(Some(
iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.expect("resolve Iota user paths")
.log_dir,
));
}
/// `None` keeps logging on stderr only (the systemd default).
pub fn startup_with_log_dir(log_dir: Option<std::path::PathBuf>) {
let (tx, rx) = mpsc::channel::<LogMessage>();
if LOGGER.set(tx).is_err() {
return;
@ -64,22 +72,15 @@ pub fn startup() {
let _ = LOG_BROADCASTER.set(broadcast_tx.clone());
thread::spawn(move || {
let working_dir = iota_util::file_util::get_directory();
let base_dir = Path::new(&working_dir);
let log_dir = base_dir.join("logs");
fs::create_dir_all(&log_dir).expect("Failed to create log directory");
let start_ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let path = log_dir.join(format!("log_{}.txt", start_ts));
let mut file = OpenOptions::new()
let mut file = log_dir.and_then(|log_dir| {
fs::create_dir_all(&log_dir).ok()?;
let start_ts = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
OpenOptions::new()
.create(true)
.append(true)
.open(path)
.expect("Failed to open log file");
.open(log_dir.join(format!("log_{start_ts}.txt")))
.ok()
});
for msg in rx {
let resolved_message = if let Some(key) = msg.translation_key {
@ -105,7 +106,9 @@ pub fn startup() {
);
let line2 = format!(" {}", timestamp);
if let Some(file) = file.as_mut() {
let _ = writeln!(file, "{}\n{}", line1, line2);
}
let _ = writeln!(std::io::stderr(), "{}\n{}", line1, line2);

View file

@ -1,118 +1,412 @@
use std::path::PathBuf;
//! Platform and deployment aware locations used by Iota.
//!
//! This module deliberately keeps environment handling in one place. In
//! particular, an override is never interpreted relative to the process
//! working directory.
use std::env;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SocketScope {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Scope {
User,
System,
}
fn home_dir() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
/// Compatibility name retained for callers which have not yet been migrated.
pub type SocketScope = Scope;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum IpcEndpoint {
UnixSocket(PathBuf),
WindowsPipe(String),
}
pub fn data_dir() -> PathBuf {
if let Some(path) = std::env::var_os("IOTA_DATA_DIR") {
return PathBuf::from(path);
#[derive(Debug)]
pub enum PathError {
MissingPlatformDirectory(&'static str),
EmptyOverride(&'static str),
RelativeOverride {
variable: &'static str,
value: PathBuf,
},
InvalidPipeName(String),
UnsupportedScope,
}
impl fmt::Display for PathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingPlatformDirectory(name) => write!(f, "missing platform directory: {name}"),
Self::EmptyOverride(name) => write!(f, "{name} must not be empty"),
Self::RelativeOverride { variable, value } => {
write!(f, "{variable} must be absolute, got {}", value.display())
}
Self::InvalidPipeName(name) => write!(f, "invalid Windows pipe name: {name}"),
Self::UnsupportedScope => write!(f, "this path scope is unsupported on this platform"),
}
}
}
impl std::error::Error for PathError {}
#[derive(Clone, Debug)]
pub struct IotaPaths {
pub scope: Scope,
pub config_dir: PathBuf,
pub config_file: PathBuf,
pub state_dir: PathBuf,
pub storage_dir: PathBuf,
pub identity_dir: PathBuf,
pub cache_dir: PathBuf,
pub runtime_dir: Option<PathBuf>,
pub log_dir: PathBuf,
/// Directory containing static web assets (not its parent).
pub asset_dir: PathBuf,
pub install_root: PathBuf,
pub ipc_endpoint: IpcEndpoint,
}
impl IotaPaths {
pub fn resolve(scope: Scope) -> Result<Self, PathError> {
let defaults = Defaults::for_scope(scope)?;
let config_dir = override_first(&["IOTA_CONFIG_DIR"])?.unwrap_or(defaults.config_dir);
// IOTA_DATA_DIR is intentionally only a compatibility alias. Parse it
// exactly like every other override; do not hide an invalid value.
let state_dir =
override_first(&["IOTA_STATE_DIR", "IOTA_DATA_DIR"])?.unwrap_or(defaults.state_dir);
let cache_dir = override_first(&["IOTA_CACHE_DIR"])?.unwrap_or(defaults.cache_dir);
let runtime_dir = override_first(&["IOTA_RUNTIME_DIR"])?.or(defaults.runtime_dir);
let log_dir = override_first(&["IOTA_LOG_DIR"])?.unwrap_or(defaults.log_dir);
let asset_dir = override_first(&["IOTA_ASSET_DIR", "IOTA_WEB_ASSET_DIR"])?
.unwrap_or(defaults.asset_dir);
let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root);
let config_file = override_first(&["IOTA_CONFIG_FILE"])?
.unwrap_or_else(|| config_dir.join("config.yaml"));
let ipc_endpoint = resolve_ipc(scope, runtime_dir.as_deref(), defaults.ipc_endpoint)?;
Ok(Self {
scope,
config_dir,
config_file,
storage_dir: state_dir.join("storage"),
identity_dir: state_dir.join("identity"),
state_dir,
cache_dir,
runtime_dir,
log_dir,
asset_dir,
install_root,
ipc_endpoint,
})
}
pub fn database_file(&self) -> PathBuf {
self.storage_dir.join("messages.sqlite3")
}
pub fn keyring_file(&self) -> PathBuf {
self.identity_dir.join("iota.mk")
}
pub fn update_staging_dir(&self) -> PathBuf {
self.cache_dir.join("updates/staging")
}
pub fn update_status_file(&self) -> PathBuf {
self.state_dir.join("update-status.json")
}
pub fn update_lock_file(&self) -> Result<PathBuf, PathError> {
self.runtime_dir
.as_ref()
.map(|p| p.join("update.lock"))
.ok_or(PathError::MissingPlatformDirectory("runtime directory"))
}
pub fn daemon_lock_file(&self) -> Result<PathBuf, PathError> {
self.runtime_dir
.as_ref()
.map(|p| p.join("daemon.lock"))
.ok_or(PathError::MissingPlatformDirectory("runtime directory"))
}
pub fn prepare_writable_directories(&self) -> std::io::Result<()> {
for directory in [
&self.state_dir,
&self.storage_dir,
&self.identity_dir,
&self.cache_dir,
&self.log_dir,
] {
create_directory(directory, self.scope == Scope::User)?;
}
if let Some(runtime) = &self.runtime_dir {
create_directory(runtime, self.scope == Scope::User)?;
}
Ok(())
}
/// Move the pre-v2 resources that were all placed directly below the
/// state root. This is deliberately idempotent: an existing destination
/// is never overwritten and the marker is only written after the moves.
pub fn migrate_legacy_layout(&self) -> std::io::Result<()> {
let marker = self.state_dir.join("path-layout-v2.json");
if marker.exists() {
return Ok(());
}
move_if_absent(&self.state_dir.join("config.yaml"), &self.config_file)?;
move_if_absent(&self.state_dir.join("certs"), &self.config_dir.join("tls"))?;
for suffix in [
"messages.sqlite3",
"messages.sqlite3-wal",
"messages.sqlite3-shm",
] {
move_if_absent(&self.state_dir.join(suffix), &self.storage_dir.join(suffix))?;
}
for name in ["users", "communities"] {
move_if_absent(&self.state_dir.join(name), &self.storage_dir.join(name))?;
}
move_if_absent(&self.state_dir.join("iota.mk"), &self.keyring_file())?;
move_if_absent(
&self.state_dir.join("update-staging"),
&self.update_staging_dir(),
)?;
// Runtime objects must not survive a layout migration or reboot.
for name in ["update.lock", "iota.sock", "iota.sock.lock"] {
let path = self.state_dir.join(name);
if path.is_file() || path.is_symlink() {
let _ = std::fs::remove_file(path);
}
}
std::fs::create_dir_all(&self.state_dir)?;
std::fs::write(marker, "{\"version\":2}\n")
}
}
fn move_if_absent(source: &Path, destination: &Path) -> std::io::Result<()> {
if !source.exists() || destination.exists() {
return Ok(());
}
let metadata = std::fs::symlink_metadata(source)?;
if metadata.file_type().is_symlink() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("refusing symlink migration source {}", source.display()),
));
}
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent)?;
}
match std::fs::rename(source, destination) {
Ok(()) => Ok(()),
Err(error) if error.raw_os_error() == Some(libc_exdev()) => {
copy_recursively(source, destination)?;
if source.is_dir() {
std::fs::remove_dir_all(source)
} else {
std::fs::remove_file(source)
}
}
Err(error) => Err(error),
}
}
// EXDEV is stable on Unix. A literal is used on non-Unix where the fallback
// copy is harmlessly skipped because rename normally remains on one volume.
#[cfg(unix)]
fn libc_exdev() -> i32 {
18
}
#[cfg(not(unix))]
fn libc_exdev() -> i32 {
-1
}
fn copy_recursively(source: &Path, destination: &Path) -> std::io::Result<()> {
if source.is_dir() {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
copy_recursively(&entry.path(), &destination.join(entry.file_name()))?;
}
Ok(())
} else {
std::fs::copy(source, destination).map(|_| ())
}
}
struct Defaults {
config_dir: PathBuf,
state_dir: PathBuf,
cache_dir: PathBuf,
runtime_dir: Option<PathBuf>,
log_dir: PathBuf,
asset_dir: PathBuf,
install_root: PathBuf,
ipc_endpoint: IpcEndpoint,
}
impl Defaults {
fn for_scope(scope: Scope) -> Result<Self, PathError> {
match scope {
Scope::System => {
#[cfg(target_os = "linux")]
{
return std::env::var_os("XDG_STATE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home_dir().join(".local/state"))
Ok(Self {
config_dir: "/etc/iota".into(),
state_dir: "/var/lib/iota".into(),
cache_dir: "/var/cache/iota".into(),
runtime_dir: Some("/run/iota".into()),
log_dir: "/var/log/iota".into(),
asset_dir: "/usr/local/share/iota/web".into(),
install_root: "/usr/local/libexec/iota".into(),
ipc_endpoint: IpcEndpoint::UnixSocket("/run/iota/iota.sock".into()),
})
}
#[cfg(not(target_os = "linux"))]
{
Err(PathError::UnsupportedScope)
}
}
Scope::User => user_defaults(),
}
}
}
#[cfg(unix)]
fn user_defaults() -> Result<Defaults, PathError> {
let home =
absolute_env("HOME")?.ok_or(PathError::MissingPlatformDirectory("home directory"))?;
let config_base = xdg_or_home("XDG_CONFIG_HOME", &home, ".config")?;
let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?;
let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?;
let data_base = xdg_or_home("XDG_DATA_HOME", &home, ".local/share")?;
let runtime = absolute_env("XDG_RUNTIME_DIR")?
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"))?
.join("iota");
Ok(Defaults {
config_dir: config_base.join("iota"),
state_dir: state_base.join("iota"),
cache_dir: cache_base.join("iota"),
runtime_dir: Some(runtime.clone()),
log_dir: state_base.join("iota/logs"),
asset_dir: data_base.join("iota/web"),
install_root: data_base.join("iota/bin"),
ipc_endpoint: IpcEndpoint::UnixSocket(runtime.join("iota.sock")),
})
}
#[cfg(target_os = "macos")]
{
return home_dir().join("Library/Application Support/Iota");
}
#[cfg(target_os = "windows")]
{
return std::env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.unwrap_or_else(home_dir)
#[cfg(windows)]
fn user_defaults() -> Result<Defaults, PathError> {
let config = absolute_env("APPDATA")?
.ok_or(PathError::MissingPlatformDirectory("Roaming AppData"))?
.join("Tensamin/Iota/config");
let local = absolute_env("LOCALAPPDATA")?
.ok_or(PathError::MissingPlatformDirectory("Local AppData"))?
.join("Tensamin/Iota");
Ok(Defaults {
config_dir: config,
state_dir: local.join("state"),
cache_dir: local.join("cache"),
runtime_dir: None,
log_dir: local.join("logs"),
asset_dir: local.join("data"),
install_root: local.join("bin"),
ipc_endpoint: IpcEndpoint::WindowsPipe(r"\\.\pipe\Tensamin.Iota.User".into()),
})
}
#[allow(unreachable_code)]
home_dir().join(".iota")
fn xdg_or_home(variable: &'static str, home: &Path, fallback: &str) -> Result<PathBuf, PathError> {
Ok(absolute_env(variable)?.unwrap_or_else(|| home.join(fallback)))
}
fn absolute_env(name: &'static str) -> Result<Option<PathBuf>, PathError> {
let Some(value) = env::var_os(name) else {
return Ok(None);
};
if value.is_empty() {
return Err(PathError::EmptyOverride(name));
}
let path = PathBuf::from(value);
if !path.is_absolute() {
return Err(PathError::RelativeOverride {
variable: name,
value: path,
});
}
Ok(Some(path))
}
fn override_first(names: &[&'static str]) -> Result<Option<PathBuf>, PathError> {
for name in names {
if let Some(value) = absolute_env(name)? {
return Ok(Some(value));
}
}
Ok(None)
}
fn resolve_ipc(
scope: Scope,
runtime: Option<&Path>,
default: IpcEndpoint,
) -> Result<IpcEndpoint, PathError> {
#[cfg(unix)]
{
if let Some(path) = absolute_env("IOTA_SOCKET")? {
return Ok(IpcEndpoint::UnixSocket(path));
}
if scope == Scope::User {
return runtime
.map(|dir| IpcEndpoint::UnixSocket(dir.join("iota.sock")))
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"));
}
}
#[cfg(windows)]
{
if let Some(name) = env::var_os("IOTA_PIPE") {
let name = name.to_string_lossy().into_owned();
if !name.starts_with(r"\\.\pipe\") {
return Err(PathError::InvalidPipeName(name));
}
return Ok(IpcEndpoint::WindowsPipe(name));
}
}
Ok(default)
}
fn create_directory(path: &Path, private: bool) -> std::io::Result<()> {
std::fs::create_dir_all(path)?;
#[cfg(unix)]
if private {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
// Compatibility helpers. New code should resolve IotaPaths once and pass it
// to its dependencies instead of calling these independently.
pub fn data_dir() -> PathBuf {
IotaPaths::resolve(Scope::User)
.expect("resolve Iota user paths")
.state_dir
}
pub fn config_dir() -> PathBuf {
if let Some(path) = std::env::var_os("IOTA_CONFIG_DIR") {
return PathBuf::from(path);
IotaPaths::resolve(Scope::User)
.expect("resolve Iota user paths")
.config_dir
}
#[cfg(target_os = "linux")]
{
return std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home_dir().join(".config"))
.join("iota");
}
#[cfg(target_os = "macos")]
{
return home_dir().join("Library/Application Support/Iota");
}
#[cfg(target_os = "windows")]
{
return std::env::var_os("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(home_dir)
.join("Tensamin/Iota");
}
#[allow(unreachable_code)]
home_dir().join(".iota")
}
pub fn socket_override() -> Option<PathBuf> {
std::env::var_os("IOTA_SOCKET").map(PathBuf::from)
absolute_env("IOTA_SOCKET").ok().flatten()
}
pub fn socket_path(scope: SocketScope) -> PathBuf {
if let Some(path) = socket_override() {
return path;
}
match scope {
SocketScope::User => data_dir().join("iota.sock"),
SocketScope::System => PathBuf::from("/run/iota/iota.sock"),
}
}
pub fn socket_lock_path(scope: SocketScope) -> PathBuf {
let socket = socket_path(scope);
PathBuf::from(format!("{}.lock", socket.display()))
}
pub fn daemon_executable() -> PathBuf {
if let Some(path) = std::env::var_os("IOTA_DAEMON_PATH") {
return PathBuf::from(path);
}
if let Ok(exe) = std::env::current_exe() {
if let Some(path) = exe.parent().map(|p| p.join("iota-daemon")) {
if path.is_file() {
return path;
}
}
}
#[cfg(target_os = "linux")]
match IotaPaths::resolve(scope)
.expect("resolve Iota paths")
.ipc_endpoint
{
let installed = PathBuf::from("/usr/local/lib/iota/iota-daemon");
if installed.is_file() {
return installed;
IpcEndpoint::UnixSocket(path) => path,
IpcEndpoint::WindowsPipe(_) => panic!("Windows IPC endpoint is not a filesystem path"),
}
}
PathBuf::from("iota-daemon")
}
pub fn updater_executable() -> PathBuf {
std::env::var_os("IOTA_UPDATER_PATH")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("iota-updater"))
pub fn socket_lock_path(scope: SocketScope) -> PathBuf {
IotaPaths::resolve(scope)
.expect("resolve Iota paths")
.daemon_lock_file()
.expect("runtime directory")
}
/// The compatibility installation helpers describe the machine installation,
/// not a user's data directory. Per-user launchers should keep an
/// `IotaPaths` instance and use its `install_root` directly.
pub fn install_root() -> PathBuf {
std::env::var_os("IOTA_INSTALL_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/usr/local/lib/iota"))
IotaPaths::resolve(Scope::System)
.expect("resolve Iota system paths")
.install_root
}
pub fn versions_dir() -> PathBuf {
install_root().join("versions")
@ -121,36 +415,91 @@ pub fn current_version_link() -> PathBuf {
install_root().join("current")
}
pub fn updater_lock_path() -> PathBuf {
data_dir().join("update.lock")
IotaPaths::resolve(Scope::User)
.expect("resolve Iota user paths")
.update_lock_file()
.expect("runtime directory")
}
pub fn updater_status_path() -> PathBuf {
data_dir().join("update-status.json")
IotaPaths::resolve(Scope::User)
.expect("resolve Iota user paths")
.update_status_file()
}
pub fn updater_staging_dir() -> PathBuf {
data_dir().join("update-staging")
IotaPaths::resolve(Scope::User)
.expect("resolve Iota user paths")
.update_staging_dir()
}
pub fn web_asset_dir() -> PathBuf {
std::env::var_os("IOTA_WEB_ASSET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| data_dir().join("web"))
IotaPaths::resolve(Scope::User)
.expect("resolve Iota user paths")
.asset_dir
}
pub fn daemon_executable() -> PathBuf {
absolute_env("IOTA_DAEMON_PATH")
.expect("valid IOTA_DAEMON_PATH")
.unwrap_or_else(|| install_root().join("current/bin/iota-daemon"))
}
pub fn updater_executable() -> PathBuf {
absolute_env("IOTA_UPDATER_PATH")
.expect("valid IOTA_UPDATER_PATH")
.unwrap_or_else(|| install_root().join("current/bin/iota-updater"))
}
pub fn daemon_endpoints() -> Vec<PathBuf> {
if let Some(path) = socket_override() {
return vec![path];
}
vec![
socket_path(SocketScope::User),
socket_path(SocketScope::System),
]
vec![socket_path(Scope::User), socket_path(Scope::System)]
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_ID: AtomicU64 = AtomicU64::new(0);
#[test]
fn system_layout_is_fhs() {
let p = IotaPaths::resolve(Scope::System).unwrap();
assert_eq!(p.config_file, PathBuf::from("/etc/iota/config.yaml"));
assert_eq!(
p.database_file(),
PathBuf::from("/var/lib/iota/storage/messages.sqlite3")
);
assert_eq!(
p.update_staging_dir(),
PathBuf::from("/var/cache/iota/updates/staging")
);
}
#[test]
fn explicit_data_directory_wins() {
assert!(!data_dir().as_os_str().is_empty());
fn migration_moves_state_resources_without_overwriting_destination() {
let root = std::env::temp_dir().join(format!(
"iota-paths-test-{}-{}",
std::process::id(),
TEST_ID.fetch_add(1, Ordering::Relaxed)
));
let state = root.join("state");
let config = root.join("config");
let paths = IotaPaths {
scope: Scope::User,
config_dir: config.clone(),
config_file: config.join("config.yaml"),
storage_dir: state.join("storage"),
identity_dir: state.join("identity"),
cache_dir: root.join("cache"),
runtime_dir: Some(root.join("runtime")),
log_dir: state.join("logs"),
asset_dir: root.join("data/web"),
install_root: root.join("bin"),
ipc_endpoint: IpcEndpoint::UnixSocket(root.join("runtime/iota.sock")),
state_dir: state.clone(),
};
std::fs::create_dir_all(state.join("users")).unwrap();
std::fs::write(state.join("messages.sqlite3"), b"db").unwrap();
std::fs::write(state.join("config.yaml"), b"web: {}\n").unwrap();
paths.migrate_legacy_layout().unwrap();
assert!(paths.database_file().is_file());
assert!(paths.storage_dir.join("users").is_dir());
assert!(paths.config_file.is_file());
assert!(state.join("path-layout-v2.json").is_file());
let _ = std::fs::remove_dir_all(root);
}
}

View file

@ -7,6 +7,7 @@ edition = "2024"
iota-logger = { path = "../iota-logger" }
iota-state = { path = "../iota-state" }
iota-util = { path = "../iota-util" }
iota-paths = { path = "../iota-paths" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }

View file

@ -1,8 +1,10 @@
use arc_swap::ArcSwap;
use iota_util::file_util::{load_file, save_file};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::OnceLock;
pub static CONFIG: Lazy<ArcSwap<IotaConfig>> =
Lazy::new(|| ArcSwap::new(Arc::new(IotaConfig::default())));
@ -19,11 +21,11 @@ pub struct IotaConfig {
pub omikron_host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub omikron_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(skip_serializing)]
pub keyring: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(skip_serializing)]
pub public_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(skip_serializing)]
pub private_key: Option<String>,
#[serde(default = "default_read_receipts_enabled")]
pub read_receipts_enabled: bool,
@ -61,7 +63,7 @@ fn default_web_bind() -> String {
"127.0.0.1".into()
}
fn default_web_asset_dir() -> String {
"web".into()
String::new()
}
impl Default for WebSettings {
fn default() -> Self {
@ -102,17 +104,27 @@ impl Default for IotaConfig {
}
pub fn load_config() {
let s = load_file("", "config.yaml");
if s.is_empty() {
load_config_from(&default_config_path());
}
/// Loading is intentionally side-effect free: a missing configuration means
/// documented defaults, not a newly-created file.
pub fn load_config_from(path: &Path) {
let s = match fs::read_to_string(path) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
Err(error) => {
eprintln!("Failed to read {}: {error}", path.display());
return;
}
};
match serde_yaml::from_str::<IotaConfig>(&s) {
Ok(parsed) => {
CONFIG.store(Arc::new(parsed));
}
Err(e) => {
eprintln!("Failed to parse config.yaml: {}. Content: '{}'", e, s);
eprintln!("Failed to parse {}: {e}", path.display());
}
}
}
@ -123,9 +135,35 @@ pub fn clear_config() {
}
pub fn save_config() {
if let Ok(yaml) = serde_yaml::to_string(&**CONFIG.load()) {
save_file("", "config.yaml", &yaml);
save_config_to(&default_config_path());
}
pub fn save_config_to(path: &Path) {
if let Ok(yaml) = serde_yaml::to_string(&**CONFIG.load()) {
if let Some(parent) = path.parent() {
if let Err(error) = fs::create_dir_all(parent) {
eprintln!(
"Cannot create configuration directory {}: {error}",
parent.display()
);
return;
}
}
let temporary = path.with_extension("yaml.tmp");
if let Err(error) = fs::write(&temporary, yaml).and_then(|_| fs::rename(&temporary, path)) {
eprintln!("Cannot save {}: {error}", path.display());
let _ = fs::remove_file(temporary);
}
}
}
fn default_config_path() -> PathBuf {
if let Some(path) = CONFIG_PATH.get() {
return path.clone();
}
iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.expect("resolve Iota user paths")
.config_file
}
pub fn modify_config(f: impl FnOnce(&mut IotaConfig)) {
@ -134,3 +172,8 @@ pub fn modify_config(f: impl FnOnce(&mut IotaConfig)) {
CONFIG.store(Arc::new(cfg));
save_config();
}
static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
pub fn configure_config_path(path: PathBuf) {
let _ = CONFIG_PATH.set(path);
}

View file

@ -1,4 +1,3 @@
use iota_util::file_util::get_directory;
use once_cell::sync::Lazy;
use r2d2::ManageConnection;
use rusqlite::Connection;
@ -56,10 +55,12 @@ where
f(&conn)
}
fn db_file_path(db_name: &str) -> String {
let mut p = PathBuf::from(get_directory());
p.push(format!("{db_name}.sqlite3"));
p.to_string_lossy().to_string()
fn db_file_path(db_name: &str) -> PathBuf {
let storage_dir = iota_util::file_util::storage_directory();
// Creating storage belongs to initialization/connection setup, never to a
// configuration read.
std::fs::create_dir_all(&storage_dir).expect("create Iota storage directory");
storage_dir.join(format!("{db_name}.sqlite3"))
}
fn run_migrations(pool: &r2d2::Pool<SqliteManager>) -> Result<(), StorageError> {

View file

@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
iota-logger = { path = "../iota-logger" }
iota-paths = { path = "../iota-paths" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }

View file

@ -9,22 +9,32 @@ use std::{
pub struct UpdateTransaction {
pub root: PathBuf,
pub staging: PathBuf,
pub lock_file: PathBuf,
}
impl UpdateTransaction {
pub fn new(root: impl Into<PathBuf>) -> Self {
let root = root.into();
Self {
staging: root.join(".staging"),
lock_file: root.join("update.lock"),
root,
}
}
pub fn from_paths(paths: &iota_paths::IotaPaths) -> Result<Self> {
Ok(Self {
root: paths.install_root.clone(),
staging: paths.update_staging_dir(),
lock_file: paths.update_lock_file().map_err(|e| anyhow::anyhow!(e))?,
})
}
pub fn acquire(&self) -> Result<fs::File> {
fs::create_dir_all(&self.root)?;
let path = self.root.join("update.lock");
if let Some(parent) = self.lock_file.parent() {
fs::create_dir_all(parent)?;
}
let file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.open(&self.lock_file)
.context("update already in progress")?;
Ok(file)
}

View file

@ -3,6 +3,7 @@ use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{self, BufReader, Read};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use sysinfo::System;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
@ -40,34 +41,19 @@ pub fn delete_user_directory(user_id: i64) {
}
pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
// Ensure the directory exists, create if necessary
if !dir.exists() {
if let Err(_) = fs::create_dir_all(&dir) {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"Directory creation failed",
));
}
}
// Create the file if it doesn't exist
if !file_path.exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"File creation failed",
));
}
let file_path = storage_file(path, name)?;
// Open the file and return a BufReader for efficient reading
let file = File::open(&file_path)?;
Ok(BufReader::new(file))
}
pub fn has_file(path: &str, name: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
let Ok(file_path) = storage_file(path, name) else {
return false;
};
let Some(dir) = file_path.parent() else {
return false;
};
if !dir.exists() {
return false;
@ -80,7 +66,9 @@ pub fn has_file(path: &str, name: &str) -> bool {
true
}
pub fn has_dir(path: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
let Ok(dir) = storage_child(path) else {
return false;
};
if !dir.exists() {
return false;
@ -90,8 +78,12 @@ pub fn has_dir(path: &str) -> bool {
}
pub fn load_file(path: &str, name: &str) -> String {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
let Ok(file_path) = storage_file(path, name) else {
return String::new();
};
let Some(dir) = file_path.parent() else {
return String::new();
};
if !dir.exists() {
return String::new();
@ -109,15 +101,17 @@ pub fn load_file(path: &str, name: &str) -> String {
}
pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error> {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
std::fs::read(file_path)
std::fs::read(storage_file(path, name)?)
}
pub fn save_file(path: &str, name: &str, value: &str) {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
let Ok(file_path) = storage_file(path, name) else {
eprintln!("[IMPORTANT] Refusing unsafe storage path");
return;
};
let Some(dir) = file_path.parent() else {
return;
};
if !dir.exists() {
if let Err(e) = fs::create_dir_all(&dir) {
@ -149,7 +143,9 @@ pub fn save_file(path: &str, name: &str, value: &str) {
}
pub fn get_children(path: &str) -> Vec<String> {
let dir = Path::new(&get_directory()).join(path);
let Ok(dir) = storage_child(path) else {
return Vec::new();
};
let mut children = Vec::new();
if let Ok(entries) = fs::read_dir(&dir) {
for entry in entries {
@ -161,8 +157,61 @@ pub fn get_children(path: &str) -> Vec<String> {
children
}
static STORAGE_DIRECTORY: OnceLock<PathBuf> = OnceLock::new();
/// Set by the daemon immediately after resolving `IotaPaths`. This keeps the
/// legacy storage helpers working while preventing them from independently
/// discovering a different (user-scope) directory in a system daemon.
pub fn configure_storage_directory(path: PathBuf) {
let _ = STORAGE_DIRECTORY.set(path);
}
pub fn storage_directory() -> PathBuf {
STORAGE_DIRECTORY.get().cloned().unwrap_or_else(|| {
iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.expect("resolve Iota user paths")
.storage_dir
})
}
/// Resolve a user supplied storage fragment without allowing it to escape the
/// resolved storage root. Legacy call sites may use nested fragments, but
/// never absolute paths or `..` components.
pub fn storage_child(path: impl AsRef<Path>) -> io::Result<PathBuf> {
let path = path.as_ref();
if path.is_absolute()
|| path.components().any(|c| {
matches!(
c,
std::path::Component::ParentDir
| std::path::Component::RootDir
| std::path::Component::Prefix(_)
)
})
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"unsafe storage path",
));
}
Ok(storage_directory().join(path))
}
pub fn storage_file(path: impl AsRef<Path>, name: impl AsRef<Path>) -> io::Result<PathBuf> {
let name = name.as_ref();
if name.components().count() != 1 || name.is_absolute() || name == Path::new(".") {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"unsafe storage file name",
));
}
storage_child(path).map(|dir| dir.join(name))
}
pub fn get_directory() -> String {
iota_paths::data_dir().to_string_lossy().to_string()
// Legacy helpers are storage-only. Configuration, keys, logs and runtime
// files must use their dedicated path APIs instead.
storage_directory().to_string_lossy().into_owned()
}
// Helper to download the zip file content to a file on disk

View file

@ -1,7 +1,5 @@
use iota_cli::{
ipc_client::IpcClient,
screens::main_screen::MainScreen,
theme,
ipc_client::IpcClient, screens::main_screen::MainScreen, theme,
ui::start_bootstrap_tui_with_theme,
};
use iota_ipc::{LocalRequest, ResponseResult};
@ -32,13 +30,28 @@ async fn main() -> ExitCode {
async fn run() -> Result<(), StartupError> {
let invocation =
CliInvocation::parse(std::env::args().skip(1)).map_err(StartupError::InvalidCommand)?;
let mut endpoints_iter = iota_paths::daemon_endpoints().into_iter();
let local_endpoint = endpoints_iter
.next()
.expect("path layer always returns an endpoint");
let system_endpoint = endpoints_iter
.next()
.unwrap_or_else(|| local_endpoint.clone());
let local_endpoint = match iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.map_err(|error| StartupError::Other(format!("Cannot resolve user IPC path: {error}")))?
.ipc_endpoint
{
iota_paths::IpcEndpoint::UnixSocket(path) => path,
iota_paths::IpcEndpoint::WindowsPipe(name) => {
return Err(StartupError::Other(format!(
"Windows IPC endpoint {name} is not supported by this client build"
)));
}
};
let system_endpoint = match iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
.map_err(|error| StartupError::Other(format!("Cannot resolve system IPC path: {error}")))?
.ipc_endpoint
{
iota_paths::IpcEndpoint::UnixSocket(path) => path,
iota_paths::IpcEndpoint::WindowsPipe(name) => {
return Err(StartupError::Other(format!(
"Windows IPC endpoint {name} is not supported by this client build"
)));
}
};
let endpoints = daemon_setup_flow::DaemonEndpoints {
local: local_endpoint,
system: system_endpoint,

View file

@ -10,6 +10,7 @@ use mtp::client::{Client, ClientConfig, Policy, Receiver, SendMode, Sender};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use mtp::crypto::{Keyring, PublicKeyBundle};
use std::env;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant};
@ -92,6 +93,19 @@ async fn is_read_receipts_enabled() -> bool {
// ============================================================================
const IOTA_KEYRING_PATH: &str = "iota.mk";
static IDENTITY_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
/// Must be called by the daemon before any Omikron connection is attempted.
/// It keeps identity material independent from the working directory.
pub fn configure_identity_path(path: PathBuf) {
let _ = IDENTITY_PATH.set(path);
}
fn identity_path() -> &'static Path {
IDENTITY_PATH
.get()
.map(PathBuf::as_path)
.unwrap_or_else(|| Path::new(IOTA_KEYRING_PATH))
}
const OMIKRON_PUBLIC_KEY_PATH: &str = "omikron.mpkb";
const RECONNECT_DELAY: Duration = Duration::from_secs(5);
const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300);
@ -436,7 +450,8 @@ impl OmikronConnection {
* that still read it directly.
*/
async fn load_or_migrate_keyring(&self) -> Keyring {
if let Ok(kr) = mtp::files::load_keyring_raw(IOTA_KEYRING_PATH) {
let path = identity_path();
if let Ok(kr) = mtp::files::load_keyring_raw(path) {
return kr;
}
@ -448,18 +463,18 @@ impl OmikronConnection {
"WARNING: No existing keyring found. Neither {} nor config.json \
contain a keyring; generating a new identity. If you already had \
an Iota identity, restore {} from a backup to avoid losing access.",
IOTA_KEYRING_PATH,
IOTA_KEYRING_PATH
path.display(),
path.display()
);
crypto_helper::generate_keyring()
});
if let Err(e) = mtp::files::save_keyring_raw(&keyring, IOTA_KEYRING_PATH) {
log!("Failed to persist {}: {}", IOTA_KEYRING_PATH, e);
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = mtp::files::save_keyring_raw(&keyring, path) {
log!("Failed to persist {}: {}", path.display(), e);
}
let b64 = crypto_helper::keyring_to_base64(&keyring);
modify_config(|cfg| cfg.keyring = Some(b64));
keyring
}
@ -1050,9 +1065,7 @@ impl OmikronConnection {
if let Some(app_pub_bundle) =
iota_util::crypto_helper::public_key_bundle_from_base64(&app_public_key)
{
let kr_str = CONFIG.load().keyring.clone().unwrap_or_default();
if let Some(keyring) = keyring_from_base64(&kr_str) {
if let Ok(keyring) = mtp::files::load_keyring_raw(identity_path()) {
if let Ok(encrypted_challenge) =
crypto_util::encrypt_challenge(&challenge, &app_pub_bundle)
{