use crate::{config, data, error};
use std::{fs, io::Write, path, process};
#[derive(Debug, Clone)]
pub struct FileManager {
vault_path: path::PathBuf,
default_extension: String,
editor: Option<Vec<String>>,
pub(crate) primary_viewer: Option<Vec<String>>,
pub(crate) primary_viewer_type: Option<config::ViewerType>,
pub(crate) secondary_viewer: Option<Vec<String>>,
pub(crate) secondary_viewer_type: Option<config::ViewerType>,
}
impl Default for FileManager {
fn default() -> Self {
Self::new(&crate::Config::default())
}
}
impl FileManager {
pub fn new(config: &crate::Config) -> Self {
Self {
vault_path: config
.vault_path
.clone()
.expect("Vault path should be set."),
default_extension: config.default_extension.clone(),
editor: config.editor.clone(),
primary_viewer: config.viewer.clone(),
primary_viewer_type: config.viewer_type,
secondary_viewer: config.secondary_viewer.clone(),
secondary_viewer_type: config.secondary_viewer_type,
}
}
pub fn get_vault_title(&self) -> String {
format!(
"Notes in {}",
self.vault_path
.as_path()
.file_name()
.and_then(|folder| folder.to_str())
.unwrap_or("Unknown Folder")
)
}
pub fn ensure_file_extension(&self, path: &mut path::PathBuf) {
if path.extension().is_none() {
path.set_extension(&self.default_extension);
}
}
pub fn rename_note_file(
&self,
index: data::NoteIndexContainer,
id: &str,
new_name: String,
) -> error::Result<()> {
if new_name.is_empty() {
return Err(error::RucolaError::Input(String::from(
"Name cannot be empty!",
)));
}
let input_path = path::Path::new(&new_name);
if input_path.components().count() > 1 {
return Err(error::RucolaError::Input(
"File name cannot be a path.".to_owned(),
));
}
let index_b = index.borrow_mut();
let note = index_b
.get(id)
.ok_or_else(|| error::RucolaError::NoteNotFound(id.to_owned()))?;
let mut new_path = note.path.clone();
new_path.set_file_name(
input_path
.file_name()
.ok_or_else(|| error::RucolaError::Input("New name cannot be empty.".to_owned()))?,
);
if new_path.extension().is_none() {
if let Some(old_extension) = note.path.extension() {
new_path.set_extension(old_extension);
} else {
self.ensure_file_extension(&mut new_path);
}
}
if let Some(parent) = new_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
fs::rename(¬e.path, &new_path)?;
let mut regex_builder = String::new();
regex_builder.push_str("(\\[\\[)(");
regex_builder.push_str(¬e.name); regex_builder.push('|');
regex_builder.push_str(id);
regex_builder.push_str(")(\\|?[^\\|^\\]^\\]]*\\]\\])");
let mut replacement_builder = String::new();
replacement_builder.push_str("${1}");
replacement_builder.push_str(&new_name);
replacement_builder.push_str("${3}");
let reg = regex::Regex::new(®ex_builder)?;
for other_note in index_b
.blinks_vec(id)
.iter()
.filter_map(|(id, _)| index_b.get(id))
{
let old_content = std::fs::read_to_string(&other_note.path)?;
let res = reg.replace_all(&old_content, &replacement_builder);
let mut file = std::fs::OpenOptions::new()
.truncate(true)
.write(true)
.read(true)
.open(&other_note.path)?;
file.write_all(res.as_bytes())?;
}
Ok(())
}
pub fn move_note_file(
&self,
index: data::NoteIndexContainer,
id: &str,
new_path_buf: String,
) -> error::Result<()> {
let index_b = index.borrow_mut();
let note = index_b
.get(id)
.ok_or_else(|| error::RucolaError::NoteNotFound(id.to_owned()))?;
let mut new_path = self.vault_path.join(new_path_buf).join(¬e.name);
self.ensure_file_extension(&mut new_path);
if let Some(parent) = new_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
fs::rename(¬e.path, &new_path)?;
Ok(())
}
pub fn delete_note_file(&self, index: data::NoteIndexContainer, id: &str) -> error::Result<()> {
if let Some(note) = index.borrow().get(id) {
fs::remove_file(¬e.path)?;
}
Ok(())
}
pub fn create_note_file(&self, input_path: &str) -> error::Result<()> {
let mut path = self.vault_path.clone();
path.push(input_path);
self.ensure_file_extension(&mut path);
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let mut file = fs::File::create(path.clone())?;
write!(
file,
"# {}",
crate::data::path_to_name(&path).unwrap_or_else(|_e| "Note".to_owned())
)?;
Ok(())
}
pub fn create_edit_command(
&self,
path: &path::PathBuf,
) -> error::Result<std::process::Command> {
self.editor
.as_ref()
.and_then(|editor_arg_list| {
let mut iter = editor_arg_list.iter();
if let Some(programm) = iter.next().filter(|editor| !editor.is_empty()) {
let mut cmd = process::Command::new(programm);
for arg in iter {
if arg == "%p" {
cmd.arg(path.canonicalize().as_ref().unwrap_or(path));
} else {
cmd.arg(arg);
}
}
Some(cmd)
} else {
None
}
})
.or_else(|| {
std::env::var("EDITOR")
.ok()
.filter(|editor| !editor.is_empty())
.map(|editor| {
let mut cmd = process::Command::new(editor);
cmd.arg(path.canonicalize().as_ref().unwrap_or(path));
cmd
})
})
.or_else(|| open::commands(path).into_iter().nth(0))
.ok_or(error::RucolaError::ApplicationMissing)
}
pub fn create_view_command(
&self,
note: &data::Note,
primary: bool,
) -> error::Result<std::process::Command> {
let vtype = if primary {
self.primary_viewer_type
} else {
self.secondary_viewer_type.or(self.primary_viewer_type)
}
.unwrap_or_default();
let path = match vtype {
config::ViewerType::Html => {
super::html_builder::name_to_html_path(¬e.name, &self.vault_path)
}
config::ViewerType::Markdown => note.path.clone(),
};
eprintln!("{:?}", path);
let viewer = if primary {
self.primary_viewer.as_ref()
} else {
self.secondary_viewer.as_ref()
};
viewer
.and_then(|viewer_arg_list| {
let mut iter = viewer_arg_list.iter();
if let Some(programm) = iter.next().filter(|viewer| !viewer.is_empty()) {
let mut cmd = process::Command::new(programm);
for arg in iter {
if arg == "%p" {
cmd.arg(path.canonicalize().as_ref().unwrap_or(&path));
} else {
cmd.arg(arg);
}
}
Some(cmd)
} else {
None
}
})
.or_else(|| open::commands(path).into_iter().nth(0))
.ok_or(error::RucolaError::ApplicationMissing)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
#[test]
fn test_edit() {
let editor = std::env::var("EDITOR");
let config = crate::Config {
vault_path: Some(std::env::current_dir().unwrap().join("tests")),
..Default::default()
};
let fm = super::FileManager::new(&config);
let path = std::env::current_dir()
.unwrap()
.join("tests/common/notes/Books.md");
if let Ok(_editor) = editor {
fm.create_edit_command(&path.to_path_buf()).unwrap();
}
}
#[test]
fn test_viewing() {
let config = crate::Config {
vault_path: Some(std::env::current_dir().unwrap().join("tests")),
..Default::default()
};
let fm = super::FileManager::new(&config);
let note = crate::data::Note::from_path(
&std::env::current_dir()
.unwrap()
.join("tests/common/notes/Books.md"),
)
.unwrap();
fm.create_view_command(¬e, true).unwrap();
fm.create_view_command(¬e, false).unwrap();
}
#[test]
fn test_create() {
let tmp = testdir::testdir!();
let config = crate::Config {
vault_path: Some(tmp.clone()),
..Default::default()
};
let fm = super::FileManager::new(&config);
fm.create_note_file("Lie Group").unwrap();
fm.create_note_file("Math/Atlas").unwrap();
let lg_path = tmp.join(String::from("Lie Group.md"));
let at_path = tmp
.join(String::from("Math"))
.join(String::from("Atlas.md"));
assert!(lg_path.exists());
assert!(at_path.exists());
let _lg = crate::data::Note::from_path(&lg_path).unwrap();
let _at = crate::data::Note::from_path(&at_path).unwrap();
}
#[test]
fn test_create_other_suffix() {
let tmp = testdir::testdir!();
let fm = super::FileManager::new(&crate::Config {
default_extension: String::from("txt"),
file_types: vec![String::from("txt")],
vault_path: Some(tmp.clone()),
..Default::default()
});
fm.create_note_file("Lie Group").unwrap();
fm.create_note_file("Math/Atlas").unwrap();
let lg_path = tmp.join(String::from("Lie Group.txt"));
let at_path = tmp
.join(String::from("Math"))
.join(String::from("Atlas.txt"));
assert!(lg_path.exists());
assert!(at_path.exists());
let _lg = crate::data::Note::from_path(&lg_path).unwrap();
let _at = crate::data::Note::from_path(&at_path).unwrap();
}
#[test]
fn test_delete() {
let tmp = testdir::testdir!();
let config = crate::Config {
vault_path: Some(tmp.clone()),
..Default::default()
};
let fm = super::FileManager::new(&config);
fm.create_note_file("Lie Group").unwrap();
fm.create_note_file("Math/Atlas").unwrap();
let lg_path = tmp.join(String::from("Lie Group.md"));
let at_path = tmp
.join(String::from("Math"))
.join(String::from("Atlas.md"));
assert!(lg_path.exists());
assert!(at_path.exists());
let tracker = crate::io::FileTracker::new(&config).unwrap();
let builder = crate::io::HtmlBuilder::new(&config);
let index = crate::data::NoteIndex::new(tracker, builder, &config).0;
let index_con = std::rc::Rc::new(std::cell::RefCell::new(index));
fm.delete_note_file(index_con.clone(), "lie-group").unwrap();
assert!(!lg_path.exists());
assert!(at_path.exists());
fm.delete_note_file(index_con.clone(), "atlas").unwrap();
assert!(!lg_path.exists());
assert!(!at_path.exists());
}
#[test]
fn test_rename() {
let tmp = testdir::testdir!();
let config = crate::Config {
vault_path: Some(tmp.clone()),
..Default::default()
};
let fm = super::FileManager::new(&config);
let lg_path = tmp.join(String::from("Lie Group.md"));
let at_path = tmp
.join(String::from("Math"))
.join(String::from("Atlas.md"));
let lg_path_after = tmp.join(String::from("Lie Soup.md"));
let at_path_after = tmp
.join(String::from("Math"))
.join(String::from("Atlantis.md"));
fm.create_note_file("Lie Group").unwrap();
fm.create_note_file("Math/Atlas").unwrap();
let tracker = crate::io::FileTracker::new(&config).unwrap();
let builder = crate::io::HtmlBuilder::new(&config);
let index = crate::data::NoteIndex::new(tracker, builder, &config).0;
assert!(index.get("atlas").is_some());
assert!(index.get("lie-group").is_some());
let index_con = std::rc::Rc::new(std::cell::RefCell::new(index));
assert!(lg_path.exists());
assert!(at_path.exists());
fm.rename_note_file(index_con.clone(), "lie-group", String::from("Lie Soup"))
.unwrap();
fm.rename_note_file(index_con.clone(), "atlas", String::from("Atlantis"))
.unwrap();
assert!(lg_path_after.exists());
assert!(at_path_after.exists());
}
#[test]
fn test_rename_updates_links() {
let tmp = testdir::testdir!();
let config = crate::Config {
vault_path: Some(tmp.clone()),
..Default::default()
};
let fm = super::FileManager::new(&config);
let at_path = tmp.join(String::from("Atlas.md"));
let ma_path = tmp.join(String::from("Manifold.md"));
let to_path = tmp.join(String::from("Topology.md"));
fm.create_note_file("Atlas").unwrap();
fm.create_note_file("Manifold").unwrap();
fm.create_note_file("Topology").unwrap();
std::fs::copy(
std::env::current_dir()
.unwrap()
.join("tests/common/notes/math/Atlas.md"),
&at_path,
)
.unwrap();
std::fs::copy(
std::env::current_dir()
.unwrap()
.join("tests/common/notes/math/Manifold.md"),
&ma_path,
)
.unwrap();
std::fs::copy(
std::env::current_dir()
.unwrap()
.join("tests/common/notes/math/Topology.md"),
&to_path,
)
.unwrap();
let tracker = crate::io::FileTracker::new(&config).unwrap();
let builder = crate::io::HtmlBuilder::new(&config);
let index = crate::data::NoteIndex::new(tracker, builder, &config).0;
let index_con = std::rc::Rc::new(std::cell::RefCell::new(index));
assert!(at_path.exists());
assert!(ma_path.exists());
assert!(to_path.exists());
let ma_content = std::fs::read_to_string(&ma_path).unwrap();
assert!(ma_content.contains("[[Atlas]]"));
assert!(!ma_content.contains("[[Atlantis]]"));
assert!(ma_content.contains("[[Topology|topological space]]"));
assert!(!ma_content.contains("[[Anthology|topological space]]"));
fm.rename_note_file(index_con.clone(), "topology", String::from("Anthology"))
.unwrap();
fm.rename_note_file(index_con.clone(), "atlas", String::from("Atlantis"))
.unwrap();
let ma_content = std::fs::read_to_string(&ma_path).unwrap();
assert!(!ma_content.contains("[[Atlas]]"));
assert!(ma_content.contains("[[Atlantis]]"));
assert!(!ma_content.contains("[[Topology|topological space]]"));
assert!(ma_content.contains("[[Anthology|topological space]]"));
}
#[test]
fn test_move() {
let tmp = testdir::testdir!();
let config = crate::Config {
vault_path: Some(tmp.clone()),
..Default::default()
};
let fm = super::FileManager::new(&config);
let lg_path = tmp.join(String::from("Lie Group.md"));
let at_path = tmp
.join(String::from("Math"))
.join(String::from("Atlas.md"));
let lg_path_after = tmp
.join(String::from("Topology"))
.join(String::from("Lie Group.md"));
let at_path_after = tmp
.join(String::from("Topology"))
.join(String::from("Atlantis"))
.join(String::from("Atlas.md"));
fm.create_note_file("Lie Group").unwrap();
fm.create_note_file("Math/Atlas").unwrap();
let tracker = crate::io::FileTracker::new(&config).unwrap();
let builder = crate::io::HtmlBuilder::new(&config);
let index = crate::data::NoteIndex::new(tracker, builder, &config).0;
let index_con = std::rc::Rc::new(std::cell::RefCell::new(index));
assert!(lg_path.exists());
assert!(at_path.exists());
fm.move_note_file(index_con.clone(), "lie-group", String::from("Topology/"))
.unwrap();
fm.move_note_file(
index_con.clone(),
"atlas",
String::from("Topology/Atlantis"),
)
.unwrap();
assert!(lg_path_after.exists());
assert!(!lg_path.exists());
assert!(at_path_after.exists());
assert!(!at_path.exists());
}
#[test]
fn test_file_endings() {
let md_ending_tar = std::env::current_dir()
.unwrap()
.join("tests/common/test.md");
let txt_ending_tar = std::env::current_dir()
.unwrap()
.join("tests/common/test.txt");
let config = crate::Config {
vault_path: Some(std::env::current_dir().unwrap().join("tests")),
..Default::default()
};
let fm = super::FileManager::new(&config);
let mut no_ending = std::env::current_dir().unwrap().join("tests/common/test");
let mut md_ending = std::env::current_dir()
.unwrap()
.join("tests/common/test.md");
let mut txt_ending = std::env::current_dir()
.unwrap()
.join("tests/common/test.txt");
fm.ensure_file_extension(&mut no_ending);
fm.ensure_file_extension(&mut md_ending);
fm.ensure_file_extension(&mut txt_ending);
assert_eq!(no_ending, md_ending_tar);
assert_eq!(md_ending, md_ending_tar);
assert_eq!(txt_ending, txt_ending_tar);
}
}