1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
use crate::storage::{Error, Result, INTERNAL_DIR_NAME, MARKHOR_EXTENSION};
use crate::storage::document::Document;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use tracing::{debug, instrument, warn};
use super::Workspace;
/// Represents a directory within a Workspace or another Folder,
/// which can contain Documents and other Folders.
#[derive(Debug, Clone)]
pub struct Folder {
// Absolute path to the folder
absolute_path: PathBuf,
// Workspace owning this document
workspace: Arc<Workspace>,
}
impl Folder {
/// Creates a Folder instance. Intended for internal use.
/// Assumes the path already points to a valid, existing directory *inside* the workspace.
pub(crate) fn new(absolute_path: PathBuf, workspace: Arc<Workspace>) -> Self {
// Consider adding an assertion or quick check in debug mode?
// debug_assert!(path.is_dir(), "Folder::new called with non-directory path");
Folder { absolute_path, workspace }
}
/// Returns the path to this folder's directory.
pub fn path(&self) -> &Path {
&self.absolute_path.strip_prefix(&self.workspace.absolute_path)
.expect("Internal error: Document is not in workspace")
}
/// Returns the name of the folder.
pub fn name(&self) -> Option<&str> {
self.absolute_path.file_name()?.to_str()
}
/// Opens the document with the specified name within this folder.
///
/// The document name should not include the `.markhor` extension.
///
/// # Errors
///
/// Returns an error if the document cannot be opened or does not exist.
#[instrument(skip(self), fields(folder_path = %self.absolute_path.display()))]
pub async fn document_by_name(&self, name: &str) -> Result<Document> {
let document_path = self.absolute_path.join(format!("{}.{}", name, MARKHOR_EXTENSION));
Document::open(document_path, self.workspace.clone()).await
}
/// Creates a new document in this folder with the specified name.
///
/// The document name should not include the `.markhor` extension.
///
/// # Errors
///
/// Returns an error if the document cannot be created or already exists.
#[instrument(skip(self), fields(folder_path = %self.absolute_path.display()))]
pub async fn create_document(&self, name: &str) -> Result<Document> {
let document_path = self.absolute_path.join(format!("{}.{}", name, MARKHOR_EXTENSION));
Document::create(document_path, self.workspace.clone()).await
}
/// Creates a new subfolder within this folder with the specified name.
///
/// # Errors
///
/// Returns an error if the subfolder cannot be created or already exists.
#[instrument(skip(self), fields(folder_path = %self.absolute_path.display()))]
pub async fn create_subfolder(&self, name: &str) -> Result<Folder> {
let subfolder_path = self.absolute_path.join(name);
fs::create_dir_all(&subfolder_path).await.map_err(Error::Io)?;
Ok(Folder::new(subfolder_path, self.workspace.clone()))
}
/// Lists the documents directly contained within this folder (non-recursive).
///
/// Invalid `.markhor` files that fail to open will be skipped and logged as warnings.
#[instrument(skip(self), fields(folder_path = %self.absolute_path.display()))]
pub async fn list_documents(&self) -> Result<Vec<Document>> {
debug!("Listing documents in directory");
let mut documents = Vec::new();
let mut read_dir = match fs::read_dir(&self.absolute_path).await {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// If the directory itself doesn't exist, return an empty list or error?
// Let's return Ok([]) as the list of documents in a non-existent dir is empty.
debug!("Directory not found, returning empty document list.");
return Ok(Vec::new());
}
Err(e) => return Err(Error::Io(e)),
};
while let Some(entry) = read_dir.next_entry().await.map_err(Error::Io)? {
// Note that DirEntry.path() returns an absolute path (which is what we need here)
let path = entry.path();
if path.is_file() {
if path.extension().and_then(|ext| ext.to_str()) == Some(MARKHOR_EXTENSION) {
debug!("Found potential content file: {}", path.display());
match Document::open(path.clone(), self.workspace.clone()).await {
Ok(doc) => documents.push(doc),
Err(e) => {
// Log and skip invalid/inaccessible content files
warn!(
"Skipping invalid or inaccessible content file '{}': {}",
path.display(),
e
);
}
}
}
}
}
debug!("Found {} valid documents", documents.len());
Ok(documents)
}
/// Lists the subfolders directly contained within this folder (non-recursive).
#[instrument(skip(self), fields(folder_path = %self.absolute_path.display()))]
pub async fn list_folders(&self) -> Result<Vec<Folder>> {
debug!("Listing subfolders");
let mut folders = Vec::new();
let mut read_dir = match fs::read_dir(&self.absolute_path).await {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
debug!("Directory not found, returning empty folder list.");
return Ok(Vec::new());
}
Err(e) => return Err(Error::Io(e)),
};
while let Some(entry) = read_dir.next_entry().await.map_err(Error::Io)? {
// Note that DirEntry.path() returns an absolute path (which is what we need here)
let path = entry.path();
if path.is_dir() {
if entry.file_name().to_str() == Some(INTERNAL_DIR_NAME) {
debug!("Skipping excluded directory: {}", path.display());
continue;
}
debug!("Found subfolder: {}", path.display());
folders.push(Folder::new(path, self.workspace.clone()));
}
}
debug!("Found {} subfolders", folders.len());
Ok(folders)
}
// Potential future methods: delete, rename, move_to, create_subfolder, etc.
}
/// Represents a scope for searching or filtering documents.
#[derive(Debug, Clone)]
pub struct Scope {
folder: Folder,
tags: Vec<String>, // TODO - tags don't exist yet
}
impl From<Folder> for Scope {
fn from(folder: Folder) -> Self {
Scope {
folder,
tags: Vec::new(),
}
}
}