atproto repo as vfs

dont use vfs crate

ptr.pet 71bf0826 b85225eb

verified
Changed files
+153 -224
src
-33
Cargo.lock
··· 112 112 "serde_json", 113 113 "tokio", 114 114 "url", 115 - "vfs", 116 115 ] 117 116 118 117 [[package]] ··· 744 743 dependencies = [ 745 744 "rand_core 0.6.4", 746 745 "subtle", 747 - ] 748 - 749 - [[package]] 750 - name = "filetime" 751 - version = "0.2.26" 752 - source = "registry+https://github.com/rust-lang/crates.io-index" 753 - checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" 754 - dependencies = [ 755 - "cfg-if", 756 - "libc", 757 - "libredox", 758 - "windows-sys 0.60.2", 759 746 ] 760 747 761 748 [[package]] ··· 1728 1715 version = "0.2.15" 1729 1716 source = "registry+https://github.com/rust-lang/crates.io-index" 1730 1717 checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 1731 - 1732 - [[package]] 1733 - name = "libredox" 1734 - version = "0.1.10" 1735 - source = "registry+https://github.com/rust-lang/crates.io-index" 1736 - checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" 1737 - dependencies = [ 1738 - "bitflags", 1739 - "libc", 1740 - "redox_syscall", 1741 - ] 1742 1718 1743 1719 [[package]] 1744 1720 name = "litemap" ··· 3556 3532 version = "0.9.5" 3557 3533 source = "registry+https://github.com/rust-lang/crates.io-index" 3558 3534 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3559 - 3560 - [[package]] 3561 - name = "vfs" 3562 - version = "0.12.2" 3563 - source = "registry+https://github.com/rust-lang/crates.io-index" 3564 - checksum = "9e723b9e1c02a3cf9f9d0de6a4ddb8cdc1df859078902fe0ae0589d615711ae6" 3565 - dependencies = [ 3566 - "filetime", 3567 - ] 3568 3535 3569 3536 [[package]] 3570 3537 name = "want"
-1
Cargo.toml
··· 4 4 edition = "2021" 5 5 6 6 [dependencies] 7 - vfs = { version = "0.12" } 8 7 bpaf = { version = "0.9", features = ["derive"] } 9 8 anyhow = "1.0" 10 9 scc = "2.1"
+24 -19
src/fuse.rs
··· 1 - use super::*; 2 1 use easy_fuser::{prelude::*, templates::DefaultFuseHandler}; 2 + use tokio::runtime::Handle; 3 3 4 4 use std::{ 5 5 ffi::{OsStr, OsString}, 6 - io::{Read, Seek, SeekFrom}, 6 + io::{Cursor, Read, Seek, SeekFrom}, 7 7 path::PathBuf, 8 + sync::Arc, 8 9 time::UNIX_EPOCH, 9 10 }; 10 11 12 + use crate::{AtpFS, FileType}; 13 + 11 14 pub struct AtpFuse { 12 15 pub fs: Arc<AtpFS>, 13 16 pub inner: DefaultFuseHandler, 17 + pub runtime: Handle, 14 18 } 15 19 16 20 impl AtpFuse { ··· 45 49 46 50 fn vfs_metadata_attr(&self, vfs_path: &str) -> FuseResult<FileAttribute> { 47 51 let meta = self 48 - .fs 49 - .metadata(vfs_path) 52 + .runtime 53 + .block_on(self.fs.metadata(vfs_path)) 50 54 .map_err(|_| ErrorKind::FileNotFound.to_error("Not found"))?; 51 55 52 56 let (kind, perm, nlink) = match meta.file_type { 53 - VfsFileType::Directory => (FileKind::Directory, 0o755, 2), 54 - VfsFileType::File => (FileKind::RegularFile, 0o644, 1), 57 + FileType::Directory => (FileKind::Directory, 0o755, 2), 58 + FileType::File => (FileKind::RegularFile, 0o644, 1), 55 59 }; 56 60 57 61 Ok(FileAttribute { ··· 115 119 let vfs_path = self.path_to_str(&file_id); 116 120 117 121 let stream = self 118 - .fs 119 - .read_dir(&vfs_path) 122 + .runtime 123 + .block_on(self.fs.read_dir(&vfs_path)) 120 124 .map_err(|_| ErrorKind::InputOutputError.to_error("Read dir failed"))?; 121 125 122 126 let mut entries = vec![ ··· 125 129 ]; 126 130 127 131 for name in stream { 128 - let kind = name 129 - .ends_with(".json") 130 - .then_some(FileKind::RegularFile) 131 - .unwrap_or(FileKind::Directory); 132 + let kind = if name.ends_with(".json") { 133 + FileKind::RegularFile 134 + } else { 135 + FileKind::Directory 136 + }; 132 137 entries.push((OsString::from(name), kind)); 133 138 } 134 139 ··· 145 150 _flags: FUSEOpenFlags, 146 151 _lock_owner: Option<u64>, 147 152 ) -> FuseResult<Vec<u8>> { 148 - let vfs_path = self.path_to_str(&file_id); 149 - let mut reader = self 150 - .fs 151 - .open_file(&vfs_path) 152 - .map_err(|_| ErrorKind::FileNotFound.to_error("File not found"))?; 153 - 154 153 // Only support absolute start seeks for now. 155 154 let pos = match seek { 156 155 SeekFrom::Start(p) => p, ··· 163 162 return Ok(Vec::new()); 164 163 } 165 164 165 + let vfs_path = self.path_to_str(&file_id); 166 + let data = self 167 + .runtime 168 + .block_on(self.fs.open_file(&vfs_path)) 169 + .map_err(|_| ErrorKind::FileNotFound.to_error("File not found"))?; 170 + let mut reader = Cursor::new(data.as_slice()); 171 + 166 172 // Seek to the requested position. 167 173 reader 168 174 .seek(SeekFrom::Start(pos)) 169 175 .map_err(|_| ErrorKind::InputOutputError.to_error("Seek failed"))?; 170 176 171 177 // Read up to `size` bytes into the buffer. 172 - // We use take to limit the read, then read_to_end or just read into buffer. 173 178 let mut buf = vec![0u8; size as usize]; 174 179 let n = reader 175 180 .read(&mut buf)
+122 -166
src/lib.rs
··· 1 1 use anyhow::{anyhow, Result}; 2 - #[cfg(target_arch = "wasm32")] 3 - use futures::executor::block_on; 4 2 use jacquard::{ 5 3 api::com_atproto::repo::{describe_repo::DescribeRepo, list_records::ListRecords}, 6 4 client::{credential_session::CredentialSession, Agent, BasicClient, MemorySessionStore}, 7 5 identity::{resolver::IdentityResolver, slingshot_resolver_default}, 6 + prelude::*, 8 7 types::{did::Did, nsid::Nsid, string::Handle}, 9 - xrpc::XrpcClient, 10 8 }; 11 9 use scc::{HashMap, HashSet}; 12 10 use url::Url; 13 - use vfs::{error::VfsErrorKind, FileSystem, SeekAndRead, VfsFileType, VfsMetadata, VfsResult}; 14 11 15 - use std::{collections::HashMap as StdHashMap, fmt::Debug, sync::Arc}; 12 + use std::{ 13 + collections::HashMap as StdHashMap, 14 + fmt::Debug, 15 + io::{self, ErrorKind}, 16 + sync::Arc, 17 + }; 16 18 17 19 pub mod cli; 18 20 #[cfg(target_os = "linux")] ··· 41 43 .ok_or_else(|| anyhow!("no pds endpoint in did doc")) 42 44 } 43 45 46 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 47 + pub enum FileType { 48 + File, 49 + Directory, 50 + } 51 + 52 + #[derive(Debug, Clone)] 53 + pub struct Metadata { 54 + pub file_type: FileType, 55 + pub len: u64, 56 + } 57 + 44 58 #[derive(Debug)] 45 59 struct CachedPage { 46 - files: StdHashMap<String, Vec<u8>>, 60 + files: StdHashMap<String, Arc<Vec<u8>>>, 47 61 next_cursor: Option<String>, 48 62 } 49 63 ··· 52 66 client: BasicClient, 53 67 cache: HashMap<String, Arc<CachedPage>>, 54 68 root_cache: HashSet<String>, 55 - #[cfg(not(target_arch = "wasm32"))] 56 - handle: tokio::runtime::Handle, 57 69 } 58 70 59 71 impl Debug for AtpFS { 60 72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 61 - f.debug_struct("AtProtoFS").field("did", &self.did).finish() 73 + f.debug_struct("AtpFS").field("did", &self.did).finish() 62 74 } 63 75 } 64 76 65 77 impl AtpFS { 66 - pub fn new(did: Did<'static>, pds: Url) -> Self { 67 - #[cfg(not(target_arch = "wasm32"))] 68 - let handle = tokio::runtime::Handle::current(); 69 - 78 + pub async fn new(did: Did<'static>, pds: Url) -> Self { 70 79 let store = MemorySessionStore::default(); 71 80 let session = 72 81 CredentialSession::new(Arc::new(store), Arc::new(slingshot_resolver_default())); 73 82 74 - #[cfg(not(target_arch = "wasm32"))] 75 - tokio::task::block_in_place(|| handle.block_on(session.set_endpoint(pds))); 76 - 77 - #[cfg(target_arch = "wasm32")] 78 - block_on(session.set_endpoint(pds)); 83 + session.set_endpoint(pds).await; 79 84 80 85 Self { 81 86 did, 82 87 client: Agent::new(session), 83 88 cache: HashMap::default(), 84 89 root_cache: HashSet::default(), 85 - #[cfg(not(target_arch = "wasm32"))] 86 - handle, 87 90 } 88 91 } 89 92 90 - #[cfg(not(target_arch = "wasm32"))] 91 - fn block_on<F: std::future::Future>(&self, future: F) -> F::Output { 92 - tokio::task::block_in_place(move || self.handle.block_on(future)) 93 - } 94 - 95 - #[cfg(target_arch = "wasm32")] 96 - fn block_on<F: std::future::Future>(&self, future: F) -> F::Output { 97 - block_on(future) 98 - } 99 - 100 93 fn segments<'a, 's>(&'s self, path: &'a str) -> Vec<&'a str> { 101 94 path.trim_matches('/') 102 95 .split('/') ··· 104 97 .collect() 105 98 } 106 99 107 - fn vfs_dir_metadata() -> VfsMetadata { 108 - VfsMetadata { 109 - file_type: VfsFileType::Directory, 100 + fn dir_metadata() -> Metadata { 101 + Metadata { 102 + file_type: FileType::Directory, 110 103 len: 0, 111 - created: None, 112 - modified: None, 113 - accessed: None, 114 104 } 115 105 } 116 106 117 - fn vfs_file_metadata(len: u64) -> VfsMetadata { 118 - VfsMetadata { 119 - file_type: VfsFileType::File, 107 + fn file_metadata(len: u64) -> Metadata { 108 + Metadata { 109 + file_type: FileType::File, 120 110 len, 121 - created: None, 122 - modified: None, 123 - accessed: None, 124 111 } 125 112 } 126 113 127 - async fn ensure_root_loaded(&self) -> VfsResult<String> { 114 + async fn ensure_root_loaded(&self) -> io::Result<()> { 128 115 if self.root_cache.is_empty() { 129 116 let request = DescribeRepo::new().repo(self.did.clone()).build(); 130 117 ··· 132 119 .client 133 120 .send(request) 134 121 .await 135 - .map_err(|e| VfsErrorKind::Other(e.to_string()))?; 122 + .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 136 123 137 124 let output = response 138 125 .into_output() 139 - .map_err(|e| VfsErrorKind::Other(e.to_string()))?; 126 + .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 140 127 141 128 for col in output.collections { 142 129 let _ = self.root_cache.insert_async(col.to_string()).await; 143 130 } 144 131 } 145 - return Ok("".to_string()); 132 + return Ok(()); 146 133 } 147 134 148 - async fn ensure_loaded(&self, path: &str) -> VfsResult<String> { 135 + async fn ensure_loaded(&self, path: &str) -> io::Result<String> { 149 136 let segs = self.segments(path); 150 137 151 138 if segs.is_empty() { 152 - return self.ensure_root_loaded().await; 139 + self.ensure_root_loaded().await?; 140 + return Ok("".to_string()); 153 141 } 154 142 155 143 let collection = segs[0]; ··· 158 146 } 159 147 160 148 if !self.root_cache.contains(collection) { 161 - return Err(VfsErrorKind::FileNotFound.into()); 149 + return Err(ErrorKind::NotFound.into()); 162 150 } 163 151 164 152 let mut current_key = collection.to_string(); ··· 174 162 parent_cursor = Some(cursor); 175 163 current_key = format!("{}/next", current_key); 176 164 } else { 177 - return Err(VfsErrorKind::FileNotFound.into()); 165 + return Err(ErrorKind::NotFound.into()); 178 166 } 179 167 } else if segment.ends_with(".json") { 180 168 break; 181 169 } else { 182 - return Err(VfsErrorKind::FileNotFound.into()); 170 + return Err(ErrorKind::NotFound.into()); 183 171 } 184 172 } 185 173 ··· 188 176 Ok(current_key) 189 177 } 190 178 191 - async fn fetch_page_if_missing(&self, key: &str, cursor: Option<String>) -> VfsResult<()> { 179 + async fn fetch_page_if_missing(&self, key: &str, cursor: Option<String>) -> io::Result<()> { 192 180 if self.cache.contains(key) { 193 181 return Ok(()); 194 182 } ··· 206 194 .client 207 195 .send(request.build()) 208 196 .await 209 - .map_err(|e| VfsErrorKind::Other(e.to_string()))?; 197 + .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 210 198 211 199 let output = response 212 200 .into_output() 213 - .map_err(|e| VfsErrorKind::Other(e.to_string()))?; 201 + .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 214 202 215 203 let mut files = StdHashMap::new(); 216 204 for rec in output.records { 217 205 if let Some(rkey) = rec.uri.rkey() { 218 206 let filename = format!("{}.json", rkey.0); 219 207 let content = serde_json::to_vec_pretty(&rec.value).unwrap_or_default(); 220 - files.insert(filename, content); 208 + files.insert(filename, Arc::new(content)); 221 209 } 222 210 } 223 211 ··· 234 222 235 223 Ok(()) 236 224 } 237 - } 238 225 239 - impl FileSystem for AtpFS { 240 - fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String> + Send>> { 241 - self.block_on(async { 242 - let segs = self.segments(path); 243 - 244 - if segs.is_empty() { 245 - self.ensure_root_loaded().await?; 246 - let mut keys = Vec::new(); 247 - self.root_cache.scan(|k| keys.push(k.clone())); 248 - return Ok(Box::new(keys.into_iter()) as Box<dyn Iterator<Item = String> + Send>); 249 - } 226 + pub async fn read_dir(&self, path: &str) -> io::Result<Vec<String>> { 227 + let segs = self.segments(path); 250 228 251 - let cache_key = self.ensure_loaded(path).await?; 229 + if segs.is_empty() { 230 + self.ensure_root_loaded().await?; 231 + let mut keys = Vec::new(); 232 + self.root_cache.scan(|k| keys.push(k.clone())); 233 + return Ok(keys); 234 + } 252 235 253 - if path.ends_with(".json") { 254 - return Err(VfsErrorKind::Other("not a directory".into()).into()); 255 - } 236 + let cache_key = self.ensure_loaded(path).await?; 256 237 257 - let page = self 258 - .cache 259 - .read(&cache_key, |_, v| v.clone()) 260 - .ok_or(VfsErrorKind::FileNotFound)?; 238 + if path.ends_with(".json") { 239 + return Err(io::Error::new(ErrorKind::Other, "not a directory")); 240 + } 261 241 262 - let mut entries: Vec<String> = page.files.keys().cloned().collect(); 263 - if page.next_cursor.is_some() { 264 - entries.push("next".to_string()); 265 - } 242 + let page = self 243 + .cache 244 + .read(&cache_key, |_, v| v.clone()) 245 + .ok_or(ErrorKind::NotFound)?; 266 246 267 - Ok(Box::new(entries.into_iter()) as Box<dyn Iterator<Item = String> + Send>) 268 - }) 269 - } 247 + let mut entries: Vec<String> = page.files.keys().cloned().collect(); 248 + if page.next_cursor.is_some() { 249 + entries.push("next".to_string()); 250 + } 270 251 271 - fn create_dir(&self, _path: &str) -> VfsResult<()> { 272 - Err(VfsErrorKind::NotSupported.into()) 252 + Ok(entries) 273 253 } 274 254 275 - fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead + Send>> { 276 - self.block_on(async { 277 - let parent_path = std::path::Path::new(path) 278 - .parent() 279 - .unwrap_or(std::path::Path::new("")) 280 - .to_str() 281 - .unwrap(); 282 - let cache_key = self.ensure_loaded(parent_path).await?; 283 - let filename = path.split('/').last().ok_or(VfsErrorKind::FileNotFound)?; 284 - 285 - let content = self 286 - .cache 287 - .read(&cache_key, |_, page| page.files.get(filename).cloned()) 288 - .flatten(); 255 + pub async fn open_file(&self, path: &str) -> io::Result<Arc<Vec<u8>>> { 256 + let parent_path = std::path::Path::new(path) 257 + .parent() 258 + .unwrap_or(std::path::Path::new("")) 259 + .to_str() 260 + .unwrap(); 261 + let cache_key = self.ensure_loaded(parent_path).await?; 262 + let filename = path.split('/').last().ok_or(ErrorKind::NotFound)?; 289 263 290 - if let Some(data) = content { 291 - return Ok(Box::new(std::io::Cursor::new(data)) as Box<dyn SeekAndRead + Send>); 292 - } 264 + let content = self 265 + .cache 266 + .read(&cache_key, |_, page| page.files.get(filename).cloned()) 267 + .flatten(); 293 268 294 - Err(VfsErrorKind::FileNotFound.into()) 295 - }) 269 + content.ok_or(ErrorKind::NotFound.into()) 296 270 } 297 271 298 - fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> { 299 - self.block_on(async { 300 - let segs = self.segments(path); 301 - if segs.is_empty() { 302 - return Ok(AtpFS::vfs_dir_metadata()); 303 - } 304 - 305 - if segs.len() == 1 { 306 - self.ensure_root_loaded().await?; 307 - if self.root_cache.contains(segs[0]) { 308 - return Ok(AtpFS::vfs_dir_metadata()); 309 - } else { 310 - return Err(VfsErrorKind::FileNotFound.into()); 311 - } 312 - } 272 + pub async fn metadata(&self, path: &str) -> io::Result<Metadata> { 273 + let segs = self.segments(path); 274 + if segs.is_empty() { 275 + return Ok(Self::dir_metadata()); 276 + } 313 277 314 - if let Some(last) = segs.last() { 315 - if *last == "next" { 316 - let parent = &path[0..path.len() - 5]; 317 - let cache_key = self.ensure_loaded(parent).await?; 318 - let has_next = self 319 - .cache 320 - .read(&cache_key, |_, v| v.next_cursor.is_some()) 321 - .unwrap_or(false); 322 - if has_next { 323 - return Ok(AtpFS::vfs_dir_metadata()); 324 - } 325 - return Err(VfsErrorKind::FileNotFound.into()); 326 - } 278 + if segs.len() == 1 { 279 + self.ensure_root_loaded().await?; 280 + if self.root_cache.contains(segs[0]) { 281 + return Ok(Self::dir_metadata()); 282 + } else { 283 + return Err(ErrorKind::NotFound.into()); 327 284 } 328 - 329 - if path.ends_with(".json") { 330 - let parent_path = std::path::Path::new(path) 331 - .parent() 332 - .unwrap() 333 - .to_str() 334 - .unwrap(); 335 - let cache_key = self.ensure_loaded(parent_path).await?; 336 - let filename = segs.last().unwrap(); 285 + } 337 286 338 - let len = self 287 + if let Some(last) = segs.last() { 288 + if *last == "next" { 289 + let parent = &path[0..path.len() - 5]; 290 + let cache_key = self.ensure_loaded(parent).await?; 291 + let has_next = self 339 292 .cache 340 - .read(&cache_key, |_, page| { 341 - page.files.get(*filename).map(|f| f.len()) 342 - }) 343 - .flatten(); 344 - 345 - if let Some(l) = len { 346 - return Ok(AtpFS::vfs_file_metadata(l as u64)); 293 + .read(&cache_key, |_, v| v.next_cursor.is_some()) 294 + .unwrap_or(false); 295 + if has_next { 296 + return Ok(Self::dir_metadata()); 347 297 } 348 - return Err(VfsErrorKind::FileNotFound.into()); 298 + return Err(ErrorKind::NotFound.into()); 349 299 } 350 - 351 - Err(VfsErrorKind::FileNotFound.into()) 352 - }) 353 - } 300 + } 354 301 355 - fn exists(&self, path: &str) -> VfsResult<bool> { 356 - Ok(self.metadata(path).is_ok()) 357 - } 302 + if path.ends_with(".json") { 303 + let parent_path = std::path::Path::new(path) 304 + .parent() 305 + .unwrap() 306 + .to_str() 307 + .unwrap(); 308 + let cache_key = self.ensure_loaded(parent_path).await?; 309 + let filename = segs.last().unwrap(); 358 310 359 - fn create_file(&self, _: &str) -> VfsResult<Box<dyn vfs::SeekAndWrite + Send>> { 360 - Err(VfsErrorKind::NotSupported.into()) 361 - } 311 + let len = self 312 + .cache 313 + .read(&cache_key, |_, page| { 314 + page.files.get(*filename).map(|f| f.len()) 315 + }) 316 + .flatten(); 362 317 363 - fn append_file(&self, _: &str) -> VfsResult<Box<dyn vfs::SeekAndWrite + Send>> { 364 - Err(VfsErrorKind::NotSupported.into()) 365 - } 318 + if let Some(l) = len { 319 + return Ok(Self::file_metadata(l as u64)); 320 + } 321 + return Err(ErrorKind::NotFound.into()); 322 + } 366 323 367 - fn remove_file(&self, _path: &str) -> VfsResult<()> { 368 - Err(VfsErrorKind::NotSupported.into()) 324 + Err(ErrorKind::NotFound.into()) 369 325 } 370 326 371 - fn remove_dir(&self, _path: &str) -> VfsResult<()> { 372 - Err(VfsErrorKind::NotSupported.into()) 327 + pub async fn exists(&self, path: &str) -> io::Result<bool> { 328 + Ok(self.metadata(path).await.is_ok()) 373 329 } 374 330 }
+7 -5
src/main.rs
··· 3 3 cli::{opts, SubCommand}, 4 4 resolve_did, resolve_pds, AtpFS, 5 5 }; 6 - use vfs::FileSystem; 7 6 8 7 use std::sync::Arc; 8 + use tokio::runtime::Handle; 9 9 10 10 async fn run_app(args: Vec<String>) -> Result<()> { 11 11 let opts = opts().run_inner(args.as_slice()); ··· 23 23 let pds = resolve_pds(&did).await?; 24 24 println!("resolved PDS: {}", pds); 25 25 26 - let fs = Arc::new(AtpFS::new(did, pds)); 26 + let fs = Arc::new(AtpFS::new(did, pds).await); 27 27 28 28 match opts.cmd { 29 29 SubCommand::Ls { path } => { 30 - println!("Listing: {}", path); 31 - let iterator = fs.read_dir(&path)?; 32 - for item in iterator { 30 + let files = fs.read_dir(&path).await?; 31 + for item in files { 33 32 println!("{}", item); 34 33 } 35 34 } ··· 40 39 41 40 let options = vec![MountOption::RO, MountOption::FSName("atproto".to_string())]; 42 41 42 + let handle = Handle::current(); 43 + 43 44 let fuse_handler = AtpFuse { 44 45 fs, 45 46 inner: DefaultFuseHandler::new(), 47 + runtime: handle, 46 48 }; 47 49 48 50 println!("mounting at {:?}...", mount_point);