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
//! Backend storage contract — no cache, no search; the façade coordinates those relics.
use async_trait::async_trait;
use super::EntryId;
use super::entry_kind::EntryKind;
use super::meta::{DocumentMeta, ListedEntry};
use crate::{Result, Timestamp};
/// Async persistence for the hierarchical `entries` table (`sqlx` today, another engine tomorrow).
#[async_trait]
pub trait Storage: Send + Sync {
/// Updates only `accessed_at` for a file row.
async fn touch(&self, file_id: EntryId) -> Result<()>;
/// Resolve an absolute path to an entry id (`/` → root directory).
async fn resolve_path(&self, path: &str, expected_kind: Option<EntryKind>) -> Result<EntryId>;
/// Canonical absolute path for any entry (e.g. `/a/b`); root is `/`.
async fn canonical_path(&self, entry_id: EntryId) -> Result<String>;
/// Parent directory canonical path for a file (for search index prefix field).
async fn parent_dir_path_for_file(&self, file_id: EntryId) -> Result<String>;
/// Create a directory at `path`. With `parents == false`, the parent directory must already exist.
/// With `parents == true`, missing ancestors are created (best-effort); if the leaf already exists as a
/// directory, returns its id (POSIX `mkdir -p` silent success). File segments on the path are rejected
/// after a full preflight walk (before any insert).
async fn create_directory(
&self,
path: &str,
description: Option<&str>,
parents: bool,
) -> Result<EntryId>;
/// Delete directory if empty; fails with [`crate::Error::NotEmpty`] if it has children.
async fn delete_directory(&self, path: &str) -> Result<()>;
/// Remove directory and full subtree (files and subdirectories).
async fn delete_directory_recursive(&self, path: &str) -> Result<()>;
/// Rename a directory’s last segment within the same parent (`old_path` → `new_path`).
async fn rename_directory(&self, old_path: &str, new_path: &str) -> Result<()>;
/// Create a file at `path` (parent directory must exist).
async fn create_file(&self, path: &str, content: impl AsRef<str> + Send) -> Result<EntryId>;
async fn delete_file(&self, file_id: EntryId) -> Result<()>;
async fn update_file(
&self,
file_id: EntryId,
new_content: impl AsRef<str> + Send,
) -> Result<()>;
async fn append_file(&self, file_id: EntryId, to_append: impl AsRef<str> + Send) -> Result<()>;
/// Bump `modified_at` only (content and `created_at` unchanged).
async fn bump_file_modified_at(&self, file_id: EntryId) -> Result<()>;
/// Set `modified_at` for a file or directory row (not root).
async fn set_entry_modified_at(&self, entry_id: EntryId, modified_at: Timestamp) -> Result<()>;
/// Move file to a new parent directory (by path); optional rename via final segment of `new_path`.
async fn move_file(
&self,
file_id: EntryId,
new_parent_path: &str,
new_name: &str,
) -> Result<()>;
/// Rename file within the same parent (`new_name` only).
async fn rename_file(&self, file_id: EntryId, new_name: impl AsRef<str> + Send) -> Result<()>;
async fn get_file_content(&self, file_id: EntryId) -> Result<String>;
/// Children of directory at `dir_path` (`/` for root), mixed kinds.
async fn list_directory(&self, dir_path: &str) -> Result<Vec<ListedEntry>>;
async fn get_file_meta(&self, file_id: EntryId) -> Result<DocumentMeta>;
/// For search hits: id → (canonical_path, content).
async fn files_display_batch(&self, ids: &[EntryId]) -> Result<Vec<(EntryId, String, String)>>;
async fn file_parent_and_content(&self, file_id: EntryId) -> Result<(EntryId, String)>;
/// All files, or only files under `directory_path` subtree, for Tantivy rebuild.
/// Tuple: `(id, parent_dir_path, file_name, description, content)`.
async fn files_for_search_reindex(
&self,
directory_path_filter: Option<&str>,
) -> Result<Vec<(EntryId, String, String, String, String)>>;
/// Fields needed for one search index row (`dir_path`, `name`, `description`, `content`).
async fn file_search_index_fields(
&self,
file_id: EntryId,
) -> Result<(String, String, String, String)>;
/// Move directory to new parent path + new name (last segment); anti-cycle enforced in SQL txn.
async fn move_directory(
&self,
dir_id: EntryId,
new_parent_path: &str,
new_name: &str,
) -> Result<()>;
/// Ensure each segment exists as a directory from root (mkdir -p).
async fn ensure_directory_path(&self, dir_path: &str) -> Result<EntryId>;
/// `description` column for a file or directory at `path` (`None` when unset).
async fn entry_description(&self, path: &str) -> Result<Option<String>>;
/// Set or clear `description` for a file or directory; `None` stores SQL NULL.
async fn set_entry_description(&self, path: &str, description: Option<&str>) -> Result<()>;
}