better-files

repository·master·Indexed 23 days ago

https://github.com/pathikrit/better-files

A pragmatic, dependency-free Scala wrapper around Java NIO designed for memory-efficient file system operations. It provides a high-level API and DSL for streaming, filtering, sorting, and compressing files, as well as utilities for globbing, checksums, POSIX permissions, and reactive file monitoring via Akka.

Tokens
8.2K
Snippets
15
Records
51
Agent score
81%

What's inside better-files

  1. Overview of better-files

    master
    better-files is a pragmatic, dependency-free, thin Scala wrapper around Java NIO. It is designed to simplify complex file system operations—such as filtering, sorting, streaming, and compressing files—while ensuring that resources are managed correctly and memory usage remains low even when processing files larger than the JVM heap.
  2. Install better-files

    master

    Add better-files to your Scala project using sbt or mill.

    // sbt
    libraryDependencies += "com.github.pathikrit" %% "better-files" % version
    // mill
    import mill._, scalalib._
    
    object moduleName extends ScalaModule {
      override def ivyDeps = Agg(
        ivy"com.github.pathikrit::better-files:${version}"
      )
    }
  3. Access classpath resources using the Resource API

    master

    The Resource API provides a way to find and load files from the classpath. The default Resource object uses the current thread's context class loader. You can access resources as InputStream, String, or URL.

    Lookup Strategies

    • Resource.url(name): Uses the current thread's context class loader.
    • Resource.from(classLoader).url(name): Uses a specific ClassLoader to find the resource.
    • Resource.at(clazz).url(name): Searches starting from a specific Class.
    • Resource.my.url(name): Searches from the class, trait, or object surrounding the call site.
    // Look up the config.properties file for this class or object.
    Resource.my.asStream("config.properties")
    
    // Find logging.properties (in the root package) somewhere on the classpath.
    Resource.url("logging.properties")
  4. Create and manage temporary files and directories

    master

    Better-files provides several ways to handle temporary resources.

    Manual Creation

    Use File.newTemporaryFile or File.newTemporaryDirectory to create resources that you manage manually.

    Automatic Cleanup with Dispose

    To avoid the overhead and shutdown delays of Java's deleteOnExit(), use File.temporaryFile or File.temporaryDirectory. These return a Dispose[File] object. When the Dispose object is closed (or used within a foreach block), the file/directory is automatically deleted.

    Scoped Usage

    Use File.usingTemporaryFile or File.usingTemporaryDirectory to execute a block of code within a temporary context. The resource is automatically cleaned up after the block completes.

  5. Perform simple File Read/Write

    master

    Use overwrite, appendLine, and append for basic I/O. You can also use the SymbolicOperations DSL for a more concise syntax.

    // Standard API
    val file = root"/tmp"/"test.txt"
    file.overwrite("hello")
    file.appendLine().append("world")
    assert(file.contentAsString() == "hello\nworld")
    
    // Using SymbolicOperations DSL
    import better.files.Dsl.SymbolicOperations
    
    file < "hello"     // same as file.overwrite("hello")
    file << "world"    // same as file.appendLines("world")
    assert(file! == "hello\nworld")
    
    // Right-associative syntax
    import better.files.Dsl.SymbolicOperations
    "hello" `>:` file
    "world" >>: file
  6. Reactive File Watching with Akka

    master

    For high-performance, reactive file monitoring, you can use the Akka-based watcher which supports dynamic dispatching via actors.

    import akka.actor.{ActorRef, ActorSystem}
    import better.files.File.home
    import better.files.FileWatcher._
    import java.nio.file.{StandardWatchEventKinds => EventType}
    
    implicit val system = ActorSystem("mySystem")
    val watcher: ActorRef = (home/"Downloads").newWatcher(recursive = true)
    
    // Register partial function for specific events
    watcher ! on(EventType.ENTRY_DELETE) {
      case file if file.isDirectory => println(s"directory $file got deleted")
      case file                     => println(s"$file got deleted")
    }
    
    // Watch multiple events
    watcher ! when(events = EventType.ENTRY_CREATE, EventType.ENTRY_MODIFY) {
      case (EventType.ENTRY_CREATE, file) => println(s"$file got created")
      case (EventType.ENTRY_MODIFY, file) => println(s"$file got modified")
    }
  7. Run scanner benchmarks

    master

    To run the performance benchmarks for various file scanners (such as JavaScanner, StreamingScanner, or BetterFilesScanner), use the following sbt command. This allows you to compare the execution times of different scanning implementations.

    > sbt "testOnly better.files.benchmarks.*"
  8. Complex file processing example with better-files

    master

    The following example demonstrates how better-files can be used to perform a complex sequence of operations: listing specific files, sorting them by size, skipping headers, distributing lines across multiple gzipped output files, and ensuring all resources are automatically closed.

    Key features used in this example:

    • import better.files._ to access the API.
    • Path joining using the / operator (e.g., outputDir / s"part-$i.csv.gz").
    • list with a predicate to filter files (e.g., .extension == Some(".csv")).
    • File.Order.bySize for sorting.
    • lineIterator for memory-efficient line-by-line processing.
    • newGzipOutputStream().printWriter() for compressed output.
    • .autoClosed to ensure resource safety in a for-comprehension.
    import better.files._
    
    def run(inputDir: File, outputDir: File, n: Int) = {
      val count = new AtomicInteger()
      val outputs = Vector.tabulate(n)(i => outputDir / s"part-$i.csv.gz")
      for {
        writers <- outputs.map(_.newGzipOutputStream().printWriter()).autoClosed
        inputFile <- inputDir.list(_.extension == Some(".csv")).toSeq.sorted(File.Order.bySize)
        line <- inputFile.lineIterator.drop(1)
      } writers(count.incrementAndGet() % n).println(line)
    }
  9. Manage File System operations

    master

    The library provides high-level wrappers for common UNIX-like operations such as touch, delete, copyTo, moveTo, and linkTo. Unlike the standard Java API, delete() and copyTo() work recursively on directories.

    file.touch()
    file.delete()             // recursive delete
    file.clear()              // clears file or deletes directory children
    file.renameTo("new_name")
    file.moveTo(destination)
    file.copyTo(destination)   // recursive copy
    file.linkTo(destination)   // ln
    file.symbolicLinkTo(destination) // ln -s
    
    // UNIX DSL style
    import better.files.Dsl._
    cp(file1, file2)
    mv(file1, file2)
    rm(file)
    ls(file)
  10. Use the Scanner API for type-safe parsing

    master
    The Scanner API is a faster, safer, and more idiomatic Scala replacement for java.util.Scanner. It supports peeking, line numbers, and custom parsers via the Scannable trait. It can also scan HLists (via Shapeless) and case classes.
  11. Monitor File System changes

    master
    The FileMonitor class provides a simple interface for watching directory changes (creation, modification, deletion). It supports recursive watching and can be used with a simple callback or by overriding onEvent for more granular control.
  12. Create and manage Temporary Files

    master
    Use File.newTemporaryFile() or File.newTemporaryDirectory() for standard temporary files. For safer resource management, use File.usingTemporaryFile or the for comprehension with .toTemporary to ensure files are deleted immediately after use, even if exceptions occur.