Add FUSE search integration with /.search/ virtual directory
- SearchOps with moka LRU cache for results and inode mapping - 256 char query length limit with truncation - Safe symlink targets with path normalization - Integrates with filesystem.rs for lookup/getattr/readdir/readlink Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -7,7 +7,13 @@ edition.workspace = true
|
|||||||
musicfs-core = { path = "../musicfs-core" }
|
musicfs-core = { path = "../musicfs-core" }
|
||||||
musicfs-cache = { path = "../musicfs-cache" }
|
musicfs-cache = { path = "../musicfs-cache" }
|
||||||
musicfs-cas = { path = "../musicfs-cas" }
|
musicfs-cas = { path = "../musicfs-cas" }
|
||||||
|
musicfs-search = { path = "../musicfs-search" }
|
||||||
fuser.workspace = true
|
fuser.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
moka.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::ops::SearchOps;
|
||||||
use fuser::{
|
use fuser::{
|
||||||
FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyOpen,
|
FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyOpen,
|
||||||
Request,
|
Request,
|
||||||
@@ -5,6 +6,7 @@ use fuser::{
|
|||||||
use musicfs_cache::{VirtualNode, VirtualTree, ROOT_INODE};
|
use musicfs_cache::{VirtualNode, VirtualTree, ROOT_INODE};
|
||||||
use musicfs_cas::FileReader;
|
use musicfs_cas::FileReader;
|
||||||
use musicfs_core::Result;
|
use musicfs_core::Result;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
@@ -14,11 +16,16 @@ use tracing::{debug, info, warn};
|
|||||||
|
|
||||||
const TTL: Duration = Duration::from_secs(1);
|
const TTL: Duration = Duration::from_secs(1);
|
||||||
const BLOCK_SIZE: u32 = 512;
|
const BLOCK_SIZE: u32 = 512;
|
||||||
|
const SEARCH_QUERY_INODE_BASE: u64 = 0xFFFF_FFFF_0000_0100;
|
||||||
|
|
||||||
pub struct MusicFs {
|
pub struct MusicFs {
|
||||||
tree: Arc<RwLock<VirtualTree>>,
|
tree: Arc<RwLock<VirtualTree>>,
|
||||||
reader: Option<Arc<FileReader>>,
|
reader: Option<Arc<FileReader>>,
|
||||||
runtime_handle: Handle,
|
runtime_handle: Handle,
|
||||||
|
search_ops: Option<SearchOps>,
|
||||||
|
query_inodes: RwLock<HashMap<String, u64>>,
|
||||||
|
inode_queries: RwLock<HashMap<u64, String>>,
|
||||||
|
next_query_inode: RwLock<u64>,
|
||||||
uid: u32,
|
uid: u32,
|
||||||
gid: u32,
|
gid: u32,
|
||||||
}
|
}
|
||||||
@@ -29,6 +36,10 @@ impl MusicFs {
|
|||||||
tree,
|
tree,
|
||||||
reader: None,
|
reader: None,
|
||||||
runtime_handle,
|
runtime_handle,
|
||||||
|
search_ops: None,
|
||||||
|
query_inodes: RwLock::new(HashMap::new()),
|
||||||
|
inode_queries: RwLock::new(HashMap::new()),
|
||||||
|
next_query_inode: RwLock::new(SEARCH_QUERY_INODE_BASE),
|
||||||
uid: unsafe { libc::getuid() },
|
uid: unsafe { libc::getuid() },
|
||||||
gid: unsafe { libc::getgid() },
|
gid: unsafe { libc::getgid() },
|
||||||
}
|
}
|
||||||
@@ -39,11 +50,46 @@ impl MusicFs {
|
|||||||
tree,
|
tree,
|
||||||
reader: Some(reader),
|
reader: Some(reader),
|
||||||
runtime_handle,
|
runtime_handle,
|
||||||
|
search_ops: None,
|
||||||
|
query_inodes: RwLock::new(HashMap::new()),
|
||||||
|
inode_queries: RwLock::new(HashMap::new()),
|
||||||
|
next_query_inode: RwLock::new(SEARCH_QUERY_INODE_BASE),
|
||||||
uid: unsafe { libc::getuid() },
|
uid: unsafe { libc::getuid() },
|
||||||
gid: unsafe { libc::getgid() },
|
gid: unsafe { libc::getgid() },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_search(mut self, search_ops: SearchOps) -> Self {
|
||||||
|
self.search_ops = Some(search_ops);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_create_query_inode(&self, query: &str) -> u64 {
|
||||||
|
let query_inodes = self.query_inodes.read().unwrap();
|
||||||
|
if let Some(&inode) = query_inodes.get(query) {
|
||||||
|
return inode;
|
||||||
|
}
|
||||||
|
drop(query_inodes);
|
||||||
|
|
||||||
|
let mut query_inodes = self.query_inodes.write().unwrap();
|
||||||
|
let mut inode_queries = self.inode_queries.write().unwrap();
|
||||||
|
let mut next_inode = self.next_query_inode.write().unwrap();
|
||||||
|
|
||||||
|
if let Some(&inode) = query_inodes.get(query) {
|
||||||
|
return inode;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inode = *next_inode;
|
||||||
|
*next_inode += 1;
|
||||||
|
query_inodes.insert(query.to_string(), inode);
|
||||||
|
inode_queries.insert(inode, query.to_string());
|
||||||
|
inode
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_query_for_inode(&self, inode: u64) -> Option<String> {
|
||||||
|
self.inode_queries.read().unwrap().get(&inode).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mount(self, mountpoint: &Path) -> Result<()> {
|
pub fn mount(self, mountpoint: &Path) -> Result<()> {
|
||||||
info!("Mounting MusicFS at {:?}", mountpoint);
|
info!("Mounting MusicFS at {:?}", mountpoint);
|
||||||
|
|
||||||
@@ -116,6 +162,31 @@ impl Filesystem for MusicFs {
|
|||||||
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
|
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
|
||||||
debug!("lookup(parent={}, name={:?})", parent, name);
|
debug!("lookup(parent={}, name={:?})", parent, name);
|
||||||
|
|
||||||
|
let name_str = name.to_string_lossy();
|
||||||
|
|
||||||
|
if parent == ROOT_INODE && SearchOps::is_search_dir_name(&name_str) {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
search_ops.lookup_search_dir(reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent == SearchOps::search_dir_inode() {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
let inode = self.get_or_create_query_inode(&name_str);
|
||||||
|
search_ops.lookup_query_dir(&name_str, inode, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(query) = self.get_query_for_inode(parent) {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
let inode = self.get_or_create_query_inode(&format!("{}:{}", query, name_str));
|
||||||
|
search_ops.lookup_result(inode, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let tree = self.tree.read().unwrap();
|
let tree = self.tree.read().unwrap();
|
||||||
|
|
||||||
if let Some(inode) = tree.lookup(parent, name) {
|
if let Some(inode) = tree.lookup(parent, name) {
|
||||||
@@ -132,6 +203,27 @@ impl Filesystem for MusicFs {
|
|||||||
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
|
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
|
||||||
debug!("getattr(ino={})", ino);
|
debug!("getattr(ino={})", ino);
|
||||||
|
|
||||||
|
if ino == SearchOps::search_dir_inode() {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
search_ops.getattr_search_dir(reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if SearchOps::is_search_inode(ino) {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
search_ops.getattr_result(ino, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.get_query_for_inode(ino).is_some() {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
search_ops.getattr_search_dir(reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let tree = self.tree.read().unwrap();
|
let tree = self.tree.read().unwrap();
|
||||||
|
|
||||||
if let Some(node) = tree.get(ino) {
|
if let Some(node) = tree.get(ino) {
|
||||||
@@ -152,6 +244,20 @@ impl Filesystem for MusicFs {
|
|||||||
) {
|
) {
|
||||||
debug!("readdir(ino={}, offset={})", ino, offset);
|
debug!("readdir(ino={}, offset={})", ino, offset);
|
||||||
|
|
||||||
|
if ino == SearchOps::search_dir_inode() {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
search_ops.readdir_search_root(offset, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(query) = self.get_query_for_inode(ino) {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
search_ops.readdir_query(&query, offset, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let tree = self.tree.read().unwrap();
|
let tree = self.tree.read().unwrap();
|
||||||
|
|
||||||
if let Some(children) = tree.readdir(ino) {
|
if let Some(children) = tree.readdir(ino) {
|
||||||
@@ -275,6 +381,19 @@ impl Filesystem for MusicFs {
|
|||||||
reply.ok();
|
reply.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn readlink(&mut self, _req: &Request, ino: u64, reply: ReplyData) {
|
||||||
|
debug!("readlink(ino={})", ino);
|
||||||
|
|
||||||
|
if SearchOps::is_search_inode(ino) {
|
||||||
|
if let Some(ref search_ops) = self.search_ops {
|
||||||
|
search_ops.readlink(ino, reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.error(libc::EINVAL);
|
||||||
|
}
|
||||||
|
|
||||||
fn write(
|
fn write(
|
||||||
&mut self,
|
&mut self,
|
||||||
_req: &Request,
|
_req: &Request,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
mod filesystem;
|
mod filesystem;
|
||||||
|
pub mod ops;
|
||||||
|
|
||||||
pub use filesystem::MusicFs;
|
pub use filesystem::MusicFs;
|
||||||
|
pub use ops::SearchOps;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod search;
|
||||||
|
|
||||||
|
pub use search::SearchOps;
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
use fuser::{FileAttr, FileType, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry};
|
||||||
|
use moka::sync::Cache;
|
||||||
|
use musicfs_search::{SearchHit, SearchIndex};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
const SEARCH_DIR_INODE: u64 = 0xFFFF_FFFF_0000_0001;
|
||||||
|
const SEARCH_RESULT_BASE: u64 = 0xFFFF_FFFF_1000_0000;
|
||||||
|
const RESULT_CACHE_MAX_ENTRIES: u64 = 1000;
|
||||||
|
const RESULT_CACHE_TTL_SECS: u64 = 300;
|
||||||
|
const INODE_CACHE_MAX_ENTRIES: u64 = 10000;
|
||||||
|
const MAX_QUERY_LENGTH: usize = 256;
|
||||||
|
|
||||||
|
pub struct SearchOps {
|
||||||
|
index: Arc<SearchIndex>,
|
||||||
|
result_cache: Cache<String, Vec<SearchHit>>,
|
||||||
|
inode_to_result: Cache<u64, (String, usize)>,
|
||||||
|
mount_point: String,
|
||||||
|
uid: u32,
|
||||||
|
gid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchOps {
|
||||||
|
pub fn new(index: Arc<SearchIndex>, mount_point: &str, uid: u32, gid: u32) -> Self {
|
||||||
|
let result_cache = Cache::builder()
|
||||||
|
.max_capacity(RESULT_CACHE_MAX_ENTRIES)
|
||||||
|
.time_to_live(Duration::from_secs(RESULT_CACHE_TTL_SECS))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let inode_to_result = Cache::builder()
|
||||||
|
.max_capacity(INODE_CACHE_MAX_ENTRIES)
|
||||||
|
.time_to_live(Duration::from_secs(RESULT_CACHE_TTL_SECS))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
index,
|
||||||
|
result_cache,
|
||||||
|
inode_to_result,
|
||||||
|
mount_point: mount_point.to_string(),
|
||||||
|
uid,
|
||||||
|
gid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_search_dir_name(name: &str) -> bool {
|
||||||
|
name == ".search"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_search_inode(inode: u64) -> bool {
|
||||||
|
inode == SEARCH_DIR_INODE || inode >= SEARCH_RESULT_BASE
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_dir_inode() -> u64 {
|
||||||
|
SEARCH_DIR_INODE
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup_search_dir(&self, reply: ReplyEntry) {
|
||||||
|
let attr = self.dir_attr(SEARCH_DIR_INODE);
|
||||||
|
reply.entry(&Duration::from_secs(60), &attr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup_query_dir(&self, query: &str, inode: u64, reply: ReplyEntry) {
|
||||||
|
let results = self.execute_query(query);
|
||||||
|
if results.is_empty() {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attr = self.dir_attr(inode);
|
||||||
|
reply.entry(&Duration::from_secs(1), &attr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup_result(&self, inode: u64, reply: ReplyEntry) {
|
||||||
|
if self.inode_to_result.contains_key(&inode) {
|
||||||
|
let attr = self.symlink_attr(inode, 256);
|
||||||
|
reply.entry(&Duration::from_secs(1), &attr, 0);
|
||||||
|
} else {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getattr_search_dir(&self, reply: ReplyAttr) {
|
||||||
|
let attr = self.dir_attr(SEARCH_DIR_INODE);
|
||||||
|
reply.attr(&Duration::from_secs(60), &attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getattr_result(&self, inode: u64, reply: ReplyAttr) {
|
||||||
|
let attr = self.symlink_attr(inode, 256);
|
||||||
|
reply.attr(&Duration::from_secs(1), &attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readdir_search_root(&self, offset: i64, mut reply: ReplyDirectory) {
|
||||||
|
let entries = vec![
|
||||||
|
(SEARCH_DIR_INODE, FileType::Directory, "."),
|
||||||
|
(1, FileType::Directory, ".."),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, (inode, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
|
||||||
|
if reply.add(*inode, (i + 1) as i64, *kind, name) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readdir_query(&self, query: &str, offset: i64, mut reply: ReplyDirectory) {
|
||||||
|
let results = self.execute_query(query);
|
||||||
|
|
||||||
|
let entries = vec![
|
||||||
|
(SEARCH_DIR_INODE + 1, FileType::Directory, ".".to_string()),
|
||||||
|
(SEARCH_DIR_INODE, FileType::Directory, "..".to_string()),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, (inode, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
|
||||||
|
if reply.add(*inode, (i + 1) as i64, *kind, name) {
|
||||||
|
reply.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_offset = entries.len();
|
||||||
|
for (i, hit) in results.iter().enumerate() {
|
||||||
|
let entry_offset = base_offset + i;
|
||||||
|
if entry_offset < offset as usize {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inode = SEARCH_RESULT_BASE + i as u64;
|
||||||
|
let name = self.result_filename(hit, i);
|
||||||
|
|
||||||
|
self.inode_to_result.insert(inode, (query.to_string(), i));
|
||||||
|
|
||||||
|
if reply.add(inode, (entry_offset + 1) as i64, FileType::Symlink, &name) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readlink(&self, inode: u64, reply: ReplyData) {
|
||||||
|
let (query, index) = match self.inode_to_result.get(&inode) {
|
||||||
|
Some((q, i)) => (q, i),
|
||||||
|
None => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let results = self.execute_query(&query);
|
||||||
|
if let Some(hit) = results.get(index) {
|
||||||
|
if let Some(target) = self.safe_symlink_target(hit.virtual_path.as_str()) {
|
||||||
|
reply.data(target.as_bytes());
|
||||||
|
} else {
|
||||||
|
reply.error(libc::EINVAL);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe_symlink_target(&self, virtual_path: &str) -> Option<String> {
|
||||||
|
let normalized = Path::new(virtual_path)
|
||||||
|
.components()
|
||||||
|
.fold(std::path::PathBuf::new(), |mut acc, comp| {
|
||||||
|
match comp {
|
||||||
|
std::path::Component::Normal(s) => acc.push(s),
|
||||||
|
std::path::Component::RootDir => acc.push("/"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
let path_str = normalized.to_string_lossy();
|
||||||
|
if path_str.contains("..") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(format!("{}{}", self.mount_point, path_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_query(&self, query: &str) -> Vec<SearchHit> {
|
||||||
|
let query = if query.len() > MAX_QUERY_LENGTH {
|
||||||
|
&query[..MAX_QUERY_LENGTH]
|
||||||
|
} else {
|
||||||
|
query
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(results) = self.result_cache.get(query) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = self.index.search(query, 1000).unwrap_or_default();
|
||||||
|
self.result_cache.insert(query.to_string(), results.clone());
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
fn result_filename(&self, hit: &SearchHit, index: usize) -> String {
|
||||||
|
let artist = hit.artist.as_deref().unwrap_or("Unknown");
|
||||||
|
let title = hit.title.as_deref().unwrap_or("Unknown");
|
||||||
|
let ext = hit.virtual_path.as_str()
|
||||||
|
.rsplit('.')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("flac");
|
||||||
|
format!("{:03}. {} - {}.{}", index + 1, artist, title, ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir_attr(&self, inode: u64) -> FileAttr {
|
||||||
|
FileAttr {
|
||||||
|
ino: inode,
|
||||||
|
size: 0,
|
||||||
|
blocks: 0,
|
||||||
|
atime: SystemTime::UNIX_EPOCH,
|
||||||
|
mtime: SystemTime::UNIX_EPOCH,
|
||||||
|
ctime: SystemTime::UNIX_EPOCH,
|
||||||
|
crtime: SystemTime::UNIX_EPOCH,
|
||||||
|
kind: FileType::Directory,
|
||||||
|
perm: 0o555,
|
||||||
|
nlink: 2,
|
||||||
|
uid: self.uid,
|
||||||
|
gid: self.gid,
|
||||||
|
rdev: 0,
|
||||||
|
blksize: 512,
|
||||||
|
flags: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symlink_attr(&self, inode: u64, target_len: u64) -> FileAttr {
|
||||||
|
FileAttr {
|
||||||
|
ino: inode,
|
||||||
|
size: target_len,
|
||||||
|
blocks: 0,
|
||||||
|
atime: SystemTime::UNIX_EPOCH,
|
||||||
|
mtime: SystemTime::UNIX_EPOCH,
|
||||||
|
ctime: SystemTime::UNIX_EPOCH,
|
||||||
|
crtime: SystemTime::UNIX_EPOCH,
|
||||||
|
kind: FileType::Symlink,
|
||||||
|
perm: 0o777,
|
||||||
|
nlink: 1,
|
||||||
|
uid: self.uid,
|
||||||
|
gid: self.gid,
|
||||||
|
rdev: 0,
|
||||||
|
blksize: 512,
|
||||||
|
flags: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use musicfs_search::SearchIndex;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_search_ops_new() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let index = Arc::new(SearchIndex::open(dir.path()).unwrap());
|
||||||
|
let _ops = SearchOps::new(index, "/mnt/music", 1000, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_search_inode() {
|
||||||
|
assert!(SearchOps::is_search_inode(SEARCH_DIR_INODE));
|
||||||
|
assert!(SearchOps::is_search_inode(SEARCH_RESULT_BASE));
|
||||||
|
assert!(SearchOps::is_search_inode(SEARCH_RESULT_BASE + 100));
|
||||||
|
assert!(!SearchOps::is_search_inode(1));
|
||||||
|
assert!(!SearchOps::is_search_inode(1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user