Example #1
0
    def run(self):
        preferences = get_preferences(self.ui_type)
        backup_directory = preferences.backup_directory
        dry_run = preferences.dry_run
        exclusions = ExclusionPreferences(
            self.backup_type.name).get_no_comments()

        tables = DynamoDBAccess(
            profile_name=preferences.aws_profile).get_table_names()
        self.info_out(f"found {len(tables)} DynamoDB tables")
        count = 0
        for table_name in tables:

            # awsimple will update immediately if number of table rows changes, but backup from scratch every so often to be safe
            cache_life = timedelta(days=1).total_seconds()

            if table_name in exclusions:
                self.info_out(f"excluding {table_name}")
            elif dry_run:
                self.info_out(f"dry run {table_name}")
            else:
                self.info_out(f"{table_name}")
                table = DynamoDBAccess(table_name, cache_life=cache_life)
                table_contents = table.scan_table_cached()

                dir_path = Path(backup_directory, "dynamodb")
                dir_path.mkdir(parents=True, exist_ok=True)
                with Path(dir_path, f"{table_name}.pickle").open("wb") as f:
                    pickle.dump(table_contents, f)
                with Path(dir_path, f"{table_name}.json").open("w") as f:
                    f.write(dynamodb_to_json(table_contents, indent=4))
                count += 1

        self.info_out(
            f"{count} tables, {count} backed up, {len(exclusions)} excluded")
Example #2
0
def cli_main(args):

    ui_type = UITypes.cli

    balsa = Balsa(__application_name__, __author__)
    balsa.log_console_prefix = "\r"
    balsa.init_logger_from_args(args)
    log.info(f"__application_name__={__application_name__}")
    log.info(f"__author__={__author__}")
    log.info(f"__version__={__version__}")

    try:
        preferences = get_preferences(ui_type)
        preferences.backup_directory = args.path  # backup classes will read the preferences DB directly
        preferences.github_token = args.token
        preferences.aws_profile = args.profile

        # If setting the exclusions, just do it for one backup type at a time.  The values are stored for subsequent runs.
        if args.exclude is not None and len(args.exclude) > 0:
            if args.s3:
                ExclusionPreferences(BackupTypes.S3.name).set(args.exclude)
            elif args.dynamodb:
                ExclusionPreferences(BackupTypes.DynamoDB.name).set(
                    args.exclude)
            elif args.github:
                ExclusionPreferences(BackupTypes.github.name).set(args.exclude)

        did_something = False
        dynamodb_local_backup = None
        s3_local_backup = None
        github_local_backup = None
        if args.s3 or args.aws:
            s3_local_backup = S3Backup(ui_type, log.info, log.warning,
                                       log.error)
            s3_local_backup.start()
            did_something = True
        if args.dynamodb or args.aws:
            dynamodb_local_backup = DynamoDBBackup(ui_type, log.info,
                                                   log.warning, log.error)
            dynamodb_local_backup.start()
            did_something = True
        if args.github:
            github_local_backup = GithubBackup(ui_type, log.info, log.warning,
                                               log.error)
            github_local_backup.start()
            did_something = True
        if not did_something:
            print(
                "nothing to do - please specify a backup to do or -h/--help for help"
            )

        if dynamodb_local_backup is not None:
            dynamodb_local_backup.join()
        if s3_local_backup is not None:
            s3_local_backup.join()
        if github_local_backup is not None:
            github_local_backup.join()
    except Exception as e:
        log.exception(e)
