Commit 06c2e2fb authored by Yuanle Song's avatar Yuanle Song
Browse files

moved some functions to Lib.fs;

logger functions now support variable number of arguments.
parent f13bbecd
Loading
Loading
Loading
Loading

Lib.fs

0 → 100644
+72 −0
Original line number Diff line number Diff line
module Mbackup.Lib

open System
open System.Text.RegularExpressions

type Logger() =
  let mutable level = Logger.DEBUG

  static member DEBUG = 10
  static member INFO = 20
  static member WARNING = 30
  static member ERROR = 40
  static member LevelToString level =
    match level with
      | 10 -> "DEBUG"
      | 20 -> "INFO"
      | 30 -> "WARNING"
      | 40 -> "ERROR"
      | _ -> failwith (sprintf "Unknown log level: %d" level)

  member this.Level = level

  member this.LogMaybe level fmt =
    let doAfter s =
      if this.Level <= level then
        let ts = DateTime.Now.ToString("s")
        printf "%s %s %s\n" ts (Logger.LevelToString level) s
      else
        printf ""
    Printf.ksprintf doAfter fmt

  member this.SetLevel level =
    this.Level = level
  member this.Debug fmt = this.LogMaybe Logger.DEBUG fmt
  member this.Info fmt = this.LogMaybe Logger.INFO fmt
  member this.Warning fmt = this.LogMaybe Logger.WARNING fmt
  member this.Error fmt = this.LogMaybe Logger.ERROR fmt

// append string s to list if pred is true
let appendWhen (pred: bool) (lst: string list) (s: string) = if pred then List.append lst [s] else lst

let GetEnv (varName: string) = Environment.GetEnvironmentVariable varName

let GetEnvDefault (varName: string) (defaultValue: string) =
  let value = Environment.GetEnvironmentVariable varName
  match value with
    | null -> defaultValue
    | "" -> defaultValue
    | _ -> value

// Convert windows path to Mingw64 path.
// Supported windows path: C:\foo, C:/foo, /c/foo
// MingwPath format: /cygdrive/c/foo
let ToMingwPath (windowsPath: string) =
  let pattern = Regex("^/([c-zC-Z])/", RegexOptions.None)
  let result =
    if pattern.IsMatch(windowsPath) then
      "/cygdrive" + windowsPath
    else
      let pattern = Regex("^([c-zC-Z]):", RegexOptions.None)
      if pattern.IsMatch(windowsPath) then
        let result = windowsPath.Replace('\\', '/')
        "/cygdrive/" + result.Substring(0, 1).ToLower() + result.Substring(2)
      else
        windowsPath
  result

let EnsureDir (path: string) = if path.EndsWith "/" then path else path + "/"
let EnsureWinDir (path: string) = if path.EndsWith "\\" then path else path + "\\"

let parseMbackupConfig fn =
  ()
+16 −75
Original line number Diff line number Diff line
@@ -8,16 +8,18 @@
//     /%appdata%/mbackup/mbackup-default.exclude
//     /%appdata%/mbackup/local.exclude (optional)

module Mbackup
module Mbackup.Program

open System
open System.IO
open System.Diagnostics
open System.Text.RegularExpressions;
open System.Text.RegularExpressions
open System.Diagnostics.CodeAnalysis

open Argu

open Mbackup.Lib

let ExitBadParam = 1
let ExitTimeout = 2
let ExitIOError = 3
@@ -41,69 +43,6 @@ with
            | Node_Name _ -> "local node's name, used in remote logging"
            | Ssh_Key _ -> "ssh private key, used when backup to remote ssh node"

type Logger() =
  let mutable level = Logger.DEBUG

  static member DEBUG = 10
  static member INFO = 20
  static member WARNING = 30
  static member ERROR = 40
  static member LevelToString level =
    match level with
      | 10 -> "DEBUG"
      | 20 -> "INFO"
      | 30 -> "WARNING"
      | 40 -> "ERROR"
      | _ -> failwith (sprintf "Unknown log level: %d" level)
  
  member this.Level = level

  member this.LogMaybe level fmt = 
    if this.Level <= level then
      Printf.ksprintf (
        fun s ->
            let time = DateTime.Now
            printfn "%02d:%02d:%02d %s %s" time.Hour time.Minute time.Second (Logger.LevelToString level) s)
        fmt
    else
      Printf.ksprintf (fun _ -> printfn "") fmt

  member this.SetLevel level =
    this.Level = level
  member this.Debug = this.LogMaybe Logger.DEBUG
  member this.Info = this.LogMaybe Logger.INFO
  member this.Warning = this.LogMaybe Logger.WARNING
  member this.Error = this.LogMaybe Logger.ERROR

