NAME
App::FileSync::Config - client and repo configuration files
SYNOPSIS
my $client = App::FileSync::Config::load_client();
$client->{repos}{docs}{root} = '/home/foo/Documents';
App::FileSync::Config::save_client($client);
my $repo = App::FileSync::Config::load_repo('/srv/filesync', 'docs');
DESCRIPTION
The client keeps one JSON file, ~/.filesync/config.json (or FILESYNC_CONFIG), listing its repos. The server keeps one JSON file per repo, ROOT/.filesync/repos/NAME/config.json, holding its policy (trash retention, deletion limits, ignore patterns) and the allowed clients. Both are written atomically.
FUNCTIONS
home_dir
Purpose: the client's state directory.
Args: none.
Returns: FILESYNC_HOME, else ~/.filesync (using HOME, then USERPROFILE).
Side Effects: none.
Usage:
my $home = App::FileSync::Config::home_dir();
EXAMPLE
print App::FileSync::Config::home_dir(), "\n"; # /home/foo/.filesync
API SPECIFICATION
INPUT
[]
OUTPUT
{ type => 'string', min => 1 }
MESSAGES
Key Meaning Resolution
config.no_home no home directory is known set FILESYNC_HOME
FORMAL SPECIFICATION
home_dir = ENV(FILESYNC_HOME) if non-empty
= (ENV(HOME) or ENV(USERPROFILE)) / ".filesync" if one is non-empty
↑ config.no_home otherwise
client_config_path
Purpose: where the client's repo list is stored.
Args: none.
Returns: FILESYNC_CONFIG, else HOME_DIR/config.json.
Side Effects: none.
Usage:
my $path = App::FileSync::Config::client_config_path();
EXAMPLE
local $ENV{FILESYNC_CONFIG} = '/tmp/test.json';
print App::FileSync::Config::client_config_path(), "\n"; # /tmp/test.json
API SPECIFICATION
INPUT
[]
OUTPUT
{ type => 'string', min => 1 }
MESSAGES
Key Meaning Resolution
config.no_home no home directory is known set FILESYNC_HOME
FORMAL SPECIFICATION
client_config_path = ENV(FILESYNC_CONFIG) if non-empty, else home_dir / "config.json"
default_client_config
Purpose: the configuration of a client that has no repos yet.
Args: none.
Returns: a new hashref { version, ssh => { command, extra_args }, repos => {} }.
Side Effects: none.
Usage:
my $config = App::FileSync::Config::default_client_config();
EXAMPLE
my $config = App::FileSync::Config::default_client_config();
$config->{client_id} = 'laptop-1';
API SPECIFICATION
INPUT
[]
OUTPUT
{
type => 'hashref',
schema => {
version => { type => 'integer' },
ssh => { type => 'hashref' },
repos => { type => 'hashref' },
},
}
MESSAGES
None.
FORMAL SPECIFICATION
default_client_config = ⟨version ↦ 1, ssh ↦ default_ssh, repos ↦ ∅⟩
load_client
Purpose: read the client's repo list.
Args: optional $path (default "client_config_path").
Returns: the configuration hashref; a missing file gives "default_client_config". repos and ssh are always hashes.
Side Effects: none.
Usage:
my $config = App::FileSync::Config::load_client($opts{config});
EXAMPLE
my $config = App::FileSync::Config::load_client();
print join(', ', sort keys %{ $config->{repos} }), "\n";
API SPECIFICATION
INPUT
[ { name => 'path', type => 'string', optional => 1 } ]
OUTPUT
{ type => 'hashref' }
MESSAGES
Key Meaning Resolution
config.parse the file is not valid JSON fix or remove it
config.not_object the JSON is not an object fix or remove it
io.read the file cannot be read check permissions
FORMAL SPECIFICATION
load_client(p) = normalise(json(FS(p))) if p ∈ dom FS
= default_client_config otherwise
save_client
Purpose: write the client's repo list.
Args: \%config, optional $path.
Returns: the path written.
Side Effects: replaces the file atomically, creating its directory.
Usage:
App::FileSync::Config::save_client($config, $path);
EXAMPLE
my $written = App::FileSync::Config::save_client(App::FileSync::Config::default_client_config());
API SPECIFICATION
INPUT
[
{ name => 'config', type => 'hashref' },
{ name => 'path', type => 'string', optional => 1 },
]
OUTPUT
{ type => 'string', min => 1 }
MESSAGES
Key Meaning Resolution
io.write the file cannot be written check permissions and space
FORMAL SPECIFICATION
ΔFS
FS'(path) = json(config) ⁀ "\n"
repo_meta_dir
Documented with "repo_config_path" below.
repo_config_path
Purpose: where a repo's metadata (and its config file) lives on the server.
Args: $root, $repo.
Returns: ROOT/.filesync/repos/REPO and .../config.json.
Side Effects: none.
Usage:
my $meta = App::FileSync::Config::repo_meta_dir($root, $repo);
EXAMPLE
print App::FileSync::Config::repo_config_path('/srv/filesync', 'docs'), "\n";
API SPECIFICATION
INPUT
[
{ name => 'root', type => 'string', min => 1 },
{ name => 'repo', type => 'string', min => 1 },
]
OUTPUT
{ type => 'string', min => 1 }
MESSAGES
None.
FORMAL SPECIFICATION
repo_meta_dir(r, n) = r / ".filesync" / "repos" / n
repo_config_path(r, n) = repo_meta_dir(r, n) / "config.json"
default_repo_config
Purpose: the policy of a newly created repo.
Args: $name.
Returns: a new hashref: 30 days of trash, at most 200 or 10% of known files deleted per session, inline conflict copies, modes preserved, symlinks not followed, no clients, the default ignore list.
Side Effects: none.
Usage:
my $config = App::FileSync::Config::default_repo_config('docs');
EXAMPLE
my $config = App::FileSync::Config::default_repo_config('photos');
$config->{trash_days} = 90;
API SPECIFICATION
INPUT
[ { name => 'name', type => 'string', min => 1 } ]
OUTPUT
{ type => 'hashref' }
MESSAGES
Key Meaning Resolution
args.invalid no name was given pass the repo name
FORMAL SPECIFICATION
default_repo_config(n) = ⟨name ↦ n, trash_days ↦ 30, max_deletes ↦ 200,
max_delete_percent ↦ 10, clients ↦ ∅, ignore ↦ DEFAULT_IGNORE, ...⟩
load_repo
Purpose: read a repo's policy from the server root.
Args: $root, $repo.
Returns: the configuration hashref; ignore is always an array and clients always a hash.
Side Effects: none.
Usage:
my $config = App::FileSync::Config::load_repo($root, 'docs');
EXAMPLE
my $config = App::FileSync::Config::load_repo('/srv/filesync', 'docs');
print "$config->{trash_days} days of trash\n";
API SPECIFICATION
INPUT
[
{ name => 'root', type => 'string', min => 1 },
{ name => 'repo', type => 'string', min => 1 },
]
OUTPUT
{ type => 'hashref' }
MESSAGES
Key Meaning Resolution
config.no_repo no such repo under the root filesync-admin repo create
config.parse the file is not valid JSON fix it by hand
config.not_object the JSON is not an object fix it by hand
FORMAL SPECIFICATION
load_repo(r, n) = normalise(json(FS(repo_config_path(r, n))))
pre repo_config_path(r, n) ∈ dom FS
save_repo
Purpose: write a repo's policy.
Args: $root, $repo, \%config.
Returns: the path written.
Side Effects: creates the repo's metadata directory; replaces the file atomically.
Usage:
App::FileSync::Config::save_repo($root, 'docs', $config);
EXAMPLE
my $config = App::FileSync::Config::load_repo($root, 'docs');
$config->{trash_days} = 60;
App::FileSync::Config::save_repo($root, 'docs', $config);
API SPECIFICATION
INPUT
[
{ name => 'root', type => 'string', min => 1 },
{ name => 'repo', type => 'string', min => 1 },
{ name => 'config', type => 'hashref' },
]
OUTPUT
{ type => 'string', min => 1 }
MESSAGES
Key Meaning Resolution
io.mkdir the directory cannot be made check permissions
io.write the file cannot be written check permissions and space
FORMAL SPECIFICATION
ΔFS
FS'(repo_config_path(r, n)) = json(config) ⁀ "\n"
LIMITATIONS
Configuration is JSON only. The process-wide settings (log level, SSH defaults) go through Object::Configure instead; see App::FileSync::Settings. Two mechanisms is one too many, but the repo list is written by
filesync initand must round-trip exactly, which Object::Configure's layered merge does not promise.A config written by a newer release is read as-is: unknown keys are kept and ignored, and there is no schema version check beyond
version.
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.