1use std::fs::{File, OpenOptions};
12use std::io;
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct FileIdentity {
19 pub dev: u64,
20 pub ino: u64,
21}
22
23pub struct DestFile {
24 file: File,
25 path: PathBuf,
26}
27
28impl DestFile {
29 pub fn open(path: &Path) -> io::Result<Self> {
33 if let Some(parent) = path.parent() {
34 if !parent.as_os_str().is_empty() {
35 std::fs::create_dir_all(parent)?;
36 }
37 }
38 let file = OpenOptions::new()
39 .read(true)
40 .write(true)
41 .create(true)
42 .truncate(false)
43 .open(path)?;
44 Ok(Self {
45 file,
46 path: path.to_path_buf(),
47 })
48 }
49
50 pub fn preallocate(&self, size: u64) -> io::Result<()> {
59 if self.file.metadata()?.len() != size {
60 self.file.set_len(size)?;
61 }
62 Ok(())
63 }
64
65 pub fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<()> {
68 #[cfg(unix)]
69 {
70 use std::os::unix::fs::FileExt;
71 self.file.write_all_at(buf, offset)
72 }
73 #[cfg(windows)]
74 {
75 use std::os::windows::fs::FileExt;
76 let mut written = 0;
77 while written < buf.len() {
78 let n = self
79 .file
80 .seek_write(&buf[written..], offset + written as u64)?;
81 if n == 0 {
82 return Err(io::Error::new(
83 io::ErrorKind::WriteZero,
84 "failed to write whole buffer",
85 ));
86 }
87 written += n;
88 }
89 Ok(())
90 }
91 }
92
93 pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
94 #[cfg(unix)]
95 {
96 use std::os::unix::fs::FileExt;
97 self.file.read_at(buf, offset)
98 }
99 #[cfg(windows)]
100 {
101 use std::os::windows::fs::FileExt;
102 self.file.seek_read(buf, offset)
103 }
104 }
105
106 pub fn sync_data(&self) -> io::Result<()> {
112 self.file.sync_data()
113 }
114
115 pub fn size(&self) -> io::Result<u64> {
118 Ok(self.file.metadata()?.len())
119 }
120
121 pub fn truncate(&self, size: u64) -> io::Result<()> {
122 self.file.set_len(size)
123 }
124
125 pub fn identity(&self) -> io::Result<FileIdentity> {
126 identity_of(&self.file)
127 }
128
129 pub fn path(&self) -> &Path {
130 &self.path
131 }
132
133 pub fn open_for_read(&self) -> io::Result<File> {
136 File::open(&self.path)
137 }
138}
139
140fn identity_of(file: &File) -> io::Result<FileIdentity> {
141 #[cfg(unix)]
142 {
143 use std::os::unix::fs::MetadataExt;
144 let m = file.metadata()?;
145 Ok(FileIdentity {
146 dev: m.dev(),
147 ino: m.ino(),
148 })
149 }
150 #[cfg(windows)]
151 {
152 use std::os::windows::fs::MetadataExt;
155 let m = file.metadata()?;
156 Ok(FileIdentity {
157 dev: m.volume_serial_number().unwrap_or(0) as u64,
158 ino: m.file_index().unwrap_or(0),
159 })
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 fn tmpdir(name: &str) -> PathBuf {
168 let dir =
169 std::env::temp_dir().join(format!("rget-file-test-{name}-{}", std::process::id()));
170 std::fs::create_dir_all(&dir).unwrap();
171 dir
172 }
173
174 #[test]
175 fn writes_at_offsets_out_of_order() {
176 let dir = tmpdir("offsets");
177 let path = dir.join("out.bin");
178 let f = DestFile::open(&path).unwrap();
179 f.preallocate(10).unwrap();
180 f.write_at(b"world", 5).unwrap();
181 f.write_at(b"hello", 0).unwrap();
182 f.sync_data().unwrap();
183 assert_eq!(std::fs::read(&path).unwrap(), b"helloworld");
184 std::fs::remove_dir_all(&dir).ok();
185 }
186
187 #[test]
188 fn opening_does_not_truncate() {
189 let dir = tmpdir("notrunc");
190 let path = dir.join("keep.bin");
191 std::fs::write(&path, b"existing").unwrap();
192 let f = DestFile::open(&path).unwrap();
193 assert_eq!(f.size().unwrap(), 8);
194 drop(f);
195 assert_eq!(std::fs::read(&path).unwrap(), b"existing");
196 std::fs::remove_dir_all(&dir).ok();
197 }
198
199 #[test]
200 fn preallocate_sets_exact_size() {
201 let dir = tmpdir("prealloc");
202 let path = dir.join("big.bin");
203 let f = DestFile::open(&path).unwrap();
204 f.preallocate(1024 * 1024).unwrap();
205 assert_eq!(f.size().unwrap(), 1024 * 1024);
206 f.preallocate(1024 * 1024).unwrap();
208 assert_eq!(f.size().unwrap(), 1024 * 1024);
209 std::fs::remove_dir_all(&dir).ok();
210 }
211
212 #[test]
213 fn identity_distinguishes_files() {
214 let dir = tmpdir("identity");
215 let a = DestFile::open(&dir.join("a.bin")).unwrap();
216 let b = DestFile::open(&dir.join("b.bin")).unwrap();
217
218 assert_ne!(a.identity().unwrap(), b.identity().unwrap());
220 let a_again = DestFile::open(&dir.join("a.bin")).unwrap();
222 assert_eq!(a.identity().unwrap(), a_again.identity().unwrap());
223
224 std::fs::remove_dir_all(&dir).ok();
227 }
228
229 #[test]
230 fn creates_missing_parents() {
231 let dir = tmpdir("parents");
232 let path = dir.join("a/b/c/deep.bin");
233 let f = DestFile::open(&path).unwrap();
234 f.write_at(b"x", 0).unwrap();
235 assert!(path.exists());
236 std::fs::remove_dir_all(&dir).ok();
237 }
238}