Skip to main content

foyer_storage/io/device/
file.rs

1// Copyright 2026 foyer Project Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    fs::{File, OpenOptions, create_dir_all},
17    path::{Path, PathBuf},
18    sync::{Arc, RwLock},
19};
20
21use foyer_common::error::{Error, Result};
22use fs4::free_space;
23
24use crate::{
25    RawFile,
26    io::{
27        PAGE,
28        device::{Device, DeviceBuilder, Partition, PartitionId, statistics::Statistics, throttle::Throttle},
29    },
30};
31
32/// Builder for a file-based device that manages a single file or a raw block device.
33#[derive(Debug)]
34pub struct FileDeviceBuilder {
35    path: PathBuf,
36    capacity: Option<usize>,
37    throttle: Throttle,
38    #[cfg(target_os = "linux")]
39    direct: bool,
40}
41
42impl FileDeviceBuilder {
43    /// Use the given file path as the file device path.
44    pub fn new(path: impl AsRef<Path>) -> Self {
45        Self {
46            path: path.as_ref().into(),
47            capacity: None,
48            throttle: Throttle::default(),
49            #[cfg(target_os = "linux")]
50            direct: false,
51        }
52    }
53
54    /// Set the capacity of the file device.
55    ///
56    /// The given capacity may be modified on build for alignment.
57    ///
58    /// The file device uses 80% of the current free disk space by default.
59    pub fn with_capacity(mut self, capacity: usize) -> Self {
60        self.capacity = Some(capacity);
61        self
62    }
63
64    /// Set the throttle of the file device.
65    pub fn with_throttle(mut self, throttle: Throttle) -> Self {
66        self.throttle = throttle;
67        self
68    }
69
70    /// Set whether the file device should use direct I/O.
71    #[cfg(target_os = "linux")]
72    pub fn with_direct(mut self, direct: bool) -> Self {
73        self.direct = direct;
74        self
75    }
76}
77
78impl DeviceBuilder for FileDeviceBuilder {
79    fn build(self) -> Result<Arc<dyn Device>> {
80        // Normalize configurations.
81
82        let align_v = |value: usize, align: usize| value - (value % align);
83
84        let capacity = self.capacity.unwrap_or_else(|| {
85            // Try to get the capacity if `path` refer to a raw block device.
86            #[cfg(unix)]
87            if let Ok(metadata) = std::fs::metadata(&self.path) {
88                let file_type = metadata.file_type();
89
90                use std::os::unix::fs::FileTypeExt;
91                if file_type.is_block_device() {
92                    return super::utils::get_dev_capacity(&self.path).unwrap();
93                }
94            }
95
96            // Create an empty directory if needed before to get free space.
97            let dir = self.path.parent().expect("path must point to a file").to_path_buf();
98            create_dir_all(&dir).unwrap();
99            free_space(&dir).unwrap() as usize / 10 * 8
100        });
101        let capacity = align_v(capacity, PAGE);
102
103        println!("==========> {capacity}");
104
105        // Build device.
106
107        let mut opts = OpenOptions::new();
108        opts.create(true).write(true).read(true);
109        #[cfg(target_os = "linux")]
110        if self.direct {
111            use std::os::unix::fs::OpenOptionsExt;
112            opts.custom_flags(libc::O_DIRECT | libc::O_NOATIME);
113        }
114
115        let file = opts.open(&self.path).map_err(Error::io_error)?;
116
117        if file.metadata().unwrap().is_file() {
118            tracing::warn!(
119                "{} {} {}",
120                "It seems a `DirectFileDevice` is used within a normal file system, which is inefficient.",
121                "Please use `DirectFileDevice` directly on a raw block device.",
122                "Or use `DirectFsDevice` within a normal file system.",
123            );
124            file.set_len(capacity as _).map_err(Error::io_error)?;
125        }
126        let file = Arc::new(file);
127
128        let statistics = Arc::new(Statistics::new(self.throttle));
129
130        let device = FileDevice {
131            file,
132            capacity,
133            statistics,
134            partitions: RwLock::new(vec![]),
135        };
136        let device: Arc<dyn Device> = Arc::new(device);
137        Ok(device)
138    }
139}
140
141/// A device upon a single file or a raw block device.
142#[derive(Debug)]
143pub struct FileDevice {
144    file: Arc<File>,
145    capacity: usize,
146    partitions: RwLock<Vec<Arc<FilePartition>>>,
147    statistics: Arc<Statistics>,
148}
149
150impl Device for FileDevice {
151    fn capacity(&self) -> usize {
152        self.capacity
153    }
154
155    fn allocated(&self) -> usize {
156        self.partitions.read().unwrap().iter().map(|p| p.size).sum()
157    }
158
159    fn create_partition(&self, size: usize) -> Result<Arc<dyn Partition>> {
160        let mut partitions = self.partitions.write().unwrap();
161        let allocated = partitions.iter().map(|p| p.size).sum::<usize>();
162        if allocated + size > self.capacity {
163            return Err(Error::no_space(self.capacity, allocated, allocated + size));
164        }
165        let offset = partitions.last().map(|p| p.offset + p.size as u64).unwrap_or_default();
166        let id = partitions.len() as PartitionId;
167        let partition = Arc::new(FilePartition {
168            file: self.file.clone(),
169            id,
170            size,
171            offset,
172            statistics: self.statistics.clone(),
173        });
174        partitions.push(partition.clone());
175        Ok(partition)
176    }
177
178    fn partitions(&self) -> usize {
179        self.partitions.read().unwrap().len()
180    }
181
182    fn partition(&self, id: PartitionId) -> Arc<dyn Partition> {
183        self.partitions.read().unwrap()[id as usize].clone()
184    }
185
186    fn statistics(&self) -> &Arc<Statistics> {
187        &self.statistics
188    }
189}
190
191#[derive(Debug)]
192pub struct FilePartition {
193    file: Arc<File>,
194    id: PartitionId,
195    size: usize,
196    offset: u64,
197    statistics: Arc<Statistics>,
198}
199
200impl Partition for FilePartition {
201    fn id(&self) -> PartitionId {
202        self.id
203    }
204
205    fn size(&self) -> usize {
206        self.size
207    }
208
209    fn translate(&self, address: u64) -> (RawFile, u64) {
210        #[cfg(any(target_family = "unix", target_family = "wasm"))]
211        let raw = {
212            use std::os::fd::AsRawFd;
213            RawFile(self.file.as_raw_fd())
214        };
215
216        #[cfg(target_family = "windows")]
217        let raw = {
218            use std::os::windows::io::AsRawHandle;
219            RawFile(self.file.as_raw_handle())
220        };
221
222        let address = self.offset + address;
223        (raw, address)
224    }
225
226    fn statistics(&self) -> &Arc<Statistics> {
227        &self.statistics
228    }
229}