diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index a71f7d9..28877e7 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -15,7 +15,7 @@ module ArgTypes = let private tryParseIntInRange (floor, ceiling) text = text |> parseInRange (floor, ceiling) - |> Result.mapError (fun _ -> InvalidNumber (text, floor, ceiling)) + |> Result.mapError (fun _ -> ParseNumberFailure (text, floor, ceiling)) type FileCount = private FileCount of int with static member val AllowedRange = 1, Int32.MaxValue @@ -25,9 +25,7 @@ module ArgTypes = |> stripSeparators |> parseInRange FileCount.AllowedRange |> Result.mapError (fun _ -> - InvalidNumber (text, - fst FileCount.AllowedRange, - snd FileCount.AllowedRange)) + ParseNumberFailure (text, fst FileCount.AllowedRange, snd FileCount.AllowedRange)) |> Result.map FileCount member this.Value = let (FileCount count) = this in count @@ -125,10 +123,11 @@ module ArgTypes = private { fileCount: int options: Options } + member x.FileCount = x.fileCount member x.Options = x.options - static member Create(count: FileCount, options: Options) = + static member Create (count: FileCount, options: Options) = { fileCount = count.Value options = { Prefix = options.Prefix diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index bff2496..2e892ef 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -9,11 +9,12 @@ module Errors = | MalformedFlags | UnsupportedFlags | DuplicateFlags - | InvalidNumber of Arg: string * Floor: int * Ceiling: int + | ParseNumberFailure of Arg: string * Floor: int * Ceiling: int | DirectoryMissing of string | DriveSpaceConfirmationFailure | DriveSpaceInsufficient of Needed: string * Actual: string - | UnknownError of string + | IoError of string + | CancelledByUser let getMessage error = match error with @@ -22,11 +23,12 @@ module Errors = | MalformedFlags -> "Malformed flag(s) found." | UnsupportedFlags -> "Unsupported flag(s) found." | DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once." - | InvalidNumber (x, f, c) -> + | ParseNumberFailure (x, f, c) -> $"Could not parse \"%s{x}\" to an integer between %s{formatInt f} and %s{formatInt c}, inclusive." | DirectoryMissing e -> $"Directory \"%s{e}\" was not found." | DriveSpaceConfirmationFailure -> "Could not confirm available drive space." | DriveSpaceInsufficient (needed, actual) -> $"Insufficient drive space. Though %s{needed} is necessary, only %s{actual} is available." - | UnknownError e -> e + | IoError e -> $"IO error: %s{e}" + | CancelledByUser -> "Cancelled." diff --git a/src/UniqueFileGenerator.Console/Io.fs b/src/UniqueFileGenerator.Console/Io.fs index d179534..9ad45f3 100644 --- a/src/UniqueFileGenerator.Console/Io.fs +++ b/src/UniqueFileGenerator.Console/Io.fs @@ -10,7 +10,12 @@ open System.IO open System.Threading module Io = - let private formatBytes (bytes: int64) : string = + let verifyDirectory dir = + match Directory.Exists dir with + | true -> Ok () + | false -> Error (DirectoryMissing dir) + + let private formatBytes (bytes: int64) = let kilobyte = 1024L let megabyte = kilobyte * 1024L let gigabyte = megabyte * 1024L @@ -23,11 +28,11 @@ module Io = | _ when bytes >= kilobyte -> sprintf "%s KB" ((float bytes / float kilobyte) |> formatFloat) | _ -> sprintf "%s bytes" (bytes |> formatInt64) - let verifyDriveSpace (args: Args) = - let bytesToKeepAvailable = 536_870_912L // 0.5 GB + let driveSpaceToKeepAvailable = 536_870_912L // 0.5 GB + let warningRatio = 0.75 - let necessaryDriveSpace = + let necessarySpace = let singleFileSize = args.Options.Size |> Option.defaultValue @@ -38,6 +43,25 @@ module Io = singleFileSize * int64 args.FileCount // Rough estimation + let confirmContinueDespiteLargeSize usableFreeSpace : bool = + let ratio = float necessarySpace / float usableFreeSpace + let isLargeRatio = ratio > warningRatio + + let confirm () = + Console.Write( + sprintf "This operation requires %s, which is %.2f%% of remaining drive space. Continue? (Y/n) " + (formatBytes necessarySpace) + (ratio * 100.0)) + + let reply = Console.ReadLine().Trim() + + [| "y"; "yes" |] + |> Array.exists (fun yesAnswer -> reply.Equals(yesAnswer, StringComparison.InvariantCultureIgnoreCase)) + + if isLargeRatio + then confirm () + else true + try let appDir = AppContext.BaseDirectory let root = Path.GetPathRoot appDir @@ -46,19 +70,15 @@ module Io = | null -> Error DriveSpaceConfirmationFailure | path -> let driveInfo = DriveInfo path - let usableFreeSpace = driveInfo.AvailableFreeSpace - bytesToKeepAvailable + let usableFreeSpace = driveInfo.AvailableFreeSpace - driveSpaceToKeepAvailable - if necessaryDriveSpace < usableFreeSpace - then Ok <| formatBytes necessaryDriveSpace - else Error <| DriveSpaceInsufficient (formatBytes necessaryDriveSpace, - formatBytes usableFreeSpace) + if necessarySpace > usableFreeSpace + then Error (DriveSpaceInsufficient (formatBytes necessarySpace, formatBytes usableFreeSpace)) + elif confirmContinueDespiteLargeSize usableFreeSpace + then Ok (formatBytes necessarySpace) + else Error CancelledByUser with - | e -> Error <| UnknownError $"%s{e.Message}" - - let verifyDirectory dir = - match Directory.Exists dir with - | true -> Ok () - | false -> Error (DirectoryMissing dir) + | e -> Error (IoError $"%s{e.Message}") let private createFile directory fileName (contents: string) = try diff --git a/src/UniqueFileGenerator.Console/Program.fs b/src/UniqueFileGenerator.Console/Program.fs index f0cdfe9..716f8a5 100644 --- a/src/UniqueFileGenerator.Console/Program.fs +++ b/src/UniqueFileGenerator.Console/Program.fs @@ -18,8 +18,8 @@ module Main = do! verifyDirectory args.Options.OutputDirectory let! spaceNeeded = verifyDriveSpace args - printLine $"This operation will use approximately %s{spaceNeeded} of drive space." - return generateFiles args + generateFiles args + return spaceNeeded } if Help.wasRequested rawArgs then @@ -27,8 +27,8 @@ module Main = 0 else match run rawArgs with - | Ok _ -> - printLine $"Done after %s{watch.ElapsedFriendly}" + | Ok spaceUsed -> + printLine $"Done after %s{watch.ElapsedFriendly}. Used approximately %s{spaceUsed} of drive space." 0 | Error e -> printError <| getMessage e diff --git a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj index 65a3c2b..f1b5ea6 100644 --- a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj +++ b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj @@ -4,6 +4,7 @@ Exe net9.0 3579 + true diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index 24da33f..3f62a7a 100644 --- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs +++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs @@ -64,21 +64,21 @@ let ``Appropriate error when invalid arg count (second pair incomplete)`` () = [] let ``Appropriate error when invalid file count`` () = let args = [| "notNumeric" |] - let expected = Error <| InvalidNumber(args[0], 1, Int32.MaxValue) + let expected = Error <| ParseNumberFailure(args[0], 1, Int32.MaxValue) let actual = validate args Assert.Equal(expected, actual) [] let ``Appropriate error when negative file count`` () = let args = [| "-1" |] - let expected = Error <| InvalidNumber(args[0], 1, Int32.MaxValue) + let expected = Error <| ParseNumberFailure(args[0], 1, Int32.MaxValue) let actual = validate args Assert.Equal(expected, actual) [] let ``Appropriate error when zero file count`` () = let args = [| "0" |] - let expected = Error <| InvalidNumber(args[0], 1, Int32.MaxValue) + let expected = Error <| ParseNumberFailure(args[0], 1, Int32.MaxValue) let actual = validate args Assert.Equal(expected, actual)