Viewing: filestat.rs

// SPDX-License-Identifier: MIT

// Copyright (c) 2025 DDN. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

use crate::{Fid, LovPattern};
use cstrbuf::CStrBuf;
use libc::{S_IFREG, S_IRGRP, S_IROTH, S_IRUSR, S_IWUSR};
use lustreapi_sys::*;
use nix::unistd::{getegid, geteuid};
use std::{
    fmt,
    path::Path,
    str::FromStr,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

/// A Rust-idiomatic representation of file stat information
///
/// This struct provides a safe interface for working with file
/// metadata using Rust's native types instead of raw C types. It automatically
/// handles conversion to the underlying `libc::stat` structure when needed.
/// It is recommended to use `StatBuilder` and not this directly.
///
/// # Examples
///
/// ```
/// # use rustreapi::hsm::Stat;
/// # use std::time::SystemTime;
/// let stat = Stat {
///     uid: 1000,
///     gid: 1000,
///     mode: 0o644,
///     size: 1024,
///     atime: SystemTime::now(),
///     mtime: SystemTime::now(),
///     ctime: SystemTime::now(),
/// };
/// ```
#[derive(Debug, Clone)]
pub struct Stat {
    pub uid: u32,
    pub gid: u32,
    pub mode: u32,
    pub size: u64,
    pub atime: SystemTime,
    pub mtime: SystemTime,
    pub ctime: SystemTime,
}

impl Default for Stat {
    fn default() -> Self {
        let now = SystemTime::now();
        Self {
            uid: geteuid().as_raw(),
            gid: getegid().as_raw(),
            mode: S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, // 0644, regular file
            size: 0,
            atime: now,
            mtime: now,
            ctime: now,
        }
    }
}

impl Stat {
    /// Convert System Time to time spec
    fn sys_to_timespec(time: SystemTime) -> timespec {
        let duration = time
            .duration_since(UNIX_EPOCH)
            .unwrap_or_else(|_| Duration::from_secs(0)); // Handle time before epoch

        timespec {
            tv_sec: duration.as_secs() as i64,
            tv_nsec: i64::from(duration.subsec_nanos()),
        }
    }

    /// Convert to the raw lib c stat struct
    fn to_raw_stat(&self) -> stat {
        let atime = Self::sys_to_timespec(self.atime);
        let mtime = Self::sys_to_timespec(self.mtime);
        let ctime = Self::sys_to_timespec(self.ctime);

        stat {
            st_dev: 0,
            st_ino: 0,
            st_nlink: 1,
            st_mode: self.mode,
            st_uid: self.uid,
            st_gid: self.gid,
            st_rdev: 0,
            st_size: self.size as i64,
            st_blksize: 0,
            st_blocks: 0,
            st_atim: atime,
            st_mtim: mtime,
            st_ctim: ctime,
            // pad and glibc_reserved fields differ between platforms
            // using default values to ensure compatibility for now.
            ..Default::default()
        }
    }
}

/// Builder for creating Stat instances
pub struct StatBuilder {
    inner: Result<Stat, Box<dyn std::error::Error + Send + Sync>>,
}

/// Builder for creating Stat instances with Rust types
///
/// This builder provides a clean interface for creating file stat information
/// using Rust's native types like `SystemTime` instead of raw C types like `timespec`.
/// It follows the same builder pattern where only the final `build()`
/// method returns a `Result`.
///
/// # Examples
///
/// ```
/// # use rustreapi::hsm::StatBuilder;
/// # use std::time::{SystemTime, UNIX_EPOCH, Duration};
/// # fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// let stat = StatBuilder::new()
///     .uid(1000)
///     .gid(1000)
///     .mode(0o644)
///     .size(2048)
///     .mtime(SystemTime::now())
///     .build()?;
/// # Ok(())
/// # }
/// ```
impl StatBuilder {
    /// Create a new StatBuilder with default values
    pub fn new() -> Self {
        Self {
            inner: Ok(Stat::default()),
        }
    }

    /// Set the user ID
    pub fn uid(self, uid: u32) -> Self {
        self.and_then(move |mut stat| {
            stat.uid = uid;
            Ok(stat)
        })
    }

    /// Set the group ID
    pub fn gid(self, gid: u32) -> Self {
        self.and_then(move |mut stat| {
            stat.gid = gid;
            Ok(stat)
        })
    }

    /// Set the file mode
    pub fn mode(self, mode: u32) -> Self {
        self.and_then(move |mut stat| {
            stat.mode = mode;
            Ok(stat)
        })
    }

    /// Set the file size
    pub fn size(self, size: u64) -> Self {
        self.and_then(move |mut stat| {
            stat.size = size;
            Ok(stat)
        })
    }

    /// Set the access time
    pub fn atime(self, atime: SystemTime) -> Self {
        self.and_then(move |mut stat| {
            stat.atime = atime;
            Ok(stat)
        })
    }

    /// Set the modification time
    pub fn mtime(self, mtime: SystemTime) -> Self {
        self.and_then(move |mut stat| {
            stat.mtime = mtime;
            Ok(stat)
        })
    }

    /// Set the creation time
    pub fn ctime(self, ctime: SystemTime) -> Self {
        self.and_then(move |mut stat| {
            stat.ctime = ctime;
            Ok(stat)
        })
    }

    /// Build the final Stat
    pub fn build(self) -> Result<Stat, Box<dyn std::error::Error + Send + Sync>> {
        self.inner
    }

    // Private helper method for chaining operations
    fn and_then<F>(self, func: F) -> Self
    where
        F: FnOnce(Stat) -> Result<Stat, Box<dyn std::error::Error + Send + Sync>>,
    {
        Self {
            inner: self.inner.and_then(func),
        }
    }
}

impl Default for StatBuilder {
    fn default() -> Self {
        Self::new()
    }
}
struct HsmStubData {
    dst: CStrBuf,
    archive: i32,
    stat: Option<Stat>,
    stripe_size: u64,
    stripe_offset: i32,
    stripe_count: i32,
    stripe_pattern: LovPattern,
    pool_name: Option<CStrBuf>,
}

/// This builder conforms to a pattern where individual setter methods
/// don't return `Result` and only the final `create()` method returns
/// a `Result`. This allows for clean, fluent chaining without error
/// handling interrupting the builder flow.
///
/// # Examples
///
/// ```no_run
/// # use rustreapi::hsm::HsmFileStub;
/// # use std::time::SystemTime;
/// # fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
/// let fid = HsmFileStub::new("/path/to/file")
///     .archive(1)
///     .pool("my_pool")
///     .uid(1000)
///     .gid(1000)
///     .size(1024)
///     .mtime(SystemTime::now())
///     .stripe_size(2 << 20)
///     .create()?;
/// # Ok(())
/// # }
/// ```
pub struct HsmFileStub {
    inner: Result<HsmStubData, Box<dyn std::error::Error + Send + Sync>>,
}

impl fmt::Display for HsmFileStub {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.inner {
            Ok(data) => {
                let dst_str = data
                    .dst
                    .to_str()
                    .expect("Destination path should be valid UTF-8");

                let pool_str = match &data.pool_name {
                    Some(p) => p.to_str().expect("Pool name should be valid UTF-8"),
                    None => "None",
                };

                let stat_info = match &data.stat {
                    Some(stat) => format!(
                        "uid: {}, gid: {}, mode: 0o{:o}, size: {}, atime: {:?}, mtime: {:?}, ctime: {:?}",
                        stat.uid,
                        stat.gid,
                        stat.mode,
                        stat.size,
                        stat.atime,
                        stat.mtime,
                        stat.ctime,
                    ),
                    None => "default".to_string(),
                };

                write!(
                    f,
                    "HsmStub {{\n  dst: \"{dst_str}\",\n  archive: {},\n  stat: [{}],\n  stripe_size: {},\n  stripe_offset: {},\n  stripe_count: {},\n  stripe_pattern: {},\n  pool_name: \"{}\",\n}}",
                    data.archive,
                    stat_info,
                    data.stripe_size,
                    data.stripe_offset,
                    data.stripe_count,
                    data.stripe_pattern,
                    pool_str,
                )
            }
            Err(e) => write!(f, "HsmStub {{ error: {e} }}"),
        }
    }
}

