Skip to content

Commit

Permalink
Merge pull request #1746 from vlukash/vlukash/http_downloader
Browse files Browse the repository at this point in the history
Add HTTP module for downloading files
  • Loading branch information
matthid authored Jan 2, 2018
2 parents 642a01a + cb2fb5e commit c666715
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 0 deletions.
2 changes: 2 additions & 0 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ let dotnetAssemblyInfos =
"Fake.DotNet.Xamarin", "Running Xamarin builds"
"Fake.IO.FileSystem", "Core Filesystem utilities"
"Fake.IO.Zip", "Core Zip functionality"
"Fake.Net.Http", "HTTP Client"
"Fake.netcore", "Command line tool"
"Fake.Runtime", "Core runtime features"
"Fake.Tools.Git", "Running git commands"
Expand Down Expand Up @@ -625,6 +626,7 @@ let netCoreProjs =
++ "src/app/Fake.Windows.*/*.fsproj"
++ "src/app/Fake.IO.*/*.fsproj"
++ "src/app/Fake.Tools.*/*.fsproj"
++ "src/app/Fake.Net.*/*.fsproj"
++ "src/app/Fake.netcore/*.fsproj"
++ "src/app/Fake.Testing.*/*.fsproj"
++ "src/app/Fake.Runtime/*.fsproj"
Expand Down
15 changes: 15 additions & 0 deletions src/Fake-netcore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fake.Api.GitHub", "app\Fake
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fake.DotNet.Xamarin", "app\Fake.DotNet.Xamarin\Fake.DotNet.Xamarin.fsproj", "{13C1F95D-2FAD-4890-BF94-0AE7CF9AB2FC}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fake.Net.Http", "app\Fake.Net.Http\Fake.Net.Http.fsproj", "{D24CEE35-B6C0-4C92-AE18-E80F90B69974}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -494,6 +496,18 @@ Global
{13C1F95D-2FAD-4890-BF94-0AE7CF9AB2FC}.Release|x64.Build.0 = Release|x64
{13C1F95D-2FAD-4890-BF94-0AE7CF9AB2FC}.Release|x86.ActiveCfg = Release|x86
{13C1F95D-2FAD-4890-BF94-0AE7CF9AB2FC}.Release|x86.Build.0 = Release|x86
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Debug|x64.ActiveCfg = Debug|x64
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Debug|x64.Build.0 = Debug|x64
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Debug|x86.ActiveCfg = Debug|x86
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Debug|x86.Build.0 = Debug|x86
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|Any CPU.Build.0 = Release|Any CPU
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|x64.ActiveCfg = Release|x64
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|x64.Build.0 = Release|x64
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|x86.ActiveCfg = Release|x86
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E2CF8635-E7C4-4470-92DD-F706F052BF7B} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
Expand Down Expand Up @@ -529,5 +543,6 @@ Global
{58A3EDF0-CA9D-4757-B1E8-2A4E3592B308} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{4BCE4F9C-8FC2-4207-81F1-20CB07D852DC} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{13C1F95D-2FAD-4890-BF94-0AE7CF9AB2FC} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{D24CEE35-B6C0-4C92-AE18-E80F90B69974} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
EndGlobalSection
EndGlobal
17 changes: 17 additions & 0 deletions src/app/Fake.Net.Http/AssemblyInfo.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Auto-Generated by FAKE; do not edit
namespace System
open System.Reflection

[<assembly: AssemblyTitleAttribute("FAKE - F# Make HTTP Client")>]
[<assembly: AssemblyProductAttribute("FAKE - F# Make")>]
[<assembly: AssemblyVersionAttribute("5.0.0")>]
[<assembly: AssemblyInformationalVersionAttribute("5.0.0-beta010")>]
[<assembly: AssemblyFileVersionAttribute("5.0.0")>]
do ()

module internal AssemblyVersionInformation =
let [<Literal>] AssemblyTitle = "FAKE - F# Make HTTP Client"
let [<Literal>] AssemblyProduct = "FAKE - F# Make"
let [<Literal>] AssemblyVersion = "5.0.0"
let [<Literal>] AssemblyInformationalVersion = "5.0.0-beta010"
let [<Literal>] AssemblyFileVersion = "5.0.0"
26 changes: 26 additions & 0 deletions src/app/Fake.Net.Http/Async.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Fake.Net.Async

