Merge pull request #466 from Iamrodos/fix/112-windows-support

fix: add Windows support with entry_points and os.replace
This commit is contained in:
Jose Diaz-Gonzalez
2025-12-11 11:48:59 -05:00
committed by GitHub
5 changed files with 117 additions and 76 deletions

View File

@@ -1,76 +1,18 @@
#!/usr/bin/env python #!/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 import sys
from github_backup.github_backup import ( from github_backup.cli import main
backup_account, from github_backup.github_backup import logger
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)
if __name__ == "__main__": if __name__ == "__main__":
try: try:

13
github_backup/__main__.py Normal file
View File

@@ -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)

82
github_backup/cli.py Normal file
View File

@@ -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)

View File

@@ -1038,7 +1038,7 @@ def download_attachment_file(url, path, auth, as_app=False, fine=False):
bytes_downloaded += len(chunk) bytes_downloaded += len(chunk)
# Atomic rename to final location # Atomic rename to final location
os.rename(temp_path, path) os.replace(temp_path, path)
metadata["size_bytes"] = bytes_downloaded metadata["size_bytes"] = bytes_downloaded
metadata["success"] = True metadata["success"] = True
@@ -1459,7 +1459,7 @@ def download_attachments(
# Rename to add extension (already atomic from download) # Rename to add extension (already atomic from download)
try: try:
os.rename(filepath, final_filepath) os.replace(filepath, final_filepath)
metadata["saved_as"] = os.path.basename(final_filepath) metadata["saved_as"] = os.path.basename(final_filepath)
except Exception as e: except Exception as e:
logger.warning( logger.warning(
@@ -1490,7 +1490,7 @@ def download_attachments(
manifest_path = os.path.join(attachments_dir, "manifest.json") manifest_path = os.path.join(attachments_dir, "manifest.json")
with open(manifest_path + ".temp", "w") as f: with open(manifest_path + ".temp", "w") as f:
json.dump(manifest, f, indent=2) 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( logger.debug(
"Wrote manifest for {0} #{1}: {2} attachments".format( "Wrote manifest for {0} #{1}: {2} attachments".format(
item_type_display, number, len(attachment_metadata_list) 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: with codecs.open(issue_file + ".temp", "w", encoding="utf-8") as f:
json_dump(issue, 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): 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: with codecs.open(pull_file + ".temp", "w", encoding="utf-8") as f:
json_dump(pull, 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): 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" temp_file = output_file_path + ".temp"
with codecs.open(temp_file, "w", encoding="utf-8") as f: with codecs.open(temp_file, "w", encoding="utf-8") as f:
f.write(new_content) 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 return True

View File

@@ -33,7 +33,11 @@ setup(
author="Jose Diaz-Gonzalez", author="Jose Diaz-Gonzalez",
author_email="github-backup@josediazgonzalez.com", author_email="github-backup@josediazgonzalez.com",
packages=["github_backup"], 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", url="http://github.com/josegonzalez/python-github-backup",
license="MIT", license="MIT",
classifiers=[ classifiers=[