mirror of
https://github.com/josegonzalez/python-github-backup.git
synced 2025-12-11 18:41:11 +01:00
fix: add Windows support with entry_points and os.replace
- Replace os.rename() with os.replace() for atomic file operations on Windows (os.rename fails if destination exists on Windows) - Add entry_points console_scripts for proper .exe generation on Windows - Create github_backup/cli.py with main() entry point - Add github_backup/__main__.py for python -m github_backup support - Keep bin/github-backup as thin wrapper for backwards compatibility Closes #112
This commit is contained in:
@@ -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:
|
||||
|
||||
13
github_backup/__main__.py
Normal file
13
github_backup/__main__.py
Normal 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
82
github_backup/cli.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
6
setup.py
6
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=[
|
||||
|
||||
Reference in New Issue
Block a user