Newer
Older
// Learn more about F# at http://fsharp.org
//
// - backup file list
// /%appdata%/mbackup/mbackup-default.list
// /%appdata%/mbackup/user-default.list
// /%appdata%/mbackup/local.list (optional)
// - exclude pattern
// /%appdata%/mbackup/mbackup-default.exclude
// /%appdata%/mbackup/local.exclude (optional)
open System
open System.Diagnostics
open Argu
let ExitBadParam = 1
let ExitTimeout = 2
| [<AltCommandLine("-n")>] Dry_Run
| Remote_User of remoteUser: string
| [<AltCommandLine("-i")>] Itemize_Changes
| Node_Name of nodeName: string
with
interface IArgParserTemplate with
member s.Usage =
match s with
| Dry_Run _ -> "only show what will be done, do not transfer any file"
| Target _ -> "rsync target, could be local dir or remote ssh dir"
| Remote_User _ -> "remote linux user to own the backup files"
| Itemize_Changes _ -> "add -i option to rsync"
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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 appDataRoamingDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) |> ToMingwPath |> EnsureDir
let programDataDir = GetEnv "PROGRAMDATA" |> ToMingwPath |> EnsureDir
let appDataLocalDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) |> ToMingwPath |> EnsureDir
let mbackupInstallDirWin = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) |> EnsureDir |> fun s -> s + "mbackup"
let mbackupInstallDir = mbackupInstallDirWin |> ToMingwPath
//let userConfigDir = appDataRoamingDir + "mbackup/"
let userConfigDir = programDataDir + "mbackup/"
let runtimeDir = appDataLocalDir + "mbackup/"
let isLocalTarget (target: string) = target.StartsWith "/"
// generate mbackup.list file
let generateMbackupList =
// TODO run make to update mbackup.list. only update if source file have changed.
null
// 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)
let parser = ArgumentParser.Create<CLIArguments>(programName = "mbackup.exe", errorHandler = errorHandler)
let results = parser.Parse argv
let dryRun = results.Contains Dry_Run
let itemizeChanges = results.Contains Itemize_Changes
let logger = Logger()
logger.Info "userConfigDir=%s" userConfigDir
logger.Info "runtimeDir=%s" runtimeDir
let rsyncCmd: string list = []
let rsyncCmd = appendWhen dryRun rsyncCmd "--dry-run"
let rsyncCmd = appendWhen itemizeChanges rsyncCmd "-i"
let rsyncCmd = List.append rsyncCmd ("-h --stats -togr --delete --delete-excluded --ignore-missing-args".Split [|' '|] |> Array.toList)
let mbackupFile = runtimeDir + "mbackup.list"
let rsyncCmd = List.append rsyncCmd [sprintf "--files-from=%s" mbackupFile]
let excludeFile = userConfigDir + "mbackup-default.exclude"
let rsyncCmd = List.append rsyncCmd [sprintf "--exclude-from=%s" excludeFile]
let localExcludeFile = userConfigDir + "local.exclude"
let rsyncCmd = appendWhen (IO.File.Exists localExcludeFile) rsyncCmd (sprintf "--exclude-from=%s" localExcludeFile)
let localLogFile = runtimeDir + "mbackup.log"
let rsyncCmd = List.append rsyncCmd [sprintf "--log-file=%s" localLogFile]
// TODO remove usage of test dir.
let mbackupInstallDirWinTest = "D:\\downloads\\apps\\mbackupTest\\"
let mbackupInstallDirTest = mbackupInstallDirWinTest |> ToMingwPath |> EnsureDir
let sshExeFile = mbackupInstallDirTest + "rsync-w64/usr/bin/ssh.exe"
let sshConfigFile = userConfigDir + "ssh_config"
let sshPrivateKeyFile = userConfigDir + "ssh_id_rsa"
let rsyncCmd = List.append rsyncCmd [sprintf "-e \"%s -F %s -i %s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\"" sshExeFile sshConfigFile sshPrivateKeyFile]
let backupTarget = results.GetResult (Target, defaultValue = Environment.GetEnvironmentVariable "TARGET")
match backupTarget with
| null ->
logger.Error "TARGET is not defined"
ExitBadParam
| _ ->
let backupTarget = ToMingwPath backupTarget
let rsyncCmd =
if not (isLocalTarget backupTarget)
then
let nodeName =
match results.GetResult Node_Name with
| null ->
match GetEnv "NODE_NAME" with
| null -> Net.Dns.GetHostName()
| X -> X
| X -> X
let remoteLogFile = sprintf "/var/log/mbackup/%s.log" nodeName
let remoteUser = results.GetResult (Remote_User, defaultValue = Environment.UserName)
let rsyncCmd = List.append rsyncCmd [sprintf "--remote-option=--log-file=%s" remoteLogFile]
let rsyncCmd = List.append rsyncCmd [sprintf "--chown=%s:%s" remoteUser remoteUser]
rsyncCmd
else
rsyncCmd
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)
if proc.WaitForExit Int32.MaxValue then
proc.ExitCode
else
logger.Error "mbackup timed out while waiting for rsync to complete"
ExitTimeout
with
| _ ->
logger.Error "Create runtime dir failed"
ExitIOError