metadata-backup 0.1.0

Program to back up file system metadata.
Documentation
// Copyright 2019 metadata-backup Authors (see AUTHORS.md)

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::time::SystemTime;

use serde::{Deserialize, Serialize};

#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
pub struct Metadata {
    pub name: String,
    pub size: u64,
    pub is_dir: bool,
    #[serde(with = "format_datetimes")]
    pub atime: Option<SystemTime>,
    #[serde(with = "format_datetimes")]
    pub mtime: Option<SystemTime>,
    #[serde(with = "format_datetimes")]
    pub ctime: Option<SystemTime>,
    pub st_mode: Option<u32>,
    pub st_mode_string: Option<String>,
    pub uid: Option<u32>,
    pub gid: Option<u32>,
    #[serde(deserialize_with = "deserialize_link::deserialize")]
    pub link: Option<String>,
}

impl Metadata {
    pub fn new<P: AsRef<Path>>(p: P) -> Result<Metadata, std::io::Error> {
        let fpath: &Path = p.as_ref();
        let name = fpath.file_name().unwrap().to_str().unwrap();
        let meta = fpath.symlink_metadata()?;
        let file_type = meta.file_type();
        let mode = if cfg!(target_os = "linux") {
            Some(meta.permissions().mode())
        } else {
            None
        };

        let (uid, gid): (Option<u32>, Option<u32>) = if cfg!(target_os = "linux") {
            (Some(meta.uid()), Some(meta.gid()))
        } else {
            (None, None)
        };

        let link: Option<String> = if file_type.is_symlink() {
            fpath.read_link()?.to_str().map(String::from)
        } else {
            None
        };

        Ok(Metadata {
            name: name.to_string(),
            size: meta.len(),
            is_dir: file_type.is_dir(),
            atime: meta.accessed().ok(),
            mtime: meta.modified().ok(),
            ctime: meta.created().ok(),
            st_mode: mode,
            st_mode_string: mode.map(strmode::strmode),
            uid: uid,
            gid: gid,
            link: link,
        })
    }
}

mod format_datetimes {
    use chrono::{DateTime, SecondsFormat, TimeZone, Utc};
    use serde::{self, Deserialize, Deserializer, Serializer};
    use std::time::SystemTime;

    pub fn serialize<S>(system_time: &Option<SystemTime>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let timestamp = match system_time {
            Some(ts) => ts
                .duration_since(SystemTime::UNIX_EPOCH)
                .map_err(serde::ser::Error::custom)?
                .as_secs(),
            None => {
                return serializer.serialize_str("");
            }
        };

        let date_time = Utc.timestamp(timestamp as i64, 0);
        let dt_str = date_time.to_rfc3339_opts(SecondsFormat::Secs, true);
        serializer.serialize_str(&dt_str)
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<SystemTime>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;

        if s == "" {
            Ok(None)
        } else {
            let dt = DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom)?;
            let ts = dt.timestamp();
            let st = SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(ts as u64));

            st.ok_or(serde::de::Error::custom(
                "Could not deserialize invalid timestamp.",
            ))
            .map(Some)
        }
    }
}

/// The `link` field must either be `None` or a canonical full path; in Rust, it
/// is Option<String>, but some serialization formats do not have a specific null
/// string. We could serialize `link` as two fields (`is_link` and `link`), but
/// since `link` can never be `Some("")`, we can just deserialize an empty
/// string as `None` and all other values as `Some(the_value)`.
mod deserialize_link {
    use serde::{self, Deserialize, Deserializer};
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;

        if s == "" {
            Ok(None)
        } else {
            Ok(Some(s))
        }
    }
}