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 init and 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.