module Async =
let result = async.Return
let map f value = async {
let! v = value
return f v
}

let bind f xAsync = async {
let! x = xAsync
return! f x
}

let apply fAsync xAsync = async {
// start the two asyncs in parallel
let! fChild = Async.StartChild fAsync
let! xChild = Async.StartChild xAsync

// wait for the results
let! f = fChild
let! x = xChild

// apply the function to the results
return f x
}
27 changes: 27 additions & 0 deletions src/app/Fake.Net.Http/Fake.Net.Http.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>1.0.0-alpha-10</VersionPrefix>
<TargetFrameworks>net46;netstandard1.6;netstandard2.0</TargetFrameworks>
<DebugType>pdbonly</DebugType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>Fake.Net.Http</AssemblyName>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);NETSTANDARD;USE_HTTPCLIENT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Async.fs" />
<Compile Include="Result.fs" />
<Compile Include="List.fs" />
<Compile Include="HttpLoader.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fake.Core.Tracing\Fake.Core.Tracing.fsproj" />
</ItemGroup>
<Import Project="..\..\..\.paket\Paket.Restore.targets" />
</Project>
138 changes: 138 additions & 0 deletions src/app/Fake.Net.Http/HttpLoader.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
namespace Fake.Net

open System
open System.IO
open System.Net.Http

open Fake.Core

open Fake.Net.Async
open Fake.Net.Result
open Fake.Net.List

/// HTTP Client for downloading files
module Http =

/// Input parameter type
type DownloadParameters = {
Uri: string
Path: string
}

/// Type aliases for local file path and error messages
type private FilePath = string
type private Err = string

/// Contains validated Uri and FilePath info for further download
type private DownloadInfo = {
Uri: Uri
LocalFilePath: FilePath
}

/// [omit]
let private createFilePath (filePathStr: string): Result<FilePath, Err list> =
try
let fullPath = Path.GetFullPath(filePathStr)
Ok (fullPath)
with
| ex ->
let err = sprintf "[%s] %s" filePathStr ex.Message
Error [err ]

/// [omit]
let private createUri (uriStr: string): Result<Uri, Err list> =
try
Ok (Uri uriStr)
with
| ex ->
let err = sprintf "[%s] %s" uriStr ex.Message
Error [err ]

/// [omit]
let private createDownloadInfo (input: DownloadParameters): Result<DownloadInfo, Err list> =
let (<!>) = Result.map
let (<*>) = Result.apply

let createDownloadInfoRecord (filePath: FilePath) (uri:Uri) =
{ Uri=uri; LocalFilePath=filePath }

let filePathResult = createFilePath input.Path
let urlResult = createUri input.Uri
createDownloadInfoRecord <!> filePathResult <*> urlResult

/// [omit]
let private printDownloadResults result =
match result with
| Ok result ->
Trace.log <| sprintf "Downloaded : [%A]" result
| Error errs ->
Trace.traceError <| sprintf "Failed: %A" errs
result

/// [omit]
let private saveStreamToFileAsync (filePath: FilePath) (stream: Stream) : Async<Result<FilePath, Err list>> =
async {
try
use fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)
do! stream.CopyToAsync(fileStream) |> Async.AwaitTask
return (Ok filePath)
with
| ex ->
let err = sprintf "[%s] %s" filePath ex.Message
return Error [err ]
}

/// [omit]
let private downloadStreamToFileAsync (info: DownloadInfo) : Async<Result<FilePath, Err list>> =
async {
use client = new HttpClient()
try
Trace.log <| sprintf "Downloading [%s] ..." info.Uri.OriginalString
// do not buffer the response
let! response = client.GetAsync(info.Uri, HttpCompletionOption.ResponseHeadersRead) |> Async.AwaitTask
response.EnsureSuccessStatusCode () |> ignore
use! stream = response.Content.ReadAsStreamAsync() |> Async.AwaitTask
return! saveStreamToFileAsync info.LocalFilePath stream
with
| ex ->
let err = sprintf "[%s] %s" info.Uri.Host ex.Message
return Error [err ]
}

