Skip to content

Commit 43b9455

Browse files
committed
Add support for parallel file download
1 parent 1a8ba9b commit 43b9455

File tree

8 files changed

+209
-61
lines changed

8 files changed

+209
-61
lines changed

build.fsx

+1
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ let netCoreProjs =
626626
++ "src/app/Fake.Windows.*/*.fsproj"
627627
++ "src/app/Fake.IO.*/*.fsproj"
628628
++ "src/app/Fake.Tools.*/*.fsproj"
629+
++ "src/app/Fake.Net.*/*.fsproj"
629630
++ "src/app/Fake.netcore/*.fsproj"
630631
++ "src/app/Fake.Testing.*/*.fsproj"
631632
++ "src/app/Fake.Runtime/*.fsproj"

src/app/Fake.Net.Http/Async.fs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace Fake.Net.Async
2+
3+
module Async =
4+
let result = async.Return
5+
let map f value = async {
6+
let! v = value
7+
return f v
8+
}
9+
10+
let bind f xAsync = async {
11+
let! x = xAsync
12+
return! f x
13+
}
14+
15+
let apply fAsync xAsync = async {
16+
// start the two asyncs in parallel
17+
let! fChild = Async.StartChild fAsync
18+
let! xChild = Async.StartChild xAsync
19+
20+
// wait for the results
21+
let! f = fChild
22+
let! x = xChild
23+
24+
// apply the function to the results
25+
return f x
26+
}

src/app/Fake.Net.Http/Fake.Net.Http.fsproj

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
</PropertyGroup>
1616
<ItemGroup>
1717
<Compile Include="AssemblyInfo.fs" />
18-
<Compile Include="ResultBuilder.fs" />
19-
<Compile Include="FilePath.fs" />
18+
<Compile Include="Async.fs" />
19+
<Compile Include="Result.fs" />
20+
<Compile Include="List.fs" />
2021
<Compile Include="HttpLoader.fs" />
2122
</ItemGroup>
2223
<ItemGroup>

src/app/Fake.Net.Http/FilePath.fs

-17
This file was deleted.

src/app/Fake.Net.Http/HttpLoader.fs

+99-31
Original file line numberDiff line numberDiff line change
@@ -6,66 +6,134 @@ open System.Net.Http
66

77
open Fake.Core
88

9-
open FilePath
10-
open ResultBuilder
9+
open Fake.Net.Async
10+
open Fake.Net.Result
11+
open Fake.Net.List
1112

12-
/// Contains
13+
/// HTTP Client for downloading files
1314
module Http =
1415

15-
let result = ResultBuilder()
16+
/// Input parameter type
17+
type DownloadParameters = {
18+
uri: string
19+
path: string
20+
}
21+
22+
/// Type aliases for local file path and error messages
23+
type FilePath = string
24+
type Err = string
25+
26+
/// Contains validated Uri and FilePath info for further download
27+
type private DownloadInfo = {
28+
uri: Uri
29+
localFilePath: FilePath
30+
}
31+
32+
/// [omit]
33+
let private createFilePath (filePathStr: string): Result<FilePath, Err list> =
34+
try
35+
let fullPath = Path.GetFullPath(filePathStr)
36+
Ok (fullPath)
37+
with
38+
| ex ->
39+
let err = sprintf "[%s] %s" filePathStr ex.Message
40+
Error [err ]
1641

17-
let createUri (uriStr: string) =
42+
/// [omit]
43+
let private createUri (uriStr: string): Result<Uri, Err list> =
1844
try
1945
Ok (Uri uriStr)
2046
with
2147
| ex ->
22-
let err = sprintf "[%s] %A" uriStr ex.Message
48+
let err = sprintf "[%s] %s" uriStr ex.Message
2349
Error [err ]
2450