let GetEnv (varName: string) = Environment.GetEnvironmentVariable varName

let GetEnvDefault (varName: string) (defaultValue: string) =
  let value = Environment.GetEnvironmentVariable varName
  match value with
    | null -> defaultValue
    | "" -> defaultValue
    | _ -> value

// Convert windows path to Mingw64 path.
// Supported windows path: C:\foo, C:/foo, /c/foo
// MingwPath format: /cygdrive/c/foo
let ToMingwPath (windowsPath: string) =
  let pattern = Regex("^/([c-zC-Z])/", RegexOptions.None)
  let result =
    if pattern.IsMatch(windowsPath) then
      "/cygdrive" + windowsPath
    else
      let pattern = Regex("^([c-zC-Z]):", RegexOptions.None)
      if pattern.IsMatch(windowsPath) then
        let result = windowsPath.Replace('\\', '/')
        "/cygdrive/" + result.Substring(0, 1).ToLower() + result.Substring(2)
      else
        windowsPath
  result

let EnsureDir (path: string) = if path.EndsWith "/" then path else path + "/"
let EnsureWinDir (path: string) = if path.EndsWith "\\" then path else path + "\\"

let appDataRoamingDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) |> ToMingwPath |> EnsureDir
let programDataDirWin = GetEnv "PROGRAMDATA" |> EnsureWinDir
let programDataDir = ToMingwPath programDataDirWin
@@ -119,12 +58,14 @@ let userHomeWin =

let userHome = userHomeWin |> ToMingwPath

//let userConfigDir = appDataRoamingDir + "mbackup/"
let userConfigDirWin = programDataDirWin + "mbackup\\"
let userConfigDir = programDataDir + "mbackup/"
let runtimeDirWin = appDataLocalDirWin + "mbackup\\"
let runtimeDir = appDataLocalDir + "mbackup/"

let mbackupConfigFile = userConfigDirWin + "mbackup.ini"
let mbackupConfig: unit = parseMbackupConfig mbackupConfigFile

let isLocalTarget (target: string) = target.StartsWith "/"

// expand user file to mingw64 rsync supported path.
@@ -186,16 +127,16 @@ let generateMbackupList (logger: Logger) =
          // skip and give a warning on non-absolute path.
          // For user-default.list, auto prefix user's home dir, auto expand Documents, Downloads etc special folder.
          File.WriteAllLines(mbackupList, allLines)
          logger.Info "%s written" mbackupList
          logger.Info "mbackup.list file written: %s" mbackupList
          true
    with
      | :? System.IO.IOException as ex ->
        logger.Error "Read/write file failed: %s %s" ex.Source ex.Message
        false
      | ex ->
        logger.Error "Read/write mbackup list file failed: %s" ex.Message
        false

// append string s to list if pred is true
let appendWhen (pred: bool) (lst: string list) (s: string) = if pred then List.append lst [s] else lst

[<EntryPoint>]
let main argv =
    let errorHandler = ProcessExiter(colorizer = function ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red)
@@ -206,8 +147,8 @@ let main argv =

    let logger = Logger()

    logger.Info "using user config dir: %s" userConfigDirWin
    logger.Info "using runtime dir: %s" runtimeDirWin
    logger.Info "user config dir: %s" userConfigDirWin
    logger.Info "runtime dir: %s" runtimeDirWin

    let rsyncCmd: string list = []
    let rsyncCmd = appendWhen dryRun rsyncCmd "--dry-run"
