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

  • explain reconstructs provenance by re-reading the layers Object::Configure merges. If a later Object::Configure adds a layer, the report says merged for values from it rather than naming it.

  • The settings are re-read on every load, explain and logger call; 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.