Skip to main content

qubit_fs/copy/
copy_options.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Copy operation options and policy types.
9
10use std::time::Duration;
11
12use crate::copy::CopyConflictPolicy;
13use crate::copy::CopyMode;
14use crate::copy::MetadataPreservePolicy;
15use crate::copy::ServerSidePreference;
16use crate::error::FsError;
17use crate::error::FsErrorKind;
18use crate::error::FsOperation;
19use crate::metadata::AtomicityRequirement;
20use crate::metadata::DurabilityRequirement;
21use crate::metadata::FileSystemCapabilities;
22use crate::metadata::FileSystemCapability;
23use crate::metadata::SymlinkPolicy;
24
25/// Options controlling file, object, or tree copy operations.
26///
27/// # Examples
28/// ```rust
29/// use qubit_fs::copy::CopyOptions;
30/// use qubit_fs::copy::ServerSidePreference;
31/// use qubit_fs::metadata::FileSystemCapabilities;
32/// let options = CopyOptions::default().with_server_side(ServerSidePreference::Require);
33/// assert!(options.validate_against(FileSystemCapabilities::new()).is_err());
34/// ```
35#[non_exhaustive]
36#[derive(Clone, Debug, PartialEq)]
37pub struct CopyOptions {
38    /// Copy source interpretation mode.
39    mode: CopyMode,
40    /// Destination conflict policy.
41    conflict: CopyConflictPolicy,
42    /// Metadata preservation policy.
43    preserve_metadata: MetadataPreservePolicy,
44    /// Server-side copy preference.
45    server_side: ServerSidePreference,
46    /// Optional symbolic-link policy overriding the filesystem default.
47    symlink_policy: Option<SymlinkPolicy>,
48    /// Whether missing destination parents should be created.
49    create_parent: bool,
50    /// Whether tree copy should continue after per-entry failures.
51    continue_on_error: bool,
52    /// Required atomicity of destination publication.
53    atomicity: AtomicityRequirement,
54    /// Required durability of destination publication.
55    durability: DurabilityRequirement,
56    /// Maximum descendant depth for tree copy.
57    max_depth: Option<usize>,
58    /// Maximum number of source entries copied by the operation.
59    ///
60    /// A file-mode copy always consumes exactly one entry, so `Some(0)`
61    /// rejects it before provider I/O. Tree-mode copies count files,
62    /// directories, symbolic links, objects, and prefixes in the aggregate.
63    max_entries: Option<usize>,
64    /// Maximum number of copied payload bytes.
65    max_bytes: Option<u64>,
66    /// Maximum cumulative elapsed duration for the copy operation.
67    ///
68    /// The budget starts when the copy operation is constructed and is
69    /// checked cooperatively around provider calls. It does not forcibly
70    /// interrupt a pending call or roll back a publication that already
71    /// completed.
72    deadline: Option<Duration>,
73}
74
75impl CopyOptions {
76    /// Returns a copy of these options with the source mode replaced.
77    #[inline]
78    #[must_use]
79    pub const fn with_mode(mut self, mode: CopyMode) -> Self {
80        self.mode = mode;
81        self
82    }
83
84    /// Returns the source interpretation mode.
85    #[inline]
86    #[must_use]
87    pub const fn mode(&self) -> CopyMode {
88        self.mode
89    }
90
91    /// Returns a copy with the destination conflict policy replaced.
92    #[inline]
93    #[must_use]
94    pub const fn with_conflict(mut self, conflict: CopyConflictPolicy) -> Self {
95        self.conflict = conflict;
96        self
97    }
98
99    /// Returns the destination conflict policy.
100    #[inline]
101    #[must_use]
102    pub const fn conflict(&self) -> CopyConflictPolicy {
103        self.conflict
104    }
105
106    /// Returns a copy with the metadata preservation policy replaced.
107    #[inline]
108    #[must_use]
109    pub const fn with_preserve_metadata(mut self, preserve_metadata: MetadataPreservePolicy) -> Self {
110        self.preserve_metadata = preserve_metadata;
111        self
112    }
113
114    /// Returns the metadata preservation policy.
115    #[inline]
116    #[must_use]
117    pub const fn preserve_metadata(&self) -> MetadataPreservePolicy {
118        self.preserve_metadata
119    }
120
121    /// Returns a copy with the server-side preference replaced.
122    #[inline]
123    #[must_use]
124    pub const fn with_server_side(mut self, server_side: ServerSidePreference) -> Self {
125        self.server_side = server_side;
126        self
127    }
128
129    /// Returns the server-side preference.
130    #[inline]
131    #[must_use]
132    pub const fn server_side(&self) -> ServerSidePreference {
133        self.server_side
134    }
135
136    /// Returns a copy with the symbolic-link policy override replaced.
137    #[inline]
138    #[must_use]
139    pub const fn with_symlink_policy(mut self, policy: SymlinkPolicy) -> Self {
140        self.symlink_policy = Some(policy);
141        self
142    }
143
144    /// Returns the optional symbolic-link policy override.
145    #[inline]
146    #[must_use]
147    pub const fn symlink_policy_override(&self) -> Option<SymlinkPolicy> {
148        self.symlink_policy
149    }
150
151    /// Returns a copy with parent creation replaced.
152    #[inline]
153    #[must_use]
154    pub const fn with_create_parent(mut self, create: bool) -> Self {
155        self.create_parent = create;
156        self
157    }
158
159    /// Returns whether missing destination parents are created.
160    #[inline]
161    #[must_use]
162    pub const fn create_parent(&self) -> bool {
163        self.create_parent
164    }
165
166    /// Returns a copy with continuation policy replaced.
167    #[inline]
168    #[must_use]
169    pub const fn with_continue_on_error(mut self, continue_on_error: bool) -> Self {
170        self.continue_on_error = continue_on_error;
171        self
172    }
173
174    /// Returns whether tree copy continues after per-entry failures.
175    #[inline]
176    #[must_use]
177    pub const fn continue_on_error(&self) -> bool {
178        self.continue_on_error
179    }
180
181    /// Returns a copy with the atomicity requirement replaced.
182    #[inline]
183    #[must_use]
184    pub const fn with_atomicity(mut self, atomicity: AtomicityRequirement) -> Self {
185        self.atomicity = atomicity;
186        self
187    }
188
189    /// Returns the atomicity requirement.
190    #[inline]
191    #[must_use]
192    pub const fn atomicity(&self) -> AtomicityRequirement {
193        self.atomicity
194    }
195
196    /// Returns a copy with the durability requirement replaced.
197    #[inline]
198    #[must_use]
199    pub const fn with_durability(mut self, durability: DurabilityRequirement) -> Self {
200        self.durability = durability;
201        self
202    }
203
204    /// Returns the durability requirement.
205    #[inline]
206    #[must_use]
207    pub const fn durability(&self) -> DurabilityRequirement {
208        self.durability
209    }
210
211    /// Returns a copy with the maximum tree depth replaced.
212    #[inline]
213    #[must_use]
214    pub const fn with_max_depth(mut self, max_depth: Option<usize>) -> Self {
215        self.max_depth = max_depth;
216        self
217    }
218
219    /// Returns the optional maximum tree depth.
220    #[inline]
221    #[must_use]
222    pub const fn max_depth(&self) -> Option<usize> {
223        self.max_depth
224    }
225
226    /// Returns a copy with the maximum source entry count replaced.
227    ///
228    /// `Some(0)` rejects every copy because even a file-mode copy consumes one
229    /// source entry. For tree copies, the limit applies to the aggregate of
230    /// files, directories, symbolic links, objects, and prefixes.
231    #[inline]
232    #[must_use]
233    pub const fn with_max_entries(mut self, max_entries: Option<usize>) -> Self {
234        self.max_entries = max_entries;
235        self
236    }
237
238    /// Returns the optional maximum source entry count.
239    ///
240    /// `None` leaves the entry count unbounded. The count includes every
241    /// source resource represented in the completed
242    /// [`CopyOutcome`](crate::copy::CopyOutcome) statistics, including
243    /// directories and prefixes.
244    #[inline]
245    #[must_use]
246    pub const fn max_entries(&self) -> Option<usize> {
247        self.max_entries
248    }
249
250    /// Returns a copy with the maximum copied byte count replaced.
251    #[inline]
252    #[must_use]
253    pub const fn with_max_bytes(mut self, max_bytes: Option<u64>) -> Self {
254        self.max_bytes = max_bytes;
255        self
256    }
257
258    /// Returns the optional maximum copied byte count.
259    #[inline]
260    #[must_use]
261    pub const fn max_bytes(&self) -> Option<u64> {
262        self.max_bytes
263    }
264
265    /// Returns a copy with the maximum elapsed duration replaced.
266    #[inline]
267    #[must_use]
268    pub const fn with_deadline(mut self, deadline: Option<Duration>) -> Self {
269        self.deadline = deadline;
270        self
271    }
272
273    /// Returns the optional maximum elapsed duration.
274    #[inline]
275    #[must_use]
276    pub const fn deadline(&self) -> Option<Duration> {
277        self.deadline
278    }
279
280    /// Creates options for copying one file-like resource.
281    ///
282    /// # Returns
283    /// Copy options with `mode` set to [`CopyMode::File`].
284    #[inline]
285    #[must_use]
286    pub fn file() -> Self {
287        Self {
288            mode: CopyMode::File,
289            ..Self::default()
290        }
291    }
292
293    /// Creates options for copying a resource tree.
294    ///
295    /// # Returns
296    /// Copy options with `mode` set to [`CopyMode::Tree`].
297    #[inline]
298    #[must_use]
299    pub fn tree() -> Self {
300        Self {
301            mode: CopyMode::Tree,
302            ..Self::default()
303        }
304    }
305
306    /// Validates required copy semantics before provider side effects.
307    ///
308    /// Preferred server-side copy may fall back and report its actual method.
309    /// Required server-side copy must fail this preflight when the configured
310    /// filesystem does not guarantee it.
311    ///
312    /// # Errors
313    ///
314    /// Returns [`FsErrorKind::RequirementNotMet`] with
315    /// [`FileSystemCapability::ServerSideCopy`] when required server-side copy
316    /// is unavailable.
317    pub fn validate_against(&self, capabilities: FileSystemCapabilities) -> Result<(), FsError> {
318        if self.server_side == ServerSidePreference::Require
319            && !capabilities.supports(FileSystemCapability::ServerSideCopy)
320        {
321            return Err(FsError::new(
322                FsErrorKind::RequirementNotMet,
323                FsOperation::Copy,
324                "server-side copy is required but not supported",
325            )
326            .with_required_capability(FileSystemCapability::ServerSideCopy));
327        }
328        let atomic_capability = match self.mode {
329            CopyMode::Auto => FileSystemCapability::AtomicFileCopy,
330            CopyMode::File => FileSystemCapability::AtomicFileCopy,
331            CopyMode::Tree => FileSystemCapability::AtomicTreeCopy,
332        };
333        let atomic_supported = match self.mode {
334            CopyMode::Auto => {
335                capabilities.supports(FileSystemCapability::AtomicFileCopy)
336                    || capabilities.supports(FileSystemCapability::AtomicTreeCopy)
337            }
338            CopyMode::File | CopyMode::Tree => capabilities.supports(atomic_capability),
339        };
340        if self.atomicity == AtomicityRequirement::Required && !atomic_supported {
341            return Err(FsError::new(
342                FsErrorKind::RequirementNotMet,
343                FsOperation::Copy,
344                "atomic copy publication is required but not supported",
345            )
346            .with_required_capability(atomic_capability));
347        }
348        let durable_capability = match self.mode {
349            CopyMode::Auto => FileSystemCapability::DurableFileCopy,
350            CopyMode::File => FileSystemCapability::DurableFileCopy,
351            CopyMode::Tree => FileSystemCapability::DurableTreeCopy,
352        };
353        let durable_supported = match self.mode {
354            CopyMode::Auto => {
355                capabilities.supports(FileSystemCapability::DurableFileCopy)
356                    || capabilities.supports(FileSystemCapability::DurableTreeCopy)
357            }
358            CopyMode::File | CopyMode::Tree => capabilities.supports(durable_capability),
359        };
360        if self.durability == DurabilityRequirement::Required && !durable_supported {
361            return Err(FsError::new(
362                FsErrorKind::RequirementNotMet,
363                FsOperation::Copy,
364                "durable copy publication is required but not supported",
365            )
366            .with_required_capability(durable_capability));
367        }
368        Ok(())
369    }
370}
371
372impl Default for CopyOptions {
373    #[inline]
374    fn default() -> Self {
375        Self {
376            mode: CopyMode::Auto,
377            conflict: CopyConflictPolicy::Fail,
378            preserve_metadata: MetadataPreservePolicy::None,
379            server_side: ServerSidePreference::Disable,
380            symlink_policy: None,
381            create_parent: false,
382            continue_on_error: false,
383            atomicity: AtomicityRequirement::NotRequired,
384            durability: DurabilityRequirement::NotRequired,
385            max_depth: None,
386            max_entries: None,
387            max_bytes: None,
388            deadline: None,
389        }
390    }
391}