Commits (3)
module Mbackup.ConfigParser
open System
open System.IO
open System.Text.RegularExpressions
open Mbackup.Lib
type WellsConfig(fn: string) =
let mutable keyValuePairs: Map<string, string> = Map.empty
let dropEmptyLinesAndComments lines = Seq.filter (fun (line: string) -> not (line.TrimStart().StartsWith("#") || line.TrimEnd().Equals(""))) lines
let dropQuotesMaybe (value: string) = if value.StartsWith("\"") || value.StartsWith("'") then value.Substring(1, value.Length - 2) else value
let toKeyValue = Seq.map (fun (line: string) ->
let result: string[] = line.Split('=', 2)
(result.[0].Trim(), dropQuotesMaybe (result.[1].Trim())))
let result = File.ReadAllLines(fn) // file IO can throw Exception
|> dropEmptyLinesAndComments
|> toKeyValue
|> Seq.fold (fun (m: Map<string, string>) (k, v) -> m.Add(k, v)) keyValuePairs
keyValuePairs <- result
member this.ConfigFile = fn
member this.GetStr key =
let getEnv (varName: string) = Environment.GetEnvironmentVariable varName
let configKeyToEnvVar (key: string) =
key.ToUpper().Replace(".", "_").Replace("-", "_")
match getEnv (configKeyToEnvVar key) with
| null | "" -> keyValuePairs.TryFind key
| envValue -> Some envValue
member this.GetBool key =
Option.map (fun value -> Regex.IsMatch(value, "^(yes|true|enable|1)$", RegexOptions.IgnoreCase)) (this.GetStr key)
member this.GetFloat key =
let value = keyValuePairs.TryGetValue key
let parseFloat s = try Some (float s) with | _ -> None
Option.map parseFloat (this.GetStr key)
member this.GetInt key =
let value = keyValuePairs.TryGetValue key
let parseInt s = try Some (int s) with | _ -> None
Option.map parseInt (this.GetStr key)
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
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
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)
let ensureDir (path: string) = if path.EndsWith "/" then path else path + "/"
let ensureWinDir (path: string) = if path.EndsWith "\\" then path else path + "\\"
This diff is collapsed.
......@@ -7,6 +7,8 @@
<Compile Include="Lib.fs" />
<Compile Include="ConfigParser.fs" />
<Compile Include="Program.fs" />
module MbackupTests
open NUnit.Framework
open Mbackup
open Mbackup.Lib
let Setup () =
......@@ -12,16 +12,48 @@ let TestDumb () =
let TestToMingwPath () =
Assert.That("/cygdrive/c/", Is.EqualTo(ToMingwPath "c:\\"))
Assert.That("/cygdrive/c/", Is.EqualTo(ToMingwPath "C:\\"))
Assert.That("/cygdrive/c/foo", Is.EqualTo(ToMingwPath "C:\\foo"))
Assert.That("/cygdrive/d/foo", Is.EqualTo(ToMingwPath "D:\\foo"))
Assert.That("/cygdrive/d/Foo", Is.EqualTo(ToMingwPath "D:\\Foo"))
Assert.That("/cygdrive/c/foo/bar/", Is.EqualTo(ToMingwPath "C:\\foo\\bar\\"))
Assert.That("/cygdrive/c/foo/bar/baz.txt", Is.EqualTo(ToMingwPath "C:\\foo\\bar\\baz.txt"))
Assert.That("/cygdrive/c/foo", Is.EqualTo(ToMingwPath "C:/foo"))
Assert.That("/cygdrive/c/foo/bar", Is.EqualTo(ToMingwPath "C:/foo/bar"))
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 TesttoMingwPath () =
Assert.That("/cygdrive/c/", Is.EqualTo(toMingwPath "c:\\"))
Assert.That("/cygdrive/c/", Is.EqualTo(toMingwPath "C:\\"))
Assert.That("/cygdrive/c/foo", Is.EqualTo(toMingwPath "C:\\foo"))
Assert.That("/cygdrive/d/foo", Is.EqualTo(toMingwPath "D:\\foo"))
Assert.That("/cygdrive/d/Foo", Is.EqualTo(toMingwPath "D:\\Foo"))
Assert.That("/cygdrive/c/foo/bar/", Is.EqualTo(toMingwPath "C:\\foo\\bar\\"))
Assert.That("/cygdrive/c/foo/bar/baz.txt", Is.EqualTo(toMingwPath "C:\\foo\\bar\\baz.txt"))
Assert.That("/cygdrive/c/foo", Is.EqualTo(toMingwPath "C:/foo"))
Assert.That("/cygdrive/c/foo/bar", Is.EqualTo(toMingwPath "C:/foo/bar"))
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
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
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"))
let TestStringSplit () =
let r = "a=b".Split('=')
Assert.That("a", Is.EqualTo(r.[0]))
Assert.That("b", Is.EqualTo(r.[1]))
let r = "a= b".Split('=')
Assert.That("a", Is.EqualTo(r.[0]))
Assert.That(" b", Is.EqualTo(r.[1]))
let r = "a=b=c".Split('=', 2)
Assert.That("a", Is.EqualTo(r.[0]))
Assert.That("b=c", Is.EqualTo(r.[1]))
* COMMENT -*- mode: org -*-
#+Date: 2019-11-12
Time-stamp: <2019-11-12>
Time-stamp: <2019-11-14>
#+STARTUP: content
* notes :entry:
** 2019-11-13 how to run it in dev env?
......@@ -17,6 +17,9 @@ file list works.
ssh transfer works.
remote logging works.
read target from config file works. now I can just run
dotnet run -- -i
** 2019-11-12 docs
- rsync
......@@ -38,6 +41,9 @@ remote logging works.
F# Collection Types - F# | Microsoft Docs
Modules - F# | Microsoft Docs
Literals - F# | Microsoft Docs
......@@ -54,18 +60,37 @@ remote logging works.
DateTime.ToString Method (System) | Microsoft Docs
Standard Date and Time Format Strings | Microsoft Docs
- FSharpLint
- FSharp.Configuration
dotnet add package FSharp.Configuration --version 1.5.0
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
** 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
* later :entry:
** 2019-11-14 supports expand Downloads dir in user-default.list
** 2019-11-14 vscode f# doesn't support open a module
it can only support open a namespace.
using the vscode Ionide-fsharp extension.
* current :entry:
** 2019-11-13 next todos
......@@ -83,6 +108,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 missing reference to System.Runtime.Caching
search: how to reference System.Runtime.Caching for dotnet core project
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:
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
where should I save the App.config xml file?
search: what is app.config
App.Config: Basics and Best Practices - SubMain Blog
What is App.config in C#.NET? How to use it? - Stack Overflow
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
- create an installer. The installer should add scheduled task on install and
delete scheduled task on removal.
......@@ -185,7 +273,7 @@ C:\ProgramData\mbackup\user-default.list
- there is no enough RAM on B75I3. I will stop bogon VM and run win 10 VM.
** 2019-11-12 mbackup for windows
** 2019-11-12 mbackup for windows :featurereq:
- features
- windows installer.
bundles mingw rsync and ssh.
......@@ -209,6 +297,32 @@ C:\ProgramData\mbackup\user-default.list
it contains hostname.
* done :entry:
** 2019-11-14 support mbackup.txt config.
config file will have wells config file format.
empty lines and comment lines are ignored.
space around = is ignored.
if there are quotes on value side, it's trimmed.
[email protected]:port/path/
# target = [email protected]:port/path/
abc.def = some value
abc.def.ghi = 1233.45
foo = yes
env variable will be
in F# code,
conf = WellsConfig("C:\path\to\file.txt")
it works.
** 2019-11-13 extra user default list.
Documents is replaced by real path.