diff --git a/src/Andromeda/AvaloniaApp/DomainTypes.fs b/src/Andromeda/AvaloniaApp/DomainTypes.fs index 88244ec..7d54a86 100644 --- a/src/Andromeda/AvaloniaApp/DomainTypes.fs +++ b/src/Andromeda/AvaloniaApp/DomainTypes.fs @@ -6,4 +6,6 @@ module DomainTypes = open GogApi.DomainTypes open Andromeda.Core - let toProductInfo game : ProductInfo = { id = game.id; title = game.name } + let toProductInfo game : ProductInfo = + { id = game.id + title = game.name |> GameName.unwrap } diff --git a/src/Andromeda/AvaloniaApp/Update.fs b/src/Andromeda/AvaloniaApp/Update.fs index f356536..6c5559d 100644 --- a/src/Andromeda/AvaloniaApp/Update.fs +++ b/src/Andromeda/AvaloniaApp/Update.fs @@ -55,7 +55,8 @@ module Update = let sub dispatch = match task with | Some _ -> - "Download installer for " + game.name + "." + GameName.unwrap game.name + |> sprintf "Download installer for %s." |> logInfo let invoke () = @@ -79,7 +80,8 @@ module Update = DispatcherTimer.Run(Func(invoke), TimeSpan.FromSeconds 0.5) |> ignore | None -> - "Use cached installer for " + game.name + "." + GameName.unwrap game.name + |> sprintf "Use cached installer for %s." |> logInfo UpdateDownloadSize(game.id, maxFileSize) @@ -136,7 +138,9 @@ module Update = |> Cmd.ofMsg | None -> if showNotification then - AddNotification $"No new version available for %s{game.name}" + GameName.unwrap game.name + |> sprintf "No new version available for %s" + |> AddNotification |> Cmd.ofMsg else Cmd.none @@ -389,7 +393,8 @@ module Update = state, Cmd.none | _ -> let invoke () = - Download.extractLibrary settings game.name filePath version + let gameName = GameName.unwrap game.name + Download.extractLibrary settings gameName filePath version let cmd = [ Cmd.ofMsg (UpdateDownloadInstalling game.id) diff --git a/src/Andromeda/AvaloniaApp/ViewComponents/Main.fs b/src/Andromeda/AvaloniaApp/ViewComponents/Main.fs index dc8509b..47f4a54 100644 --- a/src/Andromeda/AvaloniaApp/ViewComponents/Main.fs +++ b/src/Andromeda/AvaloniaApp/ViewComponents/Main.fs @@ -6,6 +6,8 @@ open Avalonia.Layout open SimpleOptics open System +open Andromeda.Core + open Andromeda.AvaloniaApp module Main = @@ -37,7 +39,7 @@ module Main = let gameName = Optic.get (MainStateOptic.game productId) state |> function - | Some game -> game.name + | Some game -> GameName.unwrap game.name | None -> "" TabItem.create [ diff --git a/src/Andromeda/Core/Andromeda.Core.fsproj b/src/Andromeda/Core/Andromeda.Core.fsproj index 5bd8210..443df82 100644 --- a/src/Andromeda/Core/Andromeda.Core.fsproj +++ b/src/Andromeda/Core/Andromeda.Core.fsproj @@ -5,10 +5,11 @@ - + - - + + + diff --git a/src/Andromeda/Core/Constants.fs b/src/Andromeda/Core/Constants.fs index cf21d6f..6348073 100644 --- a/src/Andromeda/Core/Constants.fs +++ b/src/Andromeda/Core/Constants.fs @@ -7,3 +7,4 @@ module Constants = let settingsFile = "settings.json" let versionFile = "version.txt" let installerCacheSubPath = "installers" + let tmpFolder = "tmp" diff --git a/src/Andromeda/Core/Download.fs b/src/Andromeda/Core/Download.fs index e4a1900..defd3ad 100644 --- a/src/Andromeda/Core/Download.fs +++ b/src/Andromeda/Core/Download.fs @@ -14,54 +14,60 @@ open Andromeda.Core.Helpers /// A module for everything which helps to download and install games module Download = + open System.Threading.Tasks + let client = new HttpClient() - let startFileDownload (SafeDownLink url) gameName version = - let version = - match version with - | Some v -> "-" + v - | None -> "" + type FileDownloadInfo = + { downloadTask: Task option + filePath: string + tmpPath: string } - // Remove invalid characters from gameName - let gameName = Path.removeInvalidFileNameChars gameName + let getVersionSuffix = + function + | Some version -> $"-%s{version}" + | None -> "" + + let getFileName gameName version = + let versionSuffix = getVersionSuffix version + sprintf "%s%s.%s" gameName versionSuffix SystemInfo.installerEnding + let buildFileDownload gameName version = let dir = SystemInfo.installerCachePath + let fileName = getFileName gameName version + let filepath = Path.combine dir fileName + let tmppath = Path.combine3 dir Constants.tmpFolder fileName - let filepath = - Path.Combine( - dir, - sprintf "%s%s.%s" gameName version SystemInfo.installerEnding - ) + { downloadTask = None + filePath = filepath + tmpPath = tmppath } - let tmppath = - Path.Combine( - dir, - "tmp", - sprintf "%s%s.%s" gameName version SystemInfo.installerEnding - ) + let setupDownloadTask (SafeDownLink url) path = + task { + use fileStream = File.create path - Directory.CreateDirectory(Path.Combine(dir, "tmp")) - |> ignore + let url = url.Replace("http://", "https://") - let file = FileInfo(filepath) + let! response = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead) - match file.Exists with - | false -> - let url = url.Replace("http://", "https://") + do response.EnsureSuccessStatusCode() |> ignore + do! response.Content.CopyToAsync fileStream + } + + let startFileDownload downLink gameName version = + // Remove invalid characters from gameName + let gameName = Path.removeInvalidFileNameChars gameName - let task = - task { - use fileStream = File.Create tmppath + let download = buildFileDownload gameName version - let! response = - client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead) + let file = FileInfo(download.filePath) - do response.EnsureSuccessStatusCode() |> ignore - do! response.Content.CopyToAsync fileStream - } + match file.Exists with + | false -> + let task = setupDownloadTask downLink download.tmpPath - (task |> Some, filepath, tmppath) - | true -> (None, filepath, tmppath) + { download with downloadTask = Some task } + | true -> download let rec copyDirectory (sourceDirName: string) @@ -222,7 +228,7 @@ module Download = else None - let (task, filepath, tmppath) = + let fileDownload = startFileDownload urlResponse.downlink gameName @@ -230,9 +236,9 @@ module Download = return Some( - task, - filepath, - tmppath, + fileDownload.downloadTask, + fileDownload.filePath, + fileDownload.tmpPath, info.size * 1L, checksum ) diff --git a/src/Andromeda/Core/Helpers/Path.fs b/src/Andromeda/Core/Helpers/IO.fs similarity index 81% rename from src/Andromeda/Core/Helpers/Path.fs rename to src/Andromeda/Core/Helpers/IO.fs index e27303a..494ba69 100644 --- a/src/Andromeda/Core/Helpers/Path.fs +++ b/src/Andromeda/Core/Helpers/IO.fs @@ -3,18 +3,31 @@ namespace Andromeda.Core.Helpers open System open System.IO +[] +module File = + let create path = + Path.GetDirectoryName(path: string) + |> Directory.CreateDirectory + |> ignore + + File.Create(path) + [] module Path = + let combine path1 path2 = Path.Combine(path1, path2) + let combine3 path1 path2 path3 = Path.Combine(path1, path2, path3) + // Taken and converted to F# from https://blez.wordpress.com/2013/02/18/get-file-shortcuts-target-with-c/ let getShortcutTarget (file: string) = try - if Path.GetExtension(file).ToLower().Equals(".lnk") - |> not then + if + Path.GetExtension(file).ToLower().Equals(".lnk") + |> not + then Exception("Supplied file must be a .LNK file") |> raise - let fileStream = - File.Open(file, FileMode.Open, FileAccess.Read) + let fileStream = File.Open(file, FileMode.Open, FileAccess.Read) use fileReader = new BinaryReader(fileStream) @@ -50,7 +63,7 @@ module Path = - int64 (2) // read // the base pathname. I don't need the 2 terminating nulls. let linkTarget = fileReader.ReadChars((int) pathLength) // should be unicode safe - let link = new string(linkTarget) + let link = new string (linkTarget) let start = link.IndexOf("\0\0") @@ -64,7 +77,8 @@ module Path = firstPart + secondPart else link - with _ -> "" + with + | _ -> "" let removeInvalidFileNameChars part = Path.GetInvalidFileNameChars() diff --git a/src/Andromeda/Core/Types/Domain.Modules.fs b/src/Andromeda/Core/Types/Domain.Modules.fs new file mode 100644 index 0000000..c25a016 --- /dev/null +++ b/src/Andromeda/Core/Types/Domain.Modules.fs @@ -0,0 +1,29 @@ +namespace Andromeda.Core + +[] +[] +module GameName = + let create value = GameName value + let unwrap (GameName name) = name + +[] +[] +module GamePath = + open Andromeda.Core.Helpers + + let private create value = GamePath value + + let ofName name = + name + |> GameName.unwrap + |> Path.removeInvalidFileNameChars + |> create + +[] +[] +module Game = + let create id name = + { Game.id = id + name = GameName.create name + image = None + status = Pending } diff --git a/src/Andromeda/Core/Types/Domain.Optics.fs b/src/Andromeda/Core/Types/Domain.Optics.fs new file mode 100644 index 0000000..3e66a84 --- /dev/null +++ b/src/Andromeda/Core/Types/Domain.Optics.fs @@ -0,0 +1,19 @@ +namespace Andromeda.Core + +open SimpleOptics + +[] +module GameOptic = + let id = Lens((fun (x: Game) -> x.id), (fun game value -> { game with id = value })) + + let name = + Lens((fun (x: Game) -> x.name), (fun game value -> { game with name = value })) + + let image = + Lens((fun (x: Game) -> x.image), (fun game value -> { game with image = value })) + + let status = + Lens( + (fun (x: Game) -> x.status), + (fun game value -> { game with status = value }) + ) diff --git a/src/Andromeda/Core/Types/Game.fs b/src/Andromeda/Core/Types/Domain.fs similarity index 83% rename from src/Andromeda/Core/Types/Game.fs rename to src/Andromeda/Core/Types/Domain.fs index a12c141..4b8681c 100644 --- a/src/Andromeda/Core/Types/Game.fs +++ b/src/Andromeda/Core/Types/Domain.fs @@ -28,15 +28,11 @@ type GameStatus = | Installing of filepath: string | Installed of version: string option * gameDir: string +type GameName = private GameName of string +type GamePath = private GamePath of string + type Game = { id: GogApi.DomainTypes.ProductId - name: string + name: GameName image: string option status: GameStatus } - -module Game = - let create id name = - { Game.id = id - name = name - image = None - status = Pending } diff --git a/src/Andromeda/Core/Types/Game.Optics.fs b/src/Andromeda/Core/Types/Game.Optics.fs deleted file mode 100644 index 3af7974..0000000 --- a/src/Andromeda/Core/Types/Game.Optics.fs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Andromeda.Core - -open SimpleOptics - -[] -module GameOptic = - let id = - Lens( - (fun (x: Game) -> x.id), - (fun (x: Game) (value: GogApi.DomainTypes.ProductId) -> { x with id = value }) - ) - - let name = - Lens( - (fun (x: Game) -> x.name), - (fun (x: Game) (value: string) -> { x with name = value }) - ) - - let image = - Lens( - (fun (x: Game) -> x.image), - (fun (x: Game) (value: string option) -> { x with image = value }) - ) - - let status = - Lens( - (fun (x: Game) -> x.status), - (fun (x: Game) (value: GameStatus) -> { x with status = value }) - )