25-
let showDownloadResult (result: Result<FilePath, string list>) =
51+
/// [omit]
52+
let private createDownloadInfo (input: DownloadParameters): Result<DownloadInfo, Err list> =
53+
let (<!>) = Result.map
54+
let (<*>) = Result.apply
55+
56+
let createDownloadInfoRecord (filePath: FilePath) (uri:Uri) =
57+
{ uri=uri; localFilePath=filePath }
58+
59+
let filePathResult = createFilePath input.path
60+
let urlResult = createUri input.uri
61+
createDownloadInfoRecord <!> filePathResult <*> urlResult
62+
63+
/// [omit]
64+
let private printDownloadResults result =
2665
match result with
27-
| Ok (FilePath(filePath)) ->
28-
Trace.log <| sprintf "Downloaded : [%s]" filePath
29-
| Error errs ->
30-
Trace.traceError <| sprintf "Failed: %A" errs
66+
| Ok result ->
67+
Trace.log <| sprintf "Downloaded : [%A]" result
68+
| Error errs ->
69+
Trace.traceError <| sprintf "Failed: %A" errs
70+
result
3171

32-
let saveStreamToFile (filePath: FilePath) (stream: Stream) : Async<Result<FilePath,string list>> =
72+
/// [omit]
73+
let private saveStreamToFileAsync (filePath: FilePath) (stream: Stream) : Async<Result<FilePath, Err list>> =
3374
async {
34-
let filePathStr = FilePath.value filePath
3575
try
36-
use fileStream = new FileStream(filePathStr, FileMode.Create, FileAccess.Write, FileShare.None)
76+
use fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)
3777
do! stream.CopyToAsync(fileStream) |> Async.AwaitTask
3878
return (Ok filePath)
3979
with
4080
| ex ->
41-
let err = sprintf "[%s] %A" filePathStr ex.Message
81+
let err = sprintf "[%s] %s" filePath ex.Message
4282
return Error [err ]
4383
}
4484

45-
let downloadToFileStream (filePath: FilePath) (uri:Uri) : Async<Result<FilePath,string list>> =
85+
/// [omit]
86+
let private downloadStreamToFileAsync (info: DownloadInfo) : Async<Result<FilePath, Err list>> =
4687
async {
4788
use client = new HttpClient()
4889
try
90+
Trace.log <| sprintf "Downloading [%s] ..." info.uri.Host
4991
// do not buffer the response
50-
let! response = client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead) |> Async.AwaitTask
92+
let! response = client.GetAsync(info.uri, HttpCompletionOption.ResponseHeadersRead) |> Async.AwaitTask
5193
response.EnsureSuccessStatusCode () |> ignore
52-
use! stream = response.Content.ReadAsStreamAsync() |> Async.AwaitTask
53-
return! saveStreamToFile filePath stream
94+
use! stream = response.Content.ReadAsStreamAsync() |> Async.AwaitTask
95+
return! saveStreamToFileAsync info.localFilePath stream
5496
with
55-
| ex ->
56-
let err = sprintf "[%s] %A" uri.Host ex.Message
97+
| ex ->
98+
let err = sprintf "[%s] %s" info.uri.Host ex.Message
5799
return Error [err ]
58100
}
59101

60-
/// Download file by the given file path and Url
102+
/// [omit]
103+
let private downloadFileAsync (input: DownloadParameters): Async<Result<FilePath, Err list>> =
104+
let valImp = createDownloadInfo input
105+
match valImp with
106+
| Ok x ->
107+
downloadStreamToFileAsync x
108+
| Error errs ->
109+
Async.result (Error errs)
110+
111+
/// Download file by the given file path and Uri
61112
/// string -> string -> Result<FilePath,string list>
62-
let downloadFile (filePathStr: string) (url: string) : Result<FilePath,string list> =
113+
/// ## Parameters
114+
/// - `localFilePath` - A local file path to download file
115+
/// - `uri` - A Uri to download from
116+
/// ## Returns
117+
/// - `Result` type. Success branch contains a downloaded file path. Failure branch contains a liast of errors
118+
let downloadFile (localFilePath: string) (uri: string) : Result<FilePath, Err list> =
119+
downloadFileAsync { uri=uri; path=localFilePath }
120+
|> Async.RunSynchronously
121+
|> printDownloadResults
122+
123+
/// Download list of Uri's in parallel
124+
/// DownloadParameters -> Result<FilePath, Err list>
125+
/// ## Parameters
126+
/// - `input` - List of Http.DownloadParameters. Each Http.DownloadParameters record type contains Uri and file path
127+
/// ## Returns
128+
/// - `Result` type. Success branch contains a list of downloaded file paths. Failure branch contains a liast of errors
129+
let downloadFiles (input: DownloadParameters list) =
130+
input
131+
// DownloadParameters -> "Async<Result<FilePath, Err list>> list"
132+
|> List.map downloadFileAsync
133+
// "Async<Result<FilePath, Err list>> list" -> "Async<Result<FilePath, Err list> list>"
134+
|> List.sequenceAsyncA
135+
// "Async<Result<FilePath, Err list> list>" -> "Async<Result<FilePath list, Err list>>"
136+
|> Async.map List.sequenceResultA
137+
|> Async.RunSynchronously
138+
|> printDownloadResults
63139

