use error::Error;
use fs2::FileExt;
use global_mutex;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::{from_reader, to_string_pretty};
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
lazy_static! {
static ref ADDITIONAL_SEARCH_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
}
pub fn set_additional_search_path<P: AsRef<OsStr> + ?Sized>(path: &P) {
*unwrap!(ADDITIONAL_SEARCH_PATH.lock()) = Some(From::from(path));
}
pub struct FileHandler<T> {
path: PathBuf,
_ph: PhantomData<T>,
}
impl<T> FileHandler<T> {
pub fn open<S: AsRef<OsStr> + ?Sized>(
name: &S,
assert_writable: bool,
) -> Result<FileHandler<T>, Error> {
let name = name.as_ref();
if let Some(mut path) = unwrap!(ADDITIONAL_SEARCH_PATH.lock()).clone() {
path.push(name);
if OpenOptions::new()
.read(true)
.write(assert_writable)
.open(&path)
.is_ok()
{
return Ok(FileHandler {
path,
_ph: PhantomData,
});
}
}
if let Ok(mut path) = current_bin_dir() {
path.push(name);
if OpenOptions::new()
.read(true)
.write(assert_writable)
.open(&path)
.is_ok()
{
return Ok(FileHandler {
path,
_ph: PhantomData,
});
}
}
if let Ok(mut path) = bundle_resource_dir() {
path.push(name);
if OpenOptions::new()
.read(true)
.write(assert_writable)
.open(&path)
.is_ok()
{
return Ok(FileHandler {
path,
_ph: PhantomData,
});
}
}
if let Ok(mut path) = user_app_dir() {
path.push(name);
if OpenOptions::new()
.read(true)
.write(assert_writable)
.open(&path)
.is_ok()
{
return Ok(FileHandler {
path,
_ph: PhantomData,
});
}
}
let mut path = system_cache_dir()?;
path.push(name);
match OpenOptions::new()
.read(true)
.write(assert_writable)
.open(&path)
{
Ok(_) => Ok(FileHandler {
path,
_ph: PhantomData,
}),
Err(e) => Err(From::from(e)),
}
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl<T> FileHandler<T>
where
T: Default + Serialize,
{
pub fn new<S: AsRef<OsStr> + ?Sized>(
name: &S,
is_existing_file_writable: bool,
) -> Result<FileHandler<T>, Error> {
if let Ok(fh) = Self::open(name, is_existing_file_writable) {
return Ok(fh);
}
let contents = to_string_pretty(&T::default())?.into_bytes();
let name = name.as_ref();
let _guard = global_mutex::get_mutex()
.lock()
.expect("Could not lock mutex");
if let Some(mut path) = unwrap!(ADDITIONAL_SEARCH_PATH.lock()).clone() {
path.push(name);
if let Ok(mut f) = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&path)
{
write_with_lock(&mut f, &contents)?;
return Ok(FileHandler {
path,
_ph: PhantomData,
});
}
}
if let Ok(mut path) = current_bin_dir() {
path.push(name);
if let Ok(mut f) = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&path)
{
write_with_lock(&mut f, &contents)?;
return Ok(FileHandler {
path,
_ph: PhantomData,
});
}
}
if let Ok(mut path) = user_app_dir() {
let avoid = if path.is_dir() {
false
} else {
fs::create_dir(&path).is_err()
};
if !avoid {
path.push(name);
if let Ok(mut f) = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&path)
{
write_with_lock(&mut f, &contents)?;
return Ok(FileHandler {
path,
_ph: PhantomData,
});
}
}
}
let mut path = system_cache_dir()?;
if !path.is_dir() {
fs::create_dir(&path)?;
}
path.push(name);
match OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&path)
{
Ok(mut f) => {
write_with_lock(&mut f, &contents)?;
Ok(FileHandler {
path,
_ph: PhantomData,
})
}
Err(e) => Err(From::from(e)),
}
}
}
impl<T> FileHandler<T>
where
T: DeserializeOwned,
{
#[cfg_attr(feature = "cargo-clippy", allow(redundant_closure))] pub fn read_file(&self) -> Result<T, Error> {
let mut file = File::open(&self.path)?;
let contents = shared_lock(&mut file, |file| from_reader(file))?;
Ok(contents)
}
}
impl<T> FileHandler<T>
where
T: Serialize,
{
pub fn write_file(&self, contents: &T) -> Result<(), Error> {
let contents = to_string_pretty(contents)?.into_bytes();
let _guard = global_mutex::get_mutex()
.lock()
.expect("Could not lock mutex");
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&self.path)?;
write_with_lock(&mut file, &contents)?;
Ok(())
}
}
pub fn cleanup<S: AsRef<OsStr>>(name: &S) -> io::Result<()> {
let name = name.as_ref();
let i1 = current_bin_dir().into_iter();
let i2 = user_app_dir().into_iter();
let i3 = system_cache_dir().into_iter();
let dirs = i1.chain(i2.chain(i3));
for mut path in dirs {
path.push(name);
if path.exists() {
fs::remove_file(path)?;
}
}
Ok(())
}
fn exclusive_lock<F, R, E>(file: &mut File, f: F) -> Result<R, Error>
where
F: FnOnce(&mut File) -> Result<R, E>,
Error: From<E>,
{
file.lock_exclusive()?;
let result = f(file);
file.unlock()?;
result.map_err(From::from)
}
fn shared_lock<F, R, E>(file: &mut File, f: F) -> Result<R, Error>
where
F: FnOnce(&mut File) -> Result<R, E>,
Error: From<E>,
{
file.lock_shared()?;
let result = f(file);
file.unlock()?;
result.map_err(From::from)
}
fn write_with_lock(file: &mut File, contents: &[u8]) -> Result<(), Error> {
exclusive_lock(file, |file| file.write_all(contents))
}
pub fn current_bin_dir() -> Result<PathBuf, Error> {
match env::current_exe()?.parent() {
Some(path) => Ok(path.to_path_buf()),
None => Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Current bin dir",
))),
}
}
#[cfg(not(target_os = "macos"))]
pub fn bundle_resource_dir() -> Result<PathBuf, Error> {
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Bundle resource directory only applicable to MacOs",
)))
}
#[cfg(target_os = "macos")]
pub fn bundle_resource_dir() -> Result<PathBuf, Error> {
let mut bundle_dir = env::current_exe()?
.parent()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Bundle resources directory"))?
.to_path_buf();
let is_inside_bundle = bundle_dir
.to_str()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Path is not unicode"))?
.ends_with(".app/Contents/MacOS");
if !is_inside_bundle {
return Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Not inside an Application Bundle",
)));
}
bundle_dir = bundle_dir
.parent()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Bundle resource directory"))?
.to_path_buf();
bundle_dir.push("Resources");
Ok(bundle_dir)
}
#[cfg(windows)]
pub fn user_app_dir() -> Result<PathBuf, Error> {
let path = env::var("APPDATA")?;
let app_dir = Path::new(&path);
if app_dir.is_dir() {
Ok(join_exe_file_stem(app_dir)?)
} else {
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Global user app directory not found.",
)))
}
}
#[cfg(all(unix, not(target_os = "macos")))]
pub fn user_app_dir() -> Result<PathBuf, Error> {
let mut home_dir = env::home_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found."))?;
home_dir.push(".config");
if home_dir.is_dir() {
Ok(join_exe_file_stem(&home_dir)?)
} else {
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Global user app directory not found.",
)))
}
}
#[cfg(target_os = "macos")]
pub fn user_app_dir() -> Result<PathBuf, Error> {
let mut app_dir = env::home_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found."))?;
app_dir.push("Library/Application Support");
if app_dir.is_dir() {
Ok(join_exe_file_stem(&app_dir)?)
} else {
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Global user app directory not found.",
)))
}
}
#[cfg(windows)]
pub fn system_cache_dir() -> Result<PathBuf, Error> {
let path = env::var("ALLUSERSPROFILE")?;
let sys_cache_dir = Path::new(&path);
if sys_cache_dir.is_dir() {
Ok(join_exe_file_stem(sys_cache_dir)?)
} else {
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Global system cache directory not found.",
)))
}
}
#[cfg(all(unix, not(target_os = "macos")))]
pub fn system_cache_dir() -> Result<PathBuf, Error> {
let sys_cache_dir = Path::new("/var/cache");
if sys_cache_dir.is_dir() {
Ok(join_exe_file_stem(sys_cache_dir)?)
} else {
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Global system cache directory not found.",
)))
}
}
#[cfg(target_os = "macos")]
pub fn system_cache_dir() -> Result<PathBuf, Error> {
let sys_cache_dir = Path::new("/Library/Application Support");
if sys_cache_dir.is_dir() {
Ok(join_exe_file_stem(sys_cache_dir)?)
} else {
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"Global system cache directory not found.",
)))
}
}
pub fn exe_file_stem() -> Result<OsString, Error> {
if let Ok(exe_path) = env::current_exe() {
let file_stem = exe_path.file_stem();
Ok(file_stem
.ok_or_else(|| not_found_error(&exe_path))?
.to_os_string())
} else {
Ok(From::from("default"))
}
}
pub struct ScopedUserAppDirRemover;
impl ScopedUserAppDirRemover {
fn remove_dir(&mut self) {
let _ = user_app_dir()
.and_then(|user_app_dir| fs::remove_dir_all(user_app_dir).map_err(Error::Io));
}
}
impl Drop for ScopedUserAppDirRemover {
fn drop(&mut self) {
self.remove_dir();
}
}
fn not_found_error(file_name: &Path) -> io::Error {
let mut msg: String = From::from("No file name component: ");
msg.push_str(&file_name.to_string_lossy());
io::Error::new(io::ErrorKind::NotFound, msg)
}
fn join_exe_file_stem(path: &Path) -> Result<PathBuf, Error> {
Ok(path.join(exe_file_stem()?))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn read_write_file_test() {
let _cleaner = ScopedUserAppDirRemover;
let file_handler = match FileHandler::new("test0.json", true) {
Ok(result) => result,
Err(err) => panic!("failed accessing file with error {:?}", err),
};
let test_value = 123_456_789u64;
let _ = file_handler.write_file(&test_value);
let read_value = match file_handler.read_file() {
Ok(result) => result,
Err(err) => panic!("failed reading file with error {:?}", err),
};
assert_eq!(test_value, read_value);
}
#[test]
fn existing_file_is_overwritten() {
let _cleaner = ScopedUserAppDirRemover;
let file_handler = FileHandler::new("test1.json", true).expect("failed accessing file");
let write_value0 = vec![1, 2, 3];
file_handler
.write_file(&write_value0)
.expect("failed writing file");
let write_value1 = vec![4, 5, 6];
file_handler
.write_file(&write_value1)
.expect("failed writing file");
let read_value = file_handler.read_file().expect("failed reading file");
assert_eq!(read_value, write_value1);
}
#[test]
fn concurrent_writes() {
use std::iter;
use std::sync::{Arc, Barrier};
use std::thread;
const NUM_THREADS: usize = 100;
const DATA_SIZE: usize = 10_000;
const FILE_NAME: &str = "test2.json";
let _cleaner = ScopedUserAppDirRemover;
let barrier = Arc::new(Barrier::new(NUM_THREADS));
let handles = (0..NUM_THREADS)
.map(|i| {
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
let data = iter::repeat(i).take(DATA_SIZE).collect::<Vec<_>>();
let _ = barrier.wait();
let file_handler =
FileHandler::new(FILE_NAME, true).expect("failed accessing file");
file_handler.write_file(&data).expect("failed writing file");
})
}).collect::<Vec<_>>();
for handle in handles {
unwrap!(handle.join());
}
let file_handler = FileHandler::new(FILE_NAME, true).expect("failed accessing file");
let mut data: Vec<usize> = file_handler.read_file().expect("failed reading file");
data.sort();
data.dedup();
assert_eq!(data.len(), 1);
}
#[test]
#[ignore]
#[cfg_attr(feature = "cargo-clippy", allow(ifs_same_cond))]
fn print_paths() {
let os = if cfg!(target_os = "macos") {
"macOS".to_string()
} else if cfg!(target_os = "linux") {
"Linux".to_string()
} else if cfg!(unix) {
"Unix (family)".to_string()
} else if cfg!(windows) {
"Windows".to_string()
} else {
"Unknown".to_string()
};
let current_bin_dir = match current_bin_dir() {
Ok(x) => format!("{:?}", x),
Err(x) => format!("{:?}", x),
};
let bundle_resource_dir = match bundle_resource_dir() {
Ok(x) => format!("{:?}", x),
Err(x) => format!("{:?}", x),
};
let user_app_dir = match user_app_dir() {
Ok(x) => format!("{:?}", x),
Err(x) => format!("{:?}", x),
};
let system_cache_dir = match system_cache_dir() {
Ok(x) => format!("{:?}", x),
Err(x) => format!("{:?}", x),
};
println!("=================================");
println!("Current bin dir in {}: {}", os, current_bin_dir);
println!("Current bin resource in {}: {}", os, bundle_resource_dir);
println!("Current use-app-dir in {}: {}", os, user_app_dir);
println!("Current system-cache-dir in {}: {}", os, system_cache_dir);
println!("=================================");
}
}