@@ -257,16 +198,16 @@ let main argv =
          let rsyncCmd = List.append rsyncCmd ["/"]
          let rsyncCmd = List.append rsyncCmd [backupTarget]
          let rsyncArgs = rsyncCmd |> String.concat " "
          // TODO print rsyncArgs in the same Info call when Info supports multiple params.
          logger.Info "Note: if you run the following rsync command yourself, make sure the generated file list (%s) is up-to-date." mbackupFile

          let rsyncExe = mbackupInstallDirWinTest + "rsync-w64\\usr\\bin\\rsync.exe"
          let echoExe = "C:\\Program Files\\Git\\usr\\bin\\echo.exe"
          try
            IO.Directory.CreateDirectory(runtimeDir) |> ignore
            IO.Directory.CreateDirectory(userConfigDir) |> ignore
            let proc = Process.Start(rsyncExe, rsyncArgs)
            logger.Info "%s" (rsyncExe + " " + rsyncArgs)
            logger.Info "Note: if you run the following rsync command yourself, make sure the generated file list (%s) is up-to-date.\n%s" mbackupFile (rsyncExe + " " + rsyncArgs)
            if proc.WaitForExit Int32.MaxValue then
              logger.Info "mbackup exit"
              proc.ExitCode
            else
              logger.Error "mbackup timed out while waiting for rsync to complete%s" ""
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Lib.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>

+21 −1
Original line number Diff line number Diff line
module MbackupTests

open NUnit.Framework
open Mbackup
open Mbackup.Lib

[<SetUp>]
let Setup () =
@@ -25,3 +25,23 @@ let TestToMingwPath () =
    Assert.That("/cygdrive/c/foo", Is.EqualTo(ToMingwPath "/c/foo"))
    Assert.That("/cygdrive/D/foo", Is.EqualTo(ToMingwPath "/D/foo"))
    Assert.That("/var/log", Is.EqualTo(ToMingwPath "/var/log"))

let mysprintf fmt = sprintf fmt

[<Test>]
let TestMyprintf () =
  Assert.That("123", Is.EqualTo(mysprintf "123"))
  Assert.That("123", Is.EqualTo(mysprintf "%d" 123))
  Assert.That("123 456", Is.EqualTo(mysprintf "%d %d" 123 456))

let mylogger fmt =
  let doAfter s =
    "INFO " + s
  Printf.ksprintf doAfter fmt

[<Test>]
let TestMylogger () =
  Assert.That("INFO 123", Is.EqualTo(mylogger "123"))
  Assert.That("INFO 123", Is.EqualTo(mylogger "%d" 123))
  Assert.That("INFO 123 456", Is.EqualTo(mylogger "%d %d" 123 456))
  Assert.That("INFO 123 456 a", Is.EqualTo(mylogger "%d %d %s" 123 456 "a"))
+87 −0
Original line number Diff line number Diff line
@@ -38,6 +38,9 @@ remote logging works.
  https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/
  F# Collection Types - F# | Microsoft Docs
  https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/fsharp-collection-types
  Modules - F# | Microsoft Docs
  https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/modules
  https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/namespaces
  fsharp-cheatsheet
  https://dungpa.github.io/fsharp-cheatsheet/
  Literals - F# | Microsoft Docs
@@ -54,20 +57,41 @@ remote logging works.
  MyPictures
  DesktopDirectory
  Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
  DateTime.ToString Method (System) | Microsoft Docs
  https://docs.microsoft.com/en-us/dotnet/api/system.datetime.tostring?view=netframework-4.8
  Standard Date and Time Format Strings | Microsoft Docs
  https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings?view=netframework-4.8
- FSharpLint
  http://fsprojects.github.io/FSharpLint/
  http://fsprojects.github.io/FSharpLint/index.html
- FSharp.Configuration
  http://fsprojects.github.io/FSharp.Configuration/
  dotnet add package FSharp.Configuration --version 1.5.0
  http://fsprojects.github.io/FSharp.Configuration/IniTypeProvider.html
  I will use the ini provider.
  user don't need to escape string or pay attension to spaces in ini file.
  this lib doesn't work with dotnet core 3.
- Formatted text using printf | F# for fun and profit
  https://fsharpforfunandprofit.com/posts/printf/
- 

