Backup and Restore Family Historian Settings via Windows.fh_lua--[[
@Title: Backup and Restore Family Historian Settings via Windows
@Type: Standard
@Author: Mark Draper
@Version: 3.0
@LastUpdated: 2 Jun 2026
@Licence: This plugin is copyright (c) 2026 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Backup and restore Family Historian custom settings. This plugin does all the copying
directly within Windows rather than in the plugin. This gives much faster and more convenient
operation, as well as providing the option to implement more advanced features such as
single-click rapid backup or automated backup.
]]
--[[
Version 1.0 (May 2022)
Initial Store version
Version 1.1 (May 2022)
Emulator-compatible version
Xcopy for emulator copy
Clearer label for restore option
Fixed bug with WOW64Node in Advanced mode
Version 1.1.1 (May 2022)
Xcopy options tweaked
Does not overwrite its own settings or temporary batch files
Version 1.1.2 (May 2022)
ASCII check modified to permit FH5
Version 1.2 (Jan 2023)
Checks only one instance running before restoring to prevent potential data loss
Various minor code tidies and rationalisations
Version 1.2.1 (Feb 2023)
Corrects instance count when running under FH6 (Fh vs fh!)
Version 1.3 (Sep 2024)
Corrects restore operation in emulator and new emulator check
Now fully Unicode compatible (FSO rather than lfs)
Moves temporary script files to tmp folder and improved ini file format
Now edits Registry file in FH6 as well so fully functional
Improved user dialogs and menu button labels
Various code optimisations and minor tweaks
Version 2.0 (Dec 2024)
Simplified restore without overwrite option (always restores in full)
Simplified main menu reflecting this change
Enhanced menu pop-up prompts
Supports multi-monitor extended desktops
Checks existing backups for completeness before updating or restoring
Various corrections and enhancements to Unicode support
Backup script modified to call 64-bit Registry key specifically if running in Advanced mode
(avoids potential issues with missing backup file when called directly from Windows)
Version 2.1 (May 2025)
Modified 'three test' emulator detection
CPU architecture detection
ARM64 warning
Version 2.1.1 (Jul 2025)
Corrected ARM64 warning
Version 2.1.4 (Aug 2025)
Generalised ARM64 warning
Minor tidy-up of old code
Version 3.0 (Jun 2026)
Simplified for easier maintenance by removing emulator support and legacy compatibility
Various other code refinements
]]
fhInitialise(5,0,0,'save_required')
require 'luacom'
require('iuplua')
-- warn if emulator detected
if os.getenv('WINEPREFIX') then
local msg = 'Family Historian appears to be running under an emulator. This is not supported by the ' ..
'current plugin version.\n\nSee the plugin help for a fuller discussion of backing up FH ' ..
'settings in a WINE emulator'
fhMessageBox(msg, 'OK', 'MB_ICONSTOP')
return
end
if fhGetAppVersion() > 6 then
fh = require('fhUtils')
fh.setIupDefaults()
else
iup.SetGlobal('CUSTOMQUITMESSAGE','YES')
iup.SetGlobal('UTF8MODE','YES')
end
iup.SetGlobal('UTF8MODE_FILE','YES')
FSO = luacom.CreateObject('Scripting.FileSystemObject')
-- *********************************************************************
function main()
local MainForm
if not GetOptions() then return end -- get plugin options
-- set main window static controls and options
local Heading = 'This plugin backs up and restores Family Historian settings using Windows commands.\n' ..
'All languages and Windows localisations are supported automatically.\n\n' ..
'Technically-minded users may save the backup configuration as Windows batch ' ..
'files to\nincorporate into their own backup process (including fully automated backups).'
local lblTitle = iup.label{title = Heading, alignment = 'ALEFT', padding = '10x20'}
-- folder controls and options
local lblF = iup.label{title = gblOptions.Path or '',expand = 'HORIZONTAL'}
local btnF = iup.button{title = 'Select', padding = '10x3',
tip = 'Destination folder where scripts and backup data are written.',
action = function(self) SelectFolder(MainForm) UpdateMenu() end}
local chkB = iup.toggle{title = 'Write to dated subfolder', expand = 'YES', active = 'NO',
tip = 'Write backup to dated subfolder.'}
local fraF = iup.frame{iup.vbox{lblF, chkB, btnF; alignment = 'ACENTER'}; title = 'Backup folder'}
-- general controls and options (use similar containers for all frames for consistent appearance)
local chkM1 = iup.toggle{title = 'Normal', expand = 'HORIZONTAL',
tip = 'Run backup or restore immediately.'}
local chkM2 = iup.toggle{title = 'Advanced', expand = 'HORIZONTAL',
tip = 'Save the script files without running the backup or restore.'}
local hboxM = iup.hbox{chkM1, chkM2}
local radM = iup.radio{hboxM}
local fraM = iup.frame{iup.hbox{radM}, title = 'Operating mode'}
-- button display
local btnB = iup.button{active = 'NO',
action = function(self) Backup() MainForm.BringFront = 'YES' end}
local btnR = iup.button{title = 'Restore Script', padding = '10x3',
action = function(self)
Restore(MainForm)
MainForm.BringFront = 'YES'
end}
local btnH = iup.button{title = 'Help', tip = 'Display plugin help page.\n' ..
'(hold down either Shift key to display\nSystem Information)',
action = function(self) GetHelp(MainForm) end}
local btnX = iup.button{title = 'Close', tip = 'Close plugin.',
action = function(self) return iup.CLOSE end}
local hButtons = iup.hbox{btnB, btnR, btnH, btnX; cgap = 20, cmargin = 'x15', normalizesize = 'BOTH'}
-- enhanced tool tips
btnB.TipBalloonTitleIcon = '1'
btnF.TipBalloonTitle = 'Select Folder'
btnF.TipBalloonTitleIcon = '1'
chkB.TipBalloonTitle = 'Dated Subfolder'
chkB.TipBalloonTitleIcon = '1'
chkM1.TipBalloonTitle = 'Normal Mode'
chkM1.TipBalloonTitleIcon = '2'
chkM2.TipBalloonTitle = 'Advanced Mode'
chkM2.TipBalloonTitleIcon = '1'
btnH.TipBalloonTitle = 'Plugin Help'
btnH.TipBalloonTitleIcon = '1'
btnX.TipBalloonTitle = 'Close Plugin'
btnX.TipBalloonTitleIcon = '3'
-- menu update function
function UpdateMenu()
if gblOptions.Path then
lblF.Title = gblOptions.Path
btnB.Active = 'YES'
chkB.Active = 'YES'
end
if gblOptions.Dated then
chkB.Value = 'YES'
else
chkB.Value = 'NO'
end
if gblOptions.Advanced then
chkM2.Value = 'ON'
btnB.Title = 'Backup Script'
btnR.Title = 'Restore Script'
btnB.Tip = 'Save backup batch file to defined backup folder.'
btnB.TipBalloonTitle = 'Save Backup Script'
btnR.Tip = 'Save restore batch file to defined backup folder.'
btnR.TipBalloonTitle = 'Save Restore Script'
btnR.TipBalloonTitleIcon = '1'
else
chkM1.Value = 'ON'
btnB.Title = 'Backup'
btnB.Size = btnR.Size
btnB.Tip = 'Run backup now using defined options.'
btnB.TipBalloonTitle = 'Run Backup'
btnR.Title = 'Restore'
btnR.Size = btnB.Size
btnR.Tip = 'Run restore now using defined options.'
btnR.TipBalloonTitle = 'Run Restore'
btnR.TipBalloonTitleIcon = '2'
end
end
-- callback actions
function chkM2:valuechanged_cb()
gblOptions.Advanced = (chkM2.Value == 'ON')
UpdateMenu()
end
function chkB:valuechanged_cb()
gblOptions.Dated = (chkB.Value == 'ON')
end
-- assemble form
local vForm = iup.vbox{lblTitle, fraM, fraF, hButtons;
alignment = 'ACENTER', cgap = 5, cmargin = '10x3'}
MainForm = iup.dialog{vForm; minbox='NO', maxbox='NO',
title='Backup and Restore Family Historian Settings via Windows (3.0)'}
MainForm:map() -- determine form layout prior to assigning button labels and resizing
MainForm.tipballoon = 'YES'
UpdateMenu()
if fhGetAppVersion() > 6 then
iup.SetAttribute(MainForm, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
elseif IsExtendedDesktop() then
local msg = 'Versions of Family Historian prior to 7 cannot determine which monitor ' ..
'the application is running in, so the main plugin menu will always open in the ' ..
'primary monitor.'
MessageBox(msg, 'OK', 'WARNING')
end
MainForm:popup()
SaveOptions()
end
-- *********************************************************************
function SelectFolder(MainForm)
local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path,
title = 'Select folder for scripts and backup files', parentdialog = MainForm}
filedlg:popup()
if filedlg.Status == '-1' then return end
local path = filedlg.Value
gblOptions.Path = path
gblOptions.ShortPath = FSO:GetFolder(path).ShortPath
end
-- *********************************************************************
function Backup()
-- check for existing backup
local BackupPath = gblOptions.Path
if gblOptions.Dated then
BackupPath = BackupPath .. '\\fhBackup_' .. os.date('%Y-%m-%d')
end
-- check all files and folders present if existing backup
local i = 0
if FSO:FolderExists(BackupPath .. '\\fhProgramData') then i = i + 1 end
if FSO:FolderExists(BackupPath .. '\\fhAppData') then i = i + 1 end
if FSO:FileExists(BackupPath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
if FSO:FileExists(BackupPath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
if not gblOptions.Advanced then
if i > 0 and i < 4 then
local msg = 'Existing backup in ' .. BackupPath .. ' is incomplete.\n\nIgnore error and update?'
if MessageBox(msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end
elseif i == 4 then
local stamp = FSO:GetFile(BackupPath .. '\\fhCURRENT_USER.reg').DateLastModified
local msg = os.date('Update existing backup in ' .. BackupPath ..
'?\n\nLast update: ' .. stamp)
if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Update', 2) ~= 1 then return end
end
end
local tblScript, BackupFile = GetBackupScript(BackupPath)
-- write script to file
local F = io.open(BackupFile, 'w')
if not F then
MessageBox('Cannot write backup script.', 'OK', 'ERROR', 'File Access Error')
return
end
table.insert(tblScript, '')
F:write(table.concat(tblScript, '\n'))
F:close()
-- confirm successful write
if gblOptions.Advanced then
local msg = 'Backup script written to file ' .. gblOptions.Path .. '\\fhBackup.bat.'
MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
return
end
-- implement backup
if IsExtendedDesktop() then
MessageBox('The backup will now run in the primary monitor.', 'OK', 'INFORMATION')
end
if not fhShellExecute(BackupFile) then
MessageBox('Backup failed to run.', 'OK', 'ERROR', 'Script Error')
end
end
-- *********************************************************************
function GetBackupScript(BackupPath)
-- prepare backup script
local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$')
local UserKey = 'HKCU\\SOFTWARE\\Calico Pie\\' .. AppName
local GlobalKey
local _, err = GetRegKey('HKLM\\SOFTWARE\\WOW6432Node\\Calico Pie\\' .. AppName ..
'\\2.0\\Preferences\\Language')
if not err then
GlobalKey = 'HKLM\\SOFTWARE\\WOW6432Node\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences'
else
GlobalKey = 'HKLM\\SOFTWARE\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences'
end
local tblT = {} -- build the script in this table
table.insert(tblT, '@echo off')
table.insert(tblT, 'echo Backup started...')
-- set path according to whether dated subfolder or not
if not gblOptions.Dated then
table.insert(tblT, 'set p=' .. gblOptions.ShortPath)
else
table.insert(tblT, 'set basepath=' .. gblOptions.ShortPath)
local cmd = 'for /f %%D in ( \'PowerShell Get-Date -Format "_yyyy-MM-dd"\' ) do set suffix=%%D'
table.insert(tblT, cmd)
table.insert(tblT, 'set p=%basepath%\\fhBackup%suffix%')
end
-- define command script
table.insert(tblT, 'robocopy "' .. gblOptions.ProgramData ..
'" "%p%\\fhProgramData" /MIR /XD "' .. fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Map\\Cache" /NJH')
table.insert(tblT, 'robocopy "' .. gblOptions.AppDataPath .. '" "%p%\\fhAppData" /MIR /NJH')
table.insert(tblT, 'reg export "' .. UserKey .. '" "%p%\\fhCURRENT_USER.reg" /y')
table.insert(tblT, 'reg export "' .. GlobalKey .. '" "%p%\\fhLOCAL_MACHINE.reg" /y')
-- determine file used to save command script and add self-delete command if immediate backup
local BackupFile
if gblOptions.Advanced then
BackupFile = gblOptions.ShortPath .. '\\fhBackup.bat'
else
BackupFile = gblOptions.Temp .. '\\~fhBackup.bat'
table.insert(tblT, 'pause')
table.insert(tblT, 'del "' .. BackupFile .. '"')
end
return tblT, BackupFile
end
-- *********************************************************************
function Restore(MainForm)
-- check for Advanced mode
local RestoreFolder, ShortFolder, RestoreFile, ShortFile
if gblOptions.Advanced then
RestoreFolder = gblOptions.Path
RestoreFile = gblOptions.Path .. '\\fhRestore.bat'
ShortFolder = gblOptions.ShortPath
ShortFile = gblOptions.ShortPath .. '\\fhRestore.bat'
else
RestoreFolder, ShortFolder = SelectRestoreFolder(MainForm)
if not RestoreFolder then return end
RestoreFile = gblOptions.Temp .. '\\~fhRestore.bat'
ShortFile = gblOptions.Temp .. '\\~fhRestore.bat'
end
local tblScript = GetRestoreScript(RestoreFolder, ShortFolder)
-- write script to file (path cannot be Unicode, so simple method ok)
local F = io.open(ShortFile, 'w')
if not F then
MessageBox('Cannot write restore script.', 'OK', 'ERROR', 'File Access Error')
return
end
table.insert(tblScript, '')
F:write(table.concat(tblScript, '\n'))
F:close()
-- confirm successful write
if gblOptions.Advanced then
local msg = 'Restore script written to file ' .. RestoreFile .. '.'
MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
return
end
-- remove default files and folders from Registry file before restoring
RemoveDefaults(RestoreFolder)
-- warn the user what is about to happen and request approval
local msg = 'Family Historian will now be closed in order to complete the restore operation.\n\n' ..
'Are you sure that you want to do this?'
if MessageBox(msg, 'YESNO', 'WARNING', 'Confirm Automatic Restore - CAUTION', 2) ~= 1 then return end
-- generate auto-close script
local CloseFile = gblOptions.Temp .. '\\' .. '~fhAutoClose.bat'
local tblT = {}
table.insert(tblT, '@echo off')
table.insert(tblT, 'echo Restore started...')
table.insert(tblT, 'powershell start-sleep -s 1')
table.insert(tblT, 'cls')
table.insert(tblT, 'taskkill /IM fh.exe /f')
table.insert(tblT, 'powershell start-sleep -s 1')
table.insert(tblT, 'call "' .. RestoreFile .. '"')
table.insert(tblT, 'del "' .. RestoreFile .. '"')
table.insert(tblT, 'del "' .. CloseFile .. '"')
table.insert(tblT, '')
local F = io.open(CloseFile, 'w')
if not F then
MessageBox('Cannot write auto-close script.', 'OK', 'ERROR', 'File Access Error')
return
end
F:write(table.concat(tblT, '\n'))
F:close()
-- check that only one instance of FH is running
if CountInstances() > 1 then
local msg = 'There may be more than one instance of Family Historian open.\n\n' ..
'Proceeding with the restore operation could cause data loss. Please close ' ..
'any additional copies and re-run the plugin.'
MessageBox(msg, 'OK', 'ERROR', 'Multiple Instances Detected')
os.remove(RestoreFile)
os.remove(CloseFile)
return
end
-- execute the kamikaze close and bale out...
fhShellExecute(CloseFile)
end
-- *********************************************************************
function SelectRestoreFolder(MainForm)
-- select backup to restore
local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path, parentdialog = MainForm,
title = 'Select folder that contains the backup you wish to restore'}
filedlg:popup()
if filedlg.Status == '-1' then return end
local RestorePath = filedlg.Value
local ShortPath = FSO:GetFolder(RestorePath).ShortPath
-- check folder contains an identifiable backup
local BackupVersion, B1, B2, B3
-- is there a valid new format backup in this folder?
local i = 0
if FSO:FolderExists(RestorePath .. '\\fhProgramData') then i = i + 1 end
if FSO:FolderExists(RestorePath .. '\\fhAppData') then i = i + 1 end
if FSO:FileExists(RestorePath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
if FSO:FileExists(RestorePath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
if i == 4 then
local KeyFile = RestorePath .. '\\fhProgramData\\Plugin Data\\' ..
'Backup and Restore Family Historian Settings via Windows.ini'
ShortKey = FSO:GetFile(KeyFile).ShortPath
for Line in io.lines(ShortKey) do
local k, v = Line:match('^(%w+)=([%w%p%s]+)$')
if k == 'Version' then
BackupVersion = v
B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)')
end
end
end
if not (B1 and B2 and B3) then
local msg = 'Selected folder does not appear to contain a backup.'
MessageBox(msg, 'OK', 'ERROR', 'Folder Selection Error')
return
end
-- check backup version matches
local V1, V2, V3 = fhGetAppVersion()
B1, B2, B3 = tonumber(B1), tonumber(B2), tonumber(B3)
if V1 ~= B1 then
local msg = 'Cannot restore a Version ' .. B1 .. ' backup into Version ' .. V1 .. '.'
MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
return
elseif B2 > V2 or (B2 == V2 and B3 > V3) then
local msg = 'Cannot restore a Version ' .. BackupVersion .. ' backup into Version ' ..
gblOptions.Version .. '.\n\n' ..
'You should update your present copy of Family Historian before restoring the backup.'
MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
return
elseif B2 < V2 or (B2 == V2 and B3 < V3) then
local msg = 'Restoring a Version ' .. BackupVersion .. ' backup into Version ' ..
gblOptions.Version .. ' is potentially risky.\n\nAre you sure that you want to do this?'
if MessageBox(msg, 'YESNO', 'WARNING', 'Potential Version Conflict', 2) ~= 1 then return end
end
-- get backup creation date and time
local stamp = FSO:GetFile(RestorePath .. '\\fhCURRENT_USER.reg').DateLastModified
local msg = 'Backup in ' .. RestorePath .. ' was created ' .. stamp ..
'. Is this the correct backup?'
if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Backup Selection', 2) ~= 1 then return end
return RestorePath, ShortPath
end
-- *********************************************************************
function GetRestoreScript(RestoreFolder, ShortFolder)
local tblT = {} -- build the script in this table
table.insert(tblT, '@echo off')
local Switch = '/MIR /NJH'
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhProgramData" "' ..
gblOptions.ProgramData .. '" ' .. Switch)
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhAppData" "' ..
gblOptions.AppDataPath .. '" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"')
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"')
if not gblOptions.Advanced then
table.insert(tblT, 'pause')
end
return tblT
end
-- *********************************************************************
function RemoveDefaults(RestoreFolder)
-- remove defaults that may not be valid in new installation
local FileName = RestoreFolder .. '\\fhLOCAL_MACHINE.reg'
if not FSO:FileExists(FileName) then
MessageBox('Restore folder is missing a required Registry file.', 'OK', 'ERROR')
return
end
local TextStream = FSO:OpenTextFile(FileName, 1, false, -1)
local Data = TextStream:ReadAll()
TextStream:Close()
local tblT = {}
Data = Data:gsub('\r\n\r\n', '\r\n \r\n') -- simple workaround to include blank lines
for Line in Data:gmatch('[^\r\n]+') do
if Line == ' ' then Line = '' end
local T1 = Line:match('^"Backup Directory"=')
local T2 = Line:match('^"Default File"=')
local T3 = Line:match('^"FH Root Folder"=')
local T4 = Line:match('^"Emulator%-Compat%-Mode"=')
if not (T1 or T2 or T3 or T4) then table.insert(tblT, Line) end
end
table.insert(tblT, '')
TextStream = FSO:OpenTextFile(FileName, 2, false, -1)
TextStream:Write(table.concat(tblT, '\n'))
TextStream:Close()
end
-- *********************************************************************
function GetOptions()
gblOptions = {}
local tblO = {}
-- get system level options
gblOptions.Temp = os.getenv('TEMP')
gblOptions.ProgramData = fhGetContextInfo('CI_APP_DATA_FOLDER')
local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$')
gblOptions.AppData = os.getenv('TEMP'):match('^(.+)Local\\Temp$') ..
'Roaming\\Calico Pie\\' .. AppName
gblOptions.AppDataPath = FSO:GetFolder(gblOptions.AppData).ShortPath
-- get saved options
local File = gblOptions.ProgramData .. '\\Plugin Data\\' ..
'Backup and Restore Family Historian Settings via Windows.ini'
if FSO:FileExists(File) then
for line in io.lines(File) do
local i = line:find('=')
if i then tblO[line:sub(1, i-1)] = line:sub(i+1) end
end
if tblO.Path and FSO:FolderExists(tblO.Path) then
gblOptions.Path = tblO.Path
gblOptions.ShortPath = FSO:GetFolder(gblOptions.Path).ShortPath
end
if tblO.Advanced == '1' then gblOptions.Advanced = true end
if tblO.Dated == '1' then gblOptions.Dated = true end
end
-- set version option and save back to file to ensure saved on backup
local V1, V2, V3 = fhGetAppVersion()
gblOptions.Version = V1 .. '.' .. V2 .. '.' .. V3
SaveOptions()
return true
end
-- *********************************************************************
function SaveOptions()
-- save options to file
local OptionsFolder = gblOptions.ProgramData .. '\\Plugin Data'
if not FSO:FolderExists(OptionsFolder) then
FSO:CreateFolder(OptionsFolder)
end
local OptionsFile = OptionsFolder .. '\\Backup and Restore Family Historian Settings via Windows.ini'
local F = io.open(OptionsFile, 'w')
if not F then
MessageBox('Cannot open options file.', 'OK', 'ERROR', 'File Access Error')
return
end
F:write('[Options]\n')
F:write('Version=' .. gblOptions.Version .. '\n')
if gblOptions.Path then F:write('Path=' .. gblOptions.Path .. '\n') end
if gblOptions.Advanced then F:write('Advanced=1\n') end
if gblOptions.Dated then F:write('Dated=1\n') end
F:close()
end
-- *********************************************************************
function GetHelp(MainForm)
if iup.GetGlobal('SHIFTKEY') == 'OFF' then
fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/' ..
'backup-and-restore-family-historian-settings-via-windows')
end
-- hidden diagnostic window
gblOptions.CPU = GetRegKey('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\' ..
'Environment\\PROCESSOR_ARCHITECTURE')
local tblT = {
'Processor Architecture = ' .. gblOptions.CPU or 'n/a',
'User Name (IUP) = ' .. (iup.GetGlobal('USERNAME') or ''),
'Temp Folder = ' .. gblOptions.Temp,
'ProgramData Folder = ' .. gblOptions.ProgramData:gsub(' ', ' '),
'AppData Folder = ' .. gblOptions.AppData:gsub(' ', ' ')
}
MessageBox(table.concat(tblT, '\n'), 'OK', 'INFORMATION', 'System Information')
end
-- *********************************************************************
function IsExtendedDesktop()
-- determines if system has an extended desktop
local xd = iup.GetGlobal('VIRTUALSCREEN')
local x, y, width, height = xd:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
local fs = iup.GetGlobal('FULLSIZE')
local ExtendedDesktop = fs ~= width .. 'x' .. height
return ExtendedDesktop
end
-- *********************************************************************
function CountInstances()
-- checks just a single instance of FH is running
local F = io.popen('powershell get-process *fh*')
if not F then
local msg = 'Cannot determine number of Family Historian copies running.'
MessageBox(msg, 'OK', 'ERROR', 'File Handle Access Error')
return
end
local S = F:read('*a')
F:close()
local Instances = 0
for line in S:gmatch('[^\r\n]+') do
if line:lower():match('%sfh%s+$') then
Instances = Instances + 1
end
end
return Instances
end
-- *********************************************************************
function GetRegKey(key)
local sh = luacom.CreateObject 'WScript.Shell'
local ans
if pcall(function () ans = sh:RegRead(key) end) then
return ans
else
return nil,true
end
end
-- *********************************************************************
function MessageBox(Message, Buttons, Icon, Title, Default)
-- replaces built-in function with custom version containing more options
local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
title = Title or fhGetContextInfo('CI_PLUGIN_NAME'), buttondefault = Default,
parentdialog = MainForm}
msgdlg:popup()
return tonumber(msgdlg.ButtonResponse)
end
-- *********************************************************************
main()
--[[
@Title: Backup and Restore Family Historian Settings via Windows
@Type: Standard
@Author: Mark Draper
@Version: 3.0
@LastUpdated: 2 Jun 2026
@Licence: This plugin is copyright (c) 2026 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Backup and restore Family Historian custom settings. This plugin does all the copying
directly within Windows rather than in the plugin. This gives much faster and more convenient
operation, as well as providing the option to implement more advanced features such as
single-click rapid backup or automated backup.
]]
--[[
Version 1.0 (May 2022)
Initial Store version
Version 1.1 (May 2022)
Emulator-compatible version
Xcopy for emulator copy
Clearer label for restore option
Fixed bug with WOW64Node in Advanced mode
Version 1.1.1 (May 2022)
Xcopy options tweaked
Does not overwrite its own settings or temporary batch files
Version 1.1.2 (May 2022)
ASCII check modified to permit FH5
Version 1.2 (Jan 2023)
Checks only one instance running before restoring to prevent potential data loss
Various minor code tidies and rationalisations
Version 1.2.1 (Feb 2023)
Corrects instance count when running under FH6 (Fh vs fh!)
Version 1.3 (Sep 2024)
Corrects restore operation in emulator and new emulator check
Now fully Unicode compatible (FSO rather than lfs)
Moves temporary script files to tmp folder and improved ini file format
Now edits Registry file in FH6 as well so fully functional
Improved user dialogs and menu button labels
Various code optimisations and minor tweaks
Version 2.0 (Dec 2024)
Simplified restore without overwrite option (always restores in full)
Simplified main menu reflecting this change
Enhanced menu pop-up prompts
Supports multi-monitor extended desktops
Checks existing backups for completeness before updating or restoring
Various corrections and enhancements to Unicode support
Backup script modified to call 64-bit Registry key specifically if running in Advanced mode
(avoids potential issues with missing backup file when called directly from Windows)
Version 2.1 (May 2025)
Modified 'three test' emulator detection
CPU architecture detection
ARM64 warning
Version 2.1.1 (Jul 2025)
Corrected ARM64 warning
Version 2.1.4 (Aug 2025)
Generalised ARM64 warning
Minor tidy-up of old code
Version 3.0 (Jun 2026)
Simplified for easier maintenance by removing emulator support and legacy compatibility
Various other code refinements
]]
fhInitialise(5,0,0,'save_required')
require 'luacom'
require('iuplua')
-- warn if emulator detected
if os.getenv('WINEPREFIX') then
local msg = 'Family Historian appears to be running under an emulator. This is not supported by the ' ..
'current plugin version.\n\nSee the plugin help for a fuller discussion of backing up FH ' ..
'settings in a WINE emulator'
fhMessageBox(msg, 'OK', 'MB_ICONSTOP')
return
end
if fhGetAppVersion() > 6 then
fh = require('fhUtils')
fh.setIupDefaults()
else
iup.SetGlobal('CUSTOMQUITMESSAGE','YES')
iup.SetGlobal('UTF8MODE','YES')
end
iup.SetGlobal('UTF8MODE_FILE','YES')
FSO = luacom.CreateObject('Scripting.FileSystemObject')
-- *********************************************************************
function main()
local MainForm
if not GetOptions() then return end -- get plugin options
-- set main window static controls and options
local Heading = 'This plugin backs up and restores Family Historian settings using Windows commands.\n' ..
'All languages and Windows localisations are supported automatically.\n\n' ..
'Technically-minded users may save the backup configuration as Windows batch ' ..
'files to\nincorporate into their own backup process (including fully automated backups).'
local lblTitle = iup.label{title = Heading, alignment = 'ALEFT', padding = '10x20'}
-- folder controls and options
local lblF = iup.label{title = gblOptions.Path or '',expand = 'HORIZONTAL'}
local btnF = iup.button{title = 'Select', padding = '10x3',
tip = 'Destination folder where scripts and backup data are written.',
action = function(self) SelectFolder(MainForm) UpdateMenu() end}
local chkB = iup.toggle{title = 'Write to dated subfolder', expand = 'YES', active = 'NO',
tip = 'Write backup to dated subfolder.'}
local fraF = iup.frame{iup.vbox{lblF, chkB, btnF; alignment = 'ACENTER'}; title = 'Backup folder'}
-- general controls and options (use similar containers for all frames for consistent appearance)
local chkM1 = iup.toggle{title = 'Normal', expand = 'HORIZONTAL',
tip = 'Run backup or restore immediately.'}
local chkM2 = iup.toggle{title = 'Advanced', expand = 'HORIZONTAL',
tip = 'Save the script files without running the backup or restore.'}
local hboxM = iup.hbox{chkM1, chkM2}
local radM = iup.radio{hboxM}
local fraM = iup.frame{iup.hbox{radM}, title = 'Operating mode'}
-- button display
local btnB = iup.button{active = 'NO',
action = function(self) Backup() MainForm.BringFront = 'YES' end}
local btnR = iup.button{title = 'Restore Script', padding = '10x3',
action = function(self)
Restore(MainForm)
MainForm.BringFront = 'YES'
end}
local btnH = iup.button{title = 'Help', tip = 'Display plugin help page.\n' ..
'(hold down either Shift key to display\nSystem Information)',
action = function(self) GetHelp(MainForm) end}
local btnX = iup.button{title = 'Close', tip = 'Close plugin.',
action = function(self) return iup.CLOSE end}
local hButtons = iup.hbox{btnB, btnR, btnH, btnX; cgap = 20, cmargin = 'x15', normalizesize = 'BOTH'}
-- enhanced tool tips
btnB.TipBalloonTitleIcon = '1'
btnF.TipBalloonTitle = 'Select Folder'
btnF.TipBalloonTitleIcon = '1'
chkB.TipBalloonTitle = 'Dated Subfolder'
chkB.TipBalloonTitleIcon = '1'
chkM1.TipBalloonTitle = 'Normal Mode'
chkM1.TipBalloonTitleIcon = '2'
chkM2.TipBalloonTitle = 'Advanced Mode'
chkM2.TipBalloonTitleIcon = '1'
btnH.TipBalloonTitle = 'Plugin Help'
btnH.TipBalloonTitleIcon = '1'
btnX.TipBalloonTitle = 'Close Plugin'
btnX.TipBalloonTitleIcon = '3'
-- menu update function
function UpdateMenu()
if gblOptions.Path then
lblF.Title = gblOptions.Path
btnB.Active = 'YES'
chkB.Active = 'YES'
end
if gblOptions.Dated then
chkB.Value = 'YES'
else
chkB.Value = 'NO'
end
if gblOptions.Advanced then
chkM2.Value = 'ON'
btnB.Title = 'Backup Script'
btnR.Title = 'Restore Script'
btnB.Tip = 'Save backup batch file to defined backup folder.'
btnB.TipBalloonTitle = 'Save Backup Script'
btnR.Tip = 'Save restore batch file to defined backup folder.'
btnR.TipBalloonTitle = 'Save Restore Script'
btnR.TipBalloonTitleIcon = '1'
else
chkM1.Value = 'ON'
btnB.Title = 'Backup'
btnB.Size = btnR.Size
btnB.Tip = 'Run backup now using defined options.'
btnB.TipBalloonTitle = 'Run Backup'
btnR.Title = 'Restore'
btnR.Size = btnB.Size
btnR.Tip = 'Run restore now using defined options.'
btnR.TipBalloonTitle = 'Run Restore'
btnR.TipBalloonTitleIcon = '2'
end
end
-- callback actions
function chkM2:valuechanged_cb()
gblOptions.Advanced = (chkM2.Value == 'ON')
UpdateMenu()
end
function chkB:valuechanged_cb()
gblOptions.Dated = (chkB.Value == 'ON')
end
-- assemble form
local vForm = iup.vbox{lblTitle, fraM, fraF, hButtons;
alignment = 'ACENTER', cgap = 5, cmargin = '10x3'}
MainForm = iup.dialog{vForm; minbox='NO', maxbox='NO',
title='Backup and Restore Family Historian Settings via Windows (3.0)'}
MainForm:map() -- determine form layout prior to assigning button labels and resizing
MainForm.tipballoon = 'YES'
UpdateMenu()
if fhGetAppVersion() > 6 then
iup.SetAttribute(MainForm, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
elseif IsExtendedDesktop() then
local msg = 'Versions of Family Historian prior to 7 cannot determine which monitor ' ..
'the application is running in, so the main plugin menu will always open in the ' ..
'primary monitor.'
MessageBox(msg, 'OK', 'WARNING')
end
MainForm:popup()
SaveOptions()
end
-- *********************************************************************
function SelectFolder(MainForm)
local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path,
title = 'Select folder for scripts and backup files', parentdialog = MainForm}
filedlg:popup()
if filedlg.Status == '-1' then return end
local path = filedlg.Value
gblOptions.Path = path
gblOptions.ShortPath = FSO:GetFolder(path).ShortPath
end
-- *********************************************************************
function Backup()
-- check for existing backup
local BackupPath = gblOptions.Path
if gblOptions.Dated then
BackupPath = BackupPath .. '\\fhBackup_' .. os.date('%Y-%m-%d')
end
-- check all files and folders present if existing backup
local i = 0
if FSO:FolderExists(BackupPath .. '\\fhProgramData') then i = i + 1 end
if FSO:FolderExists(BackupPath .. '\\fhAppData') then i = i + 1 end
if FSO:FileExists(BackupPath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
if FSO:FileExists(BackupPath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
if not gblOptions.Advanced then
if i > 0 and i < 4 then
local msg = 'Existing backup in ' .. BackupPath .. ' is incomplete.\n\nIgnore error and update?'
if MessageBox(msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end
elseif i == 4 then
local stamp = FSO:GetFile(BackupPath .. '\\fhCURRENT_USER.reg').DateLastModified
local msg = os.date('Update existing backup in ' .. BackupPath ..
'?\n\nLast update: ' .. stamp)
if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Update', 2) ~= 1 then return end
end
end
local tblScript, BackupFile = GetBackupScript(BackupPath)
-- write script to file
local F = io.open(BackupFile, 'w')
if not F then
MessageBox('Cannot write backup script.', 'OK', 'ERROR', 'File Access Error')
return
end
table.insert(tblScript, '')
F:write(table.concat(tblScript, '\n'))
F:close()
-- confirm successful write
if gblOptions.Advanced then
local msg = 'Backup script written to file ' .. gblOptions.Path .. '\\fhBackup.bat.'
MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
return
end
-- implement backup
if IsExtendedDesktop() then
MessageBox('The backup will now run in the primary monitor.', 'OK', 'INFORMATION')
end
if not fhShellExecute(BackupFile) then
MessageBox('Backup failed to run.', 'OK', 'ERROR', 'Script Error')
end
end
-- *********************************************************************
function GetBackupScript(BackupPath)
-- prepare backup script
local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$')
local UserKey = 'HKCU\\SOFTWARE\\Calico Pie\\' .. AppName
local GlobalKey
local _, err = GetRegKey('HKLM\\SOFTWARE\\WOW6432Node\\Calico Pie\\' .. AppName ..
'\\2.0\\Preferences\\Language')
if not err then
GlobalKey = 'HKLM\\SOFTWARE\\WOW6432Node\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences'
else
GlobalKey = 'HKLM\\SOFTWARE\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences'
end
local tblT = {} -- build the script in this table
table.insert(tblT, '@echo off')
table.insert(tblT, 'echo Backup started...')
-- set path according to whether dated subfolder or not
if not gblOptions.Dated then
table.insert(tblT, 'set p=' .. gblOptions.ShortPath)
else
table.insert(tblT, 'set basepath=' .. gblOptions.ShortPath)
local cmd = 'for /f %%D in ( \'PowerShell Get-Date -Format "_yyyy-MM-dd"\' ) do set suffix=%%D'
table.insert(tblT, cmd)
table.insert(tblT, 'set p=%basepath%\\fhBackup%suffix%')
end
-- define command script
table.insert(tblT, 'robocopy "' .. gblOptions.ProgramData ..
'" "%p%\\fhProgramData" /MIR /XD "' .. fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Map\\Cache" /NJH')
table.insert(tblT, 'robocopy "' .. gblOptions.AppDataPath .. '" "%p%\\fhAppData" /MIR /NJH')
table.insert(tblT, 'reg export "' .. UserKey .. '" "%p%\\fhCURRENT_USER.reg" /y')
table.insert(tblT, 'reg export "' .. GlobalKey .. '" "%p%\\fhLOCAL_MACHINE.reg" /y')
-- determine file used to save command script and add self-delete command if immediate backup
local BackupFile
if gblOptions.Advanced then
BackupFile = gblOptions.ShortPath .. '\\fhBackup.bat'
else
BackupFile = gblOptions.Temp .. '\\~fhBackup.bat'
table.insert(tblT, 'pause')
table.insert(tblT, 'del "' .. BackupFile .. '"')
end
return tblT, BackupFile
end
-- *********************************************************************
function Restore(MainForm)
-- check for Advanced mode
local RestoreFolder, ShortFolder, RestoreFile, ShortFile
if gblOptions.Advanced then
RestoreFolder = gblOptions.Path
RestoreFile = gblOptions.Path .. '\\fhRestore.bat'
ShortFolder = gblOptions.ShortPath
ShortFile = gblOptions.ShortPath .. '\\fhRestore.bat'
else
RestoreFolder, ShortFolder = SelectRestoreFolder(MainForm)
if not RestoreFolder then return end
RestoreFile = gblOptions.Temp .. '\\~fhRestore.bat'
ShortFile = gblOptions.Temp .. '\\~fhRestore.bat'
end
local tblScript = GetRestoreScript(RestoreFolder, ShortFolder)
-- write script to file (path cannot be Unicode, so simple method ok)
local F = io.open(ShortFile, 'w')
if not F then
MessageBox('Cannot write restore script.', 'OK', 'ERROR', 'File Access Error')
return
end
table.insert(tblScript, '')
F:write(table.concat(tblScript, '\n'))
F:close()
-- confirm successful write
if gblOptions.Advanced then
local msg = 'Restore script written to file ' .. RestoreFile .. '.'
MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
return
end
-- remove default files and folders from Registry file before restoring
RemoveDefaults(RestoreFolder)
-- warn the user what is about to happen and request approval
local msg = 'Family Historian will now be closed in order to complete the restore operation.\n\n' ..
'Are you sure that you want to do this?'
if MessageBox(msg, 'YESNO', 'WARNING', 'Confirm Automatic Restore - CAUTION', 2) ~= 1 then return end
-- generate auto-close script
local CloseFile = gblOptions.Temp .. '\\' .. '~fhAutoClose.bat'
local tblT = {}
table.insert(tblT, '@echo off')
table.insert(tblT, 'echo Restore started...')
table.insert(tblT, 'powershell start-sleep -s 1')
table.insert(tblT, 'cls')
table.insert(tblT, 'taskkill /IM fh.exe /f')
table.insert(tblT, 'powershell start-sleep -s 1')
table.insert(tblT, 'call "' .. RestoreFile .. '"')
table.insert(tblT, 'del "' .. RestoreFile .. '"')
table.insert(tblT, 'del "' .. CloseFile .. '"')
table.insert(tblT, '')
local F = io.open(CloseFile, 'w')
if not F then
MessageBox('Cannot write auto-close script.', 'OK', 'ERROR', 'File Access Error')
return
end
F:write(table.concat(tblT, '\n'))
F:close()
-- check that only one instance of FH is running
if CountInstances() > 1 then
local msg = 'There may be more than one instance of Family Historian open.\n\n' ..
'Proceeding with the restore operation could cause data loss. Please close ' ..
'any additional copies and re-run the plugin.'
MessageBox(msg, 'OK', 'ERROR', 'Multiple Instances Detected')
os.remove(RestoreFile)
os.remove(CloseFile)
return
end
-- execute the kamikaze close and bale out...
fhShellExecute(CloseFile)
end
-- *********************************************************************
function SelectRestoreFolder(MainForm)
-- select backup to restore
local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path, parentdialog = MainForm,
title = 'Select folder that contains the backup you wish to restore'}
filedlg:popup()
if filedlg.Status == '-1' then return end
local RestorePath = filedlg.Value
local ShortPath = FSO:GetFolder(RestorePath).ShortPath
-- check folder contains an identifiable backup
local BackupVersion, B1, B2, B3
-- is there a valid new format backup in this folder?
local i = 0
if FSO:FolderExists(RestorePath .. '\\fhProgramData') then i = i + 1 end
if FSO:FolderExists(RestorePath .. '\\fhAppData') then i = i + 1 end
if FSO:FileExists(RestorePath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
if FSO:FileExists(RestorePath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
if i == 4 then
local KeyFile = RestorePath .. '\\fhProgramData\\Plugin Data\\' ..
'Backup and Restore Family Historian Settings via Windows.ini'
ShortKey = FSO:GetFile(KeyFile).ShortPath
for Line in io.lines(ShortKey) do
local k, v = Line:match('^(%w+)=([%w%p%s]+)$')
if k == 'Version' then
BackupVersion = v
B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)')
end
end
end
if not (B1 and B2 and B3) then
local msg = 'Selected folder does not appear to contain a backup.'
MessageBox(msg, 'OK', 'ERROR', 'Folder Selection Error')
return
end
-- check backup version matches
local V1, V2, V3 = fhGetAppVersion()
B1, B2, B3 = tonumber(B1), tonumber(B2), tonumber(B3)
if V1 ~= B1 then
local msg = 'Cannot restore a Version ' .. B1 .. ' backup into Version ' .. V1 .. '.'
MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
return
elseif B2 > V2 or (B2 == V2 and B3 > V3) then
local msg = 'Cannot restore a Version ' .. BackupVersion .. ' backup into Version ' ..
gblOptions.Version .. '.\n\n' ..
'You should update your present copy of Family Historian before restoring the backup.'
MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
return
elseif B2 < V2 or (B2 == V2 and B3 < V3) then
local msg = 'Restoring a Version ' .. BackupVersion .. ' backup into Version ' ..
gblOptions.Version .. ' is potentially risky.\n\nAre you sure that you want to do this?'
if MessageBox(msg, 'YESNO', 'WARNING', 'Potential Version Conflict', 2) ~= 1 then return end
end
-- get backup creation date and time
local stamp = FSO:GetFile(RestorePath .. '\\fhCURRENT_USER.reg').DateLastModified
local msg = 'Backup in ' .. RestorePath .. ' was created ' .. stamp ..
'. Is this the correct backup?'
if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Backup Selection', 2) ~= 1 then return end
return RestorePath, ShortPath
end
-- *********************************************************************
function GetRestoreScript(RestoreFolder, ShortFolder)
local tblT = {} -- build the script in this table
table.insert(tblT, '@echo off')
local Switch = '/MIR /NJH'
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhProgramData" "' ..
gblOptions.ProgramData .. '" ' .. Switch)
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhAppData" "' ..
gblOptions.AppDataPath .. '" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"')
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"')
if not gblOptions.Advanced then
table.insert(tblT, 'pause')
end
return tblT
end
-- *********************************************************************
function RemoveDefaults(RestoreFolder)
-- remove defaults that may not be valid in new installation
local FileName = RestoreFolder .. '\\fhLOCAL_MACHINE.reg'
if not FSO:FileExists(FileName) then
MessageBox('Restore folder is missing a required Registry file.', 'OK', 'ERROR')
return
end
local TextStream = FSO:OpenTextFile(FileName, 1, false, -1)
local Data = TextStream:ReadAll()
TextStream:Close()
local tblT = {}
Data = Data:gsub('\r\n\r\n', '\r\n \r\n') -- simple workaround to include blank lines
for Line in Data:gmatch('[^\r\n]+') do
if Line == ' ' then Line = '' end
local T1 = Line:match('^"Backup Directory"=')
local T2 = Line:match('^"Default File"=')
local T3 = Line:match('^"FH Root Folder"=')
local T4 = Line:match('^"Emulator%-Compat%-Mode"=')
if not (T1 or T2 or T3 or T4) then table.insert(tblT, Line) end
end
table.insert(tblT, '')
TextStream = FSO:OpenTextFile(FileName, 2, false, -1)
TextStream:Write(table.concat(tblT, '\n'))
TextStream:Close()
end
-- *********************************************************************
function GetOptions()
gblOptions = {}
local tblO = {}
-- get system level options
gblOptions.Temp = os.getenv('TEMP')
gblOptions.ProgramData = fhGetContextInfo('CI_APP_DATA_FOLDER')
local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$')
gblOptions.AppData = os.getenv('TEMP'):match('^(.+)Local\\Temp$') ..
'Roaming\\Calico Pie\\' .. AppName
gblOptions.AppDataPath = FSO:GetFolder(gblOptions.AppData).ShortPath
-- get saved options
local File = gblOptions.ProgramData .. '\\Plugin Data\\' ..
'Backup and Restore Family Historian Settings via Windows.ini'
if FSO:FileExists(File) then
for line in io.lines(File) do
local i = line:find('=')
if i then tblO[line:sub(1, i-1)] = line:sub(i+1) end
end
if tblO.Path and FSO:FolderExists(tblO.Path) then
gblOptions.Path = tblO.Path
gblOptions.ShortPath = FSO:GetFolder(gblOptions.Path).ShortPath
end
if tblO.Advanced == '1' then gblOptions.Advanced = true end
if tblO.Dated == '1' then gblOptions.Dated = true end
end
-- set version option and save back to file to ensure saved on backup
local V1, V2, V3 = fhGetAppVersion()
gblOptions.Version = V1 .. '.' .. V2 .. '.' .. V3
SaveOptions()
return true
end
-- *********************************************************************
function SaveOptions()
-- save options to file
local OptionsFolder = gblOptions.ProgramData .. '\\Plugin Data'
if not FSO:FolderExists(OptionsFolder) then
FSO:CreateFolder(OptionsFolder)
end
local OptionsFile = OptionsFolder .. '\\Backup and Restore Family Historian Settings via Windows.ini'
local F = io.open(OptionsFile, 'w')
if not F then
MessageBox('Cannot open options file.', 'OK', 'ERROR', 'File Access Error')
return
end
F:write('[Options]\n')
F:write('Version=' .. gblOptions.Version .. '\n')
if gblOptions.Path then F:write('Path=' .. gblOptions.Path .. '\n') end
if gblOptions.Advanced then F:write('Advanced=1\n') end
if gblOptions.Dated then F:write('Dated=1\n') end
F:close()
end
-- *********************************************************************
function GetHelp(MainForm)
if iup.GetGlobal('SHIFTKEY') == 'OFF' then
fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/' ..
'backup-and-restore-family-historian-settings-via-windows')
end
-- hidden diagnostic window
gblOptions.CPU = GetRegKey('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\' ..
'Environment\\PROCESSOR_ARCHITECTURE')
local tblT = {
'Processor Architecture = ' .. gblOptions.CPU or 'n/a',
'User Name (IUP) = ' .. (iup.GetGlobal('USERNAME') or ''),
'Temp Folder = ' .. gblOptions.Temp,
'ProgramData Folder = ' .. gblOptions.ProgramData:gsub(' ', ' '),
'AppData Folder = ' .. gblOptions.AppData:gsub(' ', ' ')
}
MessageBox(table.concat(tblT, '\n'), 'OK', 'INFORMATION', 'System Information')
end
-- *********************************************************************
function IsExtendedDesktop()
-- determines if system has an extended desktop
local xd = iup.GetGlobal('VIRTUALSCREEN')
local x, y, width, height = xd:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
local fs = iup.GetGlobal('FULLSIZE')
local ExtendedDesktop = fs ~= width .. 'x' .. height
return ExtendedDesktop
end
-- *********************************************************************
function CountInstances()
-- checks just a single instance of FH is running
local F = io.popen('powershell get-process *fh*')
if not F then
local msg = 'Cannot determine number of Family Historian copies running.'
MessageBox(msg, 'OK', 'ERROR', 'File Handle Access Error')
return
end
local S = F:read('*a')
F:close()
local Instances = 0
for line in S:gmatch('[^\r\n]+') do
if line:lower():match('%sfh%s+$') then
Instances = Instances + 1
end
end
return Instances
end
-- *********************************************************************
function GetRegKey(key)
local sh = luacom.CreateObject 'WScript.Shell'
local ans
if pcall(function () ans = sh:RegRead(key) end) then
return ans
else
return nil,true
end
end
-- *********************************************************************
function MessageBox(Message, Buttons, Icon, Title, Default)
-- replaces built-in function with custom version containing more options
local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
title = Title or fhGetContextInfo('CI_PLUGIN_NAME'), buttondefault = Default,
parentdialog = MainForm}
msgdlg:popup()
return tonumber(msgdlg.ButtonResponse)
end
-- *********************************************************************
main()
Source:Backup-and-Restore-Family-Historian-Settings-via-Windows-9.fh_lua