foyer_storage/io/device/
fs.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 filesystem-based device that manages files in a directory.
33#[derive(Debug)]
34pub struct FsDeviceBuilder {
35    dir: PathBuf,
36    capacity: Option<usize>,
37    throttle: Throttle,
38    #[cfg(target_os = "linux")]
39    direct: bool,
40}
41
42impl FsDeviceBuilder {
43    /// Use the given file path as the file device path.
44    pub fn new(dir: impl AsRef<Path>) -> Self {
45        Self {
46            dir: dir.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 FsDeviceBuilder {
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            create_dir_all(&self.dir).unwrap();
87            free_space(&self.dir).unwrap() as usize / 10 * 8
88        });
89        let capacity = align_v(capacity, PAGE);
90
91        let statistics = Arc::new(Statistics::new(self.throttle));
92
93        // Build device.
94
95        if !self.dir.exists() {
96            create_dir_all(&self.dir).map_err(Error::io_error)?;
97        }
98
99        let device = FsDevice {
100            capacity,
101            statistics,
102            dir: self.dir,
103            #[cfg(target_os = "linux")]
104            direct: self.direct,
105            partitions: RwLock::new(vec![]),
106        };
107        let device: Arc<dyn Device> = Arc::new(device);
108        Ok(device)
109    }
110}
111
112/// A device upon a directory in a filesystem.
113#[derive(Debug)]
114pub struct FsDevice {
115    capacity: usize,
116    statistics: Arc<Statistics>,
117    dir: PathBuf,
118    #[cfg(target_os = "linux")]
119    direct: bool,
120    partitions: RwLock<Vec<Arc<FsPartition>>>,
121}
122
123impl FsDevice {
124    const PREFIX: &str = "foyer-storage-direct-fs-";
125    fn filename(partition: PartitionId) -> String {
126        format!("{prefix}{partition:08}", prefix = Self::PREFIX,)
127    }
128}
129
130impl Device for FsDevice {
131    fn capacity(&self) -> usize {
132        self.capacity
133    }
134
135    fn allocated(&self) -> usize {
136        self.partitions.read().unwrap().iter().map(|p| p.size).sum()
137    }
138
139    fn create_partition(&self, size: usize) -> Result<Arc<dyn Partition>> {
140        let mut partitions = self.partitions.write().unwrap();
141        let allocated = partitions.iter().map(|p| p.size).sum::<usize>();
142        if allocated + size > self.capacity {
143            return Err(Error::no_space(self.capacity, allocated, allocated + size));
144        }
145        let id = partitions.len() as PartitionId;
146        let path = self.dir.join(Self::filename(id));
147        let mut opts = OpenOptions::new();
148        opts.create(true).write(true).read(true);
149        #[cfg(target_os = "linux")]
150        if self.direct {
151            use std::os::unix::fs::OpenOptionsExt;
152            opts.custom_flags(libc::O_DIRECT | libc::O_NOATIME);
153        }
154        let file = opts.open(path).map_err(Error::io_error)?;
155        file.set_len(size as _).map_err(Error::io_error)?;
156
157        let partition = Arc::new(FsPartition {
158            id,
159            size,
160            file,
161            statistics: self.statistics.clone(),
162        });
163        partitions.push(partition.clone());
164        Ok(partition)
165    }
166
167    fn partitions(&self) -> usize {
168        self.partitions.read().unwrap().len()
169    }
170
171    fn partition(&self, id: PartitionId) -> Arc<dyn Partition> {
172        self.partitions.read().unwrap()[id as usize].clone()
173    }
174
175    fn statistics(&self) -> &Arc<Statistics> {
176        &self.statistics
177    }
178}
179
180#[derive(Debug)]
181pub struct FsPartition {
182    id: PartitionId,
183    size: usize,
184    statistics: Arc<Statistics>,
185    file: File,
186}
187
188impl Partition for FsPartition {
189    fn id(&self) -> PartitionId {
190        self.id
191    }
192
193    fn size(&self) -> usize {
194        self.size
195    }
196
197    fn translate(&self, address: u64) -> (RawFile, u64) {
198        #[cfg(any(target_family = "unix", target_family = "wasm"))]
199        let raw = {
200            use std::os::fd::AsRawFd;
201            RawFile(self.file.as_raw_fd())
202        };
203
204        #[cfg(target_family = "windows")]
205        let raw = {
206            use std::os::windows::io::AsRawHandle;
207            RawFile(self.file.as_raw_handle())
208        };
209
210        (raw, address)
211    }
212
213    fn statistics(&self) -> &Arc<Statistics> {
214        &self.statistics
215    }
216}