** 2019-11-13 install dir layout.
C:\Program Files\mbackup\rsync-w64\usr\bin\rsync.exe
C:\Program Files\mbackup\rsync-w64\usr\bin\ssh.exe
C:\ProgramData\mbackup\mbackup-default.exclude
C:\ProgramData\mbackup\mbackup-default.list
C:\ProgramData\mbackup\user-default.list
C:\ProgramData\mbackup\mbackup.ini

* later                                                               :entry:
** 2019-11-14 supports expand Downloads dir in user-default.list
* current                                                             :entry:
** 
** 2019-11-14 support mbackup.ini config.
options to support in ini file.
[mbackup]
target=user@host:port/path/

** 2019-11-13 next todos
- DONE fix TODOs in F# code
- DONE add rsync command and arguments for running in windows.
@@ -83,6 +107,69 @@ C:\ProgramData\mbackup\user-default.list
  command line param > env var > mbackup default value.
  support --node-name param.
- test run in console and scheduled task.
  run in console works.
  try run in scheduled task.

  SCHTASKS /Create /?
  # run mbackup 15m after user logon.
  SCHTASKS /Create /U <username> /SC ONLOGON /TN mbackup-logon /TR "\"<path\to\mbackup.exe>\" \"args\"" /DELAY 15:00
  # run mbackup at 10am and 4pm.
  SCHTASKS /Create /U <username> /SC DAILY /TN mbackup-morning /TR "\"<path\to\mbackup.exe>\" \"args\"" /ST 10:00 /ET 13:00 /K
  SCHTASKS /Create /U <username> /SC DAILY /TN mbackup-afternoon /TR "\"<path\to\mbackup.exe>\" \"args\"" /ST 16:00 /ET 19:00 /K

  # debug purpose, one time only
  SCHTASKS /Create /U <username> /SC ONCE /TN mbackup-afternoon /TR "\"<path\to\mbackup.exe>\" \"args\"" /ST 12:03

  - problems
    - how many scheduled task to run on a multi-user PC?
      each user have its own user-default.list expansion.
      should I iter over all users on PC?
      I think only current user can get it's profile dir and special dirs.
    - maybe config mbackup to run after user logon. with 15minute delay.
      always run as current user.
    - how to not require any param when running mbackup.exe?
      put TARGET and other option in a config file?
      define system level TARGET env variable.

      use config file is easier for user to edit and making the change effective.
      userConfigDir / mbackup.conf

      search: F# read config file
      FSharp.Configuration
      http://fsprojects.github.io/FSharp.Configuration/
    - FSharp.Configuration missing reference to System.Runtime.Caching
      search: how to reference System.Runtime.Caching for dotnet core project
      https://www.nuget.org/packages/System.Runtime.Caching/
      dotnet add package System.Runtime.Caching --version 4.6.0
      should I add 4.0.0? Can I use a higher version?

      still not compatible.
      search: use FSharp.Configuration with dotnet core 3
    - give up on FSharp.Configuration.
    - try this:
      FsConfig
      https://www.demystifyfp.com/FsConfig/
      AppSettings is only supported in V0.0.6 or below.
      try F# AppSettings directly.
      If nothing is easy to use, write my own parser.
      Support similar config format as python wells lib.
    - ConfigurationManager.AppSettings Property (System.Configuration) | Microsoft Docs
      https://docs.microsoft.com/en-us/dotnet/api/system.configuration.configurationmanager.appsettings?view=netframework-4.8
      where should I save the App.config xml file?
      search: what is app.config
      App.Config: Basics and Best Practices - SubMain Blog
      https://blog.submain.com/app-config-basics-best-practices/
      What is App.config in C#.NET? How to use it? - Stack Overflow
      https://stackoverflow.com/questions/13043530/what-is-app-config-in-c-net-how-to-use-it
      Okay. This is not what I want.
      This is for application configuration (rarely change), not for user configuration (can change any time).
      dotnet will create <yourapp>.exe.config from your App.config file.
    - how to use multiple files in F# dotnet core project?
      search: f# module and namespace
      search: f# module
      Modules - F# | Microsoft Docs
      https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/modules

- create an installer. The installer should add scheduled task on install and
  delete scheduled task on removal.