Skip to content

Commit 5b18a37

Browse files
authored
Merge pull request #3390 from typelevel/topic/walk-with-attributes
Add `Files.walkWithAttributes`
2 parents 7509023 + 30e09a7 commit 5b18a37

File tree

5 files changed

+94
-50
lines changed

5 files changed

+94
-50
lines changed

io/jvm-native/src/main/scala/fs2/io/file/FilesPlatform.scala

+20-11
Original file line numberDiff line numberDiff line change
@@ -391,34 +391,43 @@ private[file] trait FilesCompanionPlatform {
391391
.resource(Resource.fromAutoCloseable(javaCollection))
392392
.flatMap(ds => Stream.fromBlockingIterator[F](collectionIterator(ds), pathStreamChunkSize))
393393

394-
protected def walkEager(start: Path, options: WalkOptions): Stream[F, Path] = {
394+
protected def walkEager(start: Path, options: WalkOptions): Stream[F, PathInfo] = {
395395
val doWalk = Sync[F].interruptible {
396-
val bldr = Vector.newBuilder[Path]
396+
val bldr = Vector.newBuilder[PathInfo]
397397
JFiles.walkFileTree(
398398
start.toNioPath,
399399
if (options.followLinks) Set(FileVisitOption.FOLLOW_LINKS).asJava else Set.empty.asJava,
400400
options.maxDepth,
401401
new SimpleFileVisitor[JPath] {
402-
private def enqueue(path: JPath): FileVisitResult = {
403-
bldr += Path.fromNioPath(path)
402+
private def enqueue(path: JPath, attrs: JBasicFileAttributes): FileVisitResult = {
403+
bldr += PathInfo(Path.fromNioPath(path), new DelegatingBasicFileAttributes(attrs))
404404
FileVisitResult.CONTINUE
405405
}
406406

407407
override def visitFile(file: JPath, attrs: JBasicFileAttributes): FileVisitResult =
408-
if (Thread.interrupted()) FileVisitResult.TERMINATE else enqueue(file)
408+
if (Thread.interrupted()) FileVisitResult.TERMINATE else enqueue(file, attrs)
409409

410410
override def visitFileFailed(file: JPath, t: IOException): FileVisitResult =
411411
t match {
412412
case _: FileSystemLoopException =>
413-
if (options.allowCycles) enqueue(file) else throw t
413+
if (options.allowCycles)
414+
enqueue(
415+
file,
416+
JFiles.readAttributes(
417+
file,
418+
classOf[JBasicFileAttributes],
419+
LinkOption.NOFOLLOW_LINKS
420+
)
421+
)
422+
else throw t
414423
case _ => FileVisitResult.CONTINUE
415424
}
416425

417426
override def preVisitDirectory(
418427
dir: JPath,
419428
attrs: JBasicFileAttributes
420429
): FileVisitResult =
421-
if (Thread.interrupted()) FileVisitResult.TERMINATE else enqueue(dir)
430+
if (Thread.interrupted()) FileVisitResult.TERMINATE else enqueue(dir, attrs)
422431

423432
override def postVisitDirectory(dir: JPath, t: IOException): FileVisitResult =
424433
if (Thread.interrupted()) FileVisitResult.TERMINATE else FileVisitResult.CONTINUE
@@ -439,18 +448,18 @@ private[file] trait FilesCompanionPlatform {
439448
protected def walkJustInTime(
440449
start: Path,
441450
options: WalkOptions
442-
): Stream[F, Path] = {
451+
): Stream[F, PathInfo] = {
443452
import scala.collection.immutable.Queue
444453

445-
def loop(toWalk0: Queue[WalkEntry]): Stream[F, Path] = {
454+
def loop(toWalk0: Queue[WalkEntry]): Stream[F, PathInfo] = {
446455
val partialWalk = Sync[F].interruptible {
447-
var acc = Vector.empty[Path]
456+
var acc = Vector.empty[PathInfo]
448457
var toWalk = toWalk0
449458

450459
while (acc.size < options.chunkSize && toWalk.nonEmpty && !Thread.interrupted()) {
451460
val entry = toWalk.head
452461
toWalk = toWalk.drop(1)
453-
acc = acc :+ entry.path
462+
acc = acc :+ PathInfo(entry.path, new DelegatingBasicFileAttributes(entry.attr))
454463
if (entry.depth < options.maxDepth) {
455464
val dir =
456465
if (entry.attr.isDirectory) entry.path

io/jvm/src/main/scala/fs2/io/file/AsyncFilesPlatform.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@ package file
2525

2626
private[file] trait AsyncFilesPlatform[F[_]] { self: Files.UnsealedFiles[F] =>
2727

28-
override def walk(
28+
override def walkWithAttributes(
2929
start: Path,
3030
options: WalkOptions
31-
): Stream[F, Path] =
31+
): Stream[F, PathInfo] =
3232
if (options.chunkSize == Int.MaxValue) walkEager(start, options)
3333
else walkJustInTime(start, options)
3434

35-
protected def walkEager(start: Path, options: WalkOptions): Stream[F, Path]
35+
protected def walkEager(start: Path, options: WalkOptions): Stream[F, PathInfo]
3636

3737
protected def walkJustInTime(
3838
start: Path,
3939
options: WalkOptions
40-
): Stream[F, Path]
40+
): Stream[F, PathInfo]
4141
}

io/native/src/main/scala/fs2/io/file/AsyncFilesPlatform.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ package io
2424
package file
2525

2626
private[file] trait AsyncFilesPlatform[F[_]] { self: Files.UnsealedFiles[F] =>
27-
override def walk(
27+
override def walkWithAttributes(
2828
start: Path,
2929
options: WalkOptions
30-
): Stream[F, Path] =
30+
): Stream[F, PathInfo] =
3131
// Disable eager walks until https://github.com/scala-native/scala-native/issues/3744
3232
walkJustInTime(start, options)
3333

3434
protected def walkJustInTime(
3535
start: Path,
3636
options: WalkOptions
37-
): Stream[F, Path]
37+
): Stream[F, PathInfo]
3838
}

io/shared/src/main/scala/fs2/io/file/Files.scala

+42-32
Original file line numberDiff line numberDiff line change
@@ -385,14 +385,22 @@ sealed trait Files[F[_]] extends FilesPlatform[F] {
385385
* For example, to eagerly walk a directory while following symbolic links, emitting all
386386
* paths as a single chunk, use `walk(start, WalkOptions.Eager.withFollowLinks(true))`.
387387
*/
388-
def walk(start: Path, options: WalkOptions): Stream[F, Path]
388+
def walk(start: Path, options: WalkOptions): Stream[F, Path] =
389+
walkWithAttributes(start, options).map(_.path)
389390

390391
/** Creates a stream of paths contained in a given file tree down to a given depth.
391392
*/
392393
@deprecated("Use walk(start, WalkOptions.Default.withMaxDepth(..).withFollowLinks(..))", "3.10")
393394
def walk(start: Path, maxDepth: Int, followLinks: Boolean): Stream[F, Path] =
394395
walk(start, WalkOptions.Default)
395396

397+
/** Like `walk` but returns a `PathInfo`, which provides both the `Path` and `BasicFileAttributes`. */
398+
def walkWithAttributes(start: Path): Stream[F, PathInfo] =
399+
walkWithAttributes(start, WalkOptions.Default)
400+
401+
/** Like `walk` but returns a `PathInfo`, which provides both the `Path` and `BasicFileAttributes`. */
402+
def walkWithAttributes(start: Path, options: WalkOptions): Stream[F, PathInfo]
403+
396404
/** Writes all data to the file at the specified path.
397405
*
398406
* The file is created if it does not exist and is truncated.
@@ -517,44 +525,46 @@ object Files extends FilesCompanionPlatform with FilesLowPriority {
517525
case _: NoSuchFileException => ()
518526
})
519527

520-
def walk(start: Path, options: WalkOptions): Stream[F, Path] = {
528+
def walkWithAttributes(start: Path, options: WalkOptions): Stream[F, PathInfo] = {
521529

522-
def go(start: Path, maxDepth: Int, ancestry: List[Either[Path, FileKey]]): Stream[F, Path] =
523-
Stream.emit(start) ++ {
524-
if (maxDepth == 0) Stream.empty
525-
else
526-
Stream.eval(getBasicFileAttributes(start, followLinks = false)).mask.flatMap { attr =>
527-
if (attr.isDirectory)
528-
list(start).mask.flatMap { path =>
529-
go(path, maxDepth - 1, attr.fileKey.toRight(start) :: ancestry)
530+
def go(
531+
start: Path,
532+
maxDepth: Int,
533+
ancestry: List[Either[Path, FileKey]]
534+
): Stream[F, PathInfo] =
535+
Stream.eval(getBasicFileAttributes(start, followLinks = false)).mask.flatMap { attr =>
536+
Stream.emit(PathInfo(start, attr)) ++ {
537+
if (maxDepth == 0) Stream.empty
538+
else if (attr.isDirectory)
539+
list(start).mask.flatMap { path =>
540+
go(path, maxDepth - 1, attr.fileKey.toRight(start) :: ancestry)
541+
}
542+
else if (attr.isSymbolicLink && options.followLinks)
543+
Stream.eval(getBasicFileAttributes(start, followLinks = true)).mask.flatMap { attr =>
544+
val fileKey = attr.fileKey
545+
val isCycle = Traverse[List].existsM(ancestry) {
546+
case Right(ancestorKey) => F.pure(fileKey.contains(ancestorKey))
547+
case Left(ancestorPath) => isSameFile(start, ancestorPath)
530548
}
531-
else if (attr.isSymbolicLink && options.followLinks)
532-
Stream.eval(getBasicFileAttributes(start, followLinks = true)).mask.flatMap {
533-
attr =>
534-
val fileKey = attr.fileKey
535-
val isCycle = Traverse[List].existsM(ancestry) {
536-
case Right(ancestorKey) => F.pure(fileKey.contains(ancestorKey))
537-
case Left(ancestorPath) => isSameFile(start, ancestorPath)
538-
}
539549

540-
Stream.eval(isCycle).flatMap { isCycle =>
541-
if (!isCycle)
542-
list(start).mask.flatMap { path =>
543-
go(path, maxDepth - 1, attr.fileKey.toRight(start) :: ancestry)
544-
}
545-
else if (options.allowCycles)
546-
Stream.empty
547-
else
548-
Stream.raiseError(new FileSystemLoopException(start.toString))
550+
Stream.eval(isCycle).flatMap { isCycle =>
551+
if (!isCycle)
552+
list(start).mask.flatMap { path =>
553+
go(path, maxDepth - 1, attr.fileKey.toRight(start) :: ancestry)
549554
}
550-
555+
else if (options.allowCycles)
556+
Stream.empty
557+
else
558+
Stream.raiseError(new FileSystemLoopException(start.toString))
551559
}
552-
else
553-
Stream.empty
554-
}
560+
561+
}
562+
else
563+
Stream.empty
564+
}
555565
}
556566

557-
Stream.eval(getBasicFileAttributes(start, options.followLinks)) >> go(
567+
go(
558568
start,
559569
options.maxDepth,
560570
Nil
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2013 Functional Streams for Scala
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8+
* the Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package fs2.io.file
23+
24+
/** Provides a `Path` and its associated `BasicFileAttributes`. */
25+
case class PathInfo(path: Path, attributes: BasicFileAttributes)

0 commit comments

Comments
 (0)