Skip to content

Commit 52163a0

Browse files
committed
ADDED: python implementation especification, argument parsing and argument summary implemented
1 parent 6dcfd50 commit 52163a0

File tree

3 files changed

+178
-59
lines changed

3 files changed

+178
-59
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,6 @@ cython_debug/
160160
#.idea/
161161
backup
162162
.idea
163-
secrets
163+
secrets
164+
back1.json
165+
back1.tar.gz

options.txt

Whitespace-only changes.

src/main.py

Lines changed: 175 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
33

4+
from datetime import datetime
45
import argparse
6+
import os
57

68

7-
"""
8-
-r|--remove|--remove-folder --> Removes the backup folder after the backup
9-
-s|--scratch|--from-scratch --> Empties the backupb folder previous to start the backup
10-
-c|--compress --> Produces a compressed file with the backup folders
11-
-p|--compressed-path --> custom folder where the compressed file will be stored. Needs value., Implies -c
12-
-h|--hierarchy|--hierarchy-backup --> Clone repos keeping organization and user hierarchy in backup folder
13-
-j|--json|--generate-json --> generates a json of the of the backup up folders and repos
14-
-J|--json-path --> custom folder where the json report will be stored
15-
-b|--backup|--backup-path --> custom folder where all the clones will be done. Needs value
16-
-l|--gitlab -> Custom user that will be backed from gitlab. needs one or more value
17-
-f|--force --> Does not ask confirmation for destructive actions
18-
"""
9+
def is_file_directory_writable(file_path):
10+
if os.access(os.path.dirname(file_path), os.W_OK):
11+
return True
12+
else:
13+
return False
1914

2015

21-
def main():
16+
def is_file_writable(file_path):
17+
try:
18+
# Try opening file in write mode
19+
open(file_path, 'w')
20+
return True
21+
except (OSError, PermissionError):
22+
return False
23+
24+
25+
def parse_arguments():
2226
parser = argparse.ArgumentParser(description="Backup your git repos in your local filesystem")
2327

