1use super::block_store::{collect_body, s3_error};
2use async_trait::async_trait;
3use aws_sdk_s3::primitives::ByteStream;
4use aws_sdk_s3::Client;
5use base64::engine::general_purpose::STANDARD as BASE64;
6use base64::Engine as _;
7use std::collections::HashMap;
8use vfs::engine::{
9 InodeType, ObjectBackend, ObjectEntry, ObjectMeta, Timespec, VfsError, VfsResult,
10};
11
12#[derive(Debug, Clone, Default)]
13pub struct S3ObjectBackendOptions {
14 pub prefix: String,
15}
16
17#[derive(Debug, Clone)]
18pub struct S3ObjectBackend {
19 client: Client,
20 bucket: String,
21 options: S3ObjectBackendOptions,
22}
23
24impl S3ObjectBackend {
25 pub fn new(client: Client, bucket: impl Into<String>) -> Self {
26 Self::with_options(client, bucket, S3ObjectBackendOptions::default())
27 }
28
29 pub fn with_options(
30 client: Client,
31 bucket: impl Into<String>,
32 options: S3ObjectBackendOptions,
33 ) -> Self {
34 Self {
35 client,
36 bucket: bucket.into(),
37 options,
38 }
39 }
40
41 fn key_for(&self, key: &str) -> String {
42 format!("{}{}", self.options.prefix, key)
43 }
44
45 fn strip_prefix<'a>(&self, key: &'a str) -> &'a str {
46 key.strip_prefix(&self.options.prefix).unwrap_or(key)
47 }
48}
49
50#[async_trait]
51impl ObjectBackend for S3ObjectBackend {
52 async fn list(&self, prefix: &str) -> VfsResult<Vec<ObjectEntry>> {
53 let s3_prefix = self.key_for(prefix);
54 let mut continuation = None;
55 let mut entries = Vec::new();
56 loop {
57 let response = self
58 .client
59 .list_objects_v2()
60 .bucket(&self.bucket)
61 .prefix(&s3_prefix)
62 .delimiter("/")
63 .set_continuation_token(continuation)
64 .send()
65 .await
66 .map_err(|err| s3_error(format!("list s3 prefix '{s3_prefix}': {err}")))?;
67
68 for object in response.contents() {
69 let Some(key) = object.key() else {
70 continue;
71 };
72 entries.push(ObjectEntry {
73 name: self.strip_prefix(key).to_string(),
74 size: object.size().unwrap_or(0).max(0) as u64,
75 mtime: object
76 .last_modified()
77 .map(timespec_from_smithy)
78 .unwrap_or_else(Timespec::now),
79 is_prefix: false,
80 });
81 }
82
83 for prefix in response.common_prefixes() {
84 let Some(prefix) = prefix.prefix() else {
85 continue;
86 };
87 entries.push(ObjectEntry {
88 name: self.strip_prefix(prefix).to_string(),
89 size: 0,
90 mtime: Timespec::now(),
91 is_prefix: true,
92 });
93 }
94
95 if response.is_truncated().unwrap_or(false) {
96 continuation = response.next_continuation_token().map(ToOwned::to_owned);
97 } else {
98 break;
99 }
100 }
101 Ok(entries)
102 }
103
104 async fn head(&self, key: &str) -> VfsResult<Option<ObjectMeta>> {
105 let s3_key = self.key_for(key);
106 let response = match self
107 .client
108 .head_object()
109 .bucket(&self.bucket)
110 .key(&s3_key)
111 .send()
112 .await
113 {
114 Ok(response) => response,
115 Err(err)
116 if err.as_service_error().is_some_and(|err| {
117 err.is_not_found()
118 || matches!(err.meta().code(), Some("NotFound" | "NoSuchKey"))
119 }) =>
120 {
121 return Ok(None);
122 }
123 Err(err) => return Err(s3_error(format!("head s3 object '{s3_key}': {err}"))),
124 };
125
126 let metadata = response.metadata();
127 let kind = metadata
128 .and_then(|metadata| metadata.get("vfs-kind"))
129 .map(|kind| match kind.as_str() {
130 "directory" => InodeType::Directory,
131 "symlink" => InodeType::Symlink,
132 "character-device" => InodeType::CharacterDevice,
133 "block-device" => InodeType::BlockDevice,
134 "fifo" => InodeType::Fifo,
135 _ => InodeType::File,
136 })
137 .unwrap_or(InodeType::File);
138 let xattrs = metadata
139 .and_then(|metadata| metadata.get("vfs-xattrs"))
140 .map(|encoded| {
141 let bytes = BASE64
142 .decode(encoded)
143 .map_err(|error| VfsError::eio(format!("decode S3 xattrs: {error}")))?;
144 serde_json::from_slice(&bytes)
145 .map_err(|error| VfsError::eio(format!("parse S3 xattrs: {error}")))
146 })
147 .transpose()?
148 .unwrap_or_default();
149 let size = response.content_length().unwrap_or(0).max(0) as u64;
150 let allocated_extents = metadata
151 .and_then(|metadata| metadata.get("vfs-allocated-extents"))
152 .map(|encoded| {
153 let bytes = BASE64.decode(encoded).map_err(|error| {
154 VfsError::eio(format!("decode S3 allocated extents: {error}"))
155 })?;
156 serde_json::from_slice(&bytes)
157 .map_err(|error| VfsError::eio(format!("parse S3 allocated extents: {error}")))
158 })
159 .transpose()?
160 .unwrap_or_else(|| {
161 if size == 0 {
162 Vec::new()
163 } else {
164 vec![(0, size.div_ceil(512))]
165 }
166 });
167 let fallback_time = response
168 .last_modified()
169 .map(timespec_from_smithy)
170 .unwrap_or_else(Timespec::now);
171 Ok(Some(ObjectMeta {
172 size,
173 allocated_extents,
174 atime: metadata
175 .and_then(|metadata| metadata.get("vfs-atime"))
176 .and_then(|value| parse_timespec(value))
177 .unwrap_or(fallback_time),
178 mtime: metadata
179 .and_then(|metadata| metadata.get("vfs-mtime"))
180 .and_then(|value| parse_timespec(value))
181 .unwrap_or(fallback_time),
182 ctime: metadata
183 .and_then(|metadata| metadata.get("vfs-ctime"))
184 .and_then(|value| parse_timespec(value))
185 .unwrap_or(fallback_time),
186 birthtime: metadata
187 .and_then(|metadata| metadata.get("vfs-birthtime"))
188 .and_then(|value| parse_timespec(value))
189 .unwrap_or(fallback_time),
190 mode: metadata
191 .and_then(|metadata| metadata.get("vfs-mode"))
192 .and_then(|mode| u32::from_str_radix(mode, 8).ok())
193 .unwrap_or(0o644),
194 uid: metadata
195 .and_then(|metadata| metadata.get("vfs-uid"))
196 .and_then(|uid| uid.parse().ok())
197 .unwrap_or(0),
198 gid: metadata
199 .and_then(|metadata| metadata.get("vfs-gid"))
200 .and_then(|gid| gid.parse().ok())
201 .unwrap_or(0),
202 kind,
203 symlink_target: metadata
204 .and_then(|metadata| metadata.get("vfs-symlink-target").cloned()),
205 link_id: metadata.and_then(|metadata| metadata.get("vfs-link-id").cloned()),
206 xattrs,
207 }))
208 }
209
210 async fn get_range(&self, key: &str, off: u64, len: u64) -> VfsResult<Vec<u8>> {
211 if len == 0 {
212 return Ok(Vec::new());
213 }
214 let s3_key = self.key_for(key);
215 let end = off
216 .checked_add(len)
217 .and_then(|value| value.checked_sub(1))
218 .ok_or_else(|| vfs::engine::VfsError::einval("invalid S3 byte range"))?;
219 let response = self
220 .client
221 .get_object()
222 .bucket(&self.bucket)
223 .key(&s3_key)
224 .range(format!("bytes={off}-{end}"))
225 .send()
226 .await
227 .map_err(|err| s3_error(format!("get s3 object range '{s3_key}': {err}")))?;
228 collect_body(response.body, &s3_key).await
229 }
230
231 async fn put(&self, key: &str, data: &[u8], meta: ObjectMeta) -> VfsResult<()> {
232 let s3_key = self.key_for(key);
233 let mut metadata = HashMap::new();
234 metadata.insert("vfs-kind".to_string(), kind_name(meta.kind).to_string());
235 metadata.insert("vfs-mode".to_string(), format!("{:o}", meta.mode));
236 metadata.insert("vfs-uid".to_string(), meta.uid.to_string());
237 metadata.insert("vfs-gid".to_string(), meta.gid.to_string());
238 metadata.insert("vfs-atime".to_string(), format_timespec(meta.atime));
239 metadata.insert("vfs-mtime".to_string(), format_timespec(meta.mtime));
240 metadata.insert("vfs-ctime".to_string(), format_timespec(meta.ctime));
241 metadata.insert("vfs-birthtime".to_string(), format_timespec(meta.birthtime));
242 let encoded_extents = serde_json::to_vec(&meta.allocated_extents)
243 .map(|bytes| BASE64.encode(bytes))
244 .map_err(|error| VfsError::eio(format!("serialize S3 allocated extents: {error}")))?;
245 metadata.insert("vfs-allocated-extents".to_string(), encoded_extents);
246 if let Some(target) = meta.symlink_target {
247 metadata.insert("vfs-symlink-target".to_string(), target);
248 }
249 if let Some(link_id) = meta.link_id {
250 metadata.insert("vfs-link-id".to_string(), link_id);
251 }
252 if !meta.xattrs.is_empty() {
253 let encoded = serde_json::to_vec(&meta.xattrs)
254 .map(|bytes| BASE64.encode(bytes))
255 .map_err(|error| VfsError::eio(format!("serialize S3 xattrs: {error}")))?;
256 metadata.insert("vfs-xattrs".to_string(), encoded);
257 }
258 self.client
259 .put_object()
260 .bucket(&self.bucket)
261 .key(&s3_key)
262 .body(ByteStream::from(data.to_vec()))
263 .set_metadata(Some(metadata))
264 .send()
265 .await
266 .map_err(|err| s3_error(format!("put s3 object '{s3_key}': {err}")))?;
267 Ok(())
268 }
269
270 async fn copy(&self, src: &str, dst: &str) -> VfsResult<()> {
271 let src_key = self.key_for(src);
272 let dst_key = self.key_for(dst);
273 self.client
274 .copy_object()
275 .bucket(&self.bucket)
276 .copy_source(format!("{}/{}", self.bucket, src_key))
277 .key(&dst_key)
278 .send()
279 .await
280 .map_err(|err| s3_error(format!("copy s3 object '{src_key}' to '{dst_key}': {err}")))?;
281 Ok(())
282 }
283
284 async fn delete(&self, key: &str) -> VfsResult<()> {
285 let s3_key = self.key_for(key);
286 self.client
287 .delete_object()
288 .bucket(&self.bucket)
289 .key(&s3_key)
290 .send()
291 .await
292 .map_err(|err| s3_error(format!("delete s3 object '{s3_key}': {err}")))?;
293 Ok(())
294 }
295}
296
297fn kind_name(kind: InodeType) -> &'static str {
298 match kind {
299 InodeType::File => "file",
300 InodeType::Directory => "directory",
301 InodeType::Symlink => "symlink",
302 InodeType::CharacterDevice => "character-device",
303 InodeType::BlockDevice => "block-device",
304 InodeType::Fifo => "fifo",
305 }
306}
307
308fn timespec_from_smithy(time: &aws_sdk_s3::primitives::DateTime) -> Timespec {
309 Timespec {
310 sec: time.secs(),
311 nsec: 0,
312 }
313}
314
315fn format_timespec(time: Timespec) -> String {
316 format!("{}:{}", time.sec, time.nsec)
317}
318
319fn parse_timespec(value: &str) -> Option<Timespec> {
320 let (seconds, nanoseconds) = value.split_once(':')?;
321 let sec = seconds.parse().ok()?;
322 let nsec = nanoseconds.parse().ok()?;
323 (nsec < 1_000_000_000).then_some(Timespec { sec, nsec })
324}