/// [omit]
let private downloadFileAsync (input: DownloadParameters): Async<Result<FilePath, Err list>> =
let valImp = createDownloadInfo input
match valImp with
| Ok x ->
downloadStreamToFileAsync x
| Error errs ->
Async.result (Error errs)

/// Download file by the given file path and Uri
/// string -> string -> Result<FilePath,string list>
/// ## Parameters
/// - `localFilePath` - A local file path to download file
/// - `uri` - A Uri to download from
/// ## Returns
/// - `Result` type. Success branch contains a downloaded file path. Failure branch contains a list of errors
let downloadFile (localFilePath: string) (uri: string) : Result<string, string list> =
downloadFileAsync { Uri=uri; Path=localFilePath }
|> Async.RunSynchronously
|> printDownloadResults

/// Download list of Uri's in parallel
/// DownloadParameters -> Result<FilePath, Err list>
/// ## Parameters
/// - `input` - List of Http.DownloadParameters. Each Http.DownloadParameters record type contains Uri and file path
/// ## Returns
/// - `Result` type. Success branch contains a list of downloaded file paths. Failure branch contains a list of errors
let downloadFiles (input: DownloadParameters list) : Result<string list, string list> =
input
// DownloadParameters -> "Async<Result<FilePath, Err list>> list"
|> List.map downloadFileAsync
// "Async<Result<FilePath, Err list>> list" -> "Async<Result<FilePath, Err list> list>"
|> List.sequenceAsyncA
// "Async<Result<FilePath, Err list> list>" -> "Async<Result<FilePath list, Err list>>"
|> Async.map List.sequenceResultA
|> Async.RunSynchronously
|> printDownloadResults
56 changes: 56 additions & 0 deletions src/app/Fake.Net.Http/List.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace Fake.Net.List

open Fake.Net.Async
open Fake.Net.Result

// List extensions for traversing Result and Async types
// Functions from fsharpforfunandprofit.com, please see details here:
// https://fsharpforfunandprofit.com/posts/elevated-world-5/
module List =

/// Map a Async producing function over a list to get a new Async
/// using applicative style
/// ('a -> Async<'b>) -> 'a list -> Async<'b list>
let rec traverseAsyncA f list =

// define the applicative functions
let (<*>) = Async.apply
let retn = Async.result

// define a "cons" function
let cons head tail = head :: tail

// right fold over the list
let initState = retn []
let folder head tail =
retn cons <*> (f head) <*> tail

List.foldBack folder list initState

/// Transform a "list<Async>" into a "Async<list>"
/// and collect the results using apply.
let sequenceAsyncA x = traverseAsyncA id x

/// Map a Result producing function over a list to get a new Result
/// using applicative style
/// ('a -> Result<'b>) -> 'a list -> Result<'b list>
let rec traverseResultA f list =

// define the applicative functions
let (<*>) = Result.apply
let retn = Ok

// define a "cons" function
let cons head tail = head :: tail

// right fold over the list
let initState = retn []
let folder head tail =
retn cons <*> (f head) <*> tail

List.foldBack folder list initState

/// Transform a "list<Result>" into a "Result<list>"
/// and collect the results using apply.
let sequenceResultA x = traverseResultA id x

24 changes: 24 additions & 0 deletions src/app/Fake.Net.Http/Result.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Fake.Net.Result

module Result =

type ResultBuilder() =
member __.Bind(m, f) =
match m with
| Error e -> Error e
| Ok a -> f a

member __.Return(x) =
Ok x

let apply fResult xResult =
match fResult,xResult with
| Ok f, Ok x ->
Ok (f x)
| Error errs, Ok x ->
Error errs
| Ok f, Error errs ->
Error errs
| Error errs1, Error errs2 ->
// concat both lists of errors
Error (List.concat [errs1; errs2])
5 changes: 5 additions & 0 deletions src/app/Fake.Net.Http/paket.references
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
group netcore

FSharp.Core
NETStandard.Library
System.Net.Http

0 comments on commit c666715

Please sign in to comment.