Example #3
0
    def run(self):

        preferences = get_preferences(self.ui_type)
        dry_run = preferences.dry_run
        exclusions = ExclusionPreferences(BackupTypes.github.name).get_no_comments()

        backup_dir = Path(preferences.backup_directory, "github")
        gh = github3.login(token=preferences.github_token)
        repositories = list(gh.repositories())

        clone_count = 0
        pull_count = 0

        for github_repo in repositories:

            repo_owner_and_name = str(github_repo)
            repo_name = repo_owner_and_name.split("/")[-1]
            if any([e == repo_name for e in exclusions]):
                self.info_out(f"{repo_owner_and_name} excluded")
            elif dry_run:
                self.info_out(f'dry run {repo_owner_and_name}')
            else:
                repo_dir = Path(backup_dir, repo_owner_and_name).absolute()
                branches = github_repo.branches()

                # if we've cloned previously, just do a pull
                pull_success = False
                if repo_dir.exists():
                    try:
                        if pull_success := self.pull_branches(repo_owner_and_name, branches, repo_dir):
                            pull_count += 1
                    except GitCommandError as e:
                        self.warning_out(f'could not pull "{repo_dir}" - will try to start over and do a clone of "{repo_owner_and_name} {e}"')

                # new to us - clone the repo
                if not pull_success:
                    try:
                        if repo_dir.exists():
                            rmdir(repo_dir)

                        self.info_out(f'git clone "{repo_owner_and_name}"')

                        Repo.clone_from(github_repo.clone_url, repo_dir)
                        time.sleep(1.0)
                        self.pull_branches(repo_owner_and_name, branches, repo_dir)
                        clone_count += 1
                    except PermissionError as e:
                        self.warning_out(f"{repo_owner_and_name} : {e}")

        self.info_out(f"{len(repositories)} repos, {pull_count} pulls, {clone_count} clones, {len(exclusions)} excluded")
Example #4
0
    def __init__(self):
        super().__init__()

        self.setWindowIcon(QIcon(str(get_icon_path("png"))))

        # set task bar icon (Windows only)
        bup_app_id = f'{__author__}.{__application_name__}.{__version__}'  # arbitrary string
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
            bup_app_id)

        self.setWindowTitle(f"{__application_name__} ({__version__})")
        self.setWindowFlag(Qt.WindowMinimizeButtonHint, True)
        self.setWindowFlag(Qt.WindowMaximizeButtonHint, True)

        self.setLayout(QVBoxLayout())

        self.tab_widget = QTabWidget()
        self.layout().addWidget(self.tab_widget)

        self.run_backup_widget = RunBackupWidget()
        self.preferences_widget = PreferencesWidget()
        self.about_widget = BupAbout()

        self.tab_widget.addTab(self.run_backup_widget, "Backup")
        self.tab_widget.addTab(self.preferences_widget, "Preferences")
        self.tab_widget.addTab(self.about_widget, "About")

        preferences = get_preferences(UITypes.gui)
        width = preferences.width
        height = preferences.height
        if width is not None and width > 0 and height is not None and height > 0:
            self.resize(preferences.width, preferences.height)

        self.autobackup_timer = QTimer()
        self.autobackup_timer.timeout.connect(self.autobackup_tick)
        self.autobackup_timer.start(1000)  # once a second
        self.tick_count = 0
Example #5
0
def get_gui_preferences():
    return get_preferences(UITypes.gui)