impl HsmFileStub {
    /// Creates a new HsmFileStub builder. Any initialization
    /// errors are stored internally and will be returned by create().
    pub fn new<P: AsRef<Path>>(dst: P) -> Self {
        let result = (|| -> Result<HsmStubData, Box<dyn std::error::Error + Send + Sync>> {
            let dst = CStrBuf::from_path(dst)?;

            Ok(HsmStubData {
                dst,
                archive: 0,
                stat: None,
                stripe_size: 1 << 20,
                stripe_offset: -1,
                stripe_count: 1,
                stripe_pattern: 0,
                pool_name: None,
            })
        })();

        Self { inner: result }
    }

    /// Set the archive ID.
    pub fn archive(self, archive: i32) -> Self {
        self.and_then(move |mut data| {
            data.archive = archive;
            Ok(data)
        })
    }

    /// Get the stat struct if the builder is in a valid state.
    pub fn stat(self, stat: Stat) -> Self {
        self.and_then(move |mut data| {
            data.stat = Some(stat);
            Ok(data)
        })
    }

    /// Set UID (modifies stat attribute directly)
    pub fn uid(self, uid: u32) -> Self {
        self.and_then(move |mut data| {
            let mut stat = data.stat.unwrap_or_default();
            stat.uid = uid;
            data.stat = Some(stat);
            Ok(data)
        })
    }

