Skip to main content

qubit_fs/temp/
managed_temp_file.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Managed temporary file implementation.
11
12use std::sync::Arc;
13
14use log::warn;
15
16use crate::{
17    AtomicityRequirement,
18    CopyConflictPolicy,
19    CopyOptions,
20    DeleteOptions,
21    FileSystem,
22    FsErrorKind,
23    FsPath,
24    FsResult,
25    PersistOptions,
26    RenameOptions,
27    TempFile,
28    TempResource,
29    WriteOutcome,
30};
31
32/// Default temporary file backed by an [`Arc`] filesystem and path.
33#[derive(Debug)]
34pub struct ManagedTempFile {
35    /// Filesystem that owns the temporary path.
36    fs: Arc<dyn FileSystem>,
37    /// Temporary path.
38    path: FsPath,
39    /// Whether drop should attempt best-effort cleanup.
40    cleanup_on_drop: bool,
41}
42
43impl ManagedTempFile {
44    /// Creates a managed temporary file handle.
45    ///
46    /// # Parameters
47    /// - `fs`: Filesystem that owns the temporary path.
48    /// - `path`: Temporary path.
49    ///
50    /// # Returns
51    /// Managed temporary file handle.
52    #[inline]
53    #[must_use]
54    pub fn new(fs: Arc<dyn FileSystem>, path: FsPath) -> Self {
55        Self {
56            fs,
57            path,
58            cleanup_on_drop: true,
59        }
60    }
61
62    /// Disables drop cleanup and returns the temporary path.
63    ///
64    /// # Returns
65    /// Retained temporary path.
66    #[inline]
67    fn detach(&mut self) -> FsPath {
68        self.cleanup_on_drop = false;
69        self.path.clone()
70    }
71}
72
73impl TempResource for ManagedTempFile {
74    #[inline]
75    fn fs(&self) -> Arc<dyn FileSystem> {
76        self.fs.clone()
77    }
78
79    #[inline]
80    fn path(&self) -> &FsPath {
81        &self.path
82    }
83
84    fn cleanup(mut self: Box<Self>) -> FsResult<()> {
85        let result = self.fs.delete(
86            &self.path,
87            &DeleteOptions {
88                missing_ok: true,
89                ..DeleteOptions::default()
90            },
91        );
92        if result.is_ok() {
93            self.cleanup_on_drop = false;
94        }
95        result
96    }
97
98    fn keep(mut self: Box<Self>) -> FsResult<FsPath> {
99        Ok(self.detach())
100    }
101}
102
103impl TempFile for ManagedTempFile {
104    fn persist(mut self: Box<Self>, target: &FsPath, options: &PersistOptions) -> FsResult<WriteOutcome> {
105        let rename_options = RenameOptions {
106            overwrite: options.overwrite,
107            atomic: options.atomic,
108        };
109        match self.fs.rename(&self.path, target, &rename_options) {
110            Ok(()) => {
111                self.cleanup_on_drop = false;
112                Ok(WriteOutcome::default())
113            }
114            Err(error) if error.kind() == FsErrorKind::UnsupportedOperation && options.allow_copy_delete => {
115                let mut copy_options = CopyOptions::file();
116                copy_options.conflict = if options.overwrite {
117                    CopyConflictPolicy::Overwrite
118                } else {
119                    CopyConflictPolicy::Fail
120                };
121                copy_options.preserve_metadata = options.preserve_metadata;
122                if matches!(options.atomic, AtomicityRequirement::Required) {
123                    copy_options.server_side = crate::ServerSidePreference::Require;
124                }
125                let outcome = self.fs.copy(&self.path, target, &copy_options)?;
126                self.fs.delete(
127                    &self.path,
128                    &DeleteOptions {
129                        missing_ok: true,
130                        ..DeleteOptions::default()
131                    },
132                )?;
133                self.cleanup_on_drop = false;
134                Ok(WriteOutcome {
135                    bytes_written: Some(outcome.stats.bytes),
136                    etag: None,
137                    diagnostics: outcome.diagnostics,
138                })
139            }
140            Err(error) => Err(error),
141        }
142    }
143}
144
145impl Drop for ManagedTempFile {
146    fn drop(&mut self) {
147        if self.cleanup_on_drop {
148            let result = self.fs.delete(
149                &self.path,
150                &DeleteOptions {
151                    missing_ok: true,
152                    ..DeleteOptions::default()
153                },
154            );
155            if let Err(error) = result {
156                warn!("failed to cleanup temporary file '{}': {error}", self.path);
157            }
158        }
159    }
160}