aster_forge_cloud_files_macos_bridge/
engine.rs1use std::{num::NonZeroU64, sync::Arc};
4
5use aster_forge_cloud_files_core::{
6 ByteRange, CloudFilesBackend, CloudItemKey, CloudItemKind, ContentReadRequest, ContentRevision,
7};
8use bytes::Bytes;
9
10use crate::{
11 MacosBridgeError, MacosEnumerationPage, MacosEnumerationRequest, MacosFileProviderIdentifier,
12 MacosFileProviderItem, Result,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MacosContentFetchPlan {
18 key: CloudItemKey,
19 revision: ContentRevision,
20 size: u64,
21}
22
23impl MacosContentFetchPlan {
24 #[must_use]
26 pub const fn key(&self) -> &CloudItemKey {
27 &self.key
28 }
29
30 #[must_use]
32 pub const fn revision(&self) -> &ContentRevision {
33 &self.revision
34 }
35
36 #[must_use]
38 pub const fn size(&self) -> u64 {
39 self.size
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct MacosFetchedContentChunk {
46 offset: u64,
47 bytes: Bytes,
48}
49
50impl MacosFetchedContentChunk {
51 pub const fn offset(&self) -> u64 {
53 self.offset
54 }
55
56 pub const fn bytes(&self) -> &Bytes {
58 &self.bytes
59 }
60
61 pub fn into_parts(self) -> (u64, Bytes) {
63 (self.offset, self.bytes)
64 }
65}
66
67pub struct MacosReadOnlyEngine<B> {
69 backend: Arc<B>,
70 root_key: CloudItemKey,
71}
72
73impl<B> Clone for MacosReadOnlyEngine<B> {
74 fn clone(&self) -> Self {
75 Self {
76 backend: self.backend.clone(),
77 root_key: self.root_key.clone(),
78 }
79 }
80}
81
82impl<B> MacosReadOnlyEngine<B>
83where
84 B: CloudFilesBackend + 'static,
85{
86 pub const fn new(backend: Arc<B>, root_key: CloudItemKey) -> Self {
88 Self { backend, root_key }
89 }
90
91 #[must_use]
93 pub const fn root_key(&self) -> &CloudItemKey {
94 &self.root_key
95 }
96
97 pub async fn item(
103 &self,
104 identifier: &MacosFileProviderIdentifier,
105 ) -> Result<MacosFileProviderItem> {
106 let key = match identifier.system_container() {
107 Some(crate::MacosFileProviderSystemContainer::Root) => &self.root_key,
108 Some(
109 crate::MacosFileProviderSystemContainer::WorkingSet
110 | crate::MacosFileProviderSystemContainer::Trash,
111 ) => return Err(MacosBridgeError::UnsupportedSystemContainer),
112 None => identifier.item_key()?,
113 };
114 let item = self.backend.get_item(key).await?;
115 if item.key() != key {
116 return Err(MacosBridgeError::InvalidBackendResponse {
117 reason: "get_item returned a different scoped stable identity",
118 });
119 }
120 if item.is_root() != (key == &self.root_key) {
121 return Err(MacosBridgeError::InvalidBackendResponse {
122 reason: "get_item root shape did not match the File Provider root role",
123 });
124 }
125 MacosFileProviderItem::from_cloud_item(&item, &self.root_key)
126 }
127
128 pub async fn enumerate(
134 &self,
135 request: &MacosEnumerationRequest,
136 ) -> Result<MacosEnumerationPage> {
137 let parent = request.backend_parent(&self.root_key)?;
138 let parent_item = self.backend.get_item(parent).await?;
139 if parent_item.key() != parent {
140 return Err(MacosBridgeError::InvalidBackendResponse {
141 reason: "enumeration parent lookup returned a different stable identity",
142 });
143 }
144 if parent_item.kind() != CloudItemKind::Directory {
145 return Err(MacosBridgeError::InvalidBackendResponse {
146 reason: "enumeration parent is not a directory",
147 });
148 }
149 if parent_item.is_root() != (parent == &self.root_key) {
150 return Err(MacosBridgeError::InvalidBackendResponse {
151 reason: "enumeration parent root shape did not match the requested container",
152 });
153 }
154 let page = self.backend.list_children(parent, request.page()).await?;
155 MacosEnumerationPage::from_backend(&self.root_key, parent, page)
156 }
157
158 pub async fn prepare_content_fetch(
164 &self,
165 identifier: &MacosFileProviderIdentifier,
166 requested_revision: &ContentRevision,
167 ) -> Result<MacosContentFetchPlan> {
168 let key = identifier.item_key()?;
169 let item = self.backend.get_item(key).await?;
170 if item.key() != key {
171 return Err(MacosBridgeError::InvalidBackendResponse {
172 reason: "content metadata lookup returned a different stable identity",
173 });
174 }
175 if item.is_root() || item.kind() != CloudItemKind::File {
176 return Err(MacosBridgeError::InvalidBackendResponse {
177 reason: "content fetch target is not a regular file",
178 });
179 }
180 let content = item
181 .content()
182 .ok_or(MacosBridgeError::InvalidBackendResponse {
183 reason: "regular file omitted content metadata",
184 })?;
185 if content.revision() != requested_revision {
186 return Err(MacosBridgeError::Backend(
187 aster_forge_cloud_files_core::CloudBackendError::new(
188 aster_forge_cloud_files_core::CloudBackendErrorKind::PreconditionFailed,
189 ),
190 ));
191 }
192 Ok(MacosContentFetchPlan {
193 key: key.clone(),
194 revision: requested_revision.clone(),
195 size: content.size(),
196 })
197 }
198
199 pub async fn read_content_chunk(
205 &self,
206 plan: &MacosContentFetchPlan,
207 offset: u64,
208 maximum_length: NonZeroU64,
209 ) -> Result<MacosFetchedContentChunk> {
210 if offset >= plan.size {
211 return Err(MacosBridgeError::InvalidBackendResponse {
212 reason: "content chunk offset is outside the planned file",
213 });
214 }
215 let length = maximum_length.get().min(plan.size - offset);
216 let range = ByteRange::new(offset, length).map_err(|_| {
217 MacosBridgeError::InvalidBackendResponse {
218 reason: "content chunk range exceeded the planned file",
219 }
220 })?;
221 let request =
222 ContentReadRequest::range(plan.key.clone(), plan.revision.clone(), plan.size, range);
223 let response = self.backend.read_content(&request).await?;
224 request.validate_response(&response).map_err(|_| {
225 MacosBridgeError::InvalidBackendResponse {
226 reason: "content response violated the requested revision or chunk extent",
227 }
228 })?;
229 let (_, response_offset, bytes, _) = response.into_parts();
230 Ok(MacosFetchedContentChunk {
231 offset: response_offset,
232 bytes,
233 })
234 }
235}