Skip to content
Commits on Source (3)
......@@ -27,6 +27,10 @@ type WellsConfig(fn: string) =
match getEnv (configKeyToEnvVar key) with
| null | "" -> keyValuePairs.TryFind key
| envValue -> Some envValue
member this.GetStrDefault key defaultValue =
match this.GetStr key with
| None -> defaultValue
| Some value -> value
member this.GetBool key =
Option.map (fun value -> Regex.IsMatch(value, "^(yes|true|enable|1)$", RegexOptions.IgnoreCase)) (this.GetStr key)
member this.GetFloat key =
......
// Learn more about F# at http://fsharp.org
//
// - mbackup config file
// %programdata%/mbackup/mbackup-config.txt
// - backup file list
// /%appdata%/mbackup/mbackup-default.list
// /%appdata%/mbackup/user-default.list
// /%appdata%/mbackup/local.list (optional)
// %programdata%/mbackup/default-list.txt
// %programdata%/mbackup/user-default-list.txt
// %programdata%/mbackup/local-list.txt (optional)
// - exclude pattern
// /%appdata%/mbackup/mbackup-default.exclude
// /%appdata%/mbackup/local.exclude (optional)
// %programdata%/mbackup/default-exclude.txt
// %programdata%/mbackup/local-exclude.txt (optional)
module Mbackup.Program
......@@ -30,6 +32,20 @@ let ExitUserError = 4
let version = Reflection.Assembly.GetEntryAssembly().GetName().Version
let versionStr = version.ToString()
// base filename for use in mbackup for windows.
// I use .txt and .log extension because user can open/edit them easily.
module MbackupFileName =
let DefaultList = "default-list.txt"
let DefaultExclude = "default-exclude.txt"
let LocalList = "local-list.txt"
let LocalExclude = "local-exclude.txt"
let UserDefaultList = "user-default-list.txt"
let Config = "mbackup-config.txt"
// run time files
let GeneratedList = "mbackup-list.txt"
let Log = "mbackup.log"
[<SuppressMessage("*", "UnionCasesNames")>]
type CLIArguments =
| [<AltCommandLine("-n")>] Dry_Run
......@@ -50,6 +66,11 @@ type CLIArguments =
| Ssh_Key _ -> "ssh private key, used when backup to remote ssh node"
| Version _ -> "show mbackup version and exit"
type MbackupRuntimeConfig =
{ Logger: Logger
Config: WellsConfig
Options: ParseResults<CLIArguments> }
let programFilesDirWin = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) |> ensureWinDir
let programFilesDir = toMingwPath programFilesDirWin
let mbackupProgramDirWin = programFilesDirWin + "mbackup\\"
......@@ -74,7 +95,6 @@ let mbackupInstallDir = mbackupInstallDirWin |> toMingwPath
let userHomeWin =
getEnvDefault "HOME" (Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)) |> ensureWinDir
let userHome = userHomeWin |> toMingwPath
let userConfigDirWin = programDataDirWin + "mbackup\\"
......@@ -82,10 +102,8 @@ let userConfigDir = programDataDir + "mbackup/"
let runtimeDirWin = appDataLocalDirWin + "mbackup\\"
let runtimeDir = appDataLocalDir + "mbackup/"
let mbackupConfigFile = userConfigDirWin + "mbackup.txt"
// return true if target is a local dir. local dir can be unix style or windows style.
let isLocalTarget (target: string) = target.StartsWith "/" || Regex.IsMatch(target, "^[c-z]:", RegexOptions.IgnoreCase)
let isLocalTarget (target: string) = target.StartsWith "/" || Regex.IsMatch(target, "^[a-z]:", RegexOptions.IgnoreCase)
// expand user file to mingw64 rsync supported path.
// abc -> /cygdrive/c/Users/<user>/abc
......@@ -120,19 +138,20 @@ let expandUserFile (fn: string) =
if fn.StartsWith("/") then fn
else userHome + fn
// generate mbackup.list file
let generateMbackupList (logger: Logger) =
// TODO how to only regenerate if source file have changed? should I bundle GNU make with mbackup?
// just compare mbackup.list mtime with its source files?
let mbackupDefaultList = userConfigDirWin + "mbackup-default.list"
let mbackupLocalList = userConfigDirWin + "local.list"
let mbackupUserDefaultList = userConfigDirWin + "user-default.list"
let mbackupList = runtimeDirWin + "mbackup.list"
// local functions
// read mbackup list file
let readMbackupListFile fn =
let dropEmptyLinesAndComments lines =
Seq.filter (fun (line: string) -> not (line.TrimStart().StartsWith("#") || line.TrimEnd().Equals(""))) lines
let readMbackupListFile fn = File.ReadAllLines(fn) |> dropEmptyLinesAndComments
File.ReadAllLines(fn) |> dropEmptyLinesAndComments
// generate MbackupFileName.GeneratedList file
let generateMbackupList (logger: Logger) =
// TODO how to only regenerate if source file have changed? should I bundle GNU make with mbackup?
// just compare MbackupFileName.GeneratedList mtime with its source files?
let mbackupDefaultList = userConfigDirWin + MbackupFileName.DefaultList
let mbackupLocalList = userConfigDirWin + MbackupFileName.LocalList
let mbackupUserDefaultList = userConfigDirWin + MbackupFileName.UserDefaultList
let mbackupList = runtimeDirWin + MbackupFileName.GeneratedList
try
let defaultListLines = readMbackupListFile mbackupDefaultList |> Seq.map toMingwPath
......@@ -144,24 +163,22 @@ let generateMbackupList (logger: Logger) =
with
| :? FileNotFoundException -> (true, Seq.empty)
| ex ->
logger.Error "Read mbackupLocalList failed: %s" ex.Message
logger.Error "Read mbackupLocalList %s failed: %s" mbackupLocalList ex.Message
(false, Seq.empty)
match localListLinesMaybe with
| (false, _) -> failwith "Read mbackup local.list file failed"
| (false, _) -> failwith "Read mbackupLocalList failed"
| (true, localListLines) ->
let userDefaultListLines = readMbackupListFile mbackupUserDefaultList |> Seq.map expandUserFile
let allLines = Seq.append (Seq.append defaultListLines localListLines) userDefaultListLines
// For mbackup-default.list and local.list, exclude empty lines and comment lines.
// 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.
// For DefaultList and LocalList, exclude empty lines and comment lines.
// TODO skip and give a warning on non-absolute path.
// For UserDefaultList, auto prefix user's home dir, auto expand Documents, Downloads etc special folder.
Directory.CreateDirectory(runtimeDirWin) |> ignore
File.WriteAllLines(mbackupList, allLines)
logger.Info
"mbackup.list file written: %s"
mbackupList
logger.Info "GeneratedList written: %s" mbackupList
true
with
| :? System.IO.IOException as ex ->
| :? IOException as ex ->
logger.Error "Read/write file failed: %s %s" ex.Source ex.Message
false
| ex ->
......@@ -170,10 +187,12 @@ let generateMbackupList (logger: Logger) =
exception PrivateKeyNotFoundException of string
let addOptionsForRemoteBackup (results: ParseResults<CLIArguments>) (logger: Logger) (rsyncCmd: string list) =
let addOptionsForRemoteBackup (rc: MbackupRuntimeConfig) (rsyncCmd: string list) =
let options = rc.Options
let sshExeFile = mbackupProgramDir + "rsync-w64/usr/bin/ssh.exe"
let sshConfigFile = userHome + ".ssh/config"
let sshPrivateKeyFile = results.GetResult(Ssh_Key, defaultValue = userHome + ".ssh/id_rsa") |> toMingwPath
let sshKnownHostsFile = userHome + ".ssh/known_hosts"
let sshPrivateKeyFile = options.GetResult(Ssh_Key, rc.Config.GetStrDefault "ssh-key" (userHome + ".ssh/id_rsa")) |> toMingwPath
let sshPrivateKeyFileWin = toWinPath sshPrivateKeyFile
if not (File.Exists(sshPrivateKeyFileWin)) then
raise (PrivateKeyNotFoundException("ssh private key doesn't exist: " + sshPrivateKeyFileWin))
......@@ -184,55 +203,69 @@ let addOptionsForRemoteBackup (results: ParseResults<CLIArguments>) (logger: Log
let rsyncCmd =
List.append rsyncCmd
[ sprintf "-e \"'%s'%s -i %s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\"" sshExeFile
sshConfigFileOption sshPrivateKeyFile ]
[ sprintf "-e \"'%s'%s -i %s -o StrictHostKeyChecking=ask -o UserKnownHostsFile=%s\""
sshExeFile sshConfigFileOption sshPrivateKeyFile sshKnownHostsFile]
let nodeName = results.GetResult(Node_Name, defaultValue = Net.Dns.GetHostName())
let nodeName = options.GetResult(Node_Name, (rc.Config.GetStrDefault "node-name" (Net.Dns.GetHostName())))
let remoteLogFile = sprintf "/var/log/mbackup/%s.log" nodeName
let remoteUser = results.GetResult(Remote_User, defaultValue = Environment.UserName)
let remoteUser = options.GetResult(Remote_User, (rc.Config.GetStrDefault "remote-user" 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
[<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
if results.Contains Version then
let logger = Logger()
let options =
let errorHandler = ProcessExiter(colorizer = function ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red)
let parser = ArgumentParser.Create<CLIArguments>(programName = "mbackup.exe", errorHandler = errorHandler)
parser.Parse argv
let rc = {
MbackupRuntimeConfig.Config =
let mbackupConfigFile = userConfigDirWin + MbackupFileName.Config
WellsConfig(mbackupConfigFile)
Logger = logger
Options = options
}
if options.Contains Version then
printfn "mbackup %s" versionStr
Environment.Exit(ExitSuccess)
let dryRun = results.Contains Dry_Run
let itemizeChanges = results.Contains Itemize_Changes
let logger = Logger()
logger.Info "user config dir: %s" userConfigDirWin
logger.Info "runtime dir: %s" runtimeDirWin
logger.Debug "program dir: %s" mbackupProgramDirWin
let rsyncCmd: string list = []
let rsyncCmd = appendWhen dryRun rsyncCmd "--dry-run"
let rsyncCmd = appendWhen itemizeChanges rsyncCmd "-i"
let rsyncCmd = appendWhen (options.Contains Dry_Run) rsyncCmd "--dry-run"
let rsyncCmd = appendWhen (options.Contains Itemize_Changes) rsyncCmd "-i"
let rsyncCmd =
List.append rsyncCmd
("-h --stats -togr --delete --delete-excluded --ignore-missing-args".Split [| ' ' |] |> Array.toList)
let mbackupFile = runtimeDir + "mbackup.list"
if not (generateMbackupList logger) then failwith "Generate mbackup.list failed"
let rsyncCmd = List.append rsyncCmd [ sprintf "--files-from=%s" mbackupFile ]
if not (generateMbackupList logger) then
failwith (sprintf "Generate %s failed" MbackupFileName.GeneratedList)
let generatedFileList = runtimeDir + MbackupFileName.GeneratedList
let rsyncCmd = List.append rsyncCmd [ sprintf "--files-from=%s" generatedFileList ]
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 rsyncCmd = List.append rsyncCmd [ sprintf "--exclude-from=%s" (userConfigDir + MbackupFileName.DefaultExclude) ]
let localLogFile = runtimeDir + "mbackup.log"
let rsyncCmd = List.append rsyncCmd [ sprintf "--log-file=%s" localLogFile ]
let runtimeLocalExcludeFile = runtimeDir + MbackupFileName.LocalExclude
let rsyncCmd =
let localExcludeFile = userConfigDir + MbackupFileName.LocalExclude
if File.Exists localExcludeFile then
let convertAbsPathToMingwStyle (line: string) =
if Regex.IsMatch(line, "[a-z]:", RegexOptions.IgnoreCase) then
toMingwPath line
else
line
let lines =
readMbackupListFile localExcludeFile
|> Seq.map convertAbsPathToMingwStyle
File.WriteAllLines(runtimeLocalExcludeFile, lines)
appendWhen (File.Exists localExcludeFile) rsyncCmd (sprintf "--exclude-from=%s" runtimeLocalExcludeFile)
let rsyncCmd = List.append rsyncCmd [ sprintf "--log-file=%s" (runtimeDir + MbackupFileName.Log) ]
// precedence: command line argument > environment variable > config file
let normalizeTarget target =
......@@ -240,10 +273,9 @@ let main argv =
else target
let backupTargetMaybe =
match results.TryGetResult Target with
match options.TryGetResult Target with
| None ->
let mbackupConfig = WellsConfig(mbackupConfigFile)
let backupTargetMaybe = mbackupConfig.GetStr("target")
let backupTargetMaybe = rc.Config.GetStr("target")
Option.map normalizeTarget backupTargetMaybe
| Some backupTarget -> Some(normalizeTarget backupTarget)
......@@ -255,7 +287,7 @@ let main argv =
try
let rsyncCmd =
if not (isLocalTarget backupTarget) then
addOptionsForRemoteBackup results logger rsyncCmd
addOptionsForRemoteBackup rc rsyncCmd
else
rsyncCmd
let rsyncCmd = List.append rsyncCmd [ "/" ]
......@@ -266,7 +298,7 @@ let main argv =
Directory.CreateDirectory(userConfigDirWin) |> ignore
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)
generatedFileList (rsyncExe + " " + rsyncArgs)
let processStartInfo =
ProcessStartInfo(
FileName = rsyncExe,
......
# files/dirs to exclude, for syntax, check rsync patterns
# This file is reserved when uninstall/upgrade mbackup.
# lines started with # are comments.
# example:
# *.o
# C:\foo\bar.iso
# local dirs to backup.
# This file is reserved when uninstall/upgrade mbackup.
# lines started with # are comments.
# example:
# C:\mydir
# D:\some dir\some file.doc
# example config file
# example config file, lines start with # are comments.
# target=d:\backup\
# target=myhost.example.com:/data/backup/somedir/
# remote-user=foo
......@@ -6,7 +6,7 @@
<RuntimeIdentifiers>win10-x64</RuntimeIdentifiers>
<RootNamespace>Mbackup</RootNamespace>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>0.2.3.0</Version>
<Version>0.3.0.0</Version>
</PropertyGroup>
<ItemGroup>
......
......@@ -14,58 +14,85 @@
<Directory Id="MBACKUP_DATA" Name="mbackup"/>
</Directory>
<Directory Id="DesktopFolder" Name="Desktop" />
<Directory Id="ProgramMenuFolder" />
<Directory Id="ProgramMenuFolder">
<Directory Id="mbackup_start_menu_dir" Name="mbackup" />
</Directory>
</Directory>
<DirectoryRef Id="MBACKUP_DATA">
<Component Id="default_exclude.txt" Guid="*">
<File Id="default_exclude.txt" Source="mbackup-config\default-exclude.txt" KeyPath="yes"/>
</Component>
<Component Id="default_list.txt" Guid="*">
<File Id="default_list.txt" Source="mbackup-config\default-list.txt" KeyPath="yes"/>
</Component>
<Component Id="local_list.txt" NeverOverwrite="yes" Permanent="yes" Guid="*">
<!-- do not overwrite this component/file if it already exists on target system -->
<File Id="local_list.txt" Source="mbackup-config\local-list.txt" KeyPath="yes"/>
</Component>
<Component Id="local_exclude.txt" NeverOverwrite="yes" Permanent="yes" Guid="*">
<!-- do not overwrite this component/file if it already exists on target system -->
<File Id="local_exclude.txt" Source="mbackup-config\local-exclude.txt" KeyPath="yes"/>
</Component>
<Component Id="mbackup_config.txt" NeverOverwrite="yes" Permanent="yes" Guid="*">
<!-- do not overwrite this component/file if it already exists on target system -->
<File Id="mbackup_config.txt" Source="mbackup-config\mbackup-config.txt" KeyPath="yes"/>
</Component>
<Component Id="user_default_list.txt" Guid="*">
<File Id="user_default_list.txt" Source="mbackup-config\user-default-list.txt" KeyPath="yes"/>
</Component>
</DirectoryRef>
<DirectoryRef Id="DesktopFolder">
<Component Id="ApplicationShortcutDesktop" Guid="*">
<Shortcut Id="ApplicationDesktopShortcut"
<Component Id="mbackup_desktop_shortcuts" Guid="*">
<Shortcut Id="DesktopMbackupExe"
Name="mbackup"
Description="Backup your computer using rsync"
Target="[#filE3FFB5E42FD6DDB7AD945F29409779F0]"
Target="[!filE3FFB5E42FD6DDB7AD945F29409779F0]"
WorkingDirectory="MBACKUP_PROGRAM_FILES"/>
<RemoveFolder Id="DesktopShortcut" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\mbackup" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
<RegistryValue Root="HKCU" Key="Software\mbackup\desktopshortcuts" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
<DirectoryRef Id="ProgramMenuFolder">
<Component Id="ApplicationShortcutStartMenu" Guid="F7C168BA-BDC9-4C4D-8AE7-722AC399AFD5">
<Shortcut Id="ApplicationStartMenuShortcut"
<DirectoryRef Id="mbackup_start_menu_dir">
<Component Id="mbackup_start_menu" Guid="F7C168BA-BDC9-4C4D-8AE7-722AC399AFD5">
<Shortcut Id="StartMenuMbackupExe"
Name="mbackup"
Description="Backup your computer using rsync"
Target="[#filE3FFB5E42FD6DDB7AD945F29409779F0]"
Target="[!filE3FFB5E42FD6DDB7AD945F29409779F0]"
WorkingDirectory="MBACKUP_PROGRAM_FILES"/>
<Shortcut Id="StartMenuMbackupConfig"
Name="mbackup-config.txt"
Description="mbackup config file"
Target="[!mbackup_config.txt]"
WorkingDirectory="MBACKUP_PROGRAM_FILES"/>
<Shortcut Id="StartMenuLocalList"
Name="local-list.txt"
Description="mbackup local-list.txt file"
Target="[!local_list.txt]"
WorkingDirectory="MBACKUP_PROGRAM_FILES"/>
<RemoveFolder Id="StartMenuShortCut" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\mbackup" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
<Shortcut Id="StartMenuLocalExclude"
Name="local-exclude.txt"
Description="mbackup local-exclude.txt file"
Target="[!local_exclude.txt]"
WorkingDirectory="MBACKUP_PROGRAM_FILES"/>
<RemoveFolder Id="MBACKUP_START_MENU_DIR" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\mbackup\startmenushortcuts" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
<DirectoryRef Id="MBACKUP_DATA">
<Component Id="mbackup_default.exclude" Guid="*">
<File Id="mbackup_default.exclude" Source="mbackup-config\mbackup-default.exclude" KeyPath="yes"/>
</Component>
<Component Id="mbackup_default.list" Guid="*">
<File Id="mbackup_default.list" Source="mbackup-config\mbackup-default.list" KeyPath="yes"/>
</Component>
<Component Id="user_default.list" Guid="*">
<File Id="user_default.list" Source="mbackup-config\user-default.list" KeyPath="yes"/>
</Component>
<Component Id="mbackup.txt" NeverOverwrite="yes" Permanent="yes" Guid="*">
<!-- do not overwrite this component/file if it already exists on target system -->
<File Id="mbackup.txt" Source="mbackup-config\mbackup.txt" KeyPath="yes"/>
</Component>
</DirectoryRef>
<Feature Id="MainApplication" Title="Main Application" Level="1">
<ComponentGroupRef Id="MbackupHeatGenerated" />
<ComponentGroupRef Id="RsyncHeatGenerated" />
<ComponentRef Id="mbackup_default.exclude" />
<ComponentRef Id="mbackup_default.list" />
<ComponentRef Id="user_default.list" />
<ComponentRef Id="mbackup.txt" />
<ComponentRef Id="ApplicationShortcutStartMenu" />
<ComponentRef Id="ApplicationShortcutDesktop" />
<ComponentRef Id="default_exclude.txt" />
<ComponentRef Id="default_list.txt" />
<ComponentRef Id="local_list.txt" />
<ComponentRef Id="local_exclude.txt" />
<ComponentRef Id="mbackup_config.txt" />
<ComponentRef Id="user_default_list.txt" />
<ComponentRef Id="mbackup_start_menu" />
<ComponentRef Id="mbackup_desktop_shortcuts" />
</Feature>
</Product>
</Wix>
This diff is collapsed.