-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackup_code_laundry_incremental.sh
More file actions
executable file
·464 lines (402 loc) · 15 KB
/
backup_code_laundry_incremental.sh
File metadata and controls
executable file
·464 lines (402 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
#!/bin/bash
# Incremental GitHub to Google Drive Backup Script for Code Laundry Repositories
# This script backs up only recently modified repositories from the code-laundry GitHub organization
# to Google Drive in the folder: CodeLaundry/Cloud Backup/GitHub
#
# Features:
# - Only backs up repositories with commits in the last N days (default: 30)
# - Configurable time window via -d flag
set -e
# Configuration
GITHUB_ORG="code-laundry"
GDRIVE_FOLDER="CodeLaundry/Cloud Backups/GitHub"
BACKUP_DIR="${TMPDIR:-/tmp}/code-laundry-backup-$$"
RCLONE_REMOTE="gdrive"
DEFAULT_DAYS=30
# Logging configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}" .sh)"
LOCAL_ZIPS_DIR="${SCRIPT_DIR}/zips"
LOG_DIR="${SCRIPT_DIR}/logs"
LOG_TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
LOG_FILE="${LOG_DIR}/${SCRIPT_NAME}_${LOG_TIMESTAMP}.log"
# Create logs and local zips directories if they don't exist
mkdir -p "${LOG_DIR}"
mkdir -p "${LOCAL_ZIPS_DIR}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Log functions write to both console (with colors) and log file (without colors)
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1" >> "${LOG_FILE}"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $1" >> "${LOG_FILE}"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1" >> "${LOG_FILE}"
}
log_skip() {
echo -e "${BLUE}[SKIP]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [SKIP] $1" >> "${LOG_FILE}"
}
# Check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Install Homebrew if not present (macOS)
install_homebrew() {
if ! command_exists brew; then
log_info "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
}
# Install GitHub CLI
install_gh() {
if ! command_exists gh; then
log_info "Installing GitHub CLI..."
if [[ "$OSTYPE" == "darwin"* ]]; then
install_homebrew
brew install gh
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
else
log_error "Unsupported OS. Please install GitHub CLI manually: https://cli.github.com/"
exit 1
fi
fi
}
# Install rclone
install_rclone() {
if ! command_exists rclone; then
log_info "Installing rclone..."
if [[ "$OSTYPE" == "darwin"* ]]; then
install_homebrew
brew install rclone
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
curl https://rclone.org/install.sh | sudo bash
else
log_error "Unsupported OS. Please install rclone manually: https://rclone.org/install/"
exit 1
fi
fi
}
# Check GitHub authentication
check_gh_auth() {
if ! gh auth status >/dev/null 2>&1; then
log_info "GitHub CLI not authenticated. Starting authentication..."
gh auth login
fi
log_info "GitHub CLI authenticated."
}
# Check rclone Google Drive configuration
check_rclone_config() {
if ! rclone listremotes | grep -q "^${RCLONE_REMOTE}:"; then
log_warn "rclone remote '${RCLONE_REMOTE}' not configured."
log_info "Starting rclone configuration for Google Drive..."
echo ""
echo "Please follow these steps:"
echo "1. Choose 'n' for new remote"
echo "2. Name it: gdrive"
echo "3. Choose 'drive' (Google Drive)"
echo "4. Leave client_id and client_secret blank (press Enter)"
echo "5. Choose '1' for full access"
echo "6. Leave root_folder_id blank"
echo "7. Leave service_account_file blank"
echo "8. Choose 'n' for advanced config"
echo "9. Choose 'y' for auto config (will open browser)"
echo "10. Choose 'n' for shared drive"
echo "11. Confirm with 'y'"
echo "12. Quit with 'q'"
echo ""
rclone config
fi
# Verify the remote works
if ! rclone lsd "${RCLONE_REMOTE}:" >/dev/null 2>&1; then
log_error "Cannot access Google Drive. Please reconfigure rclone."
rclone config
fi
log_info "rclone Google Drive configured."
}
# Create backup directory
setup_backup_dir() {
log_info "Creating backup directory: ${BACKUP_DIR}"
mkdir -p "${BACKUP_DIR}"
cd "${BACKUP_DIR}"
}
# Get all repositories from the organization
get_repositories() {
log_info "Fetching repository list from ${GITHUB_ORG}..."
gh repo list "${GITHUB_ORG}" --limit 1000 --json name,isArchived --jq '.[] | select(.isArchived == false) | .name'
}
# Check if repository has commits within the specified number of days (across all branches)
has_recent_commits() {
local repo_name="$1"
local days="$2"
local since_date
local commit_count
local branches
local branch
# Calculate the date N days ago in ISO 8601 format
if [[ "$OSTYPE" == "darwin"* ]]; then
since_date=$(date -v-${days}d -u +"%Y-%m-%dT%H:%M:%SZ")
else
since_date=$(date -u -d "${days} days ago" +"%Y-%m-%dT%H:%M:%SZ")
fi
# Get all branches for this repository
branches=$(gh api "repos/${GITHUB_ORG}/${repo_name}/branches" --jq '.[].name' 2>/dev/null || echo "")
if [[ -z "${branches}" ]]; then
# Fallback to default branch if we can't get branches
commit_count=$(gh api "repos/${GITHUB_ORG}/${repo_name}/commits?since=${since_date}&per_page=1" --jq 'length' 2>/dev/null || echo "0")
if [[ "${commit_count}" -gt 0 ]]; then
return 0
fi
return 1
fi
# Check each branch for recent commits
while IFS= read -r branch; do
if [[ -n "${branch}" ]]; then
commit_count=$(gh api "repos/${GITHUB_ORG}/${repo_name}/commits?sha=${branch}&since=${since_date}&per_page=1" --jq 'length' 2>/dev/null || echo "0")
if [[ "${commit_count}" -gt 0 ]]; then
return 0 # Has recent commits on this branch
fi
fi
done <<< "${branches}"
return 1 # No recent commits on any branch
}
# Get the date of the last commit for display purposes (across all branches)
get_last_commit_date() {
local repo_name="$1"
local branches
local branch
local latest_date=""
local commit_date
# Get all branches for this repository
branches=$(gh api "repos/${GITHUB_ORG}/${repo_name}/branches" --jq '.[].name' 2>/dev/null || echo "")
if [[ -z "${branches}" ]]; then
# Fallback to default branch
gh api "repos/${GITHUB_ORG}/${repo_name}/commits?per_page=1" --jq '.[0].commit.committer.date // empty' 2>/dev/null || echo "unknown"
return
fi
# Check each branch and find the most recent commit
while IFS= read -r branch; do
if [[ -n "${branch}" ]]; then
commit_date=$(gh api "repos/${GITHUB_ORG}/${repo_name}/commits?sha=${branch}&per_page=1" --jq '.[0].commit.committer.date // empty' 2>/dev/null || echo "")
if [[ -n "${commit_date}" ]]; then
if [[ -z "${latest_date}" ]] || [[ "${commit_date}" > "${latest_date}" ]]; then
latest_date="${commit_date}"
fi
fi
fi
done <<< "${branches}"
if [[ -n "${latest_date}" ]]; then
echo "${latest_date}"
else
echo "unknown"
fi
}
# Clone and zip a repository (all branches)
backup_repository() {
local repo_name="$1"
local zip_file="${repo_name}.zip"
log_info "Processing repository: ${repo_name}"
# Clone the repository with all branches
log_info " Cloning ${repo_name} (all branches)..."
if ! gh repo clone "${GITHUB_ORG}/${repo_name}" "${repo_name}" -- --quiet 2>/dev/null; then
log_error " Failed to clone ${repo_name}"
return 1
fi
# Fetch all remote branches and check them out locally
log_info " Fetching all branches..."
(
cd "${repo_name}"
# Fetch all remote branches
git fetch --all --quiet 2>/dev/null || true
# Check out each remote branch as a local branch
for remote_branch in $(git branch -r | grep -v '\->' | grep -v 'HEAD' | sed 's/origin\///'); do
if [[ "${remote_branch}" != "$(git rev-parse --abbrev-ref HEAD)" ]]; then
git branch --track "${remote_branch}" "origin/${remote_branch}" 2>/dev/null || true
fi
done
)
local branch_count
branch_count=$(cd "${repo_name}" && git branch | wc -l | tr -d ' ')
log_info " Found ${branch_count} branch(es)"
# Create zip file (including .git directory to preserve all branches)
log_info " Creating zip archive..."
zip -rq "${zip_file}" "${repo_name}"
# Remove cloned directory to save space
rm -rf "${repo_name}"
# Save a copy of the zip to the local zips folder
log_info " Saving zip to local backups folder..."
cp "${zip_file}" "${LOCAL_ZIPS_DIR}/${zip_file}"
# Upload to Google Drive
log_info " Uploading to Google Drive..."
# Check if file already exists and delete it (for replacement)
if rclone lsf "${RCLONE_REMOTE}:${GDRIVE_FOLDER}/${zip_file}" >/dev/null 2>&1; then
log_info " Replacing existing file..."
rclone deletefile "${RCLONE_REMOTE}:${GDRIVE_FOLDER}/${zip_file}" 2>/dev/null || true
fi
# Upload the file
if rclone copy "${zip_file}" "${RCLONE_REMOTE}:${GDRIVE_FOLDER}/" --progress; then
log_info " Successfully uploaded ${zip_file}"
else
log_error " Failed to upload ${zip_file}"
return 1
fi
# Remove local zip file
rm -f "${zip_file}"
return 0
}
# Cleanup function
cleanup() {
log_info "Cleaning up..."
if [[ -d "${BACKUP_DIR}" ]]; then
rm -rf "${BACKUP_DIR}"
fi
}
# Default values
days_threshold=${DEFAULT_DAYS}
force_backup=false
# Parse command line arguments
while getopts "d:fh" opt; do
case ${opt} in
d)
days_threshold="${OPTARG}"
if ! [[ "${days_threshold}" =~ ^[0-9]+$ ]]; then
log_error "Invalid number of days: ${days_threshold}"
exit 1
fi
;;
f)
force_backup=true
;;
h)
echo "Usage: $0 [-d days] [-f] [-h]"
echo ""
echo "Options:"
echo " -d days Number of days to look back for commits (default: ${DEFAULT_DAYS})"
echo " -f Force backup all repositories (ignore modification check)"
echo " -h Show this help message"
echo ""
echo "Examples:"
echo " $0 # Backup repos with commits in last 30 days"
echo " $0 -d 7 # Backup repos with commits in last 7 days"
echo " $0 -d 90 # Backup repos with commits in last 90 days"
echo " $0 -f # Force backup all repositories"
exit 0
;;
*)
echo "Usage: $0 [-d days] [-f] [-h]"
exit 1
;;
esac
done
# Main execution
main() {
echo ""
echo "=========================================="
echo " Code Laundry Incremental Backup Script"
echo "=========================================="
echo ""
log_info "Log file: ${LOG_FILE}"
echo ""
if [[ "${force_backup}" == "true" ]]; then
log_warn "Force mode enabled - will backup all repositories"
else
log_info "Backing up repositories with commits in the last ${days_threshold} days"
fi
echo ""
# Set trap for cleanup on exit
trap cleanup EXIT
# Install and check dependencies
log_info "Checking dependencies..."
install_gh
install_rclone
# Authenticate and configure
check_gh_auth
check_rclone_config
# Ensure Google Drive folder exists
log_info "Ensuring Google Drive folder exists: ${GDRIVE_FOLDER}"
rclone mkdir "${RCLONE_REMOTE}:${GDRIVE_FOLDER}" 2>/dev/null || true
# Setup backup directory
setup_backup_dir
# Get repositories
repos=$(get_repositories)
if [[ -z "$repos" ]]; then
log_error "No repositories found in ${GITHUB_ORG}"
exit 1
fi
# Count repositories
repo_count=$(echo "$repos" | wc -l | tr -d ' ')
log_info "Found ${repo_count} repositories to check"
echo ""
# Backup each repository
success_count=0
fail_count=0
skip_count=0
while IFS= read -r repo; do
if [[ -n "$repo" ]]; then
echo "----------------------------------------"
log_info "Checking: ${repo}"
if [[ "${force_backup}" == "true" ]]; then
# Force backup - skip modification check
if backup_repository "$repo"; then
((success_count++))
else
((fail_count++))
fi
else
# Check if repository has recent commits
if has_recent_commits "${repo}" "${days_threshold}"; then
log_info " Has commits within the last ${days_threshold} days"
if backup_repository "$repo"; then
((success_count++))
else
((fail_count++))
fi
else
last_commit=$(get_last_commit_date "${repo}")
log_skip "${repo} - No commits in last ${days_threshold} days (last: ${last_commit})"
((skip_count++))
fi
fi
echo ""
fi
done <<< "$repos"
# Create master zip of all individual repo zips
if [[ -d "${LOCAL_ZIPS_DIR}" ]] && [[ -n "$(ls -A "${LOCAL_ZIPS_DIR}" 2>/dev/null)" ]]; then
MASTER_ZIP="${SCRIPT_DIR}/code-laundry-backup_${LOG_TIMESTAMP}.zip"
log_info "Creating master zip archive: ${MASTER_ZIP}"
(cd "${SCRIPT_DIR}" && zip -rq "${MASTER_ZIP}" "zips/")
rm -rf "${LOCAL_ZIPS_DIR}"
log_info "Master zip saved: ${MASTER_ZIP}"
else
log_warn "No zips found in local folder — master zip not created."
fi
echo ""
# Summary
echo "=========================================="
echo " Backup Complete"
echo "=========================================="
echo ""
log_info "Successfully backed up: ${success_count} repositories"
log_info "Skipped (no recent commits): ${skip_count} repositories"
if [[ $fail_count -gt 0 ]]; then
log_warn "Failed to backup: ${fail_count} repositories"
fi
echo ""
log_info "Backups stored in: Google Drive/${GDRIVE_FOLDER}"
log_info "Master zip stored in: ${SCRIPT_DIR}"
}
# Run main function
main "$@"