exa/src/fs/file.rs

395 lines
13 KiB
Rust
Raw Normal View History

2015-05-16 15:10:58 +00:00
//! Files, and methods and fields to access their metadata.
use std::ascii::AsciiExt;
use std::env::current_dir;
use std::fs;
2015-12-15 21:47:37 +00:00
use std::io::Result as IOResult;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use fs::dir::Dir;
use fs::fields as f;
/// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259
/// which is currently unstable and lacks vision for stabilization,
/// see https://github.com/rust-lang/rust/issues/27712
#[allow(dead_code, non_camel_case_types)]
mod modes {
pub type mode_t = u32;
pub const USER_READ: mode_t = 0o400;
pub const USER_WRITE: mode_t = 0o200;
pub const USER_EXECUTE: mode_t = 0o100;
pub const USER_RWX: mode_t = 0o700;
pub const GROUP_READ: mode_t = 0o040;
pub const GROUP_WRITE: mode_t = 0o020;
pub const GROUP_EXECUTE: mode_t = 0o010;
pub const GROUP_RWX: mode_t = 0o070;
pub const OTHER_READ: mode_t = 0o004;
pub const OTHER_WRITE: mode_t = 0o002;
pub const OTHER_EXECUTE: mode_t = 0o001;
pub const OTHER_RWX: mode_t = 0o007;
pub const ALL_READ: mode_t = 0o444;
pub const ALL_WRITE: mode_t = 0o222;
pub const ALL_EXECUTE: mode_t = 0o111;
pub const ALL_RWX: mode_t = 0o777;
pub const SETUID: mode_t = 0o4000;
pub const SETGID: mode_t = 0o2000;
pub const STICKY_BIT: mode_t = 0o1000;
}
2015-05-16 15:10:58 +00:00
2015-01-24 12:38:05 +00:00
/// A **File** is a wrapper around one of Rust's Path objects, along with
/// associated data about the file.
///
/// Each file is definitely going to have its filename displayed at least
2015-05-16 17:16:35 +00:00
/// once, have its file extension extracted at least once, and have its metadata
2015-01-24 12:38:05 +00:00
/// information queried at least once, so it makes sense to do all this at the
/// start and hold on to all the information.
2015-05-12 14:38:12 +00:00
pub struct File<'dir> {
2015-05-16 15:10:58 +00:00
/// The filename portion of this file's path, including the extension.
///
/// This is used to compare against certain filenames (such as checking if
/// its “Makefile” or something) and to highlight only the filename in
/// colour when displaying the path.
pub name: String,
2015-05-16 15:10:58 +00:00
/// The files names extension, if present, extracted from the name.
///
/// This is queried many times over, so its worth caching it.
pub ext: Option<String>,
2015-05-16 15:10:58 +00:00
/// The path that begat this file.
///
/// Even though the file's name is extracted, the path needs to be kept
/// around, as certain operations involve looking up the file's absolute
/// location (such as the Git status, or searching for compiled files).
pub path: PathBuf,
2015-05-16 15:10:58 +00:00
/// A cached `metadata` call for this file.
///
/// This too is queried multiple times, and is *not* cached by the OS, as
/// it could easily change between invocations - but exa is so short-lived
/// it's better to just cache it.
pub metadata: fs::Metadata,
2015-05-16 15:10:58 +00:00
/// A reference to the directory that contains this file, if present.
///
/// Filenames that get passed in on the command-line directly will have no
/// parent directory reference - although they technically have one on the
/// filesystem, we'll never need to look at it, so it'll be `None`.
/// However, *directories* that get passed in will produce files that
/// contain a reference to it, which is used in certain operations (such
/// as looking up a file's Git status).
pub dir: Option<&'dir Dir>,
}
2015-05-12 14:38:12 +00:00
impl<'dir> File<'dir> {
2015-09-03 17:48:53 +00:00
2015-05-16 15:10:58 +00:00
/// Create a new `File` object from the given `Path`, inside the given
/// `Dir`, if appropriate.
2015-01-24 12:38:05 +00:00
///
/// This uses `symlink_metadata` instead of `metadata`, which doesn't
/// follow symbolic links.
2015-12-15 21:47:37 +00:00
pub fn from_path(path: &Path, parent: Option<&'dir Dir>) -> IOResult<File<'dir>> {
fs::symlink_metadata(path).map(|metadata| File::with_metadata(metadata, path, parent))
}
2014-11-25 20:50:23 +00:00
2015-05-16 17:16:35 +00:00
/// Create a new File object from the given metadata result, and other data.
pub fn with_metadata(metadata: fs::Metadata, path: &Path, parent: Option<&'dir Dir>) -> File<'dir> {
let filename = match path.file_name() {
Some(name) => name.to_string_lossy().to_string(),
None => String::new(),
};
File {
2015-09-03 17:48:53 +00:00
path: path.to_path_buf(),
dir: parent,
metadata: metadata,
ext: ext(path),
name: filename,
}
}
2015-05-16 15:10:58 +00:00
/// Whether this file is a directory on the filesystem.
pub fn is_directory(&self) -> bool {
2015-05-16 17:16:35 +00:00
self.metadata.is_dir()
}
2015-09-03 17:48:53 +00:00
/// If this file is a directory on the filesystem, then clone its
/// `PathBuf` for use in one of our own `Dir` objects, and read a list of
/// its contents.
///
/// Returns an IO error upon failure, but this shouldn't be used to check
/// if a `File` is a directory or not! For that, just use `is_directory()`.
2015-12-15 21:47:37 +00:00
pub fn to_dir(&self, scan_for_git: bool) -> IOResult<Dir> {
2015-09-03 09:51:59 +00:00
Dir::read_dir(&*self.path, scan_for_git)
}
2015-05-16 15:10:58 +00:00
/// Whether this file is a regular file on the filesystem - that is, not a
/// directory, a link, or anything else treated specially.
pub fn is_file(&self) -> bool {
2015-05-16 17:16:35 +00:00
self.metadata.is_file()
}
2015-05-16 15:10:58 +00:00
/// Whether this file is both a regular file *and* executable for the
/// current user. Executable files have different semantics than
/// executable directories, and so should be highlighted differently.
pub fn is_executable_file(&self) -> bool {
let bit = modes::USER_EXECUTE;
2015-05-16 17:16:35 +00:00
self.is_file() && (self.metadata.permissions().mode() & bit) == bit
}
2015-05-16 15:10:58 +00:00
/// Whether this file is a symlink on the filesystem.
pub fn is_link(&self) -> bool {
2015-05-16 17:16:35 +00:00
self.metadata.file_type().is_symlink()
}
2015-05-16 15:10:58 +00:00
/// Whether this file is a named pipe on the filesystem.
pub fn is_pipe(&self) -> bool {
false // TODO: Still waiting on this one...
}
2015-05-16 15:10:58 +00:00
/// Whether this file is a dotfile, based on its name. In Unix, file names
/// beginning with a dot represent system or configuration files, and
/// should be hidden by default.
pub fn is_dotfile(&self) -> bool {
self.name.starts_with(".")
}
/// Assuming the current file is a symlink, follows the link and
/// returns a File object from the path the link points to.
///
/// If statting the file fails (usually because the file on the
/// other end doesn't exist), returns the *filename* of the file
/// that should be there.
Replace Cells with growable TextCells A recent change to ansi-term [1] means that `ANSIString`s can now hold either owned *or* borrowed data (Rust calls this the Cow type). This means that we can delay formatting ANSIStrings into ANSI-control-code-formatted strings until it's absolutely necessary. The process for doing this was: 1. Replace the `Cell` type with a `TextCell` type that holds a vector of `ANSIString` values instead of a formatted string. It still does the width tracking. 2. Rework the details module's `render` functions to emit values of this type. 3. Similarly, rework the functions that produce cells containing filenames to use a `File` value's `name` field, which is an owned `String` that can now be re-used. 4. Update the printing, formatting, and width-calculating code in the details and grid-details views to produce a table by adding vectors together instead of adding strings together, delaying the formatting as long as it can. This results in fewer allocations (as fewer `String` values are produced), and makes the API tidier (as fewer `String` values are being passed around without having their contents specified). This also paves the way to Windows support, or at least support for non-ANSI terminals: by delaying the time until strings are formatted, it'll now be easier to change *how* they are formatted. Casualties include: - Bump to ansi_term v0.7.1, which impls `PartialEq` and `Debug` on `ANSIString`. - The grid_details and lines views now need to take a vector of files, rather than a borrowed slice, so the filename cells produced now own the filename strings that get taken from files. - Fixed the signature of `File#link_target` to specify that the file produced refers to the same directory, rather than some phantom directory with the same lifetime as the file. (This was wrong from the start, but it broke nothing until now) References: [1]: ansi-term@f6a6579ba8174de1cae64d181ec04af32ba2a4f0
2015-12-17 00:25:20 +00:00
pub fn link_target(&self) -> Result<File<'dir>, String> {
let path = match fs::read_link(&self.path) {
Ok(path) => path,
Err(_) => return Err(self.name.clone()),
};
let target_path = match self.dir {
Some(dir) => dir.join(&*path),
None => path
};
let filename = match target_path.file_name() {
Some(name) => name.to_string_lossy().to_string(),
None => String::new(),
};
2014-11-25 20:50:23 +00:00
// Use plain `metadata` instead of `symlink_metadata` - we *want* to follow links.
2015-05-16 17:16:35 +00:00
if let Ok(metadata) = fs::metadata(&target_path) {
Ok(File {
2015-09-03 17:48:53 +00:00
path: target_path.to_path_buf(),
dir: self.dir,
metadata: metadata,
ext: ext(&target_path),
name: filename,
})
}
else {
Err(target_path.display().to_string())
}
}
2015-05-16 15:10:58 +00:00
/// This file's number of hard links.
2015-01-24 12:38:05 +00:00
///
2015-05-16 15:10:58 +00:00
/// It also reports whether this is both a regular file, and a file with
/// multiple links. This is important, because a file with multiple links
/// is uncommon, while you can come across directories and other types
/// with multiple links much more often. Thus, it should get highlighted
/// more attentively.
2015-05-12 02:33:40 +00:00
pub fn links(&self) -> f::Links {
2015-06-16 23:49:29 +00:00
let count = self.metadata.nlink();
2015-05-12 02:33:40 +00:00
f::Links {
count: count,
multiple: self.is_file() && count > 1,
}
2015-01-24 12:38:05 +00:00
}
2015-05-16 15:10:58 +00:00
/// This file's inode.
2015-05-12 02:33:40 +00:00
pub fn inode(&self) -> f::Inode {
2015-06-16 23:49:29 +00:00
f::Inode(self.metadata.ino())
2015-01-24 12:38:05 +00:00
}
2015-05-16 15:10:58 +00:00
/// This file's number of filesystem blocks.
///
/// (Not the size of each block, which we don't actually report on)
2015-05-12 02:33:40 +00:00
pub fn blocks(&self) -> f::Blocks {
if self.is_file() || self.is_link() {
2015-06-16 23:49:29 +00:00
f::Blocks::Some(self.metadata.blocks())
2015-01-24 12:38:05 +00:00
}
else {
2015-05-12 02:33:40 +00:00
f::Blocks::None
2015-01-24 12:38:05 +00:00
}
}
2015-05-16 15:10:58 +00:00
/// The ID of the user that own this file.
2015-05-12 02:33:40 +00:00
pub fn user(&self) -> f::User {
2015-06-16 23:49:29 +00:00
f::User(self.metadata.uid())
2015-01-24 12:38:05 +00:00
}
2015-05-16 15:10:58 +00:00
/// The ID of the group that owns this file.
2015-05-12 02:33:40 +00:00
pub fn group(&self) -> f::Group {
2015-06-16 23:49:29 +00:00
f::Group(self.metadata.gid())
2015-01-24 12:38:05 +00:00
}
2015-05-16 15:10:58 +00:00
/// This file's size, if it's a regular file.
2015-01-24 12:38:05 +00:00
///
/// For directories, no size is given. Although they do have a size on
/// some filesystems, I've never looked at one of those numbers and gained
2015-05-16 15:10:58 +00:00
/// any information from it. So it's going to be hidden instead.
2015-05-12 02:33:40 +00:00
pub fn size(&self) -> f::Size {
if self.is_directory() {
2015-05-12 02:33:40 +00:00
f::Size::None
2014-11-25 20:50:23 +00:00
}
else {
2015-05-16 17:16:35 +00:00
f::Size::Some(self.metadata.len())
}
}
pub fn modified_time(&self) -> f::Time {
f::Time(self.metadata.mtime())
}
pub fn created_time(&self) -> f::Time {
f::Time(self.metadata.ctime())
}
pub fn accessed_time(&self) -> f::Time {
f::Time(self.metadata.mtime())
}
2015-05-16 15:10:58 +00:00
/// This file's 'type'.
2015-01-24 12:38:05 +00:00
///
2015-05-16 15:10:58 +00:00
/// This is used in the leftmost column of the permissions column.
2015-01-24 12:38:05 +00:00
/// Although the file type can usually be guessed from the colour of the
/// file, `ls` puts this character there, so people will expect it.
pub fn type_char(&self) -> f::Type {
if self.is_file() {
2015-05-12 02:33:40 +00:00
f::Type::File
}
else if self.is_directory() {
2015-05-12 02:33:40 +00:00
f::Type::Directory
}
else if self.is_pipe() {
2015-05-12 02:33:40 +00:00
f::Type::Pipe
}
else if self.is_link() {
2015-05-12 02:33:40 +00:00
f::Type::Link
}
else {
2015-05-12 02:33:40 +00:00
f::Type::Special
}
}
2015-05-16 15:10:58 +00:00
/// This file's permissions, with flags for each bit.
2015-09-03 17:48:53 +00:00
///
/// The extended-attribute '@' character that you see in here is in fact
/// added in later, to avoid querying the extended attributes more than
/// once. (Yes, it's a little hacky.)
2015-05-12 02:33:40 +00:00
pub fn permissions(&self) -> f::Permissions {
2015-05-16 17:16:35 +00:00
let bits = self.metadata.permissions().mode();
let has_bit = |bit| { bits & bit == bit };
2015-05-12 02:33:40 +00:00
f::Permissions {
user_read: has_bit(modes::USER_READ),
user_write: has_bit(modes::USER_WRITE),
user_execute: has_bit(modes::USER_EXECUTE),
group_read: has_bit(modes::GROUP_READ),
group_write: has_bit(modes::GROUP_WRITE),
group_execute: has_bit(modes::GROUP_EXECUTE),
other_read: has_bit(modes::OTHER_READ),
other_write: has_bit(modes::OTHER_WRITE),
other_execute: has_bit(modes::OTHER_EXECUTE),
}
}
2015-01-24 12:38:05 +00:00
2015-05-16 15:10:58 +00:00
/// Whether this file's extension is any of the strings that get passed in.
///
/// This will always return `false` if the file has no extension.
2015-05-09 15:10:26 +00:00
pub fn extension_is_one_of(&self, choices: &[&str]) -> bool {
match self.ext {
Some(ref ext) => choices.contains(&&ext[..]),
None => false,
}
}
2015-05-16 15:10:58 +00:00
/// Whether this file's name, including extension, is any of the strings
/// that get passed in.
2015-05-09 15:10:26 +00:00
pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
choices.contains(&&self.name[..])
}
2015-05-16 15:10:58 +00:00
/// This file's Git status as two flags: one for staged changes, and the
/// other for unstaged changes.
///
/// This requires looking at the `git` field of this file's parent
/// directory, so will not work if this file has just been passed in on
/// the command line.
2015-05-12 02:33:40 +00:00
pub fn git_status(&self) -> f::Git {
match self.dir {
2015-05-12 02:33:40 +00:00
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
Some(d) => {
let cwd = match current_dir() {
Err(_) => Path::new(".").join(&self.path),
Ok(dir) => dir.join(&self.path),
};
d.git_status(&cwd, self.is_directory())
},
}
}
2015-01-24 12:38:05 +00:00
}
impl<'a> AsRef<File<'a>> for File<'a> {
fn as_ref(&self) -> &File<'a> {
&self
}
}
/// Extract an extension from a file path, if one is present, in lowercase.
2015-01-24 12:38:05 +00:00
///
/// The extension is the series of characters after the last dot. This
/// deliberately counts dotfiles, so the ".git" folder has the extension "git".
///
/// ASCII lowercasing is used because these extensions are only compared
/// against a pre-compiled list of extensions which are known to only exist
/// within ASCII, so it's alright.
fn ext(path: &Path) -> Option<String> {
let name = match path.file_name() {
Some(f) => f.to_string_lossy().to_string(),
None => return None,
};
name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
}
2015-01-25 13:04:15 +00:00
2015-06-08 20:33:39 +00:00
#[cfg(test)]
mod test {
use super::ext;
use std::path::Path;
2015-01-25 13:04:15 +00:00
2015-01-25 13:47:07 +00:00
#[test]
fn extension() {
assert_eq!(Some("dat".to_string()), ext(Path::new("fester.dat")))
2015-01-25 13:47:07 +00:00
}
#[test]
fn dotfile() {
assert_eq!(Some("vimrc".to_string()), ext(Path::new(".vimrc")))
2015-01-25 13:47:07 +00:00
}
#[test]
fn no_extension() {
assert_eq!(None, ext(Path::new("jarlsberg")))
}
2015-01-25 13:04:15 +00:00
}