@@ -4,10 +4,10 @@ import java.io.{BufferedInputStream, BufferedReader, ByteArrayOutputStream, Inpu
4
4
import java .nio .file .{FileAlreadyExistsException , Files }
5
5
import java .nio .file .attribute .{PosixFilePermission , PosixFilePermissions }
6
6
import java .util .zip .GZIPOutputStream
7
-
8
7
import better .files .File .OpenOptions
9
8
import cromwell .util .TryWithResource .tryWithResource
10
9
10
+ import java .nio .channels .Channels
11
11
import scala .jdk .CollectionConverters ._
12
12
import scala .concurrent .ExecutionContext
13
13
import scala .io .Codec
@@ -86,12 +86,34 @@ trait EvenBetterPathMethods {
86
86
87
87
final def tailed (tailedSize : Int ) = TailedWriter (this , tailedSize)
88
88
89
+ /**
90
+ * Similar to newInputStream, but is overridable in subclasses to provide a different InputStream.
91
+ *
92
+ * Naming comes from the original method used from Google Cloud Storage, executeMediaAsInputStream()
93
+ */
89
94
def mediaInputStream (implicit ec : ExecutionContext ): InputStream = {
95
+ // Use locally(ec) to avoid "parameter value ec in method mediaInputStream is never used"
96
+ // It is used in the overridden methods, but not here.
90
97
// See https://github.com/scala/bug/issues/10347 and https://github.com/scala/bug/issues/10790
91
98
locally(ec)
92
99
newInputStream
93
100
}
94
101
102
+ /**
103
+ * A tail version of mediaInputStream that reads the last numBytes of the file.
104
+ */
105
+ def mediaInputStreamTail (numBytes : Long )(implicit ec : ExecutionContext ): InputStream = {
106
+ // Use locally(ec) to avoid "parameter value ec in method mediaInputStreamTail is never used"
107
+ // It is used in the overridden methods, but not here.
108
+ // See https://github.com/scala/bug/issues/10347 and https://github.com/scala/bug/issues/10790
109
+ locally(ec)
110
+ val channel = newFileChannel
111
+ val size = channel.size()
112
+ val startPosition = Math .max(0 , size - numBytes)
113
+ channel.position(startPosition)
114
+ Channels .newInputStream(channel)
115
+ }
116
+
95
117
protected def gzipByteArray (byteArray : Array [Byte ]): Array [Byte ] = {
96
118
val byteStream = new ByteArrayOutputStream
97
119
tryWithResource(() => new GZIPOutputStream (byteStream)) {
@@ -132,6 +154,14 @@ trait EvenBetterPathMethods {
132
154
def withBufferedStream [A ](f : BufferedInputStream => A )(implicit ec : ExecutionContext ): A =
133
155
tryWithResource(() => new BufferedInputStream (this .mediaInputStream))(f).recoverWith(fileIoErrorPf).get
134
156
157
+ /**
158
+ * Similar to withBufferedStream, but reads from the tail of the stream.
159
+ */
160
+ def withBufferedStreamTail [A ](numBytes : Long )(f : BufferedInputStream => A )(implicit ec : ExecutionContext ): A =
161
+ tryWithResource(() => new BufferedInputStream (this .mediaInputStreamTail(numBytes)))(f)
162
+ .recoverWith(fileIoErrorPf)
163
+ .get
164
+
135
165
/**
136
166
* Returns an Array[Byte] from a Path. Limit the array size to "limit" byte if defined.
137
167
* @throws IOException if failOnOverflow is true and the file is larger than limit
@@ -151,9 +181,35 @@ trait EvenBetterPathMethods {
151
181
}
152
182
153
183
/**
154
- * Reads the first limitBytes of a file and makes a String. Prepend with an annotation at the start (to say that this is the
155
- * first n bytes).
184
+ * Reads the last bytes of a file and returns a String. If the file is larger than limit, it will
185
+ * drop the first line and return the rest of the file as a String.
186
+ */
187
+ def tailLines (limitBytes : Int )(implicit ec : ExecutionContext ): String =
188
+ // Add one because we'll be dropping until the end of the first line if the file is larger than limitBytes
189
+ withBufferedStreamTail(limitBytes + 1L ) { bufferedStream =>
190
+ val bytes = bufferedStream.readAllBytes()
191
+ if (bytes.length <= limitBytes) {
192
+ new String (bytes, Codec .UTF8 .charSet)
193
+ } else {
194
+ val newlineIndex = bytes.indexOf('\n ' )
195
+ if (newlineIndex < 0 ) {
196
+ " "
197
+ } else {
198
+ new String (bytes.drop(newlineIndex + 1 ), Codec .UTF8 .charSet)
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Reads the limitBytes of a file and makes a String. Prepend with an annotation at the start (to say that this is a
205
+ * limited number of bytes).
156
206
*/
157
207
def annotatedContentAsStringWithLimit (limitBytes : Int )(implicit ec : ExecutionContext ): String =
158
- s " [First $limitBytes bytes]: " + new String (limitFileContent(Option (limitBytes), failOnOverflow = false ))
208
+ try
209
+ s " [Last $limitBytes bytes]: " + tailLines(limitBytes)
210
+ catch {
211
+ // If anything goes wrong, such as we cannot seek to the end of the file, we will just return the first n bytes.
212
+ case _ : Exception =>
213
+ s " [First $limitBytes bytes]: " + new String (limitFileContent(Option (limitBytes), failOnOverflow = false ))
214
+ }
159
215
}
0 commit comments