Skip to content

Commit 1906b33

Browse files
authored
Merge pull request #3299 from lhns/feature/file-handle-seekable-byte-channel
FileHandle from SeekableByteChannel
2 parents fbd0f25 + 5c23dab commit 1906b33

File tree

4 files changed

+100
-3
lines changed

4 files changed

+100
-3
lines changed

build.sbt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq(
233233
),
234234
ProblemFilters.exclude[ReversedMissingMethodProblem](
235235
"fs2.concurrent.Channel.closeWithElement"
236+
),
237+
ProblemFilters.exclude[InheritedNewAbstractMethodProblem](
238+
"fs2.io.file.Files.openSeekableByteChannel"
236239
)
237240
)
238241

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ package io
2424
package file
2525

2626
import java.nio.ByteBuffer
27-
import java.nio.channels.{FileChannel, FileLock}
27+
import java.nio.channels.{FileChannel, FileLock, SeekableByteChannel}
2828
import java.nio.file.{OpenOption, Path => JPath}
2929

3030
import cats.effect.kernel.{Async, Resource, Sync}
@@ -119,4 +119,59 @@ private[file] trait FileHandleCompanionPlatform {
119119
override def write(bytes: Chunk[Byte], offset: Long): F[Int] =
120120
F.blocking(chan.write(bytes.toByteBuffer, offset))
121121
}
122+
123+
/** Creates a `FileHandle[F]` from a `java.nio.channels.SeekableByteChannel`. Because a `SeekableByteChannel` doesn't provide all the functionalities required by `FileHandle` some features like locking will be unavailable. */
124+
private[file] def makeFromSeekableByteChannel[F[_]](
125+
chan: SeekableByteChannel,
126+
unsupportedOperationException: => Throwable
127+
)(implicit F: Sync[F]): FileHandle[F] =
128+
new FileHandle[F] {
129+
type Lock = Unit
130+
131+
override def force(metaData: Boolean): F[Unit] =
132+
F.raiseError(unsupportedOperationException)
133+
134+
override def lock: F[Lock] =
135+
F.raiseError(unsupportedOperationException)
136+
137+
override def lock(position: Long, size: Long, shared: Boolean): F[Lock] =
138+
F.raiseError(unsupportedOperationException)
139+
140+
override def read(numBytes: Int, offset: Long): F[Option[Chunk[Byte]]] =
141+
F.blocking {
142+
val buf = ByteBuffer.allocate(numBytes)
143+
val len = chan.synchronized {
144+
// don't set position on sequential operations because not all file systems support setting the position
145+
if (chan.position() != offset) { chan.position(offset); () }
146+
chan.read(buf)
147+
}
148+
if (len < 0) None
149+
else if (len == 0) Some(Chunk.empty)
150+
else Some(Chunk.array(buf.array, 0, len))
151+
}
152+
153+
override def size: F[Long] =
154+
F.blocking(chan.size)
155+
156+
override def truncate(size: Long): F[Unit] =
157+
F.blocking { chan.truncate(size); () }
158+
159+
override def tryLock: F[Option[Lock]] =
160+
F.raiseError(unsupportedOperationException)
161+
162+
override def tryLock(position: Long, size: Long, shared: Boolean): F[Option[Lock]] =
163+
F.raiseError(unsupportedOperationException)
164+
165+
override def unlock(f: Lock): F[Unit] =
166+
F.unit
167+
168+
override def write(bytes: Chunk[Byte], offset: Long): F[Int] =
169+
F.blocking {
170+
chan.synchronized {
171+
// don't set position on sequential operations because not all file systems support setting the position
172+
if (chan.position() != offset) { chan.position(offset); () }
173+
chan.write(bytes.toByteBuffer)
174+
}
175+
}
176+
}
122177
}

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ package file
2626
import cats.effect.kernel.{Async, Resource, Sync}
2727
import cats.syntax.all._
2828

29-
import java.nio.channels.FileChannel
29+
import java.nio.channels.{FileChannel, SeekableByteChannel}
3030
import java.nio.file.{Files => JFiles, Path => JPath, _}
3131
import java.nio.file.attribute.{
3232
BasicFileAttributeView,
@@ -47,6 +47,12 @@ private[file] trait FilesPlatform[F[_]] extends DeprecatedFilesApi[F] { self: Fi
4747
/** Creates a `FileHandle` for the supplied NIO `FileChannel`. JVM only. */
4848
def openFileChannel(channel: F[FileChannel]): Resource[F, FileHandle[F]]
4949

50+
/** Creates a `FileHandle` for the supplied NIO `SeekableByteChannel`. Because a `SeekableByteChannel` doesn't provide all the functionalities required by `FileHandle` some features like locking will be unavailable. JVM only. */
51+
def openSeekableByteChannel(
52+
channel: F[SeekableByteChannel],
53+
unsupportedOperationException: => Throwable
54+
): Resource[F, FileHandle[F]]
55+
5056
/** Gets the contents of the specified directory whose paths match the supplied glob pattern.
5157
*
5258
* Example glob patterns: `*.scala`, `*.{scala,java}`
@@ -314,11 +320,25 @@ private[file] trait FilesCompanionPlatform {
314320
def open(path: Path, flags: Flags): Resource[F, FileHandle[F]] =
315321
openFileChannel(
316322
Sync[F].blocking(FileChannel.open(path.toNioPath, flags.value.map(_.option): _*))
317-
)
323+
).recoverWith { case unsupportedOperationException: UnsupportedOperationException =>
324+
// not all file systems support file channels
325+
openSeekableByteChannel(
326+
Sync[F].blocking(JFiles.newByteChannel(path.toNioPath, flags.value.map(_.option): _*)),
327+
unsupportedOperationException
328+
)
329+
}
318330

319331
def openFileChannel(channel: F[FileChannel]): Resource[F, FileHandle[F]] =
320332
Resource.make(channel)(ch => Sync[F].blocking(ch.close())).map(ch => FileHandle.make(ch))
321333

334+
def openSeekableByteChannel(
335+
channel: F[SeekableByteChannel],
336+
unsupportedOperationException: => Throwable
337+
): Resource[F, FileHandle[F]] =
338+
Resource
339+
.make(channel)(ch => Sync[F].blocking(ch.close()))
340+
.map(ch => FileHandle.makeFromSeekableByteChannel(ch, unsupportedOperationException))
341+
322342
def realPath(path: Path): F[Path] =
323343
Sync[F].blocking(Path.fromNioPath(path.toNioPath.toRealPath()))
324344

io/jvm/src/test/scala/fs2/io/file/JvmFilesSuite.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,23 @@ class JvmFilesSuite extends Fs2Suite with BaseFileSuite {
6262
}
6363
}
6464

65+
test("read from SeekableByteChannel") {
66+
Stream
67+
.resource(
68+
tempFile
69+
.evalMap(modify)
70+
.flatMap(path =>
71+
Files[IO].openSeekableByteChannel(
72+
IO.blocking(JFiles.newByteChannel(path.toNioPath, Flag.Read.option)),
73+
new UnsupportedOperationException()
74+
)
75+
)
76+
.map(new ReadCursor[IO](_, 0))
77+
)
78+
.flatMap(_.readAll(4096).void.stream)
79+
.compile
80+
.toList
81+
.assertEquals(List[Byte](0, 1, 2, 3))
82+
}
83+
6584
}

0 commit comments

Comments
 (0)