    /// Set GID (modifies stat attribute directly)
    pub fn gid(self, gid: u32) -> Self {
        self.and_then(move |mut data| {
            let mut stat = data.stat.unwrap_or_default();
            stat.gid = gid;
            data.stat = Some(stat);
            Ok(data)
        })
    }

    /// Set mode (modifies stat attribute directly)
    pub fn mode(self, mode: u32) -> Self {
        self.and_then(move |mut data| {
            let mut stat = data.stat.unwrap_or_default();
            stat.mode = mode;
            data.stat = Some(stat);
            Ok(data)
        })
    }

    /// Set file size (modifies stat attribute directly)
    pub fn size(self, size: u64) -> Self {
        self.and_then(move |mut data| {
            let mut stat = data.stat.unwrap_or_default();
            stat.size = size;
            data.stat = Some(stat);
            Ok(data)
        })
    }

    /// Set mtime (modifies stat attribute directly)
    pub fn mtime(self, mtime: SystemTime) -> Self {
        self.and_then(move |mut data| {
            let mut stat = data.stat.unwrap_or_default();
            stat.mtime = mtime;
            data.stat = Some(stat);
            Ok(data)
        })
    }

    /// Set atime (modifies stat attribute directly)
    pub fn atime(self, atime: SystemTime) -> Self {
        self.and_then(move |mut data| {
            let mut stat = data.stat.unwrap_or_default();
            stat.atime = atime;
            data.stat = Some(stat);
            Ok(data)
        })
    }

    /// Set ctime (modifies stat attribute directly)
    pub fn ctime(self, ctime: SystemTime) -> Self {
        self.and_then(move |mut data| {
            let mut stat = data.stat.unwrap_or_default();
            stat.ctime = ctime;
            data.stat = Some(stat);
            Ok(data)
        })
    }

    /// Set stripe size.
    pub fn stripe_size(self, size: u64) -> Self {
        self.and_then(move |mut data| {
            data.stripe_size = size;
            Ok(data)
        })
    }

    /// Set stripe offset.
    pub fn stripe_offset(self, offset: i32) -> Self {
        self.and_then(move |mut data| {
            data.stripe_offset = offset;
            Ok(data)
        })
    }

    /// Set stripe count.
    pub fn stripe_count(self, count: i32) -> Self {
        self.and_then(move |mut data| {
            data.stripe_count = count;
            Ok(data)
        })
    }

    /// Set stripe pattern.
    pub fn stripe_pattern(self, pattern: LovPattern) -> Self {
        self.and_then(move |mut data| {
            data.stripe_pattern = pattern;
            Ok(data)
        })
    }

    /// Set pool name.
    pub fn pool(self, pool_name: &str) -> Self {
        self.and_then(move |mut data| {
            data.pool_name = Some(CStrBuf::from_str(pool_name)?);
            Ok(data)
        })
    }

    /// Creates the HSM stub. This is where all accumulated errors are handled.
    pub fn create(self) -> Result<Fid, Box<dyn std::error::Error + Send + Sync>> {
        let data = self.inner?; // Handle any builder errors first

        let newfid: Fid = Default::default();

        // Use provided stat or create defaults
        let stat = data.stat.unwrap_or_default();
        let raw_stat = stat.to_raw_stat();

        let pool_ptr = match &data.pool_name {
            Some(p) => p.as_ptr() as *mut std::os::raw::c_char,
            None => std::ptr::null_mut(),
        };

        let rc = unsafe {
            llapi_hsm_import(
                data.dst.as_ptr(),
                data.archive,
                &raw_stat,
                data.stripe_size,
                data.stripe_offset,
                data.stripe_count,
                data.stripe_pattern,
                pool_ptr,
                &mut lu_fid::from(newfid),
            )
        };

        if rc == 0 {
            Ok(newfid)
        } else {
            Err(format!("llapi_hsm_import failed with code: {rc}").into())
        }
    }

    // Private helper method for chaining operations on the inner Result
    fn and_then<F>(self, func: F) -> Self
    where
        F: FnOnce(HsmStubData) -> Result<HsmStubData, Box<dyn std::error::Error + Send + Sync>>,
    {
        Self {
            inner: self.inner.and_then(func),
        }
    }
}