1use std::{
2 collections::HashSet,
3 io,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use cap_fs_ext::DirExt as _;
9use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt};
10#[cfg(windows)]
11use cap_std::fs::MetadataExt;
12#[cfg(windows)]
13use cap_std::fs::OpenOptionsExt as _;
14use cap_std::fs::{Dir, OpenOptions};
15#[cfg(unix)]
16use cap_std::fs::{DirBuilderExt as _, OpenOptionsExt as _, PermissionsExt as _};
17
18use super::{
19 LocalStore, StagedFile, StoreFile, StoreMoveError, StoreRemoveError, StoreWriter,
20 is_reconcile_dir, validate_relative, validate_temp,
21};
22use crate::{AttachmentDecoder, AttachmentError, AttachmentFailureCause as Cause, DecodedMeta};
23
24pub async fn create_private_directory(path: &Path) -> io::Result<()> {
27 #[cfg(unix)]
28 {
29 let path = path.to_path_buf();
30 xmtp_common::task::spawn_blocking(move || create_private_directories(&path))
31 .await
32 .map_err(io::Error::other)?
33 }
34 #[cfg(not(unix))]
35 {
36 tokio::fs::create_dir_all(path).await
37 }
38}
39
40#[derive(Clone, Debug)]
42pub struct NativeStore {
43 root: PathBuf,
44 root_dir: Arc<Dir>,
46 #[cfg(test)]
47 forced_hard_link_error: Option<std::io::ErrorKind>,
48 #[cfg(test)]
49 forced_source_unlink_error: Option<std::io::ErrorKind>,
50 #[cfg(test)]
51 fallback_race_bytes: Option<Vec<u8>>,
52 #[cfg(test)]
53 forced_chmod_error: bool,
54 #[cfg(test)]
55 forced_foreign_owner: bool,
56 #[cfg(test)]
57 forced_modified_error: bool,
58}
59
60impl NativeStore {
61 pub async fn new(root: impl AsRef<Path>) -> Result<Self, AttachmentError> {
62 let root = std::path::absolute(root.as_ref())
63 .map_err(|_| AttachmentError::new(Cause::LocalStorage))?;
64 #[cfg(unix)]
65 {
66 let path = root.clone();
67 xmtp_common::task::spawn_blocking(move || create_private_directories(&path))
68 .await
69 .map_err(storage_error)?
70 .map_err(storage_error)?;
71 }
72 #[cfg(not(unix))]
73 tokio::fs::create_dir_all(&root)
74 .await
75 .map_err(storage_error)?;
76 let root_dir =
77 Dir::open_ambient_dir(&root, cap_std::ambient_authority()).map_err(storage_error)?;
78 Ok(Self {
79 root,
80 root_dir: Arc::new(root_dir),
81 #[cfg(test)]
82 forced_hard_link_error: None,
83 #[cfg(test)]
84 forced_source_unlink_error: None,
85 #[cfg(test)]
86 fallback_race_bytes: None,
87 #[cfg(test)]
88 forced_chmod_error: false,
89 #[cfg(test)]
90 forced_foreign_owner: false,
91 #[cfg(test)]
92 forced_modified_error: false,
93 })
94 }
95
96 #[cfg(test)]
97 pub(crate) fn with_forced_hard_link_error(mut self, kind: std::io::ErrorKind) -> Self {
98 self.forced_hard_link_error = Some(kind);
99 self
100 }
101
102 #[cfg(test)]
103 pub(crate) fn with_forced_source_unlink_error(mut self, kind: std::io::ErrorKind) -> Self {
104 self.forced_source_unlink_error = Some(kind);
105 self
106 }
107
108 #[cfg(test)]
109 pub(crate) fn with_fallback_destination_race(mut self, bytes: Vec<u8>) -> Self {
110 self.fallback_race_bytes = Some(bytes);
111 self
112 }
113
114 #[cfg(test)]
115 pub(crate) fn with_forced_chmod_error(mut self) -> Self {
116 self.forced_chmod_error = true;
117 self
118 }
119
120 #[cfg(test)]
121 pub(crate) fn with_forced_foreign_owner(mut self) -> Self {
122 self.forced_foreign_owner = true;
123 self
124 }
125
126 #[cfg(test)]
127 pub(crate) fn with_forced_modified_error(mut self) -> Self {
128 self.forced_modified_error = true;
129 self
130 }
131
132 fn force_foreign_owner(&self) -> bool {
133 #[cfg(test)]
134 {
135 self.forced_foreign_owner
136 }
137 #[cfg(not(test))]
138 {
139 false
140 }
141 }
142
143 fn force_chmod_error(&self) -> bool {
144 #[cfg(test)]
145 {
146 self.forced_chmod_error
147 }
148 #[cfg(not(test))]
149 {
150 false
151 }
152 }
153
154 #[cfg(unix)]
155 fn open_managed_child(&self, parent: &Dir, name: &str, created: bool) -> io::Result<Dir> {
156 use cap_fs_ext::OsMetadataExt as _;
157 use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
158
159 let before = parent.symlink_metadata(name)?;
160 if !before.is_dir()
161 || is_link(&before)
162 || self.force_foreign_owner()
163 || before.uid() != unsafe { libc::geteuid() }
164 {
165 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
166 }
167 #[cfg(test)]
168 if created {
169 record_initial_mode(before.permissions().mode(), 0o700);
170 }
171 #[cfg(not(test))]
172 let _ = created;
173 let child = match open_child_mode_handle(parent, name) {
174 Ok(child) => child,
175 Err(error)
176 if error.kind() == io::ErrorKind::PermissionDenied
177 && before.permissions().mode() & 0o7777 != 0o700
178 && !self.force_chmod_error() =>
179 {
180 repair_unreadable_child_mode(parent, name, &before)?;
181 open_child_mode_handle(parent, name)?
182 }
183 Err(error) => return Err(error),
184 };
185 let metadata = child.metadata()?;
186 if !metadata.is_dir()
187 || metadata.uid() != before.uid()
188 || metadata.dev() != before.dev()
189 || metadata.ino() != before.ino()
190 {
191 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
192 }
193 if metadata.permissions().mode() & 0o7777 != 0o700 {
194 if self.force_chmod_error() {
195 if created {
196 tracing::warn!(
197 name,
198 "could not set private attachment directory permissions"
199 );
200 } else {
201 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
202 }
203 } else {
204 set_private_child_mode(&child)?;
205 }
206 }
207 Ok(Dir::from_std_file(child))
208 }
209
210 fn remove_source(&self, parent: &Dir, name: &str) -> io::Result<()> {
211 #[cfg(test)]
212 if let Some(kind) = self.forced_source_unlink_error {
213 return Err(io::Error::from(kind));
214 }
215 parent.remove_file(name)
216 }
217
218 fn hard_link(
219 &self,
220 from_parent: &Dir,
221 from_name: &str,
222 to_parent: &Dir,
223 to_name: &str,
224 ) -> io::Result<()> {
225 #[cfg(test)]
226 if let Some(kind) = self.forced_hard_link_error {
227 return Err(io::Error::from(kind));
228 }
229 from_parent.hard_link(from_name, to_parent, to_name)
230 }
231
232 fn parent(&self, relative: &str, create: bool) -> io::Result<(Dir, String)> {
233 let (parent, name) = relative.rsplit_once('/').unwrap_or(("", relative));
234 let mut directory = self.root_dir.try_clone()?;
235 if !parent.is_empty() {
236 for part in parent.split('/') {
237 directory = match directory.symlink_metadata(part) {
238 Ok(_) => {
239 #[cfg(unix)]
240 {
241 self.open_managed_child(&directory, part, false)?
242 }
243 #[cfg(not(unix))]
244 {
245 directory.open_dir_nofollow(part)?
246 }
247 }
248 Err(error) if create && error.kind() == io::ErrorKind::NotFound => {
249 #[cfg(unix)]
250 let mut builder = cap_std::fs::DirBuilder::new();
251 #[cfg(unix)]
252 builder.mode(0o700);
253 #[cfg(unix)]
254 let create_result = directory.create_dir_with(part, &builder);
255 #[cfg(not(unix))]
256 let create_result = directory.create_dir(part);
257 let created = match create_result {
258 Ok(()) => true,
259 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => false,
260 Err(error) => return Err(error),
261 };
262 #[cfg(not(unix))]
263 let _ = created;
264 #[cfg(unix)]
265 {
266 self.open_managed_child(&directory, part, created)?
267 }
268 #[cfg(not(unix))]
269 {
270 directory.open_dir_nofollow(part)?
271 }
272 }
273 Err(error) => return Err(error),
274 };
275 }
276 }
277 Ok((directory, name.to_owned()))
278 }
279}
280
281#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
282fn open_child_mode_handle(parent: &Dir, name: &str) -> io::Result<std::fs::File> {
283 let fd = rustix::fs::openat(
284 parent,
285 name,
286 rustix::fs::OFlags::RDONLY
287 | rustix::fs::OFlags::DIRECTORY
288 | rustix::fs::OFlags::NOFOLLOW
289 | rustix::fs::OFlags::CLOEXEC,
290 rustix::fs::Mode::empty(),
291 )?;
292 Ok(fd.into())
293}
294
295#[cfg(all(
296 unix,
297 not(any(target_vendor = "apple", target_os = "linux", target_os = "android"))
298))]
299fn open_child_mode_handle(parent: &Dir, name: &str) -> io::Result<std::fs::File> {
300 let mut options = OpenOptions::new();
301 options.read(true).follow(FollowSymlinks::No);
302 let file = parent.open_with(name, &options)?;
303 if !file.metadata()?.is_dir() {
304 return Err(io::Error::from(io::ErrorKind::NotADirectory));
305 }
306 Ok(file.into_std())
307}
308
309#[cfg(any(target_os = "linux", target_os = "android"))]
310fn repair_unreadable_child_mode(
311 parent: &Dir,
312 name: &str,
313 before: &cap_std::fs::Metadata,
314) -> io::Result<()> {
315 use cap_fs_ext::OsMetadataExt as _;
316 use std::os::{
317 fd::AsRawFd as _,
318 unix::fs::{MetadataExt as _, PermissionsExt as _},
319 };
320
321 let pinned = rustix::fs::openat(
323 parent,
324 name,
325 rustix::fs::OFlags::PATH
326 | rustix::fs::OFlags::DIRECTORY
327 | rustix::fs::OFlags::NOFOLLOW
328 | rustix::fs::OFlags::CLOEXEC,
329 rustix::fs::Mode::empty(),
330 )?;
331 let pinned: std::fs::File = pinned.into();
332 let metadata = pinned.metadata()?;
333 if !metadata.is_dir()
334 || metadata.uid() != before.uid()
335 || metadata.dev() != before.dev()
336 || metadata.ino() != before.ino()
337 || metadata.uid() != unsafe { libc::geteuid() }
338 {
339 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
340 }
341 let pinned_path = format!("/proc/self/fd/{}", pinned.as_raw_fd());
344 std::fs::set_permissions(pinned_path, std::fs::Permissions::from_mode(0o700))
345}
346
347#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
348fn repair_unreadable_child_mode(
349 parent: &Dir,
350 name: &str,
351 before: &cap_std::fs::Metadata,
352) -> io::Result<()> {
353 use cap_fs_ext::OsMetadataExt as _;
354
355 let current = parent.symlink_metadata(name)?;
356 if !current.is_dir()
357 || is_link(¤t)
358 || current.uid() != unsafe { libc::geteuid() }
359 || current.dev() != before.dev()
360 || current.ino() != before.ino()
361 {
362 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
363 }
364 rustix::fs::chmodat(
365 parent,
366 name,
367 rustix::fs::Mode::from_raw_mode(0o700),
368 rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
369 )?;
370 Ok(())
371}
372
373#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
374fn set_private_child_mode(file: &std::fs::File) -> io::Result<()> {
375 rustix::fs::fchmod(file, rustix::fs::Mode::from_raw_mode(0o700))?;
376 Ok(())
377}
378
379#[cfg(all(
380 unix,
381 not(any(target_vendor = "apple", target_os = "linux", target_os = "android"))
382))]
383fn set_private_child_mode(file: &std::fs::File) -> io::Result<()> {
384 use std::os::unix::fs::PermissionsExt as _;
385
386 file.set_permissions(std::fs::Permissions::from_mode(0o700))
387}
388
389#[cfg(unix)]
390fn create_private_directories(path: &Path) -> io::Result<()> {
391 use std::os::unix::fs::PermissionsExt as _;
392
393 let mut current = PathBuf::new();
394 for component in path.components() {
395 current.push(component.as_os_str());
396 let mut builder = std::fs::DirBuilder::new();
397 std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
398 match builder.create(¤t) {
399 Ok(()) => {
400 let directory = open_created_directory(¤t)?;
401 #[cfg(test)]
402 record_initial_mode(directory.metadata()?.permissions().mode(), 0o700);
403 directory.set_permissions(std::fs::Permissions::from_mode(0o700))?;
404 }
405 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
406 if !std::fs::metadata(¤t)?.is_dir() {
407 return Err(io::Error::from(io::ErrorKind::NotADirectory));
408 }
409 }
410 Err(error) => return Err(error),
411 }
412 }
413 Ok(())
414}
415
416#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
417fn open_created_directory(path: &Path) -> io::Result<std::fs::File> {
418 let fd = rustix::fs::openat(
419 rustix::fs::CWD,
420 path,
421 rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::DIRECTORY | rustix::fs::OFlags::NOFOLLOW,
422 rustix::fs::Mode::empty(),
423 )?;
424 Ok(fd.into())
425}
426
427#[cfg(all(
428 unix,
429 not(any(target_vendor = "apple", target_os = "linux", target_os = "android"))
430))]
431fn open_created_directory(path: &Path) -> io::Result<std::fs::File> {
432 std::fs::File::open(path)
433}
434
435#[cfg(all(test, unix))]
436static INITIAL_MODES: std::sync::Mutex<Vec<(u32, u32)>> = std::sync::Mutex::new(Vec::new());
437
438#[cfg(all(test, unix))]
439fn record_initial_mode(mode: u32, expected: u32) {
440 if std::env::var_os("XMTP_TEST_INITIAL_MODES").is_some() {
441 INITIAL_MODES.lock().unwrap().push((mode & 0o777, expected));
442 }
443}
444
445#[cfg(all(test, unix))]
446pub(super) fn take_initial_modes() -> Vec<(u32, u32)> {
447 std::mem::take(&mut INITIAL_MODES.lock().unwrap())
448}
449
450fn storage_error(_: impl Sized) -> AttachmentError {
451 AttachmentError::new(Cause::LocalStorage)
452}
453
454#[cfg(windows)]
455fn is_link(metadata: &cap_std::fs::Metadata) -> bool {
456 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
457 metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
458}
459
460#[cfg(not(windows))]
461fn is_link(metadata: &cap_std::fs::Metadata) -> bool {
462 metadata.file_type().is_symlink()
463}
464
465fn entry_paths(dir: &Dir, prefix: &Path, paths: &mut HashSet<PathBuf>) -> io::Result<()> {
467 for entry in dir.entries()? {
468 let name = entry?.file_name();
469 let path = prefix.join(&name);
470 let metadata = dir.symlink_metadata(&name)?;
471 if metadata.is_dir() && !is_link(&metadata) {
472 entry_paths(&dir.open_dir_nofollow(&name)?, &path, paths)?;
473 }
474 paths.insert(path);
475 }
476 Ok(())
477}
478
479fn exists_nofollow(parent: &Dir, name: &str) -> io::Result<bool> {
480 match parent.symlink_metadata(name) {
481 Ok(metadata) if is_link(&metadata) => Err(io::Error::new(
482 io::ErrorKind::InvalidData,
483 "attachment path is a symlink",
484 )),
485 Ok(_) => Ok(true),
486 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
487 Err(error) => Err(error),
488 }
489}
490
491#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
492fn rename_no_replace(
493 from_parent: &Dir,
494 from_name: &str,
495 to_parent: &Dir,
496 to_name: &str,
497) -> io::Result<()> {
498 rustix::fs::renameat_with(
499 from_parent,
500 from_name,
501 to_parent,
502 to_name,
503 rustix::fs::RenameFlags::NOREPLACE,
504 )
505 .map_err(Into::into)
506}
507
508#[cfg(windows)]
509fn rename_no_replace(
510 from_parent: &Dir,
511 from_name: &str,
512 to_parent: &Dir,
513 to_name: &str,
514) -> io::Result<()> {
515 use std::{
516 ffi::OsStr,
517 os::windows::{ffi::OsStrExt, io::AsRawHandle},
518 };
519 use windows_sys::Win32::Storage::FileSystem::{
520 DELETE, FILE_ADD_FILE, FILE_FLAG_BACKUP_SEMANTICS, FILE_READ_ATTRIBUTES, FILE_RENAME_INFO,
521 FileRenameInfo, SetFileInformationByHandle,
522 };
523
524 let mut options = OpenOptions::new();
525 options
526 .read(true)
527 .access_mode(DELETE | FILE_READ_ATTRIBUTES)
528 .follow(FollowSymlinks::No);
529 let source = from_parent.open_with(from_name, &options)?;
530 let mut directory_options = OpenOptions::new();
531 directory_options
532 .read(true)
533 .access_mode(FILE_ADD_FILE)
534 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS);
535 let destination = to_parent.open_with(".", &directory_options)?;
536 let name: Vec<u16> = OsStr::new(to_name).encode_wide().collect();
537 let name_bytes = name
538 .len()
539 .checked_mul(2)
540 .ok_or(io::ErrorKind::InvalidInput)?;
541 let size = std::mem::offset_of!(FILE_RENAME_INFO, FileName)
542 .checked_add(name_bytes)
543 .ok_or(io::ErrorKind::InvalidInput)?
544 .max(std::mem::size_of::<FILE_RENAME_INFO>());
545 let size = u32::try_from(size).map_err(|_| io::ErrorKind::InvalidInput)?;
546 let name_bytes = u32::try_from(name_bytes).map_err(|_| io::ErrorKind::InvalidInput)?;
547 let words = (size as usize).div_ceil(std::mem::size_of::<usize>());
548 let mut storage = vec![0_usize; words];
549 let info = storage.as_mut_ptr().cast::<FILE_RENAME_INFO>();
550 unsafe {
552 (*info).Anonymous.ReplaceIfExists = false;
553 (*info).RootDirectory = destination.as_raw_handle();
554 (*info).FileNameLength = name_bytes;
555 std::ptr::copy_nonoverlapping(
556 name.as_ptr(),
557 std::ptr::addr_of_mut!((*info).FileName).cast::<u16>(),
558 name.len(),
559 );
560 if SetFileInformationByHandle(source.as_raw_handle(), FileRenameInfo, info.cast(), size)
561 == 0
562 {
563 return Err(io::Error::last_os_error());
564 }
565 }
566 Ok(())
567}
568
569#[cfg(not(any(
570 target_vendor = "apple",
571 target_os = "linux",
572 target_os = "android",
573 windows
574)))]
575fn rename_no_replace(
576 _from_parent: &Dir,
577 _from_name: &str,
578 _to_parent: &Dir,
579 _to_name: &str,
580) -> io::Result<()> {
581 Err(io::Error::new(
584 io::ErrorKind::Unsupported,
585 "atomic no-replace rename is unavailable",
586 ))
587}
588
589#[async_trait::async_trait]
590impl LocalStore for NativeStore {
591 async fn open_read(&self, path: &str) -> Result<StagedFile, AttachmentError> {
592 validate_relative(path)?;
593 let (parent, name) = self.parent(path, false).map_err(storage_error)?;
594 let mut options = OpenOptions::new();
595 options.read(true).follow(FollowSymlinks::No);
596 let file = parent.open_with(&name, &options).map_err(storage_error)?;
597 if !file.metadata().map_err(storage_error)?.is_file() {
598 return Err(storage_error(()));
599 }
600 Ok(StagedFile {
601 path: self.root.join(path),
602 opened: Some(Arc::new(file.into_std())),
603 })
604 }
605
606 async fn create_temp(&self, path: &str) -> Result<StoreWriter, AttachmentError> {
607 validate_temp(path)?;
608 let (parent, name) = self.parent(path, true).map_err(storage_error)?;
609 let mut options = OpenOptions::new();
610 options
611 .write(true)
612 .create_new(true)
613 .follow(FollowSymlinks::No);
614 #[cfg(unix)]
615 options.mode(0o600);
616 let file = parent.open_with(&name, &options).map_err(storage_error)?;
617 #[cfg(all(test, unix))]
618 record_initial_mode(
619 file.metadata().map_err(storage_error)?.permissions().mode(),
620 0o600,
621 );
622 #[cfg(unix)]
623 if let Err(error) = file.set_permissions(cap_std::fs::Permissions::from_mode(0o600)) {
624 let _ = parent.remove_file(&name);
625 return Err(storage_error(error));
626 }
627 Ok(StoreWriter {
628 file: tokio::fs::File::from_std(file.into_std()),
629 })
630 }
631
632 async fn create_dir_if_absent(&self, path: &str) -> Result<bool, AttachmentError> {
633 validate_relative(path)?;
634 let (parent, name) = self.parent(path, true).map_err(storage_error)?;
635 #[cfg(unix)]
636 let mut builder = cap_std::fs::DirBuilder::new();
637 #[cfg(unix)]
638 builder.mode(0o700);
639 #[cfg(unix)]
640 let result = parent.create_dir_with(&name, &builder);
641 #[cfg(not(unix))]
642 let result = parent.create_dir(&name);
643 let created = match result {
644 Ok(()) => true,
645 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => false,
646 Err(error) => return Err(storage_error(error)),
647 };
648 #[cfg(unix)]
649 let _child = self
650 .open_managed_child(&parent, &name, created)
651 .map_err(storage_error)?;
652 #[cfg(not(unix))]
653 let _child = parent.open_dir_nofollow(&name).map_err(storage_error)?;
654 Ok(created)
655 }
656
657 async fn rename(&self, from: &str, to: &str) -> Result<(), StoreMoveError> {
658 validate_relative(from)?;
659 validate_relative(to)?;
660 let (from_parent, from_name) = self.parent(from, false).map_err(storage_error)?;
661 let source = from_parent
662 .symlink_metadata(&from_name)
663 .map_err(storage_error)?;
664 if !source.is_file() || is_link(&source) {
665 return Err(storage_error(()).into());
666 }
667 let (to_parent, to_name) = self.parent(to, true).map_err(storage_error)?;
668 if exists_nofollow(&to_parent, &to_name).map_err(storage_error)? {
669 return Err(StoreMoveError::DestinationExists);
670 }
671 match self.hard_link(&from_parent, &from_name, &to_parent, &to_name) {
672 Ok(()) => match self.remove_source(&from_parent, &from_name) {
673 Ok(()) => Ok(()),
674 Err(_) => {
675 let _ = to_parent.remove_file(&to_name);
676 Err(storage_error(()).into())
677 }
678 },
679 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
680 Err(StoreMoveError::DestinationExists)
681 }
682 Err(_) => {
683 if exists_nofollow(&to_parent, &to_name).map_err(storage_error)? {
686 return Err(StoreMoveError::DestinationExists);
687 }
688 #[cfg(test)]
689 if let Some(bytes) = &self.fallback_race_bytes {
690 use std::io::Write;
691 let mut options = OpenOptions::new();
692 options.write(true).create_new(true);
693 to_parent
694 .open_with(&to_name, &options)
695 .and_then(|mut file| file.write_all(bytes))
696 .map_err(storage_error)?;
697 }
698 rename_no_replace(&from_parent, &from_name, &to_parent, &to_name).map_err(|error| {
699 if error.kind() == io::ErrorKind::AlreadyExists {
700 StoreMoveError::DestinationExists
701 } else {
702 storage_error(error).into()
703 }
704 })
705 }
706 }
707 }
708
709 async fn replace(&self, from: &str, to: &str) -> Result<(), AttachmentError> {
710 validate_relative(from)?;
711 validate_relative(to)?;
712 let (from_parent, from_name) = self.parent(from, false).map_err(storage_error)?;
713 let source = from_parent
714 .symlink_metadata(&from_name)
715 .map_err(storage_error)?;
716 if !source.is_file() || is_link(&source) {
717 return Err(storage_error(()));
718 }
719 let (to_parent, to_name) = self.parent(to, true).map_err(storage_error)?;
720 match to_parent.symlink_metadata(&to_name) {
721 Ok(metadata) if !metadata.is_file() || is_link(&metadata) => {
722 return Err(storage_error(()));
723 }
724 Ok(_) => {}
725 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
726 Err(error) => return Err(storage_error(error)),
727 }
728 from_parent
729 .rename(&from_name, &to_parent, &to_name)
730 .map_err(storage_error)
731 }
732
733 async fn remove_dir_all(&self, path: &str) -> Result<(), StoreRemoveError> {
734 validate_relative(path)?;
735 let (parent, name) = self.parent(path, false).map_err(storage_error)?;
736 let open = || -> io::Result<Dir> {
737 #[cfg(unix)]
738 {
739 self.open_managed_child(&parent, &name, false)
740 }
741 #[cfg(not(unix))]
742 {
743 parent.open_dir_nofollow(&name)
744 }
745 };
746 let child = open().map_err(storage_error)?;
747 let mut before = HashSet::new();
750 entry_paths(&child, Path::new(""), &mut before).map_err(storage_error)?;
751 let Err(error) = child.remove_open_dir_all() else {
752 return Ok(());
753 };
754 let mut after = HashSet::new();
755 let removed_any = open()
756 .and_then(|child| entry_paths(&child, Path::new(""), &mut after))
757 .map_or(true, |()| !before.is_subset(&after));
758 Err(StoreRemoveError {
759 error: storage_error(error),
760 removed_any,
761 })
762 }
763
764 async fn prepare_remove_dir(&self, path: &str) -> Result<bool, AttachmentError> {
765 validate_relative(path)?;
766 let (parent, name) = match self.parent(path, false) {
767 Ok(value) => value,
768 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
769 Err(error) => return Err(storage_error(error)),
770 };
771 if !exists_nofollow(&parent, &name).map_err(storage_error)? {
772 return Ok(false);
773 }
774 #[cfg(unix)]
775 self.open_managed_child(&parent, &name, false)
776 .map_err(storage_error)?;
777 #[cfg(not(unix))]
778 parent.open_dir_nofollow(&name).map_err(storage_error)?;
779 Ok(true)
780 }
781
782 async fn remove_empty_dir(&self, path: &str) -> Result<(), AttachmentError> {
783 validate_relative(path)?;
784 let (parent, name) = self.parent(path, false).map_err(storage_error)?;
785 #[cfg(unix)]
786 self.open_managed_child(&parent, &name, false)
787 .map_err(storage_error)?;
788 #[cfg(not(unix))]
789 parent.open_dir_nofollow(&name).map_err(storage_error)?;
790 parent.remove_dir(&name).map_err(storage_error)
791 }
792
793 async fn remove_file(&self, path: &str) -> Result<(), AttachmentError> {
794 validate_relative(path)?;
795 let (parent, name) = self.parent(path, false).map_err(storage_error)?;
796 let metadata = parent.symlink_metadata(&name).map_err(storage_error)?;
797 if !metadata.is_file() || is_link(&metadata) {
798 return Err(storage_error(()));
799 }
800 parent.remove_file(&name).map_err(storage_error)
801 }
802
803 async fn exists(&self, path: &str) -> Result<bool, AttachmentError> {
804 validate_relative(path)?;
805 let (parent, name) = match self.parent(path, false) {
806 Ok(value) => value,
807 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
808 Err(error) => return Err(storage_error(error)),
809 };
810 exists_nofollow(&parent, &name).map_err(storage_error)
811 }
812
813 async fn sync(&self, writer: &mut StoreWriter) -> Result<(), AttachmentError> {
814 writer
815 .file
816 .sync_all()
817 .await
818 .map_err(|_| AttachmentError::new(Cause::LocalStorage))
819 }
820
821 async fn finish_decode(
822 &self,
823 decoder: AttachmentDecoder,
824 source: &str,
825 output: &str,
826 ) -> Result<DecodedMeta, AttachmentError> {
827 validate_temp(source)?;
828 validate_temp(output)?;
829 let (source_parent, source_name) = self.parent(source, false).map_err(storage_error)?;
830 let mut read_options = OpenOptions::new();
831 read_options.read(true).follow(FollowSymlinks::No);
832 let input = source_parent
833 .open_with(&source_name, &read_options)
834 .map_err(storage_error)?;
835 let (output_parent, output_name) = self.parent(output, true).map_err(storage_error)?;
836 let mut write_options = OpenOptions::new();
837 write_options
838 .write(true)
839 .create_new(true)
840 .follow(FollowSymlinks::No);
841 #[cfg(unix)]
842 write_options.mode(0o600);
843 let decoded = output_parent
844 .open_with(&output_name, &write_options)
845 .map_err(storage_error)?;
846 #[cfg(unix)]
847 if let Err(error) = decoded.set_permissions(cap_std::fs::Permissions::from_mode(0o600)) {
848 let _ = output_parent.remove_file(&output_name);
849 return Err(storage_error(error));
850 }
851 xmtp_common::task::spawn_blocking(move || {
852 let mut input = input.into_std();
853 let mut decoded = decoded.into_std();
854 let meta = decoder.finish(&mut input, &mut decoded)?;
855 decoded
856 .sync_all()
857 .map_err(|_| AttachmentError::new(Cause::LocalStorage))?;
858 Ok(meta)
859 })
860 .await
861 .map_err(|_| AttachmentError::new(Cause::LocalStorage))?
862 }
863
864 async fn list_files(&self) -> Result<Vec<StoreFile>, AttachmentError> {
865 use cap_std::time::SystemClock;
866 let root = self.root_dir.clone();
867 #[cfg(any(unix, test))]
868 let store = self.clone();
869 xmtp_common::task::spawn_blocking(move || -> Result<Vec<StoreFile>, AttachmentError> {
870 let mut files = Vec::new();
871 let mut dirs = vec![(root.try_clone().map_err(storage_error)?, String::new())];
872 while let Some((dir, prefix)) = dirs.pop() {
873 for entry in dir.entries().map_err(storage_error)? {
874 let entry = entry.map_err(storage_error)?;
875 let Ok(name) = entry.file_name().into_string() else {
876 continue;
877 };
878 let descend = prefix.is_empty() && is_reconcile_dir(&name);
879 let path = if prefix.is_empty() {
880 name.clone()
881 } else {
882 format!("{prefix}/{name}")
883 };
884 let metadata = dir.symlink_metadata(&name).map_err(storage_error)?;
885 if metadata.is_dir() && !is_link(&metadata) {
886 if descend {
887 #[cfg(unix)]
888 let child = store
889 .open_managed_child(&dir, &name, false)
890 .map_err(storage_error)?;
891 #[cfg(not(unix))]
892 let child = dir.open_dir_nofollow(&name).map_err(storage_error)?;
893 dirs.push((child, path));
894 }
895 } else if metadata.is_file() && !is_link(&metadata) && !prefix.is_empty() {
896 #[cfg(test)]
897 let modified = if store.forced_modified_error {
898 Err(io::Error::other("forced modification time error"))
899 } else {
900 metadata.modified()
901 };
902 #[cfg(not(test))]
903 let modified = metadata.modified();
904 let modified_at_ns = match modified
905 .map_err(storage_error)?
906 .duration_since(SystemClock::UNIX_EPOCH)
907 {
908 Ok(age) => age.as_nanos().min(i64::MAX as u128) as i64,
909 Err(error) => {
910 -(error.duration().as_nanos().min(i64::MAX as u128) as i64)
911 }
912 };
913 files.push(StoreFile {
914 path,
915 modified_at_ns,
916 });
917 }
918 }
919 }
920 Ok(files)
921 })
922 .await
923 .map_err(storage_error)?
924 }
925}