Skip to content

Commit

Permalink
Streaming docs and naming change
Browse files Browse the repository at this point in the history
  • Loading branch information
Lanayx committed Feb 12, 2024
1 parent dcfde12 commit 2e69311
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 20 deletions.
6 changes: 3 additions & 3 deletions src/Oxpecker/Oxpecker.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
<Authors>Vladimir Shchur, F# community</Authors>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<Version>0.7.0</Version>
<PackageVersion>0.7.0</PackageVersion>
<PackageReleaseNotes>Added bytes handler</PackageReleaseNotes>
<Version>0.7.1</Version>
<PackageVersion>0.7.1</PackageVersion>
<PackageReleaseNotes>Streaming docs and small naming change</PackageReleaseNotes>
</PropertyGroup>
<PropertyGroup>
<WarningLevel>3</WarningLevel>
Expand Down
70 changes: 69 additions & 1 deletion src/Oxpecker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ An in depth functional reference to all of Oxpecker's default features.
- [Authentication and Authorization](#authentication-and-authorization)
- [Conditional Requests](#conditional-requests)
- [Response Writing](#response-writing)
- [Streaming](#streaming)

## Fundamentals

Expand Down Expand Up @@ -121,7 +122,7 @@ type Person = { Name : string }
let sayHelloWorld : EndpointHandler =
fun (ctx: HttpContext) ->
task {
let! person = ctx.BindJsonAsync<Person>()
let! person = ctx.BindJson<Person>()
let greeting = sprintf "Hello World, from %s" person.Name
return! text greeting ctx
}
Expand Down Expand Up @@ -1366,3 +1367,70 @@ let myHtmlView (htmlView: MyHtmlElement) : EndpointHandler =
fun (ctx: HttpContext) -> ctx.WriteMyHtmlView htmlView
```

### Streaming

Sometimes a large file or block of data has to be send to a client and in order to avoid loading the entire data into memory a Oxpecker web application can use streaming to send a response in a more efficient way.

The `WriteStream` extension method and the `streamData` endpoint handler can be used to stream an object of type `Stream` to a client.

Both functions accept the following parameters:

- `enableRangeProcessing`: If true a client can request a sub range of data to be streamed (useful when a client wants to continue streaming after a paused download, or when internet connection has been lost, etc.)
- `stream`: The stream object to be returned to the client.
- `eTag`: Entity header tag used for conditional requests (see [Conditional Requests](#conditional-requests)).
- `lastModified`: Last modified timestamp used for conditional requests (see [Conditional Requests](#conditional-requests)).

If the `eTag` or `lastModified` timestamp are set then both functions will also set the `ETag` and/or `Last-Modified` HTTP headers during the response:

```fsharp
let someStream : Stream = ...
let someHandler : EndpointHandler =
fun (ctx: HttpContext) ->
task {
// Do stuff
return! ctx.WriteStream(
true, // enableRangeProcessing
someStream,
None, // eTag
None) // lastModified
}
// or...
let someHandler : EndpointHandler =
// Do stuff
streamData
true // enableRangeProcessing
someStream
None // eTag
None // lastModified
```

In most cases a web application will want to stream a file directly from the local file system. In this case you can use the `WriteFileStream` extension method or the `streamFile` http handler, which are both the same as `WriteStream` and `streamData` except that they accept a relative or absolute `filePath` instead of a `Stream` object:

```fsharp
let someHandler : EndpointHandler =
fun (ctx: HttpContext) ->
task {
// Do stuff
return! ctx.WriteFileStream(
true, // enableRangeProcessing
"large-file.zip",
None, // eTag
None) // lastModified
}
// or...
let someHandler : EndpointHandler =
// Do stuff
streamFile
true // enableRangeProcessing
"large-file.zip"
None // eTag
None // lastModified
```

All streaming functions in Oxpecker will also validate conditional HTTP headers, including the `If-Range` HTTP header if `enableRangeProcessing` has been set to `true`.

27 changes: 11 additions & 16 deletions src/Oxpecker/Streaming.fs
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,7 @@ type StreamingExtensions() =
static member internal RangeUnit(_: HttpContext) = "bytes"

[<Extension>]
static member internal WriteStreamToBodyAsync
(
ctx: HttpContext,
stream: Stream,
rangeBoundary: RangeBoundary option
) =
static member internal WriteStreamToBody(ctx: HttpContext, stream: Stream, rangeBoundary: RangeBoundary option) =
task {
try
use input = stream
Expand Down Expand Up @@ -190,7 +185,7 @@ type StreamingExtensions() =
/// <param name="lastModified">An optional parameter denoting the last modified date time of the data.</param>
/// <returns>Task of Some HttpContext after writing to the body of the response.</returns>
[<Extension>]
static member WriteStreamAsync
static member WriteStream
(
ctx: HttpContext,
enableRangeProcessing: bool,
Expand All @@ -207,21 +202,21 @@ type StreamingExtensions() =
| AllConditionsMet
| NoConditionsSpecified ->
if not stream.CanSeek then
return! ctx.WriteStreamToBodyAsync(stream, None)
return! ctx.WriteStreamToBody(stream, None)
elif not enableRangeProcessing then
return! ctx.WriteStreamToBodyAsync(stream, None)
return! ctx.WriteStreamToBody(stream, None)
else
// Set HTTP header to tell clients that Range processing is enabled
ctx.SetHttpHeader(HeaderNames.AcceptRanges, ctx.RangeUnit())
match RangeHelper.parseRange ctx.Request with
| None -> return! ctx.WriteStreamToBodyAsync(stream, None)
| None -> return! ctx.WriteStreamToBody(stream, None)
| Some ranges ->
// Check and validate If-Range HTTP header
match RangeHelper.isIfRangeValid ctx.Request eTag lastModified with
| false -> return! ctx.WriteStreamToBodyAsync(stream, None)
| false -> return! ctx.WriteStreamToBody(stream, None)
| true ->
match RangeHelper.validateRanges ranges stream.Length with
| Ok range -> return! ctx.WriteStreamToBodyAsync(stream, Some range)
| Ok range -> return! ctx.WriteStreamToBody(stream, Some range)
| Error _ ->
// If the range header was invalid then return an error response
ctx.SetHttpHeader(HeaderNames.ContentRange, $"%s{ctx.RangeUnit()} */%i{stream.Length}")
Expand All @@ -240,7 +235,7 @@ type StreamingExtensions() =
/// <param name="lastModified">An optional parameter denoting the last modified date time of the file.</param>
/// <returns>Task of Some HttpContext after writing to the body of the response.</returns>
[<Extension>]
static member WriteFileStreamAsync
static member WriteFileStream
(
ctx: HttpContext,
enableRangeProcessing: bool,
Expand All @@ -256,7 +251,7 @@ type StreamingExtensions() =
let env = ctx.GetHostingEnvironment()
Path.Combine(env.ContentRootPath, filePath)
use stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)
return! ctx.WriteStreamAsync(enableRangeProcessing, stream, eTag, lastModified)
return! ctx.WriteStream(enableRangeProcessing, stream, eTag, lastModified)
}

// ---------------------------
Expand All @@ -280,7 +275,7 @@ let streamData
(eTag: EntityTagHeaderValue option)
(lastModified: DateTimeOffset option)
: EndpointHandler =
fun (ctx: HttpContext) -> ctx.WriteStreamAsync(enableRangeProcessing, stream, eTag, lastModified)
fun (ctx: HttpContext) -> ctx.WriteStream(enableRangeProcessing, stream, eTag, lastModified)

/// <summary>
/// Streams a file to the client.
Expand All @@ -299,4 +294,4 @@ let streamFile
(eTag: EntityTagHeaderValue option)
(lastModified: DateTimeOffset option)
: EndpointHandler =
fun (ctx: HttpContext) -> ctx.WriteFileStreamAsync(enableRangeProcessing, filePath, eTag, lastModified)
fun (ctx: HttpContext) -> ctx.WriteFileStream(enableRangeProcessing, filePath, eTag, lastModified)

0 comments on commit 2e69311

Please sign in to comment.