2428
# Optional arguments
29+
parser.add_argument("-p", "--provider", "--providers",
30+
type=str,
31+
nargs="+", # Allow one or more values
32+
help="Adds custom GitLab providers",
33+
dest="custom_providers")
2534
parser.add_argument("-r", "--remove", "--remove-folder",
2635
action="store_true",
2736
help="Removes the backup folder after the backup.",
@@ -37,17 +46,32 @@ def main():
3746
help="Produces a compressed file with the backup folders.",
3847
default=False,
3948
dest="produce_compressed")
40-
parser.add_argument("-p", "--compressed-path",
49+
parser.add_argument("-C", "--compressed-path",
4150
type=str,
4251
metavar="PATH",
4352
help="Custom path where the compressed file will be stored. Implies -c.",
44-
default="backup/backup.tar.gz",
4553
dest="compressed_path")
46-
parser.add_argument("-h", "--hierarchy", "--hierarchy-backup", "--keep-hierarchy",
54+
parser.add_argument("-y", "--hierarchy", "--hierarchy-backup", "--keep-hierarchy",
4755
action="store_true",
4856
help="Clone repos keeping organizations and user hierarchy in the backup folder.",
4957
default=False,
5058
dest="reflect_hierarchy")
59+
parser.add_argument("-F", "--flatten", "--Flatten",
60+
type=str,
61+
choices=["user", "provider", "organization"],
62+
nargs="+", # Allow one or more values
63+
help="Directory levels that will be flatten in the backup folder. Implies -y except when "
64+
"flattening all directory level",
65+
dest="flatten_directories")
66+
parser.add_argument("-l", "--handle-collision", "--handle-collision-strategy",
67+
choices=["rename", "ignore", "remove"],
68+
help="Strategy to follow to handle collisions (a repo that because of its name has to be cloned"
69+
" in the same folder path as another one):"
70+
"rename: Use a systematic name (the shortest possible) for the new repo that produces the"
71+
" collision"
72+
"ignore: Ignores repos that have filename collisions."
73+
"remove: Removes the repo already cloned and clones the new one with the same name.",
74+
dest="collision_strategy")
5175
parser.add_argument("-j", "--json", "--generate-json", "--produce-json",
5276
action="store_true",
5377
help="Generates a JSON report of the backup folders and repos.",
@@ -57,20 +81,12 @@ def main():
5781
type=str,
5882
metavar="PATH",
5983
help="Custom path where the JSON report will be stored. Implies -j.",
60-
default="backup/backup.json",
6184
dest="json_path")
6285
parser.add_argument("-b", "--backup", "--backup-path",
6386
type=str,
6487
metavar="PATH",
6588
help="Custom folder where all the clones will be done. Requires a value.",
66-
default="backup",
6789
dest="backup_path")
68-
parser.add_argument("-l", "--gitlab",
69-
type=str,
70-
metavar="GITLAB_USERS",
71-
nargs="+",
72-
help="One or more GitLab usernames to back up. Separate multiple usernames with spaces.",
73-
dest="gitlab_users")
7490
parser.add_argument("-f", "--force",
7591
action="store_true",
7692
help="Does not ask confirmation for destructive actions.",
@@ -81,65 +97,166 @@ def main():
8197
help="Enable verbose output during backup.",
8298
default=False,
8399
dest="is_verbose")
84-
parser.add_argument("github_users",
85-
type=str,
86-
metavar="GITHUB_USERS",
87-
nargs="*", # Zero or more GitHub usernames
88-
help="One or more GitHub usernames to back up. Separate multiple usernames with spaces. "
89-
"Positional argument",
90-
dest="github_users")
100+
parser.add_argument("--exclude-github",
101+
action="store_true",
102+
help="Exclude GitHub repositories from the backup.")
103+
parser.add_argument("--exclude-gitlab",
104+
action="store_true",
105+
help="Exclude GitLab repositories from the backup.")
106+
# Positional argument for usernames of the profiles to scrap
107+
parser.add_argument("usernames",
108+
nargs="+",
109+
metavar="USERNAME1 USERNAME2 USERNAME3 ...",
110+
help="List of usernames to back up from GitHub and GitLab.")
91111

92112
args = parser.parse_args()
93113

94-
# Check if at least one of the user lists is provided (GitHub or GitLab)
95-
if not args.github and not args.gitlab:
96-
parser.error("You must provide at least one GitHub or GitLab user.")
114+
# Generate same date string for all entities
115+
datetime_string = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") # ISO 8601-like format
116+
117+
# Supply default backup directory
118+
if not args.backup_path:
119+
args.backup_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backup" + datetime_string)
120+
121+
# Check existence and access of backup directory
122+
if not os.path.exists(args.backup_path):
123+
try:
124+
os.makedirs(args.backup_path)
125+
except OSError as e:
126+
parser.error(f"The folder for the backup {args.backup_path} does not exist and cannot be created.")
127+
else:
128+
if not os.access(args.backup_path, os.W_OK):
129+
parser.error(f"The folder for the backup {args.backup_path} cannot be written or there is some problem "
130+
f"with it: ")
131+
132+
# Flattening all directory levels makes hierarchy disappears, so it makes no sense to select both configurations
133+
if args.flatten_directories \
134+
and "rename" in args.flatten_directories \
135+
and "ignore" in args.flatten_directories \
136+
and "remove" in args.flatten_directories \
137+
and args.reflect_hierarchy:
138+
parser.error("You cannot use -y when flattening all directory levels with -F because it makes no sense.")
139+
140+
# When keeping the complete hierarchy we ensure that there will be no collisions
141+
if not args.flatten_directories \
142+
and args.reflect_hierarchy \
143+
and args.collision_strategy:
144+
parser.error("You do not need to supply a collision strategy with -l when keeping the hierarchy with -y and not"
145+
" flattening any directory (not supplying -F)")
97146

98-
# Compressed path implies -c
99-
if args.compressed_path:
147+
# Supply default collision strategy if needed
148+
if args.reflect_hierarchy \
149+
and args.flatten_directories \
150+
and not args.collision_strategy:
151+
args.collision_strategy = "rename"
152+
153+
if not args.collision_strategy:
154+
args.collision_strategy = "rename"
155+
156+
# If path supplied -c implicit
157+
if not args.produce_compressed and args.compressed_path:
100158
args.produce_compressed = True
101159

102-
# JSON path implies -j
103-
if args.json_path:
160+
# If -c but no path provided set to default value
161+
if args.produce_compressed and not args.compressed_path:
162+
args.compressed_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backup" + datetime_string +
163+
".tar.gz")
164+
165+
# Check access to json file
166+
if args.compressed_path and not is_file_directory_writable(args.compressed_path):
167+
parser.error("File " + args.compressed_path + " is not writable because its directory cannot be accessed.")
168+
169+
# Check write access to json file
170+
if args.compressed_path and not is_file_writable(args.compressed_path):
171+
parser.error("File " + args.compressed_path + " is not writable.")
172+
173+
# If path supplied -j implicit
174+
if not args.produce_json and args.json_path:
104175
args.produce_json = True
105176

106-
print(args)
177+
# If -j but no path provided set to default value
178+
if args.produce_json and not args.json_path:
179+
args.json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backup" + datetime_string + ".json")
180+
181+
# Check access to json file
182+
if args.json_path and not is_file_directory_writable(args.json_path):
183+
parser.error("File " + args.json_path + " is not writable because its directory cannot be accessed.")
184+
185+
# Check write access to json file
186+
if args.json_path and not is_file_writable(args.json_path):
187+
parser.error("File " + args.json_path + " is not writable.")
107188

108-
print("Backup summary:")
189+
# Check if at least one source is included
190+
if args.exclude_github and args.exclude_gitlab and not args.custom_providers:
191+
parser.error("You cannot exclude both GitHub and GitLab and not supply a custom provider. At least one provider"
192+
" must be included.")
109193

110-
if args.github_users:
111-
print("Backing up the following GitHub users:")
112-
for user in args.github_users:
113-
print(f" - {user}")
194+
return args
114195

115-
if args.gitlab:
116-
print("Backing up the following GitLab users:")
117-
for user in args.gitlab:
118-
print(f" - {user}")
196+
197+
def print_summary(args):
198+
print("BACKUP SUMMARY:")
199+
print("Backing up the following users:")
200+
for user in args.usernames:
201+
print(f" - {user}")
202+
203+
print("Using the following providers:")
204+
for provider in args.custom_providers:
205+
print(f" - {provider}")
206+
print(" - GitHub (github.com)")
207+
print(" - GitLab (gitlab.com)")
119208

120209
if args.backup_path:
121-
print("Backup folder: " + args.backup_path)
210+
print("* Backup folder: " + args.backup_path)
122211

123212
if args.produce_compressed:
124-
print("Compressed backup path: " + args.compressed_path)
213+
print("* Compressed backup path: " + args.compressed_path)
125214

126215
if args.produce_json:
127-
print("JSON summary path: " + args.json_path)
216+
print("* JSON summary path: " + args.json_path)
128217

218+
print("* Empty backup folder before performing backup (start from scratch): ", end="")
129219
if args.empty_backup_folder_first:
130-
print("Empty backup folder before performing backup (start from scratch): " + args.empty_backup_folder_first)
220+
print("Yes")
221+
else:
222+
print("No")
131223

224+
print("* Remove backup folder after performing backup: ", end="")
132225
if args.remove_backup_folder_afterwards:
133-
print("Remove backup folder after performing backup: " + args.remove_backup_folder_afterwards)
226+
print(" Yes")
227+
else:
228+
print(" No")
134229

230+
print("* Produces a hierarchical structure for the backup: ", end="")
135231
if args.reflect_hierarchy:
136-
print("Produces a hierarchical structure for the backup: " + args.remove_backup_folder_afterwards)
232+
print(" Yes")
233+
else:
234+
print(" No")
235+
236+
if args.flatten_directories:
237+
print("* Directory levels to flatten: " +
238+
", ".join(args.flatten_directories))
239+
240+
if args.collision_strategy:
241+
print("* Strategy to avoid collision in the folder names of the repos: " + args.collision_strategy)
137242

138-
if args.verbose:
139-
print("Verbose: " + args.verbose)
243+
print("* Verbose output: ", end="")
244+
if args.is_verbose:
245+
print(" Yes")
246+
else:
247+
print(" No")
140248

141-
if args.force:
142-
print("Force: " + args.force)
249+
print("* Do not ask for confirmation on destructive actions: ", end="")
250+
if args.is_forced:
251+
print(" Yes")
252+
else:
253+
print(" No")
254+
255+
256+
def main():
257+
args = parse_arguments()
258+
if args.is_verbose:
259+
print_summary(args)
143260

144261

145262
if __name__ == "__main__":

0 commit comments

Comments
 (0)