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::io::AsRawHandle;
156 use windows_sys::Win32::Foundation::HANDLE;
157 use windows_sys::Win32::Storage::FileSystem::{
158 BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
159 };
160
161 let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
162 let ok = unsafe { GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info) };
165 if ok == 0 {
166 return Err(io::Error::last_os_error());
167 }
168 Ok(FileIdentity {
169 dev: u64::from(info.dwVolumeSerialNumber),
170 ino: (u64::from(info.nFileIndexHigh) << 32) | u64::from(info.nFileIndexLow),
171 })
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn tmpdir(name: &str) -> PathBuf {
180 let dir =
181 std::env::temp_dir().join(format!("rget-file-test-{name}-{}", std::process::id()));
182 std::fs::create_dir_all(&dir).unwrap();
183 dir
184 }
185
186 #[test]
187 fn writes_at_offsets_out_of_order() {
188 let dir = tmpdir("offsets");
189 let path = dir.join("out.bin");
190 let f = DestFile::open(&path).unwrap();
191 f.preallocate(10).unwrap();
192 f.write_at(b"world", 5).unwrap();
193 f.write_at(b"hello", 0).unwrap();
194 f.sync_data().unwrap();
195 assert_eq!(std::fs::read(&path).unwrap(), b"helloworld");
196 std::fs::remove_dir_all(&dir).ok();
197 }
198
199 #[test]
200 fn opening_does_not_truncate() {
201 let dir = tmpdir("notrunc");
202 let path = dir.join("keep.bin");
203 std::fs::write(&path, b"existing").unwrap();
204 let f = DestFile::open(&path).unwrap();
205 assert_eq!(f.size().unwrap(), 8);
206 drop(f);
207 assert_eq!(std::fs::read(&path).unwrap(), b"existing");
208 std::fs::remove_dir_all(&dir).ok();
209 }
210
211 #[test]
212 fn preallocate_sets_exact_size() {
213 let dir = tmpdir("prealloc");
214 let path = dir.join("big.bin");
215 let f = DestFile::open(&path).unwrap();
216 f.preallocate(1024 * 1024).unwrap();
217 assert_eq!(f.size().unwrap(), 1024 * 1024);
218 f.preallocate(1024 * 1024).unwrap();
220 assert_eq!(f.size().unwrap(), 1024 * 1024);
221 std::fs::remove_dir_all(&dir).ok();
222 }
223
224 #[test]
225 fn identity_distinguishes_files() {
226 let dir = tmpdir("identity");
227 let a = DestFile::open(&dir.join("a.bin")).unwrap();
228 let b = DestFile::open(&dir.join("b.bin")).unwrap();
229
230 assert_ne!(a.identity().unwrap(), b.identity().unwrap());
232 let a_again = DestFile::open(&dir.join("a.bin")).unwrap();
234 assert_eq!(a.identity().unwrap(), a_again.identity().unwrap());
235
236 std::fs::remove_dir_all(&dir).ok();
239 }
240
241 #[test]
242 fn creates_missing_parents() {
243 let dir = tmpdir("parents");
244 let path = dir.join("a/b/c/deep.bin");
245 let f = DestFile::open(&path).unwrap();
246 f.write_at(b"x", 0).unwrap();
247 assert!(path.exists());
248 std::fs::remove_dir_all(&dir).ok();
249 }
250}