diff --git a/bin/github-backup b/bin/github-backup index dcac622..c922888 100755 --- a/bin/github-backup +++ b/bin/github-backup @@ -1,76 +1,18 @@ #!/usr/bin/env python +""" +Backwards-compatible wrapper script. + +The recommended way to run github-backup is via the installed command +(pip install github-backup) or python -m github_backup. + +This script is kept for backwards compatibility with existing installations +that may reference this path directly. +""" -import logging -import os import sys -from github_backup.github_backup import ( - backup_account, - backup_repositories, - check_git_lfs_install, - filter_repositories, - get_auth, - get_authenticated_user, - logger, - mkdir_p, - parse_args, - retrieve_repositories, -) - -# INFO and DEBUG go to stdout, WARNING and above go to stderr -log_format = logging.Formatter( - fmt="%(asctime)s.%(msecs)03d: %(message)s", - datefmt="%Y-%m-%dT%H:%M:%S", -) - -stdout_handler = logging.StreamHandler(sys.stdout) -stdout_handler.setLevel(logging.DEBUG) -stdout_handler.addFilter(lambda r: r.levelno < logging.WARNING) -stdout_handler.setFormatter(log_format) - -stderr_handler = logging.StreamHandler(sys.stderr) -stderr_handler.setLevel(logging.WARNING) -stderr_handler.setFormatter(log_format) - -logging.basicConfig(level=logging.INFO, handlers=[stdout_handler, stderr_handler]) - - -def main(): - args = parse_args() - - if args.private and not get_auth(args): - logger.warning( - "The --private flag has no effect without authentication. " - "Use -t/--token, -f/--token-fine, or -u/--username to authenticate." - ) - - if args.quiet: - logger.setLevel(logging.WARNING) - - output_directory = os.path.realpath(args.output_directory) - if not os.path.isdir(output_directory): - logger.info("Create output directory {0}".format(output_directory)) - mkdir_p(output_directory) - - if args.lfs_clone: - check_git_lfs_install() - - if args.log_level: - log_level = logging.getLevelName(args.log_level.upper()) - if isinstance(log_level, int): - logger.root.setLevel(log_level) - - if not args.as_app: - logger.info("Backing up user {0} to {1}".format(args.user, output_directory)) - authenticated_user = get_authenticated_user(args) - else: - authenticated_user = {"login": None} - - repositories = retrieve_repositories(args, authenticated_user) - repositories = filter_repositories(args, repositories) - backup_repositories(args, output_directory, repositories) - backup_account(args, output_directory) - +from github_backup.cli import main +from github_backup.github_backup import logger if __name__ == "__main__": try: diff --git a/github_backup/__main__.py b/github_backup/__main__.py new file mode 100644 index 0000000..0b4a7c3 --- /dev/null +++ b/github_backup/__main__.py @@ -0,0 +1,13 @@ +"""Allow running as: python -m github_backup""" + +import sys + +from github_backup.cli import main +from github_backup.github_backup import logger + +if __name__ == "__main__": + try: + main() + except Exception as e: + logger.error(str(e)) + sys.exit(1) diff --git a/github_backup/cli.py b/github_backup/cli.py new file mode 100644 index 0000000..98f8d4a --- /dev/null +++ b/github_backup/cli.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +"""Command-line interface for github-backup.""" + +import logging +import os +import sys + +from github_backup.github_backup import ( + backup_account, + backup_repositories, + check_git_lfs_install, + filter_repositories, + get_auth, + get_authenticated_user, + logger, + mkdir_p, + parse_args, + retrieve_repositories, +) + +# INFO and DEBUG go to stdout, WARNING and above go to stderr +log_format = logging.Formatter( + fmt="%(asctime)s.%(msecs)03d: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) + +stdout_handler = logging.StreamHandler(sys.stdout) +stdout_handler.setLevel(logging.DEBUG) +stdout_handler.addFilter(lambda r: r.levelno < logging.WARNING) +stdout_handler.setFormatter(log_format) + +stderr_handler = logging.StreamHandler(sys.stderr) +stderr_handler.setLevel(logging.WARNING) +stderr_handler.setFormatter(log_format) + +logging.basicConfig(level=logging.INFO, handlers=[stdout_handler, stderr_handler]) + + +def main(): + """Main entry point for github-backup CLI.""" + args = parse_args() + + if args.private and not get_auth(args): + logger.warning( + "The --private flag has no effect without authentication. " + "Use -t/--token, -f/--token-fine, or -u/--username to authenticate." + ) + + if args.quiet: + logger.setLevel(logging.WARNING) + + output_directory = os.path.realpath(args.output_directory) + if not os.path.isdir(output_directory): + logger.info("Create output directory {0}".format(output_directory)) + mkdir_p(output_directory) + + if args.lfs_clone: + check_git_lfs_install() + + if args.log_level: + log_level = logging.getLevelName(args.log_level.upper()) + if isinstance(log_level, int): + logger.root.setLevel(log_level) + + if not args.as_app: + logger.info("Backing up user {0} to {1}".format(args.user, output_directory)) + authenticated_user = get_authenticated_user(args) + else: + authenticated_user = {"login": None} + + repositories = retrieve_repositories(args, authenticated_user) + repositories = filter_repositories(args, repositories) + backup_repositories(args, output_directory, repositories) + backup_account(args, output_directory) + + +if __name__ == "__main__": + try: + main() + except Exception as e: + logger.error(str(e)) + sys.exit(1) diff --git a/github_backup/github_backup.py b/github_backup/github_backup.py index 0282809..14dd167 100644 --- a/github_backup/github_backup.py +++ b/github_backup/github_backup.py @@ -1038,7 +1038,7 @@ def download_attachment_file(url, path, auth, as_app=False, fine=False): bytes_downloaded += len(chunk) # Atomic rename to final location - os.rename(temp_path, path) + os.replace(temp_path, path) metadata["size_bytes"] = bytes_downloaded metadata["success"] = True @@ -1459,7 +1459,7 @@ def download_attachments( # Rename to add extension (already atomic from download) try: - os.rename(filepath, final_filepath) + os.replace(filepath, final_filepath) metadata["saved_as"] = os.path.basename(final_filepath) except Exception as e: logger.warning( @@ -1490,7 +1490,7 @@ def download_attachments( manifest_path = os.path.join(attachments_dir, "manifest.json") with open(manifest_path + ".temp", "w") as f: json.dump(manifest, f, indent=2) - os.rename(manifest_path + ".temp", manifest_path) # Atomic write + os.replace(manifest_path + ".temp", manifest_path) # Atomic write logger.debug( "Wrote manifest for {0} #{1}: {2} attachments".format( item_type_display, number, len(attachment_metadata_list) @@ -1811,7 +1811,7 @@ def backup_issues(args, repo_cwd, repository, repos_template): with codecs.open(issue_file + ".temp", "w", encoding="utf-8") as f: json_dump(issue, f) - os.rename(issue_file + ".temp", issue_file) # Unlike json_dump, this is atomic + os.replace(issue_file + ".temp", issue_file) # Atomic write def backup_pulls(args, repo_cwd, repository, repos_template): @@ -1886,7 +1886,7 @@ def backup_pulls(args, repo_cwd, repository, repos_template): with codecs.open(pull_file + ".temp", "w", encoding="utf-8") as f: json_dump(pull, f) - os.rename(pull_file + ".temp", pull_file) # Unlike json_dump, this is atomic + os.replace(pull_file + ".temp", pull_file) # Atomic write def backup_milestones(args, repo_cwd, repository, repos_template): @@ -2203,5 +2203,5 @@ def json_dump_if_changed(data, output_file_path): temp_file = output_file_path + ".temp" with codecs.open(temp_file, "w", encoding="utf-8") as f: f.write(new_content) - os.rename(temp_file, output_file_path) # Atomic on POSIX systems + os.replace(temp_file, output_file_path) # Atomic write return True diff --git a/setup.py b/setup.py index 374e6ec..7835a32 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,11 @@ setup( author="Jose Diaz-Gonzalez", author_email="github-backup@josediazgonzalez.com", packages=["github_backup"], - scripts=["bin/github-backup"], + entry_points={ + "console_scripts": [ + "github-backup=github_backup.cli:main", + ], + }, url="http://github.com/josegonzalez/python-github-backup", license="MIT", classifiers=[