Example #6
0
    def run(self):

        preferences = get_preferences(self.ui_type)
        dry_run = preferences.dry_run

        backup_directory = os.path.join(preferences.backup_directory, "s3")

        os.makedirs(backup_directory, exist_ok=True)

        s3_access = S3Access(profile_name=preferences.aws_profile)

        decoding = "utf-8"

        # we delete all whitespace below
        ls_re = re.compile(r"TotalObjects:([0-9]+)TotalSize:([0-9]+)")

        buckets = s3_access.bucket_list()
        self.info_out(f"found {len(buckets)} buckets")

        count = 0
        exclusions_no_comments = ExclusionPreferences(
            BackupTypes.S3.name).get_no_comments()
        for bucket_name in buckets:

            # do the sync
            if bucket_name in exclusions_no_comments:
                self.info_out(f"excluding {bucket_name}")
            else:
                if dry_run:
                    self.info_out(f"dry run {bucket_name}")
                else:
                    self.info_out(f"{bucket_name}")

                # try to find the AWS CLI app
                paths = [
                    (Path("venv", "Scripts", "python.exe").absolute(),
                     Path("venv", "Scripts", "aws").absolute()),  # local venv
                    (Path("python.exe").absolute(),
                     Path("Scripts", "aws").absolute())  # installed app
                ]
                aws_cli_path = None
                python_path = None
                for p, a in paths:
                    if p.exists() and a.exists():
                        aws_cli_path = a
                        python_path = p
                        break

                if aws_cli_path is None:
                    log.error(f"AWS CLI executable not found ({paths=})")
                elif python_path is None:
                    log.error(f"Python executable not found ({paths=})")
                else:
                    aws_cli_path = f'"{str(aws_cli_path)}"'  # from Path to str, with quotes for installed app
                    # AWS CLI app also needs the python executable to be in the path if it's not in the same dir, which happens when this program is installed.
                    # Make the directory of our python.exe the first in the list so it's found and not any of the others that may or may not be in the PATH.
                    env_var = deepcopy(os.environ)
                    env_var[
                        "path"] = f"{str(python_path.parent)};{env_var.get('path', '')}"

                    destination = Path(backup_directory, bucket_name)
                    os.makedirs(destination, exist_ok=True)
                    s3_bucket_path = f"s3://{bucket_name}"
                    # Don't use --delete.  We want to keep 'old' files locally.
                    sync_command_line = [
                        aws_cli_path, "s3", "sync", s3_bucket_path,
                        str(destination.absolute())
                    ]
                    if dry_run:
                        sync_command_line.append("--dryrun")
                    sync_command_line_str = " ".join(sync_command_line)
                    log.info(sync_command_line_str)

                    try:
                        sync_result = subprocess.run(sync_command_line_str,
                                                     stdout=subprocess.PIPE,
                                                     shell=True,
                                                     env=env_var)
                    except FileNotFoundError as e:
                        self.error_out(
                            f'error executing {" ".join(sync_command_line)} {e}'
                        )
                        return

                    for line in sync_result.stdout.decode(
                            decoding).splitlines():
                        log.info(line.strip())

                    # check the results
                    ls_command_line = [
                        aws_cli_path, "s3", "ls", "--summarize", "--recursive",
                        s3_bucket_path
                    ]
                    ls_command_line_str = " ".join(ls_command_line)
                    log.info(ls_command_line_str)
                    ls_result = subprocess.run(ls_command_line_str,
                                               stdout=subprocess.PIPE,
                                               shell=True,
                                               env=env_var)
                    ls_stdout = "".join([
                        c for c in ls_result.stdout.decode(decoding)
                        if c not in " \r\n"
                    ])  # remove all whitespace
                    if len(ls_stdout) == 0:
                        self.error_out(
                            f'"{ls_command_line_str}" failed ({ls_stdout=}) - check internet connection'
                        )
                    else:
                        ls_parsed = ls_re.search(ls_stdout)
                        if ls_parsed is None:
                            self.error_out(
                                f"parse error:\n{ls_command_line_str=}\n{ls_stdout=}"
                            )
                        else:
                            count += 1
                            s3_object_count = int(ls_parsed.group(1))
                            s3_total_size = int(ls_parsed.group(2))
                            local_size, local_count = get_dir_size(destination)

                            # rough check that the sync worked
                            if s3_total_size > local_size:
                                # we're missing files
                                message = "not all files backed up"
                                output_routine = self.error_out
                            elif s3_total_size != local_size:
                                # Compare size, not number of files, since aws s3 sync does not copy files of zero size.
                                message = "mismatch"
                                output_routine = self.warning_out
                            else:
                                message = "match"
                                output_routine = log.info
                            output_routine(
                                f"{bucket_name} : {message} (s3_count={s3_object_count}, local_count={local_count}; s3_total_size={s3_total_size}, local_size={local_size})"
                            )

        self.info_out(
            f"{len(buckets)} buckets, {count} backed up, {len(exclusions_no_comments)} excluded"
        )
Example #7
0
 def closeEvent(self, close_event: QCloseEvent) -> None:
     self.run_backup_widget.save_state()
     preferences = get_preferences(UITypes.gui)
     preferences.width = self.width()
     preferences.height = self.height()
Example #8
0
def test_preferences():

    # todo: this currently just tests what's ever in the preferences, which can be nothing - in the future add writing test data
    gui_preferences = get_preferences(UITypes.cli)
    pprint(gui_preferences)