use std::path::{Path, PathBuf};
use crate::error::NativeError;
use crate::platform::Platform;
const PUBLIC_DIR_ENV: &str = "RAHTI_PUBLIC_DIR";
const SPILL_DIR_ENV: &str = "RAHTI_SPILL_DIR";
pub const DATA_DIR_ENV: &str = "RAHTI_NATIVE_DATA_DIR";
pub const CONFIG_DIR_ENV: &str = "RAHTI_NATIVE_CONFIG_DIR";
pub const CACHE_DIR_ENV: &str = "RAHTI_NATIVE_CACHE_DIR";
pub const RESOURCE_DIR_ENV: &str = "RAHTI_NATIVE_RESOURCE_DIR";
pub const ASSET_STAMP: &str = ".rahti-assets";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppPaths {
identifier: String,
data: PathBuf,
config: PathBuf,
cache: PathBuf,
resources: PathBuf,
}
impl AppPaths {
pub fn resolve(identifier: &str) -> Result<Self, NativeError> {
crate::config::check_identifier(identifier)
.map_err(|message| NativeError::new("paths", message))?;
let resources = match env_path(RESOURCE_DIR_ENV) {
Some(dir) => dir,
None => default_resource_dir()?,
};
let data = match env_path(DATA_DIR_ENV) {
Some(dir) => dir,
None => default_data_dir(identifier)?,
};
let config = env_path(CONFIG_DIR_ENV)
.or_else(|| default_config_dir(identifier))
.unwrap_or_else(|| data.join("config"));
let cache = env_path(CACHE_DIR_ENV).unwrap_or_else(|| data.join("cache"));
Ok(AppPaths {
identifier: identifier.to_string(),
data,
config,
cache,
resources,
})
}
pub fn from_host(
identifier: &str,
data: impl Into<PathBuf>,
cache: impl Into<PathBuf>,
resources: impl Into<PathBuf>,
) -> Result<Self, NativeError> {
crate::config::check_identifier(identifier)
.map_err(|message| NativeError::new("paths", message))?;
let data = data.into();
Ok(AppPaths {
identifier: identifier.to_string(),
config: data.join("config"),
cache: cache.into(),
resources: resources.into(),
data,
})
}
pub fn identifier(&self) -> &str {
&self.identifier
}
pub fn data(&self) -> &Path {
&self.data
}
pub fn config(&self) -> &Path {
&self.config
}
pub fn cache(&self) -> &Path {
&self.cache
}
pub fn resources(&self) -> &Path {
&self.resources
}
pub fn public(&self) -> PathBuf {
self.data.join("assets")
}
pub fn bundled_public(&self) -> PathBuf {
self.resources.join("public")
}
pub fn database(&self) -> PathBuf {
self.data.join("app.db")
}
pub fn uploads(&self) -> PathBuf {
self.data.join("uploads")
}
pub fn spill(&self) -> PathBuf {
self.cache.join("uploads")
}
pub fn logs(&self) -> PathBuf {
self.data.join("logs")
}
pub fn temp(&self) -> PathBuf {
self.cache.join("tmp")
}
pub fn exports(&self) -> PathBuf {
self.data.join("exports")
}
pub fn secret_file(&self) -> PathBuf {
self.data.join(crate::secret::SECRET_FILE)
}
pub fn prepare(&self) -> Result<(), NativeError> {
for dir in [
self.data.clone(),
self.config.clone(),
self.cache.clone(),
self.public(),
self.uploads(),
self.spill(),
self.logs(),
self.temp(),
self.exports(),
] {
std::fs::create_dir_all(&dir).map_err(|e| NativeError::io("paths", &dir, e))?;
}
Ok(())
}
pub fn sqlite_url(&self) -> String {
let path = self.database().display().to_string().replace('\\', "/");
format!("sqlite://{path}?mode=rwc")
}
pub fn apply_environment(&self, public: &Path) {
unsafe {
std::env::set_var(PUBLIC_DIR_ENV, public);
std::env::set_var(SPILL_DIR_ENV, self.spill());
}
}
pub fn apply_sqlite_database_url(&self) {
unsafe {
std::env::set_var("DATABASE_URL", self.sqlite_url());
}
}
}
pub struct EmbeddedAsset<'a> {
pub path: &'a str,
pub bytes: &'a [u8],
}
pub fn stage_embedded_assets(
assets: &[EmbeddedAsset<'_>],
destination: &Path,
version: &str,
) -> Result<bool, NativeError> {
let stamp = destination.join(ASSET_STAMP);
if std::fs::read_to_string(&stamp).is_ok_and(|current| current.trim() == version.trim()) {
return Ok(false);
}
if assets.is_empty() {
return Err(NativeError::at(
"assets",
destination,
"this package embeds no static assets, so every stylesheet and the browser \
runtime would 404.\n \
The shell embeds the project's `public/` at compile time — check that the \
directory exists and is not empty.",
));
}
if destination.exists() {
std::fs::remove_dir_all(destination)
.map_err(|e| NativeError::io("assets", destination, e))?;
}
for asset in assets {
if asset.path.contains("..") || Path::new(asset.path).is_absolute() {
return Err(NativeError::at(
"assets",
asset.path,
"an embedded asset path leaves the asset directory",
));
}
let target = destination.join(asset.path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).map_err(|e| NativeError::io("assets", parent, e))?;
}
std::fs::write(&target, asset.bytes).map_err(|e| NativeError::io("assets", &target, e))?;
}
std::fs::write(&stamp, version).map_err(|e| NativeError::io("assets", &stamp, e))?;
Ok(true)
}
pub fn stage_public_assets(
source: &Path,
destination: &Path,
version: &str,
) -> Result<bool, NativeError> {
let stamp = destination.join(ASSET_STAMP);
if std::fs::read_to_string(&stamp).is_ok_and(|current| current.trim() == version.trim()) {
return Ok(false);
}
if !source.is_dir() {
return Err(NativeError::at(
"assets",
source,
"the package has no public assets to stage",
));
}
if destination.exists() {
std::fs::remove_dir_all(destination)
.map_err(|e| NativeError::io("assets", destination, e))?;
}
copy_tree(source, destination)?;
std::fs::write(&stamp, version).map_err(|e| NativeError::io("assets", &stamp, e))?;
Ok(true)
}
fn copy_tree(source: &Path, destination: &Path) -> Result<(), NativeError> {
std::fs::create_dir_all(destination).map_err(|e| NativeError::io("assets", destination, e))?;
let entries = std::fs::read_dir(source).map_err(|e| NativeError::io("assets", source, e))?;
for entry in entries {
let entry = entry.map_err(|e| NativeError::io("assets", source, e))?;
let from = entry.path();
let to = destination.join(entry.file_name());
let kind = entry
.file_type()
.map_err(|e| NativeError::io("assets", &from, e))?;
if kind.is_dir() {
copy_tree(&from, &to)?;
} else {
std::fs::copy(&from, &to).map_err(|e| NativeError::io("assets", &from, e))?;
}
}
Ok(())
}
fn env_path(name: &str) -> Option<PathBuf> {
let value = std::env::var(name).ok()?;
let value = value.trim();
(!value.is_empty()).then(|| PathBuf::from(value))
}
fn default_data_dir(identifier: &str) -> Result<PathBuf, NativeError> {
match Platform::current() {
Platform::Windows => Ok(required_env("LOCALAPPDATA")?.join(identifier)),
Platform::Android => Err(NativeError::new(
"paths",
format!(
"an Android package must be told where its files are: set {DATA_DIR_ENV}, \
or build the paths with `AppPaths::from_host`.\n \
Only the Java side knows the internal files directory, so it cannot be \
derived here."
),
)),
Platform::Other => {
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir);
Ok(home.join(".local/share").join(identifier))
}
}
}
fn default_config_dir(identifier: &str) -> Option<PathBuf> {
match Platform::current() {
Platform::Windows => std::env::var_os("APPDATA")
.map(PathBuf::from)
.map(|dir| dir.join(identifier)),
_ => None,
}
}
fn default_resource_dir() -> Result<PathBuf, NativeError> {
let exe = std::env::current_exe()
.map_err(|e| NativeError::new("paths", format!("cannot locate the executable: {e}")))?;
Ok(exe
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(".")))
}
fn required_env(name: &str) -> Result<PathBuf, NativeError> {
std::env::var_os(name).map(PathBuf::from).ok_or_else(|| {
NativeError::new(
"paths",
format!("{name} is not set, so there is nowhere to keep this application's data"),
)
})
}