Skip to main content

computer_transfer/
pack.rs

1//! Writes a file or a folder as a tar stream.
2
3use std::{
4    fs,
5    io::{self, Read, Write},
6    path::{Path, PathBuf},
7    time::UNIX_EPOCH,
8};
9
10use computer_protocol::{SKIP_REPORT_ENTRY, SkipList};
11use tar::{Builder, EntryType, Header};
12
13use crate::{Failure, Progress, rules::Platform};
14
15/// What was sent.
16#[derive(Debug)]
17pub struct Packed {
18    pub progress: Progress,
19    pub skipped: SkipList,
20}
21
22/// Reads exactly `left` bytes of a file and fails if the file ends sooner.
23///
24/// A tar header states the size up front, so a file that shrinks must stop the transfer
25/// instead of leaving a corrupt archive.
26struct Exact<'a, R> {
27    inner: R,
28    left: u64,
29    copied: &'a mut u64,
30}
31
32impl<R: Read> Read for Exact<'_, R> {
33    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
34        if self.left == 0 {
35            return Ok(0);
36        }
37        let want = usize::try_from(self.left).map_or(buf.len(), |left| left.min(buf.len()));
38        let n = self.inner.read(&mut buf[..want])?;
39        if n == 0 {
40            return Err(io::Error::new(
41                io::ErrorKind::UnexpectedEof,
42                "the file got shorter while it was being copied",
43            ));
44        }
45        self.left -= n as u64;
46        *self.copied += n as u64;
47        Ok(n)
48    }
49}
50
51struct Packer<W: Write> {
52    builder: Builder<W>,
53    platform: Platform,
54    progress: Progress,
55    skipped: SkipList,
56}
57
58/// Sends `source` to `out` as a tar archive.
59///
60/// The archive's first entry is the root, named after the last part of `source`. A path that is
61/// itself a link is followed. Links inside a folder are sent as links, and pipes, sockets, and
62/// devices are skipped and listed in the closing report entry. `platform` is the system being
63/// read from.
64///
65/// # Errors
66///
67/// Fails when the source cannot be read at all, when a file changes size while it is sent, or
68/// when `out` fails.
69pub fn pack<W: Write>(source: &Path, out: W, platform: Platform) -> Result<Packed, Failure> {
70    let mut packer = Packer {
71        builder: Builder::new(out),
72        platform,
73        progress: Progress::default(),
74        skipped: SkipList::default(),
75    };
76    match packer.run(source) {
77        Ok(()) => Ok(Packed {
78            progress: packer.progress,
79            skipped: packer.skipped,
80        }),
81        Err(error) => Err(Failure::new(
82            format!("sending {} failed: {error}", source.display()),
83            packer.progress,
84        )),
85    }
86}
87
88impl<W: Write> Packer<W> {
89    fn run(&mut self, source: &Path) -> io::Result<()> {
90        let meta = fs::metadata(source).map_err(|error| {
91            if error.kind() == io::ErrorKind::NotFound {
92                io::Error::new(error.kind(), "it does not exist")
93            } else {
94                error
95            }
96        })?;
97        let name = root_name(source)?;
98        let name = Path::new(&name);
99        if meta.is_dir() {
100            self.add_dir(source, name, &meta)?;
101        } else if meta.is_file() {
102            self.add_file(source, name)?;
103        } else {
104            return Err(io::Error::other(
105                "it is a pipe, device, or socket, not a file or folder",
106            ));
107        }
108        self.finish()
109    }
110
111    fn finish(&mut self) -> io::Result<()> {
112        let json = serde_json::to_vec(&self.skipped).map_err(io::Error::other)?;
113        let mut header = header(EntryType::Regular, 0o644, 0);
114        header.set_size(json.len() as u64);
115        self.builder
116            .append_data(&mut header, SKIP_REPORT_ENTRY, json.as_slice())?;
117        self.builder.finish()?;
118        self.builder.get_mut().flush()
119    }
120
121    fn add_dir(&mut self, dir: &Path, rel: &Path, meta: &fs::Metadata) -> io::Result<()> {
122        let mut head = header(EntryType::Directory, 0o755, mtime(meta));
123        self.builder.append_data(&mut head, rel, io::empty())?;
124        self.progress.folders += 1;
125
126        let listing = match fs::read_dir(dir) {
127            Ok(listing) => listing,
128            Err(error) => {
129                self.skip(rel, format!("the folder cannot be read: {error}"));
130                return Ok(());
131            }
132        };
133        let mut children = Vec::new();
134        for child in listing {
135            match child {
136                Ok(child) => children.push(child),
137                Err(error) => self.skip(rel, format!("an entry cannot be read: {error}")),
138            }
139        }
140        children.sort_by_key(fs::DirEntry::file_name);
141        for child in children {
142            let Some(name) = child.file_name().to_str().map(str::to_owned) else {
143                self.skip(
144                    &rel.join(child.file_name()),
145                    "the name is not valid UTF-8".to_owned(),
146                );
147                continue;
148            };
149            let path = child.path();
150            let rel = rel.join(&name);
151            let meta = match fs::symlink_metadata(&path) {
152                Ok(meta) => meta,
153                Err(error) => {
154                    self.skip(&rel, format!("it cannot be read: {error}"));
155                    continue;
156                }
157            };
158            let kind = meta.file_type();
159            if kind.is_dir() {
160                self.add_dir(&path, &rel, &meta)?;
161            } else if kind.is_file() {
162                self.add_file(&path, &rel)?;
163            } else if kind.is_symlink() {
164                self.add_link(&path, &rel, &meta)?;
165            } else {
166                self.skip(&rel, "it is a pipe, device, or socket".to_owned());
167            }
168        }
169        Ok(())
170    }
171
172    fn add_file(&mut self, path: &Path, rel: &Path) -> io::Result<()> {
173        let file = match fs::File::open(path) {
174            Ok(file) => file,
175            Err(error) if rel.components().count() > 1 => {
176                self.skip(rel, format!("it cannot be opened: {error}"));
177                return Ok(());
178            }
179            Err(error) => return Err(error),
180        };
181        let meta = file.metadata()?;
182        let mut head = header(EntryType::Regular, file_mode(&meta), mtime(&meta));
183        head.set_size(meta.len());
184        let mut copied = 0;
185        let data = Exact {
186            inner: file,
187            left: meta.len(),
188            copied: &mut copied,
189        };
190        let sent = self.builder.append_data(&mut head, rel, data);
191        self.progress.bytes += copied;
192        sent.map_err(|error| io::Error::new(error.kind(), format!("{}: {error}", rel.display())))?;
193        self.progress.files += 1;
194        Ok(())
195    }
196
197    fn add_link(&mut self, path: &Path, rel: &Path, meta: &fs::Metadata) -> io::Result<()> {
198        let target = match fs::read_link(path) {
199            Ok(target) => target,
200            Err(error) => {
201                self.skip(rel, format!("the link cannot be read: {error}"));
202                return Ok(());
203            }
204        };
205        if self.platform == Platform::Windows && (target.has_root() || has_prefix(&target)) {
206            self.skip(
207                rel,
208                "the link points to an absolute Windows path that does not exist on the computer"
209                    .to_owned(),
210            );
211            return Ok(());
212        }
213        let mut head = header(EntryType::Symlink, 0o777, mtime(meta));
214        self.builder.append_link(&mut head, rel, &target)?;
215        self.progress.files += 1;
216        Ok(())
217    }
218
219    fn skip(&mut self, rel: &Path, reason: String) {
220        self.skipped.push(rel.display().to_string(), reason);
221    }
222}
223
224fn has_prefix(path: &Path) -> bool {
225    matches!(
226        path.components().next(),
227        Some(std::path::Component::Prefix(_))
228    )
229}
230
231/// The name the root is sent under: the last part of the path as given, else of its real path.
232///
233/// # Errors
234///
235/// Fails when the path has no name or the name is not valid UTF-8.
236pub fn root_name(source: &Path) -> io::Result<String> {
237    let given: Option<PathBuf> = source.file_name().map(PathBuf::from);
238    let name = match given {
239        Some(name) => name,
240        None => fs::canonicalize(source)?
241            .file_name()
242            .map(PathBuf::from)
243            .ok_or_else(|| io::Error::other("it has no name, give a path to a file or folder"))?,
244    };
245    name.into_os_string()
246        .into_string()
247        .map_err(|_| io::Error::other("its name is not valid UTF-8"))
248}
249
250fn header(kind: EntryType, mode: u32, mtime: u64) -> Header {
251    let mut header = Header::new_gnu();
252    header.set_entry_type(kind);
253    header.set_mode(mode);
254    header.set_mtime(mtime);
255    header.set_size(0);
256    header
257}
258
259fn mtime(meta: &fs::Metadata) -> u64 {
260    meta.modified()
261        .ok()
262        .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
263        .map_or(0, |since| since.as_secs())
264}
265
266#[cfg(unix)]
267fn file_mode(meta: &fs::Metadata) -> u32 {
268    use std::os::unix::fs::PermissionsExt;
269    meta.permissions().mode() & 0o777
270}
271
272#[cfg(not(unix))]
273fn file_mode(meta: &fs::Metadata) -> u32 {
274    if meta.permissions().readonly() {
275        0o444
276    } else {
277        0o644
278    }
279}