|
49 | 49 | //! no equivalent of `FSKit`'s `update_state` push hook. |
50 | 50 | //! - [`ProjFsPresenter::evict_item`] logs and returns `Ok(())`; `ProjFS` |
51 | 51 | //! manages projection cache eviction at the OS layer. |
52 | | -//! - [`ProjFsPresenter::fetch_contents`] intentionally bails: on-demand |
53 | | -//! reads flow through the `GetFileData` callback, not through direct |
54 | | -//! calls into this method, so there is nothing for it to do. |
| 52 | +//! - [`ProjFsPresenter::fetch_contents`] fetches file contents through |
| 53 | +//! the installed [`ContentProvider`] and caches them on disk. When no |
| 54 | +//! provider is installed, the method bails: on-demand reads must then |
| 55 | +//! flow through the `GetFileData` callback instead. |
55 | 56 | //! - [`ProjFsPresenter::start`] marks the mount directory as a |
56 | 57 | //! placeholder via `PrjMarkDirectoryAsPlaceholder` and begins |
57 | 58 | //! virtualising via `PrjStartVirtualizing`. |
@@ -839,6 +840,10 @@ pub struct ProjFsPresenter { |
839 | 840 | /// `CallbackContextInner` so both callbacks see the same map. |
840 | 841 | #[cfg_attr(not(target_os = "windows"), allow(dead_code))] |
841 | 842 | cancellation_tokens: Arc<Mutex<HashMap<i32, CancellationToken>>>, |
| 843 | + /// Directory where `fetch_contents` caches downloaded files. Each |
| 844 | + /// file is named after its `ItemId` (with path separators replaced |
| 845 | + /// by `_`), written to a `.tmp` file first, then atomically renamed. |
| 846 | + cache_dir: PathBuf, |
842 | 847 | } |
843 | 848 |
|
844 | 849 | /// Owning record of the heap allocation handed to `ProjFS` via |
@@ -921,6 +926,7 @@ impl ProjFsPresenter { |
921 | 926 | callback_ctx: Arc::new(tokio::sync::Mutex::new(None)), |
922 | 927 | content_provider: None, |
923 | 928 | cancellation_tokens: Arc::new(Mutex::new(HashMap::new())), |
| 929 | + cache_dir: std::env::temp_dir().join("cascade-projfs-cache"), |
924 | 930 | } |
925 | 931 | } |
926 | 932 |
|
@@ -962,12 +968,31 @@ impl ProjFsPresenter { |
962 | 968 | self |
963 | 969 | } |
964 | 970 |
|
| 971 | + /// Override the cache directory used by [`Self::fetch_contents`]. |
| 972 | + #[must_use] |
| 973 | + pub fn with_cache_dir(mut self, dir: impl Into<PathBuf>) -> Self { |
| 974 | + self.cache_dir = dir.into(); |
| 975 | + self |
| 976 | + } |
| 977 | + |
965 | 978 | /// The configured mount point. |
966 | 979 | #[must_use] |
967 | 980 | pub fn mount_point(&self) -> &Path { |
968 | 981 | &self.mount_point |
969 | 982 | } |
970 | 983 |
|
| 984 | + /// The configured cache directory. |
| 985 | + #[must_use] |
| 986 | + pub fn cache_dir(&self) -> &Path { |
| 987 | + &self.cache_dir |
| 988 | + } |
| 989 | + |
| 990 | + /// Derive a cache file path for an item. Path separators in the id |
| 991 | + /// are replaced with `_` so the result is a flat filename. |
| 992 | + fn cache_path_for(&self, id: &ItemId) -> PathBuf { |
| 993 | + self.cache_dir.join(id.0.replace(['/', '\\'], "_")) |
| 994 | + } |
| 995 | + |
971 | 996 | /// Access the configured content provider, if any. Exposed for |
972 | 997 | /// tests and the (future) consistency checks that want to confirm |
973 | 998 | /// the presenter was built with one before `start()`. |
@@ -1015,10 +1040,55 @@ impl VfsPresenter for ProjFsPresenter { |
1015 | 1040 | } |
1016 | 1041 |
|
1017 | 1042 | async fn fetch_contents(&self, id: &ItemId) -> anyhow::Result<PathBuf> { |
1018 | | - tracing::debug!(id = %id, "fetch_contents (not yet implemented)"); |
1019 | | - anyhow::bail!( |
1020 | | - "ProjFS fetch_contents is not yet implemented; the GetFileData callback should drive this" |
1021 | | - ) |
| 1043 | + let cache_path = self.cache_path_for(id); |
| 1044 | + if cache_path.exists() { |
| 1045 | + return Ok(cache_path); |
| 1046 | + } |
| 1047 | + |
| 1048 | + let Some(provider) = &self.content_provider else { |
| 1049 | + anyhow::bail!( |
| 1050 | + "no ContentProvider installed; ProjFS reads flow through \ |
| 1051 | + GetFileData callbacks, not fetch_contents" |
| 1052 | + ); |
| 1053 | + }; |
| 1054 | + |
| 1055 | + let file_size = { |
| 1056 | + let items = self.items.read().await; |
| 1057 | + let item = items |
| 1058 | + .get(&id.0) |
| 1059 | + .ok_or_else(|| anyhow::anyhow!("item not found: {id}"))?; |
| 1060 | + item.size.unwrap_or(0) |
| 1061 | + }; |
| 1062 | + |
| 1063 | + tokio::fs::create_dir_all(&self.cache_dir).await?; |
| 1064 | + |
| 1065 | + let mut all_bytes = Vec::new(); |
| 1066 | + let mut offset = 0u64; |
| 1067 | + const READ_CHUNK: u32 = 1024 * 1024; |
| 1068 | + loop { |
| 1069 | + let remaining = file_size.saturating_sub(offset); |
| 1070 | + let to_read = if remaining == 0 { |
| 1071 | + READ_CHUNK |
| 1072 | + } else { |
| 1073 | + u32::try_from(remaining) |
| 1074 | + .unwrap_or(READ_CHUNK) |
| 1075 | + .min(READ_CHUNK) |
| 1076 | + }; |
| 1077 | + let chunk = provider.read_range(id, offset, to_read)?; |
| 1078 | + if chunk.is_empty() { |
| 1079 | + break; |
| 1080 | + } |
| 1081 | + offset += chunk.len() as u64; |
| 1082 | + all_bytes.extend_from_slice(&chunk); |
| 1083 | + if remaining > 0 && chunk.len() < to_read as usize { |
| 1084 | + break; |
| 1085 | + } |
| 1086 | + } |
| 1087 | + |
| 1088 | + let temp_path = cache_path.with_extension("tmp"); |
| 1089 | + tokio::fs::write(&temp_path, &all_bytes).await?; |
| 1090 | + tokio::fs::rename(&temp_path, &cache_path).await?; |
| 1091 | + Ok(cache_path) |
1022 | 1092 | } |
1023 | 1093 |
|
1024 | 1094 | async fn evict_item(&self, id: &ItemId) -> anyhow::Result<()> { |
|
0 commit comments