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::{create_dir_all, File, OpenOptions},
17    path::{Path, PathBuf},
18    sync::{Arc, RwLock},
19};
20
21use foyer_common::error::{Error, Result};
22use fs4::free_space;
23
24use crate::{
25    io::{
26        device::{statistics::Statistics, throttle::Throttle, Device, DeviceBuilder, Partition, PartitionId},
27        PAGE,
28    },
29    RawFile,
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({
85            // Create an empty directory before to get free space.
86            let dir = self.path.parent().expect("path must point to a file").to_path_buf();
87            create_dir_all(&dir).unwrap();
88            free_space(&dir).unwrap() as usize / 10 * 8
89        });
90        let capacity = align_v(capacity, PAGE);
91
92        // Build device.
93
94        let mut opts = OpenOptions::new();
95        opts.create(true).write(true).read(true);
96        #[cfg(target_os = "linux")]
97        if self.direct {
98            use std::os::unix::fs::OpenOptionsExt;
99            opts.custom_flags(libc::O_DIRECT | libc::O_NOATIME);
100        }
101
102        let file = opts.open(&self.path).map_err(Error::io_error)?;
103
104        if file.metadata().unwrap().is_file() {
105            tracing::warn!(
106                "{} {} {}",
107                "It seems a `DirectFileDevice` is used within a normal file system, which is inefficient.",
108                "Please use `DirectFileDevice` directly on a raw block device.",
109                "Or use `DirectFsDevice` within a normal file system.",
110            );
111            file.set_len(capacity as _).map_err(Error::io_error)?;
112        }
113        let file = Arc::new(file);
114
115        let statistics = Arc::new(Statistics::new(self.throttle));
116
117        let device = FileDevice {
118            file,
119            capacity,
120            statistics,
121            partitions: RwLock::new(vec![]),
122        };
123        let device: Arc<dyn Device> = Arc::new(device);
124        Ok(device)
125    }
126}
127
128/// A device upon a single file or a raw block device.
129#[derive(Debug)]
130pub struct FileDevice {
131    file: Arc<File>,
132    capacity: usize,
133    partitions: RwLock<Vec<Arc<FilePartition>>>,
134    statistics: Arc<Statistics>,
135}
136
137impl Device for FileDevice {
138    fn capacity(&self) -> usize {
139        self.capacity
140    }
141
142    fn allocated(&self) -> usize {
143        self.partitions.read().unwrap().iter().map(|p| p.size).sum()
144    }
145
146    fn create_partition(&self, size: usize) -> Result<Arc<dyn Partition>> {
147        let mut partitions = self.partitions.write().unwrap();
148        let allocated = partitions.iter().map(|p| p.size).sum::<usize>();
149        if allocated + size > self.capacity {
150            return Err(Error::no_space(self.capacity, allocated, allocated + size));
151        }
152        let offset = partitions.last().map(|p| p.offset + p.size as u64).unwrap_or_default();
153        let id = partitions.len() as PartitionId;
154        let partition = Arc::new(FilePartition {
155            file: self.file.clone(),
156            id,
157            size,
158            offset,
159            statistics: self.statistics.clone(),
160        });
161        partitions.push(partition.clone());
162        Ok(partition)
163    }
164
165    fn partitions(&self) -> usize {
166        self.partitions.read().unwrap().len()
167    }
168
169    fn partition(&self, id: PartitionId) -> Arc<dyn Partition> {
170        self.partitions.read().unwrap()[id as usize].clone()
171    }
172
173    fn statistics(&self) -> &Arc<Statistics> {
174        &self.statistics
175    }
176}
177
178#[derive(Debug)]
179pub struct FilePartition {
180    file: Arc<File>,
181    id: PartitionId,
182    size: usize,
183    offset: u64,
184    statistics: Arc<Statistics>,
185}
186
187impl Partition for FilePartition {
188    fn id(&self) -> PartitionId {
189        self.id
190    }
191
192    fn size(&self) -> usize {
193        self.size
194    }
195
196    fn translate(&self, address: u64) -> (RawFile, u64) {
197        #[cfg(any(target_family = "unix", target_family = "wasm"))]
198        let raw = {
199            use std::os::fd::AsRawFd;
200            RawFile(self.file.as_raw_fd())
201        };
202
203        #[cfg(target_family = "windows")]
204        let raw = {
205            use std::os::windows::io::AsRawHandle;
206            RawFile(self.file.as_raw_handle())
207        };
208
209        let address = self.offset + address;
210        (raw, address)
211    }
212
213    fn statistics(&self) -> &Arc<Statistics> {
214        &self.statistics
215    }
216}