|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +import os |
| 4 | +from datetime import datetime |
| 5 | + |
| 6 | +from FileService import is_file_directory_writable, is_file_writable |
| 7 | +from defines import FlattenLevel, CollisionStrategy |
| 8 | +import argparse |
| 9 | + |
| 10 | + |
| 11 | +def build_argument_parser(): |
| 12 | + parser = argparse.ArgumentParser(description="Backup your git repos in your local filesystem") |
| 13 | + |
| 14 | + # Optional arguments |
| 15 | + parser.add_argument("-p", "--provider", "--providers", |
| 16 | + help="Adds custom GitLab providers", |
| 17 | + type=str, |
| 18 | + nargs="+", |
| 19 | + dest="custom_providers") |
| 20 | + parser.add_argument("-f", "--force", |
| 21 | + help="Does not ask confirmation for destructive actions.", |
| 22 | + # type=bool, |
| 23 | + # nargs=0, |
| 24 | + dest="is_forced", |
| 25 | + action="store_true", |
| 26 | + default=False) |
| 27 | + parser.add_argument("-r", "--remove", "--remove-folder", |
| 28 | + help="Removes the backup content after the backup.", |
| 29 | + # type=bool, |
| 30 | + # nargs=0, |
| 31 | + dest="remove_backup_folder_afterwards", |
| 32 | + action="store_true", |
| 33 | + default=False) |
| 34 | + parser.add_argument("-s", "--scratch", "--from-scratch", |
| 35 | + help="Empties the backup folder before starting the backup.", |
| 36 | + # type=bool, |
| 37 | + # nargs=0, |
| 38 | + dest="empty_backup_folder_first", |
| 39 | + action="store_true", |
| 40 | + default=False) |
| 41 | + parser.add_argument("-c", "--compress", |
| 42 | + help="Produces a compressed file with the backup folders.", |
| 43 | + # type=bool, |
| 44 | + # nargs=0, |
| 45 | + dest="produce_compressed", |
| 46 | + action="store_true", |
| 47 | + default=False) |
| 48 | + parser.add_argument("-C", "--compressed-path", |
| 49 | + help="Custom path where the compressed file will be stored. Implies -c.", |
| 50 | + type=str, |
| 51 | + nargs='?', |
| 52 | + dest="compressed_path", |
| 53 | + metavar="FILE_PATH") |
| 54 | + parser.add_argument("-y", "--hierarchy", "--hierarchy-backup", "--keep-hierarchy", |
| 55 | + help="Clone repos keeping organizations and user hierarchy in the backup folder.", |
| 56 | + # type=bool, |
| 57 | + # nargs=0, |
| 58 | + dest="reflect_hierarchy", |
| 59 | + action="store_true", |
| 60 | + default=False) |
| 61 | + parser.add_argument("-F", "--flatten", "--Flatten", |
| 62 | + help="Hierarchy levels of the backup that will not be present in the hierarchical structure" |
| 63 | + " of the folder " |
| 64 | + "structure of the backup. Implies -y except when " |
| 65 | + "flattening all directory level:" + |
| 66 | + FlattenLevel.ROOT.name + ": Flattens root folder in the backup." + |
| 67 | + FlattenLevel.USER.name + ": Flattens user folders in the backup" + |
| 68 | + FlattenLevel.PROVIDER.name + ": Flattens provider folder in the backup." + |
| 69 | + FlattenLevel.ORGANIZATION.name + ": Flattens organization folder in the backup.", |
| 70 | + type=str, |
| 71 | + nargs="+", |
| 72 | + dest="flatten_directories", |
| 73 | + choices=[name for name in FlattenLevel]) |
| 74 | + parser.add_argument("-l", "--handle-collision", "--handle-collision-strategy", |
| 75 | + help="Strategy to follow to handle collisions (a repo that because of its name has to be cloned" |
| 76 | + " in the same folder path as another one):" + |
| 77 | + CollisionStrategy.RENAME.name + ": Use a systematic name (the shortest possible) for the " |
| 78 | + "new repo that produces the collision" + |
| 79 | + CollisionStrategy.SYSTEMATIC.name + ": Use a systematic name for all repos" + |
| 80 | + CollisionStrategy.IGNORE.name + ": Ignores repos that have filename collisions." + |
| 81 | + CollisionStrategy.REMOVE.name + ": Removes the repo already cloned and clones the new one " |
| 82 | + "with the same name.", |
| 83 | + type=str, |
| 84 | + nargs='?', |
| 85 | + dest="collision_strategy", |
| 86 | + choices=[name for name in CollisionStrategy]) |
| 87 | + parser.add_argument("-j", "--json", "--generate-json", "--produce-json", |
| 88 | + help="Generates a JSON report of the backup folders and repos.", |
| 89 | + # type=bool, |
| 90 | + # nargs=0, |
| 91 | + dest="produce_json", |
| 92 | + action="store_true", |
| 93 | + default=False) |
| 94 | + parser.add_argument("-J", "--json-path", |
| 95 | + help="Custom path where the JSON report will be stored. Implies -j. Requires a value.", |
| 96 | + type=str, |
| 97 | + nargs='?', |
| 98 | + dest="json_path", |
| 99 | + metavar="FILE_PATH") |
| 100 | + parser.add_argument("-b", "--backup-directory", "--backup-folder", |
| 101 | + help="Custom folder where the backups will be stored.", |
| 102 | + type=str, |
| 103 | + nargs='?', |
| 104 | + dest="backup_folder", |
| 105 | + metavar="DIRECTORY_PATH") |
| 106 | + parser.add_argument("-n", "--backup-name", |
| 107 | + help="Custom name for the root backup folder.", |
| 108 | + type=str, |
| 109 | + nargs='?', |
| 110 | + dest="backup_name", |
| 111 | + metavar="DIRECTORY_NAME") |
| 112 | + parser.add_argument("-v", "--verbose", |
| 113 | + help="Enable verbose output during backup.", |
| 114 | + # type=bool, |
| 115 | + # nargs=0, |
| 116 | + dest="is_verbose", |
| 117 | + action="store_true", |
| 118 | + default=False) |
| 119 | + parser.add_argument("--exclude-github", |
| 120 | + help="Exclude GitHub repositories from the backup.", |
| 121 | + # type=bool, |
| 122 | + # nargs=0, |
| 123 | + dest="exclude_github", |
| 124 | + action="store_true"), |
| 125 | + parser.add_argument("--exclude-gitlab", |
| 126 | + help="Exclude GitLab repositories from the backup.", |
| 127 | + # type=bool, |
| 128 | + # nargs=0, |
| 129 | + dest="exclude_gitlab", |
| 130 | + action="store_true") |
| 131 | + parser.add_argument("--exclude-enterprise", |
| 132 | + help="Exclude \"official\" providers (github.com and gitlab.com).", |
| 133 | + # type=bool, |
| 134 | + # nargs=0, |
| 135 | + dest="exclude_enterprise", |
| 136 | + action="store_true") |
| 137 | + # Positional argument for usernames of the profiles to scrap |
| 138 | + parser.add_argument("usernames", |
| 139 | + help="List of usernames to back up.", |
| 140 | + # type=List[str], |
| 141 | + nargs="+", |
| 142 | + # dest="usernames", |
| 143 | + metavar="USERNAME1 USERNAME2 USERNAME3 ...") |
| 144 | + return parser |
| 145 | + |
| 146 | + |
| 147 | +def parse_arguments(parser: argparse.ArgumentParser): |
| 148 | + args = parser.parse_args() |
| 149 | + |
| 150 | + if not args.backup_name: |
| 151 | + args.backup_name = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") # ISO 8601-like format |
| 152 | + |
| 153 | + # Supply default backup directory |
| 154 | + if not args.backup_folder: |
| 155 | + args.backup_folder = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "backup") |
| 156 | + |
| 157 | + # Check existence and access of backup directory |
| 158 | + if not os.path.exists(args.backup_folder): |
| 159 | + try: |
| 160 | + os.makedirs(args.backup_folder) |
| 161 | + except OSError as e: |
| 162 | + parser.error(f"The folder for the backup {args.backup_folder} does not exist and cannot be created. Error: " |
| 163 | + + e.__str__()) |
| 164 | + else: |
| 165 | + if not os.access(args.backup_folder, os.W_OK): |
| 166 | + parser.error(f"The folder for the backup {args.backup_folder} cannot be written or there is some problem " |
| 167 | + f"with it: ") |
| 168 | + |
| 169 | + # Flattening all directory levels makes hierarchy disappears, so it makes no sense to select both configurations |
| 170 | + if args.flatten_directories \ |
| 171 | + and "rename" in args.flatten_directories \ |
| 172 | + and "ignore" in args.flatten_directories \ |
| 173 | + and "remove" in args.flatten_directories \ |
| 174 | + and args.reflect_hierarchy: |
| 175 | + parser.error("You cannot use -y when flattening all directory levels with -F because it makes no sense.") |
| 176 | + |
| 177 | + # When keeping the complete hierarchy we ensure that there will be no collisions |
| 178 | + if not args.flatten_directories \ |
| 179 | + and args.reflect_hierarchy \ |
| 180 | + and args.collision_strategy: |
| 181 | + parser.error("You do not need to supply a collision strategy with -l when keeping the hierarchy with -y and not" |
| 182 | + " flattening any directory (not supplying -F)") |
| 183 | + |
| 184 | + # Supply default collision strategy if needed |
| 185 | + if args.reflect_hierarchy \ |
| 186 | + and args.flatten_directories \ |
| 187 | + and not args.collision_strategy: |
| 188 | + args.collision_strategy = "rename" |
| 189 | + |
| 190 | + if not args.collision_strategy: |
| 191 | + args.collision_strategy = "rename" |
| 192 | + |
| 193 | + # If path supplied -c implicit |
| 194 | + if not args.produce_compressed and args.compressed_path: |
| 195 | + args.produce_compressed = True |
| 196 | + |
| 197 | + # If -c but no path provided set to default value |
| 198 | + if args.produce_compressed and not args.compressed_path: |
| 199 | + args.compressed_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backup" + args.backup_name + |
| 200 | + ".tar.gz") |
| 201 | + |
| 202 | + # Check access to json file |
| 203 | + if args.compressed_path and not is_file_directory_writable(args.compressed_path): |
| 204 | + parser.error("File " + args.compressed_path + " is not writable because its directory cannot be accessed.") |
| 205 | + |
| 206 | + # Check write access to json file |
| 207 | + if args.compressed_path and not is_file_writable(args.compressed_path): |
| 208 | + parser.error("File " + args.compressed_path + " is not writable.") |
| 209 | + |
| 210 | + # If path supplied -j implicit |
| 211 | + if not args.produce_json and args.json_path: |
| 212 | + args.produce_json = True |
| 213 | + |
| 214 | + # If -j but no path provided set to default value |
| 215 | + if args.produce_json and not args.json_path: |
| 216 | + args.json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backup" + args.backup_name + ".json") |
| 217 | + |
| 218 | + # Check access to json file |
| 219 | + if args.json_path and not is_file_directory_writable(args.json_path): |
| 220 | + parser.error("File " + args.json_path + " is not writable because its directory cannot be accessed.") |
| 221 | + |
| 222 | + # Check write access to json file |
| 223 | + if args.json_path and not is_file_writable(args.json_path): |
| 224 | + parser.error("File " + args.json_path + " is not writable.") |
| 225 | + |
| 226 | + # Check if at least one source is included |
| 227 | + if args.exclude_github and args.exclude_gitlab: |
| 228 | + parser.error("You cannot exclude both GitHub and GitLab. At least one provider must be included.") |
| 229 | + |
| 230 | + if args.custom_providers: |
| 231 | + custom_providers = [] |
| 232 | + for custom_provider in args.custom_providers: |
| 233 | + if args.exclude_github: |
| 234 | + custom_providers.append({'url': custom_provider, 'provider': "GitLab"}) |
| 235 | + if args.exclude_gitlab: |
| 236 | + custom_providers.append({'url': custom_provider, 'provider': "GitHub"}) |
| 237 | + return args |
0 commit comments