64-
let downloadResult = result {
65-
let! filePath = FilePath.create filePathStr
66-
let! uri = createUri url
67-
let! result = downloadToFileStream filePath uri |> Async.RunSynchronously
68-
return result
69-
}
70-
do showDownloadResult downloadResult
71-
downloadResult

src/app/Fake.Net.Http/List.fs

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
namespace Fake.Net.List
2+
3+
open Fake.Net.Async
4+
open Fake.Net.Result
5+
6+
// List extensions for traversing Result and Async types
7+
// Functions from fsharpforfunandprofit.com, please see details here:
8+
// https://fsharpforfunandprofit.com/posts/elevated-world-5/
9+
module List =
10+
11+
/// Map a Async producing function over a list to get a new Async
12+
/// using applicative style
13+
/// ('a -> Async<'b>) -> 'a list -> Async<'b list>
14+
let rec traverseAsyncA f list =
15+
16+
// define the applicative functions
17+
let (<*>) = Async.apply
18+
let retn = Async.result
19+
20+
// define a "cons" function
21+
let cons head tail = head :: tail
22+
23+
// right fold over the list
24+
let initState = retn []
25+
let folder head tail =
26+
retn cons <*> (f head) <*> tail
27+
28+
List.foldBack folder list initState
29+
30+
/// Transform a "list<Async>" into a "Async<list>"
31+
/// and collect the results using apply.
32+
let sequenceAsyncA x = traverseAsyncA id x
33+
34+
/// Map a Result producing function over a list to get a new Result
35+
/// using applicative style
36+
/// ('a -> Result<'b>) -> 'a list -> Result<'b list>
37+
let rec traverseResultA f list =
38+
39+
// define the applicative functions
40+
let (<*>) = Result.apply
41+
let retn = Ok
42+
43+
// define a "cons" function
44+
let cons head tail = head :: tail
45+
46+
// right fold over the list
47+
let initState = retn []
48+
let folder head tail =
49+
retn cons <*> (f head) <*> tail
50+
51+
List.foldBack folder list initState
52+
53+
/// Transform a "list<Result>" into a "Result<list>"
54+
/// and collect the results using apply.
55+
let sequenceResultA x = traverseResultA id x
56+

src/app/Fake.Net.Http/Result.fs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Fake.Net.Result
2+
3+
module Result =
4+
5+
type ResultBuilder() =
6+
member __.Bind(m, f) =
7+
match m with
8+
| Error e -> Error e
9+
| Ok a -> f a
10+
11+
member __.Return(x) =
12+
Ok x
13+
14+
let apply fResult xResult =
15+
match fResult,xResult with
16+
| Ok f, Ok x ->
17+
Ok (f x)
18+
| Error errs, Ok x ->
19+
Error errs
20+
| Ok f, Error errs ->
21+
Error errs
22+
| Error errs1, Error errs2 ->
23+
// concat both lists of errors
24+
Error (List.concat [errs1; errs2])

src/app/Fake.Net.Http/ResultBuilder.fs

-11
This file was deleted.

0 commit comments

Comments
 (0)