diff --git a/docs/usage/borgfs.rst b/docs/usage/borgfs.rst new file mode 100644 index 000000000..162589ca0 --- /dev/null +++ b/docs/usage/borgfs.rst @@ -0,0 +1 @@ +.. include:: borgfs.rst.inc diff --git a/setup.py b/setup.py index 42b272613..eaae5c417 100644 --- a/setup.py +++ b/setup.py @@ -238,10 +238,11 @@ class build_usage(Command): # allows us to build docs without the C modules fully loaded during help generation from borg.archiver import Archiver parser = Archiver(prog='borg').build_parser() + borgfs_parser = Archiver(prog='borgfs').build_parser() - self.generate_level("", parser, Archiver) + self.generate_level("", parser, Archiver, {'borgfs': borgfs_parser}) - def generate_level(self, prefix, parser, Archiver): + def generate_level(self, prefix, parser, Archiver, extra_choices=None): is_subcommand = False choices = {} for action in parser._actions: @@ -249,6 +250,8 @@ class build_usage(Command): is_subcommand = True for cmd, parser in action.choices.items(): choices[prefix + cmd] = parser + if extra_choices is not None: + choices.update(extra_choices) if prefix and not choices: return print('found commands: %s' % list(choices.keys())) @@ -501,12 +504,13 @@ class build_man(Command): # allows us to build docs without the C modules fully loaded during help generation from borg.archiver import Archiver parser = Archiver(prog='borg').build_parser() + borgfs_parser = Archiver(prog='borgfs').build_parser() - self.generate_level('', parser, Archiver) + self.generate_level('', parser, Archiver, {'borgfs': borgfs_parser}) self.build_topic_pages(Archiver) self.build_intro_page() - def generate_level(self, prefix, parser, Archiver): + def generate_level(self, prefix, parser, Archiver, extra_choices=None): is_subcommand = False choices = {} for action in parser._actions: @@ -514,6 +518,8 @@ class build_man(Command): is_subcommand = True for cmd, parser in action.choices.items(): choices[prefix + cmd] = parser + if extra_choices is not None: + choices.update(extra_choices) if prefix and not choices: return @@ -521,7 +527,10 @@ class build_man(Command): if command.startswith('debug') or command == 'help': continue - man_title = 'borg-' + command.replace(' ', '-') + if command == "borgfs": + man_title = command + else: + man_title = 'borg-' + command.replace(' ', '-') print('building man page', man_title + '(1)', file=sys.stderr) is_intermediary = self.generate_level(command + ' ', parser, Archiver) @@ -536,7 +545,10 @@ class build_man(Command): write('| borg', '[common options]', command, subcommand, '...') self.see_also.setdefault(command, []).append('%s-%s' % (command, subcommand)) else: - write('borg', '[common options]', command, end='') + if command == "borgfs": + write(command, end='') + else: + write('borg', '[common options]', command, end='') self.write_usage(write, parser) write('\n') diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 329dd4a6d..a8cab84be 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2458,7 +2458,67 @@ class Archiver: mid_common_parser.set_defaults(paths=[], patterns=[]) parser.common_options.add_common_group(mid_common_parser, '_midcommand') - subparsers = parser.add_subparsers(title='required arguments', metavar='') + mount_epilog = process_epilog(""" + This command mounts an archive as a FUSE filesystem. This can be useful for + browsing an archive or restoring individual files. Unless the ``--foreground`` + option is given the command will run in the background until the filesystem + is ``umounted``. + + The command ``borgfs`` provides a wrapper for ``borg mount``. This can also be + used in fstab entries: + ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0`` + + To allow a regular user to use fstab entries, add the ``user`` option: + ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0`` + + For mount options, see the fuse(8) manual page. Additional mount options + supported by borg: + + - versions: when used with a repository mount, this gives a merged, versioned + view of the files in the archives. EXPERIMENTAL, layout may change in future. + - allow_damaged_files: by default damaged files (where missing chunks were + replaced with runs of zeros by borg check ``--repair``) are not readable and + return EIO (I/O error). Set this option to read such files. + + The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users + to tweak the performance. It sets the number of cached data chunks; additional + memory usage can be up to ~8 MiB times this number. The default is the number + of CPU cores. + + When the daemonized process receives a signal or crashes, it does not unmount. + Unmounting in these cases could cause an active rsync or similar process + to unintentionally delete data. + + When running in the foreground ^C/SIGINT unmounts cleanly, but other + signals or crashes do not. + """) + + if parser.prog == 'borgfs': + parser.description = self.do_mount.__doc__ + parser.epilog = mount_epilog + parser.formatter_class = argparse.RawDescriptionHelpFormatter + parser.help = 'mount repository' + subparser = parser + else: + subparsers = parser.add_subparsers(title='required arguments', metavar='') + subparser = subparsers.add_parser('mount', parents=[common_parser], add_help=False, + description=self.do_mount.__doc__, + epilog=mount_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='mount repository') + subparser.set_defaults(func=self.do_mount) + subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(), + help='repository/archive to mount') + subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str, + help='where to mount filesystem') + subparser.add_argument('-f', '--foreground', dest='foreground', + action='store_true', + help='stay in foreground, do not daemonize') + subparser.add_argument('-o', dest='options', type=str, + help='Extra mount options') + define_archive_filters_group(subparser) + if parser.prog == 'borgfs': + return parser serve_epilog = process_epilog(""" This command starts a repository server process. This command is usually not used manually. @@ -3242,57 +3302,6 @@ class Archiver: define_archive_filters_group(subparser) define_exclusion_group(subparser) - mount_epilog = process_epilog(""" - This command mounts an archive as a FUSE filesystem. This can be useful for - browsing an archive or restoring individual files. Unless the ``--foreground`` - option is given the command will run in the background until the filesystem - is ``umounted``. - - The command ``borgfs`` provides a wrapper for ``borg mount``. This can also be - used in fstab entries: - ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0`` - - To allow a regular user to use fstab entries, add the ``user`` option: - ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0`` - - For mount options, see the fuse(8) manual page. Additional mount options - supported by borg: - - - versions: when used with a repository mount, this gives a merged, versioned - view of the files in the archives. EXPERIMENTAL, layout may change in future. - - allow_damaged_files: by default damaged files (where missing chunks were - replaced with runs of zeros by borg check ``--repair``) are not readable and - return EIO (I/O error). Set this option to read such files. - - The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users - to tweak the performance. It sets the number of cached data chunks; additional - memory usage can be up to ~8 MiB times this number. The default is the number - of CPU cores. - - When the daemonized process receives a signal or crashes, it does not unmount. - Unmounting in these cases could cause an active rsync or similar process - to unintentionally delete data. - - When running in the foreground ^C/SIGINT unmounts cleanly, but other - signals or crashes do not. - """) - subparser = subparsers.add_parser('mount', parents=[common_parser], add_help=False, - description=self.do_mount.__doc__, - epilog=mount_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help='mount repository') - subparser.set_defaults(func=self.do_mount) - subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(), - help='repository/archive to mount') - subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str, - help='where to mount filesystem') - subparser.add_argument('-f', '--foreground', dest='foreground', - action='store_true', - help='stay in foreground, do not daemonize') - subparser.add_argument('-o', dest='options', type=str, - help='Extra mount options') - define_archive_filters_group(subparser) - umount_epilog = process_epilog(""" This command un-mounts a FUSE filesystem that was mounted with ``borg mount``. @@ -4039,10 +4048,6 @@ def sig_trace_handler(sig_no, stack): # pragma: no cover def main(): # pragma: no cover - # provide 'borg mount' behaviour when the main script/executable is named borgfs - if os.path.basename(sys.argv[0]) == "borgfs": - sys.argv.insert(1, "mount") - # Make sure stdout and stderr have errors='replace' to avoid unicode # issues when print()-ing unicode file names sys.stdout = ErrorIgnoringTextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, 'replace', line_buffering=True) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index a38667e6a..1c2e83708 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -3688,14 +3688,17 @@ def get_all_parsers(): Return dict mapping command to parser. """ parser = Archiver(prog='borg').build_parser() + borgfs_parser = Archiver(prog='borgfs').build_parser() parsers = {} - def discover_level(prefix, parser, Archiver): + def discover_level(prefix, parser, Archiver, extra_choices=None): choices = {} for action in parser._actions: if action.choices is not None and 'SubParsersAction' in str(action.__class__): for cmd, parser in action.choices.items(): choices[prefix + cmd] = parser + if extra_choices is not None: + choices.update(extra_choices) if prefix and not choices: return @@ -3703,7 +3706,7 @@ def get_all_parsers(): discover_level(command + " ", parser, Archiver) parsers[command] = parser - discover_level("", parser, Archiver) + discover_level("", parser, Archiver, {'borgfs': borgfs_parser}) return parsers