path-extra 0.1.2

Extra methods on `Path` for ergonomic and chainable `fs` operations.
Documentation

path-extra

github.com crates.io docs.rs

Extra methods on Path for ergonomic and chainable fs operations.

Stop juggling fs functions, start chaining Path methods.

Supports std (default) and tokio (feature flag).

Problem

The standard library scatters file system operations across std::fs as free functions:

// Path is always the odd one out
let config_file = config_dir.join("config.toml");
fs::create_dir_all(&config_dir)?;
fs::write(&config_file, config)?;
fs::set_permissions(&config_file, permissions)?;

Solution

With PathExt, file system operations are now where they belong — on the Path itself:

use path_extra::std::path::PathExt as _;

// Path stays in focus and chains naturally
config_dir.create_dir_all()?
    .join("config.toml")
    .write(config)?
    .set_permissions(permissions)?;

Installation

# Supports std by default
cargo add path-extra

# To enable Tokio support
cargo add path-extra --features=tokio

Features

Method chaining

Operations on the same Path compose elegantly into a single expression:

let metadata = config_file.metadata()?;

// Atomic file update
config_file.with_extension("tmp")
    .write_new(config)?
    .set_permissions(metadata.permissions())?
    .rename(config_file)?;

Graceful NotFound / AlreadyExists

Use ergonomic _if_exists / _if_not_exists variants to deal with Ok(None) instead:

// Before: Tedious and easy to get wrong
let config = match fs::read(&config_file) {
    Ok(config) => config,
    Err(err) if err.kind() == io::ErrorKind::NotFound => DEFAULT_CONFIG,
    Err(err) => return Err(err),
};

// After: Intent is clear, noise is gone
let config = config_file.read_if_exists()?.unwrap_or(DEFAULT_CONFIG);
use path_extra::std::path::{OptionPathExt as _, PathExt as _};

// Idempotent file creation
config_dir
    .create_dir_all()?
    .join("config.toml")
    .write_new_if_not_exists(DEFAULT_CONFIG)?
    .set_permissions(permissions)?;

Symlink flavors

target.symlink(&link)?;          // Store target as-is
target.symlink_absolute(&link)?; // Resolve target to absolute
target.symlink_relative(&link)?; // Compute relative target from link parent

Atomic linking

target.symlink_atomic(&link)?;
target.symlink_absolute_atomic(&link)?;
target.symlink_relative_atomic(&link)?;

target.hard_link_atomic(&link)?;

Symlink-aware checks

Both .exists() and .try_exists() don't work with broken symlinks:

// Before: Broken symlink
broken.exists()      // false
broken.try_exists()? // false

// After: See the symlink itself
broken.exists_nofollow()      // true
broken.try_exists_nofollow()? // true

The following _nofollow variants are also provided for convenience:

// Infallible — Return false on error like .exists()
path.exists_nofollow()
path.is_file_nofollow()
path.is_dir_nofollow()

// Fallible — Propagate errors just like .try_exists()
path.try_exists_nofollow()
path.try_is_file_nofollow()
path.try_is_dir_nofollow()

Tokio

The standard Path only has sync methods, using the wrong ones will block your async runtime.

Enable the tokio feature flag and PathExt gives you async methods directly on Path:

use path_extra::tokio::path::PathExt as _;

let metadata = config_file.metadata_async().await?;

config_file.with_extension("tmp")
    .write_new(config).await?
    .set_permissions(metadata.permissions()).await?
    .rename(config_file).await?;

Import AsyncPathExt to chain the entire expression and .await only once at the end:

use path_extra::tokio::path::{AsyncPathExt as _, PathExt as _};

config_file.with_extension("tmp")
    .write_new(config)
    .set_permissions(metadata.permissions())
    .rename(config_file)
    .await?;

Both OptionPathExt and AsyncOptionPathExt are available under tokio as well.