-
Notifications
You must be signed in to change notification settings - Fork 423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support ranges request handling #1527
Changes from 6 commits
7cca165
768edb8
3f70387
c704d72
6781147
b06f697
be312a2
0859152
b4df79a
bc5000c
3c2dce6
a424004
5aa30ff
9b267aa
8a49d7e
eefc5d9
432b280
ef6a368
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package sttp.tapir.static | ||
|
||
import sttp.model.ContentRangeUnits | ||
import sttp.monad.MonadError | ||
|
||
import java.nio.file.Paths | ||
import scala.util.{Failure, Success, Try} | ||
|
||
object FilesUtil { | ||
|
||
def apply[F[_]: MonadError]( | ||
systemPath: String | ||
): HeadInput => F[Either[StaticErrorOutput, HeadOutput]] = { | ||
Try(Paths.get(systemPath).toRealPath()) match { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't |
||
case Success(realSystemPath) => | ||
_ => | ||
val file = realSystemPath.toFile | ||
MonadError[F].blocking( | ||
Right( | ||
HeadOutput.SupportRanges( | ||
Some(ContentRangeUnits.Bytes), | ||
Some(file.length()), | ||
Some(contentTypeFromName(file.getName)) | ||
) | ||
) | ||
) | ||
case Failure(e) => _ => MonadError[F].error(e) | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,10 @@ | ||
package sttp.tapir.static | ||
|
||
import sttp.model.headers.ETag | ||
import sttp.model.headers.{ETag, Range} | ||
import sttp.model.{Header, HeaderNames, MediaType, StatusCode} | ||
import sttp.monad.MonadError | ||
import sttp.tapir.{FileRange, _} | ||
import sttp.tapir.server.ServerEndpoint | ||
import sttp.model.headers.Range | ||
import sttp.tapir.{FileRange, _} | ||
|
||
import java.io.InputStream | ||
import java.time.Instant | ||
|
@@ -53,8 +52,8 @@ trait TapirStaticContentEndpoints { | |
}(_.map(_.toString)) | ||
|
||
private def staticEndpoint[T]( | ||
prefix: EndpointInput[Unit], | ||
body: EndpointOutput[T] | ||
prefix: EndpointInput[Unit], | ||
body: EndpointOutput[T] | ||
): Endpoint[StaticInput, StaticErrorOutput, StaticOutput[T], Any] = { | ||
endpoint.get | ||
.in(prefix) | ||
|
@@ -63,8 +62,8 @@ trait TapirStaticContentEndpoints { | |
.and(ifNoneMatchHeader) | ||
.and(ifModifiedSinceHeader) | ||
.and(rangeHeader) | ||
.map[StaticInput]((t: (List[String], Option[List[ETag]], Option[Instant], Option[Range])) => StaticInput(t._1, t._2, t._3, t._4))(fi => | ||
(fi.path, fi.ifNoneMatch, fi.ifModifiedSince, fi.range) | ||
.map[StaticInput]((t: (List[String], Option[List[ETag]], Option[Instant], Option[Range])) => StaticInput(t._1, t._2, t._3, t._4))( | ||
fi => (fi.path, fi.ifNoneMatch, fi.ifModifiedSince, fi.range) | ||
) | ||
) | ||
.errorOut( | ||
|
@@ -78,8 +77,7 @@ trait TapirStaticContentEndpoints { | |
StatusCode.BadRequest, | ||
emptyOutputAs(StaticErrorOutput.BadRequest), | ||
StaticErrorOutput.BadRequest.getClass | ||
) | ||
, | ||
), | ||
oneOfMappingClassMatcher( | ||
StatusCode.RangeNotSatisfiable, | ||
emptyOutputAs(StaticErrorOutput.RangeNotSatisfiable), | ||
|
@@ -99,8 +97,9 @@ trait TapirStaticContentEndpoints { | |
.and(etagHeader) | ||
.and(header[Option[String]](HeaderNames.AcceptRanges)) | ||
.and(header[Option[String]](HeaderNames.ContentRange)) | ||
.map[StaticOutput.FoundPartial[T]]((t: (T, Option[Instant], Option[Long], Option[MediaType], Option[ETag], Option[String], Option[String])) => | ||
StaticOutput.FoundPartial(t._1, t._2, t._3, t._4, t._5, t._6, t._7) | ||
.map[StaticOutput.FoundPartial[T]]( | ||
(t: (T, Option[Instant], Option[Long], Option[MediaType], Option[ETag], Option[String], Option[String])) => | ||
StaticOutput.FoundPartial(t._1, t._2, t._3, t._4, t._5, t._6, t._7) | ||
)(fo => (fo.body, fo.lastModified, fo.contentLength, fo.contentType, fo.etag, fo.acceptRanges, fo.contentRange)), | ||
classOf[StaticOutput.FoundPartial[T]] | ||
), | ||
|
@@ -120,6 +119,37 @@ trait TapirStaticContentEndpoints { | |
) | ||
} | ||
|
||
private def headEndpoint( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mayber similarly - |
||
prefix: EndpointInput[Unit] | ||
): Endpoint[HeadInput, StaticErrorOutput, HeadOutput, Any] = { | ||
endpoint.head | ||
.in(prefix) | ||
.in(pathsWithoutDots.map[HeadInput](t => HeadInput(t))(_.path)) | ||
.errorOut( | ||
oneOf[StaticErrorOutput]( | ||
oneOfMappingClassMatcher( | ||
StatusCode.BadRequest, | ||
emptyOutputAs(StaticErrorOutput.BadRequest), | ||
StaticErrorOutput.BadRequest.getClass | ||
) | ||
) | ||
) | ||
.out( | ||
oneOf[HeadOutput]( | ||
oneOfMappingClassMatcher( | ||
StatusCode.Ok, | ||
header[Option[String]](HeaderNames.AcceptRanges) | ||
.and(header[Option[Long]](HeaderNames.ContentLength)) | ||
.and(contentTypeHeader) | ||
.map[HeadOutput.SupportRanges]((t: (Option[String], Option[Long], Option[MediaType])) => | ||
HeadOutput.SupportRanges(t._1, t._2, t._3) | ||
)(fo => (fo.acceptRanges, fo.contentLength, fo.contentType)), | ||
classOf[HeadOutput.SupportRanges] | ||
) | ||
) | ||
) | ||
} | ||
|
||
def filesEndpoint(prefix: EndpointInput[Unit]): Endpoint[StaticInput, StaticErrorOutput, StaticOutput[FileRange], Any] = | ||
staticEndpoint(prefix, fileRangeBody) | ||
|
||
|
@@ -136,10 +166,16 @@ trait TapirStaticContentEndpoints { | |
* A request to `/static/files/css/styles.css` will try to read the `/home/app/static/css/styles.css` file. | ||
*/ | ||
def filesServerEndpoint[F[_]](prefix: EndpointInput[Unit])( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and here -> |
||
systemPath: String | ||
systemPath: String | ||
): ServerEndpoint[StaticInput, StaticErrorOutput, StaticOutput[FileRange], Any, F] = | ||
ServerEndpoint(filesEndpoint(prefix), (m: MonadError[F]) => Files(systemPath)(m)) | ||
|
||
/** A server endpoint, used to verify if sever supports range requests for file under particular path */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not only, it also checks if the file exists and what it's length |
||
def headServerEndpoint[F[_]](prefix: EndpointInput[Unit])( | ||
systemPath: String | ||
): ServerEndpoint[HeadInput, StaticErrorOutput, HeadOutput, Any, F] = | ||
ServerEndpoint(headEndpoint(prefix), (m: MonadError[F]) => FilesUtil(systemPath)(m)) | ||
|
||
/** A server endpoint, which exposes a single file from local storage found at `systemPath`, using the given `path`. | ||
* | ||
* {{{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,7 @@ | ||
package sttp.tapir.static | ||
|
||
import sttp.model.MediaType | ||
import sttp.model.headers.ETag | ||
import sttp.model.headers.Range | ||
import sttp.model.headers.{ETag, Range} | ||
|
||
import java.time.Instant | ||
|
||
|
@@ -13,24 +12,34 @@ case class StaticInput( | |
range: Option[Range] | ||
) | ||
|
||
case class HeadInput( | ||
path: List[String] | ||
) | ||
|
||
trait StaticErrorOutput | ||
object StaticErrorOutput { | ||
case object NotFound extends StaticErrorOutput | ||
case object BadRequest extends StaticErrorOutput | ||
case object RangeNotSatisfiable extends StaticErrorOutput | ||
} | ||
|
||
trait HeadOutput | ||
object HeadOutput { | ||
case class SupportRanges(acceptRanges: Option[String], contentLength: Option[Long], contentType: Option[MediaType]) extends HeadOutput | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe simply |
||
case class NotSupportRanges() extends HeadOutput | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this used? |
||
} | ||
|
||
trait StaticOutput[+T] | ||
object StaticOutput { | ||
case object NotModified extends StaticOutput[Nothing] | ||
case class FoundPartial[T]( | ||
body: T, | ||
lastModified: Option[Instant], | ||
contentLength: Option[Long], | ||
contentType: Option[MediaType], | ||
etag: Option[ETag], | ||
acceptRanges: Option[String], | ||
contentRange: Option[String] | ||
body: T, | ||
lastModified: Option[Instant], | ||
contentLength: Option[Long], | ||
contentType: Option[MediaType], | ||
etag: Option[ETag], | ||
acceptRanges: Option[String], | ||
contentRange: Option[String] | ||
) extends StaticOutput[T] | ||
case class Found[T]( | ||
body: T, | ||
|
@@ -39,4 +48,4 @@ object StaticOutput { | |
contentType: Option[MediaType], | ||
etag: Option[ETag] | ||
) extends StaticOutput[T] | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this can be simply part of
files
, where we have method.head
and.get
?