edadma / path   0.0.4

ISC License GitHub
Scala versions: 3.x
Scala.js versions: 1.x
Scala Native versions: 0.5

Path

Maven Central Last Commit GitHub Scala Version ScalaJS Version Scala Native Version

A clean, modern file system library for Scala that works consistently across JVM, Scala.js, and Scala Native.

Why Path?

Java's file operations are verbose and clunky:

// Java way - verbose and error-prone
val configPath = Paths.get("config").resolve("app.conf")
if (Files.exists(configPath)) {
  val content = new String(Files.readAllBytes(configPath), StandardCharsets.UTF_8)
}

// Path way - clean and fluent
val configPath = Path("config") / "app.conf"
if (configPath.exists) {
  val content = configPath.readText()
}

Path gives you:

  • Fluent API: Chain operations naturally with /
  • Cross-platform: Same code works on JVM, JS, and Native
  • Method-based: File operations as methods, not static functions
  • Rich path manipulation: Extensions, subpaths, absolute conversion
  • Complete metadata: Permissions, file types, comparisons
  • Glob support: Built-in pattern matching for file listing
  • Type safety: Case class benefits (equality, hashing, pattern matching)

Quick Start

import io.github.edadma.path._

// Create paths naturally
val projectDir = Path("src") / "main" / "scala"
val configFile = Path("/etc") / "myapp" / "config.json"

// File operations as methods (the way it should be!)
if (configFile.exists) {
  val config = configFile.readText()
  println(s"Config: $config")
}

// Rich path manipulation
val scalaFile = Path("MyClass.scala")
println(s"Extension: ${scalaFile.extension}")           // .scala
println(s"Name: ${scalaFile.nameWithoutExtension}")     // MyClass
val javaFile = scalaFile.withExtension("java")          // MyClass.java

// Convert to absolute paths
val absolutePath = projectDir.toAbsolutePath
println(s"Absolute: $absolutePath")

// Create directory structure
val buildDir = Path("target") / "classes"
buildDir.createDirectories()

// List files with patterns
val scalaFiles = projectDir.listDirectory("*.scala")
scalaFiles.foreach { entry =>
  println(s"Found: ${entry.name}")
}

// Check file permissions and metadata
if (configFile.isReadable && !configFile.isEmpty) {
  println("Config file is readable and not empty")
}

Core Features

Path Construction and Manipulation

// Path construction and combination
val base = Path("/home/user")
val docs = base / "documents" / "projects"

// Rich path manipulation
val sourceFile = Path("/projects/myapp/src/main/scala/MyClass.scala")

// File extensions
println(sourceFile.extension)              // .scala
println(sourceFile.nameWithoutExtension)   // MyClass
val testFile = sourceFile.withExtension("test.scala")  // MyClass.test.scala

// Path segments and subpaths
println(sourceFile.subpath(2, 5))          // src/main/scala
println(sourceFile.startsWith(Path("/projects")))  // true
println(sourceFile.endsWith(Path("MyClass.scala"))) // true

// Convert relative to absolute
val relPath = Path("src/main/scala")
val absPath = relPath.toAbsolutePath     // /current/working/dir/src/main/scala

// Relative paths between locations
val targetDir = Path("/home/user/projects/myapp/target")
val relative = targetDir.relativeTo(base)  // projects/myapp/target

// Path normalization
val messy = Path("src/../config/./app.conf")
val clean = messy.normalize                 // config/app.conf

// Parent and filename
val file = Path("/home/user/document.pdf")
println(file.parent)    // Some(/home/user)
println(file.filename)  // document.pdf

File System Metadata

val file = Path("important-document.pdf")

// Existence and type checking
println(s"Exists: ${file.exists}")
println(s"Is file: ${file.isFile}")
println(s"Is directory: ${file.isDirectory}")
println(s"Is symbolic link: ${file.isSymbolicLink}")

// Permission checking
println(s"Readable: ${file.isReadable}")
println(s"Writable: ${file.isWritable}")
println(s"Executable: ${file.isExecutable}")

// File properties
println(s"Size: ${file.size} bytes")
println(s"Modified: ${file.lastModified}")
println(s"Empty: ${file.isEmpty}")

