NAME
App::FileSync::Settings - process-wide settings and logging for App::FileSync
SYNOPSIS
use App::FileSync::Settings;
my $logger = App::FileSync::Settings::logger(verbose => 1);
$logger->error('...');
my $settings = App::FileSync::Settings::load();
my $level = $settings->{log_level};
my $report = App::FileSync::Settings::explain();
printf "%s = %s (%s)\n", $_->{key}, $_->{value}, $_->{label}
for @{ $report->{settings} };
DESCRIPTION
Settings that apply to the process rather than to one repo - the log level, format and destination, plus the SSH identity, known-hosts file and remote server command - are read through Object::Configure. The repos themselves remain in ~/.filesync/config.json, which filesync init writes; this module does not replace that store.
The settings file is ~/.conf/app-filesync.json when it exists, or the path in FILESYNC_SETTINGS. The section key is App__FileSync, and a UNIVERSAL section in the same directory applies to every class that uses Object::Configure. Environment variables named App__FileSync__key override the file:
# ~/.conf/app-filesync.json
{ "App__FileSync": { "log_level": "debug", "log_format": "%level%: %message%" } }
# or
export App__FileSync__log_level=debug
log_file sends the log to a file instead of standard error; leading directories are created. log_format is a Log::Abstraction format string.
ssh_identity, ssh_known_hosts and server_command are the defaults for every SSH repo, so a per-repo identity, known_hosts or server_command (written by filesync init --identity and friends) only has to be set when it differs. server_command defaults to filesync-server:
{ "App__FileSync": {
"ssh_identity" : "~/.ssh/filesync_ed25519",
"ssh_known_hosts" : "~/.ssh/filesync_known_hosts",
"server_command" : "/usr/local/bin/filesync-server"
} }
Command output (sync summaries, plans, --json) is written to standard output by the commands themselves; the logger carries diagnostics on standard error, so a cron job can separate the two. --verbose raises the level to debug and --quiet lowers it to error for that run, overriding the file.
wants_plain_end_of_scope_hook and use_plain_end_of_scope_hook are aliases of the functions in App::FileSync::Platform, kept for existing callers.
FUNCTIONS
config_file
Purpose: the settings file that would be used.
Args: an optional explicit path.
Returns: the explicit path when one is given; otherwise FILESYNC_SETTINGS or ~/.conf/app-filesync.json if that file exists; otherwise undef.
Side Effects: none.
Usage:
my $file = App::FileSync::Settings::config_file();
EXAMPLE
my $file = App::FileSync::Settings::config_file() // 'none';
print "settings: $file\n";
API SPECIFICATION
INPUT
[ { name => 'given', type => 'string', optional => 1 } ]
OUTPUT
{ type => 'string', optional => 1 }
MESSAGES
None.
FORMAL SPECIFICATION
config_file(g) = g if g ≠ ""
config_file(g) = d if d = default_path ∧ d ∈ dom FS
config_file(g) = ⊥ otherwise
load
Purpose: the merged process settings.
Args: optional config_file and config_dirs (as for "configure" in Object::Configure).
Returns: a hashref with every key in @SETTING_KEYS, built-in defaults overlaid by the settings file, the UNIVERSAL section and App__FileSync__* environment variables.
Side Effects: reads the settings file. Dies (through Object::Configure) when an explicit file does not exist.
Usage:
my $settings = App::FileSync::Settings::load(config_file => $path);
EXAMPLE
print App::FileSync::Settings::load()->{log_level}, "\n"; # warning
API SPECIFICATION
INPUT
{
config_file => { type => 'string', optional => 1 },
config_dirs => { type => 'arrayref', optional => 1 },
}
OUTPUT
{ type => 'hashref' }
MESSAGES
Key Meaning Resolution
args.invalid an argument has the wrong type fix the call
Object::Configure's own error for a missing explicit file is passed through.
FORMAL SPECIFICATION
load(a) = defaults ⊕ file(config_file(a)) ⊕ universal ⊕ env
explain
Purpose: where each effective setting came from, for filesync settings.
Args: as "load".
Returns: { settings_file => $path_or_undef, settings => \@rows }, one row per key in @SETTING_KEYS. Each row has the effective value, a source of default, file, env or merged, and a human-readable label naming the file section, environment variable or built-in default that supplied it.
The values are whatever "load" returns; the provenance is reconstructed from the same layers Object::Configure merges. A value no modelled layer accounts for (a global section, say) is reported as merged rather than claimed as a default.
Side Effects: reads the settings file and any universal file beside it.
Usage:
my $report = App::FileSync::Settings::explain();
EXAMPLE
for my $row (@{ App::FileSync::Settings::explain()->{settings} }) {
printf "%-16s %-20s %s\n", $row->{key}, $row->{value}, $row->{label};
}
API SPECIFICATION
INPUT
{
config_file => { type => 'string', optional => 1 },
config_dirs => { type => 'arrayref', optional => 1 },
}
OUTPUT
{
type => 'hashref',
schema => {
settings_file => { type => 'string', optional => 1 },
settings => { type => 'arrayref' },
},
}
MESSAGES
As "load".
FORMAL SPECIFICATION
∀ k : SETTING_KEYS •
row(k).value = load()(k)
row(k).source = last layer ∈ ⟨default, universal, class⟩ that sets k,
or merged if that layer's value ≠ row(k).value
PSEUDOCODE
load the settings
explain the class and universal sections of the settings file, if any
for each key:
start from the built-in default
let the universal section, then the class section, override it
without a file, let UNIVERSAL__key then App__FileSync__key override it
if the loaded value differs from the winner, call it merged
explain_repos
Purpose: how the SSH settings resolve for every repo in a client config.
Args: config (the hashref from App::FileSync::Config::load_client), report (the result of "explain"), config_file (the client config path, used in labels).
Returns: an arrayref, one entry per repo in name order, with repo, transport, server (when the repo records one) and ssh: one {key, value, source, label} row per setting, where source is repo (the repo's own entry), process (the settings reported by explain) or default (ssh's own default). port and host_key_checking rows carry repo_only => 1. ssh is undef for the local transport, which never runs ssh.
Side Effects: none.
Usage:
my $repos = App::FileSync::Settings::explain_repos(
config => $config, report => App::FileSync::Settings::explain(), config_file => $path,
);
EXAMPLE
for my $repo (@{ $repos }) {
next unless $repo->{ssh};
print "$repo->{repo}: $_->{key} = $_->{value} ($_->{label})\n" for @{ $repo->{ssh} };
}
API SPECIFICATION
INPUT
{
config => { type => 'hashref', optional => 1 },
report => { type => 'hashref', optional => 1 },
config_file => { type => 'string', optional => 1 },
}
OUTPUT
{ type => 'arrayref' }
MESSAGES
Key Meaning Resolution
args.invalid an argument has the wrong type fix the call
FORMAL SPECIFICATION
∀ r : dom config.repos • ∀ k : REPO_SSH_SETTINGS •
value(r, k) = repo(r)(k) if repo(r)(k) ≠ ""
= process(k) else if process(k) ≠ ""
= "" otherwise
PSEUDOCODE
for each repo, by name:
record its transport and server
if it uses ssh:
for each SSH setting take the repo's value, else the process
setting, else ssh's default
add the repo-only port and host-key-checking rows
logger
Purpose: the diagnostics logger for this run.
Args: as "load", plus level (overrides the settings), verbose (debug) and quiet (errors only). A command-line flag describes this run, so it beats the settings file.
Returns: a Log::Abstraction writing to standard error, or to log_file.
Side Effects: reads the settings; creates log_file's directory.
Usage:
my $logger = App::FileSync::Settings::logger(verbose => $opts{verbose});
EXAMPLE
my $logger = App::FileSync::Settings::logger(level => 'info');
$logger->info('starting');
API SPECIFICATION
INPUT
{
config_file => { type => 'string', optional => 1 },
config_dirs => { type => 'arrayref', optional => 1 },
level => { type => 'string', optional => 1 },
verbose => { type => 'boolean', optional => 1 },
quiet => { type => 'boolean', optional => 1 },
}
OUTPUT
{ type => 'object', isa => 'Log::Abstraction' }
MESSAGES
Key Meaning Resolution
settings.bad_level log_level is not a known level use one of @VALID_LEVELS
io.mkdir log_file's directory cannot be made fix log_file
FORMAL SPECIFICATION
level = debug if verbose
= error else if quiet
= args.level else if set
= settings.log_level else if set
= warning otherwise
pre lc level ∈ lc VALID_LEVELS
PSEUDOCODE
load the settings
choose the level: argument, then settings, then the default
reject an unknown level
send output to log_file (creating its directory) or standard error
build the logger; apply --verbose or --quiet over it
LIMITATIONS
explainreconstructs provenance by re-reading the layers Object::Configure merges. If a later Object::Configure adds a layer, the report saysmergedfor values from it rather than naming it.The settings are re-read on every
load,explainandloggercall; nothing is cached, so a long-running process sees edits but pays a file read per call.
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.