// Compare files
val backup = Path("document-backup.pdf")
if (file.isSameFile(backup)) {
  println("Files are identical (same inode/reference)")
}

File Operations

val file = Path("data.txt")

// Text files
file.writeText("Hello, World!")
val content = file.readText()

// Binary files  
val data = Array[Byte](1, 2, 3, 4)
file.writeBytes(data)
val bytes = file.readBytes

// Check if file/directory is empty
if (file.isEmpty) {
  println("File has no content")
}

Directory Operations

val dir = Path("build")

// Create directories
dir.createDirectories()  // Creates parents too

// List contents with filtering
val allFiles = dir.listDirectory()
val jsonFiles = dir.listDirectory("*.json")
val appFiles = dir.listDirectory("app*")

allFiles.foreach { entry =>
  val typeStr = entry.fileType match {
    case FileType.File => "FILE"
    case FileType.Directory => "DIR"
    case FileType.SymbolicLink => "LINK"
    case FileType.Other => "OTHER"
  }
  println(s"$typeStr: ${entry.name}")
}

// Check if directory is empty
if (dir.isEmpty) {
  println("Directory contains no files")
}

File Management

val source = Path("document.pdf")
val backup = Path("backup") / "document.pdf"
val archive = Path("archive") / "document.pdf"

// Copy and move operations
source.copyTo(backup)    // Copy to backup location
source.moveTo(archive)   // Move to archive

// Cleanup
backup.delete()

Advanced Path Operations

Working with Extensions

// Handle various file types
val files = Vector(
  Path("README.md"),
  Path("app.conf"),
  Path("data.json"),
  Path("script.sh"),
  Path("archive.tar.gz")
)

files.foreach { file =>
  println(s"File: ${file.filename}")
  println(s"  Extension: '${file.extension}'")
  println(s"  Without ext: '${file.nameWithoutExtension}'")
  println(s"  As backup: ${file.withExtension(file.extension + ".bak")}")
  println()
}

Path Hierarchy Navigation

val projectRoot = Path("/projects/myapp")
val sourceDir = projectRoot / "src" / "main" / "scala"
val testDir = projectRoot / "src" / "test" / "scala"

// Check relationships
println(sourceDir.startsWith(projectRoot))  // true
println(sourceDir.endsWith(Path("scala")))  // true

// Get relative paths between directories
val relativeToTest = sourceDir.relativeTo(testDir)
println(relativeToTest)  // ../../main/scala

// Extract specific path segments
val pathSegments = sourceDir.subpath(1, 4)  // projects/myapp/src

File System Analysis

def analyzeDirectory(dir: Path): Unit = {
  if (!dir.exists || !dir.isDirectory) {
    println(s"$dir is not a valid directory")
    return
  }
  
  val entries = dir.listDirectory()
  val (files, dirs, links) = entries.partition(_.fileType).fold(
    (Vector.empty, Vector.empty, Vector.empty)
  ) { case ((f, d, l), entry) =>
    entry.fileType match {
      case FileType.File => (f :+ entry, d, l)
      case FileType.Directory => (f, d :+ entry, l)
      case FileType.SymbolicLink => (f, d, l :+ entry)
      case FileType.Other => (f, d, l)
    }
  }
  
  println(s"Analysis of $dir:")
  println(s"  Files: ${files.length}")
  println(s"  Directories: ${dirs.length}")
  println(s"  Symbolic links: ${links.length}")
  println(s"  Empty: ${dir.isEmpty}")
  
  // Find largest files
  val fileSizes = files.map { entry =>
    val filePath = dir / entry.name
    (entry.name, filePath.size)
  }.sortBy(-_._2)
  
  println("  Largest files:")
  fileSizes.take(3).foreach { case (name, size) =>
    println(s"    $name: $size bytes")
  }
}

Cross-Platform Architecture

Path provides a unified API while using the best platform-specific implementations:

  • JVM: Uses java.nio.files for robust, high-performance file operations
  • Scala.js: Uses Node.js fs and path modules for full file system access
  • Scala Native: Uses Java NIO compatibility layer for native performance

The same Path code compiles and runs identically across all platforms.

Installation

Add to your build.sbt:

libraryDependencies += "io.github.edadma" %% "path" % "0.0.2"

For cross-platform projects:

// shared/src/main/scala - your cross-platform code using Path
// jvm/src/main/scala    - JVM-specific implementations  
// js/src/main/scala     - JS-specific implementations
// native/src/main/scala - Native-specific implementations

Examples

Configuration Management

val configDir = Path(System.getProperty("user.home")) / ".myapp"
configDir.createDirectories()

val configFile = configDir / "config.json"
if (!configFile.exists) {
  configFile.writeText("""{"theme": "dark", "autoSave": true}""")
}

// Validate config file
if (configFile.isReadable && !configFile.isEmpty) {
  val config = configFile.readText()
  println(s"Loaded config: ${config.length} characters")
}

Build Tool Integration

val sourceDir = Path("src") / "main" / "scala"
val targetDir = Path("target") / "classes"

// Find all Scala source files
val scalaFiles = sourceDir.listDirectory("*.scala")
println(s"Found ${scalaFiles.length} Scala files")

// Analyze source files
scalaFiles.foreach { entry =>
  val sourcePath = sourceDir / entry.name
  println(s"${sourcePath.filename}: ${sourcePath.size} bytes")
  
  // Convert to target path
  val targetPath = targetDir / sourcePath.nameWithoutExtension.withExtension("class")
  println(s"${targetPath}")
}

// Compile to target directory
targetDir.createDirectories()

Log File Management

val logDir = Path("logs")
logDir.createDirectories()

// Find and archive old logs
val logFiles = logDir.listDirectory("*.log")
val archiveDir = logDir / "archive"

if (logFiles.nonEmpty) {
  archiveDir.createDirectories()
  
  logFiles.foreach { entry =>
    val logFile = logDir / entry.name
    
    // Check if log file should be archived (e.g., older than 30 days)
    val ageMs = System.currentTimeMillis() - logFile.lastModified
    val ageDays = ageMs / (1000 * 60 * 60 * 24)
    
    if (ageDays > 30) {
      val archiveFile = archiveDir / entry.name
      println(s"Archiving ${logFile.filename} (${ageDays} days old)")
      logFile.moveTo(archiveFile)
    }
  }
}

// Clean up empty directories
if (logDir.isEmpty) {
  println("Log directory is empty")
}

File System Utilities

def findLargestFiles(dir: Path, count: Int = 10): Vector[(Path, Long)] = {
  if (!dir.exists || !dir.isDirectory) {
    return Vector.empty
  }
  
  val allFiles = dir.listDirectory()
    .filter(_.fileType == FileType.File)
    .map(entry => dir / entry.name)
    .filter(_.isReadable)
  
  allFiles
    .map(file => (file, file.size))
    .sortBy(-_._2)
    .take(count)
}

def findFilesByExtension(dir: Path, extension: String): Vector[Path] = {
  val pattern = if (extension.startsWith(".")) s"*$extension" else s"*.$extension"
  
  dir.listDirectory(pattern)
    .map(entry => dir / entry.name)
    .filter(_.isFile)
}

// Usage
val projectDir = Path(".")
val largestFiles = findLargestFiles(projectDir)
val scalaFiles = findFilesByExtension(projectDir, ".scala")

println("Largest files:")
largestFiles.foreach { case (file, size) =>
  println(s"  ${file.filename}: $size bytes")
}

println(s"\nFound ${scalaFiles.length} Scala files")

Testing

Path includes comprehensive tests covering:

  • Path construction and manipulation
  • File and directory operations
  • Extension handling and path conversion
  • Permission and metadata checking
  • Cross-platform compatibility
  • Package manager scenarios

Run tests with:

sbt test

Contributing

This library was built to solve real-world file system challenges in Scala. Contributions are welcome!

Upcoming features:

  • Watch APIs for file system monitoring
  • Streaming operations for large files
  • Advanced glob patterns with recursion
  • File system event notifications

License

ISC License - see LICENSE file for details.


Finally, a file system library that doesn't make you hate working with files in Scala.