-
Notifications
You must be signed in to change notification settings - Fork 0
Add daemon mode for automatic daily backups with Docker support #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
5262e2e
3bca8bc
6301562
ae861dd
a5d3d68
721d02a
c52ad87
b250411
32f772d
0f91b7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # GitHub Personal Access Token (optional but recommended for higher API rate limits) | ||
| # Get one from: https://github.com/settings/tokens | ||
| GITHUB_TOKEN=your_github_token_here | ||
|
|
||
| # Comma-separated list of GitHub users/organizations to backup | ||
| # Example: SCHEDULED_USERS=octocat,github,spring-projects | ||
| SCHEDULED_USERS=octocat |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||
| # Build stage | ||||||||
| FROM maven:3.9-eclipse-temurin-17 AS build | ||||||||
| WORKDIR /app | ||||||||
| COPY pom.xml . | ||||||||
| COPY src ./src | ||||||||
| RUN mvn clean package -DskipTests | ||||||||
|
|
||||||||
| # Runtime stage | ||||||||
| FROM eclipse-temurin:17-jre-alpine | ||||||||
| WORKDIR /app | ||||||||
|
|
||||||||
| # Install git (required for cloning repositories) | ||||||||
| RUN apk add --no-cache git | ||||||||
|
|
||||||||
| # Copy the built jar from build stage | ||||||||
| COPY --from=build /app/target/gh-backup-1.0.0.jar gh-backup.jar | ||||||||
|
|
||||||||
| # Create backup directory | ||||||||
| RUN mkdir -p /backups | ||||||||
|
|
||||||||
| # Set default environment variables | ||||||||
| ENV BACKUP_DIRECTORY=/backups | ||||||||
| ENV GITHUB_TOKEN="" | ||||||||
| ENV SCHEDULED_USERS="" | ||||||||
|
|
||||||||
| # Expose port for web mode (optional) | ||||||||
|
||||||||
| # Expose port for web mode (optional) | |
| # EXPOSE is only used when running this image in web mode with a different profile; | |
| # the default daemon mode does not listen on port 8080. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 721d02a. Added clarifying comments that EXPOSE 8080 is only used when running the container in web mode with -Dspring.profiles.active=web, and that the default daemon mode does not use this port.
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,23 @@ | ||||
| version: '3.8' | ||||
|
|
||||
|
||||
| version: '3.8' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 721d02a. Removed deprecated version: '3.8' field from docker-compose.yml to use the latest Compose file format.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,13 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/bin/sh | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set -e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Build Java command with environment variables | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAVA_OPTS="-Dspring.profiles.active=daemon" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAVA_OPTS="$JAVA_OPTS -Dbackup.directory=\"${BACKUP_DIRECTORY}\"" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -n "$SCHEDULED_USERS" ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAVA_OPTS="$JAVA_OPTS -Dbackup.scheduled.users=\"${SCHEDULED_USERS}\"" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAVA_OPTS="$JAVA_OPTS -Dbackup.directory=\"${BACKUP_DIRECTORY}\"" | |
| if [ -n "$SCHEDULED_USERS" ]; then | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.scheduled.users=\"${SCHEDULED_USERS}\"" | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.directory=${BACKUP_DIRECTORY}" | |
| if [ -n "$SCHEDULED_USERS" ]; then | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.scheduled.users=${SCHEDULED_USERS}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 721d02a. Removed escaped quotes around ${BACKUP_DIRECTORY} and ${SCHEDULED_USERS} to prevent quotes from being passed as part of the property values to Java.
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The double quotes around the environment variable expansion will be passed literally to Java, causing the property value to include quotes. This will result in Java receiving -Dbackup.scheduled.users="value" with quotes as part of the value. Remove the escaped quotes around the variable expansion.
| JAVA_OPTS="$JAVA_OPTS -Dbackup.directory=\"${BACKUP_DIRECTORY}\"" | |
| if [ -n "$SCHEDULED_USERS" ]; then | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.scheduled.users=\"${SCHEDULED_USERS}\"" | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.directory=${BACKUP_DIRECTORY}" | |
| if [ -n "$SCHEDULED_USERS" ]; then | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.scheduled.users=${SCHEDULED_USERS}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 721d02a. Removed escaped quotes around environment variable expansions in docker-entrypoint.sh.
Copilot
AI
Apr 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The entrypoint builds JAVA_OPTS as a string and then executes exec java $JAVA_OPTS ... unquoted. This causes word-splitting and enables option injection if BACKUP_DIRECTORY or SCHEDULED_USERS contains spaces or starts with something like -D.../-X.... It also breaks valid paths containing spaces. Build the argument list safely (e.g., using set -- java ... and appending quoted -D...=$VAR args, then exec "$@" -jar ...) so env values are treated as data, not additional flags.
| JAVA_OPTS="-Dspring.profiles.active=daemon" | |
| # Set backup directory if provided (defaults to /backups in Dockerfile ENV) | |
| if [ -n "$BACKUP_DIRECTORY" ]; then | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.directory=${BACKUP_DIRECTORY}" | |
| fi | |
| # Set scheduled users if provided | |
| if [ -n "$SCHEDULED_USERS" ]; then | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.scheduled.users=${SCHEDULED_USERS}" | |
| fi | |
| # Execute the application | |
| exec java $JAVA_OPTS -jar gh-backup.jar "$@" | |
| set -- java "-Dspring.profiles.active=daemon" | |
| # Set backup directory if provided (defaults to /backups in Dockerfile ENV) | |
| if [ -n "$BACKUP_DIRECTORY" ]; then | |
| set -- "$@" "-Dbackup.directory=${BACKUP_DIRECTORY}" | |
| fi | |
| # Set scheduled users if provided | |
| if [ -n "$SCHEDULED_USERS" ]; then | |
| set -- "$@" "-Dbackup.scheduled.users=${SCHEDULED_USERS}" | |
| fi | |
| # Execute the application | |
| set -- "$@" -jar gh-backup.jar "$@" | |
| exec "$@" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,10 @@ | |
|
|
||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
| import org.springframework.scheduling.annotation.EnableScheduling; | ||
|
|
||
| @SpringBootApplication | ||
| @EnableScheduling | ||
|
||
| public class GhBackupApplication { | ||
|
|
||
| public static void main(String[] args) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,87 @@ | ||||||
| package com.github.backup; | ||||||
|
|
||||||
| import org.springframework.beans.factory.annotation.Value; | ||||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||||
| import org.springframework.scheduling.annotation.Scheduled; | ||||||
| import org.springframework.stereotype.Service; | ||||||
|
|
||||||
| import jakarta.annotation.PostConstruct; | ||||||
| import java.io.IOException; | ||||||
| import java.time.LocalDateTime; | ||||||
| import java.time.format.DateTimeFormatter; | ||||||
| import java.util.Arrays; | ||||||
| import java.util.List; | ||||||
|
|
||||||
| /** | ||||||
| * Service that runs scheduled backups at configured intervals. | ||||||
| * Enabled when backup.mode=daemon. | ||||||
| */ | ||||||
| @Service | ||||||
| @ConditionalOnProperty(name = "backup.mode", havingValue = "daemon") | ||||||
| public class ScheduledBackupService { | ||||||
|
|
||||||
| private static final long BACKUP_INTERVAL_MS = 86400000; // 24 hours in milliseconds | ||||||
|
||||||
| private static final int SEPARATOR_LENGTH = 80; | ||||||
|
|
||||||
| private final BackupService backupService; | ||||||
| private final List<String> scheduledUsers; | ||||||
| private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | ||||||
|
|
||||||
| public ScheduledBackupService(BackupService backupService, | ||||||
| @Value("${backup.scheduled.users:}") String scheduledUsersConfig) { | ||||||
| this.backupService = backupService; | ||||||
| // Parse comma-separated list of users/orgs | ||||||
| this.scheduledUsers = scheduledUsersConfig.isBlank() | ||||||
| ? List.of() | ||||||
| : Arrays.stream(scheduledUsersConfig.split(",")) | ||||||
| .map(String::trim) | ||||||
| .filter(s -> !s.isEmpty()) | ||||||
| .toList(); | ||||||
| } | ||||||
|
|
||||||
| @PostConstruct | ||||||
| public void init() { | ||||||
| if (scheduledUsers.isEmpty()) { | ||||||
| System.out.println("⚠ Warning: No users/organizations configured for scheduled backups."); | ||||||
| System.out.println("Set the 'backup.scheduled.users' property with a comma-separated list."); | ||||||
| System.out.println("Example: backup.scheduled.users=octocat,github"); | ||||||
| } else { | ||||||
| System.out.println("GitHub Backup Daemon Mode"); | ||||||
| System.out.println("========================"); | ||||||
| System.out.println("Scheduled backups enabled for: " + String.join(", ", scheduledUsers)); | ||||||
| System.out.println("Backup interval: Every 24 hours"); | ||||||
| System.out.println("First backup will run immediately, then every 24 hours."); | ||||||
| System.out.println(); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Runs backup for all configured users/organizations. | ||||||
| * Executes on startup and then every 24 hours. | ||||||
| */ | ||||||
| @Scheduled(fixedRate = BACKUP_INTERVAL_MS, initialDelay = 0) | ||||||
|
||||||
| @Scheduled(fixedRate = BACKUP_INTERVAL_MS, initialDelay = 0) | |
| @Scheduled(fixedDelay = BACKUP_INTERVAL_MS, initialDelay = 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 721d02a. Changed to fixedDelay (via fixedDelayString) to ensure next backup starts only after the previous one completes, preventing concurrent backups for large organizations.
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console output is used directly with System.out.println and System.err.println instead of a proper logging framework. While this works for a scheduled service, consider using SLF4J logger for consistency with Spring Boot best practices. This would allow users to configure log levels and output destinations through standard Spring Boot logging configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 721d02a. Replaced System.out/System.err with SLF4J logger, allowing users to configure log levels and output destinations through standard Spring Boot logging configuration.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Daemon mode configuration | ||
| spring.main.web-application-type=none | ||
| backup.mode=daemon | ||
|
|
||
| # Comma-separated list of GitHub users/organizations to backup automatically | ||
| # Example: backup.scheduled.users=octocat,github,spring-projects | ||
| backup.scheduled.users= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| package com.github.backup; | ||
|
|
||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.mockito.Mock; | ||
| import org.mockito.MockitoAnnotations; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
| import static org.mockito.Mockito.*; | ||
|
|
||
| class ScheduledBackupServiceTest { | ||
|
|
||
| @Mock | ||
| private BackupService backupService; | ||
|
|
||
| private ScheduledBackupService scheduledBackupService; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| MockitoAnnotations.openMocks(this); | ||
| } | ||
|
|
||
| @Test | ||
| void testScheduledBackupService_WithNoUsers() { | ||
| scheduledBackupService = new ScheduledBackupService(backupService, ""); | ||
|
|
||
| // Should not throw exception with empty users list | ||
| assertDoesNotThrow(() -> scheduledBackupService.runScheduledBackup()); | ||
|
|
||
| // Should not call backup service when no users configured | ||
| verifyNoInteractions(backupService); | ||
| } | ||
|
|
||
| @Test | ||
| void testScheduledBackupService_WithSingleUser() throws IOException { | ||
| scheduledBackupService = new ScheduledBackupService(backupService, "octocat"); | ||
|
|
||
| scheduledBackupService.runScheduledBackup(); | ||
|
|
||
| // Should call backup service once for the configured user | ||
| verify(backupService, times(1)).backupUserRepositories("octocat"); | ||
| } | ||
|
|
||
| @Test | ||
| void testScheduledBackupService_WithMultipleUsers() throws IOException { | ||
| scheduledBackupService = new ScheduledBackupService(backupService, "octocat,github,spring-projects"); | ||
|
|
||
| scheduledBackupService.runScheduledBackup(); | ||
|
|
||
| // Should call backup service for each configured user | ||
| verify(backupService, times(1)).backupUserRepositories("octocat"); | ||
| verify(backupService, times(1)).backupUserRepositories("github"); | ||
| verify(backupService, times(1)).backupUserRepositories("spring-projects"); | ||
| } | ||
|
|
||
| @Test | ||
| void testScheduledBackupService_WithWhitespace() throws IOException { | ||
| scheduledBackupService = new ScheduledBackupService(backupService, " octocat , github , spring-projects "); | ||
|
|
||
| scheduledBackupService.runScheduledBackup(); | ||
|
|
||
| // Should trim whitespace from user names | ||
| verify(backupService, times(1)).backupUserRepositories("octocat"); | ||
| verify(backupService, times(1)).backupUserRepositories("github"); | ||
| verify(backupService, times(1)).backupUserRepositories("spring-projects"); | ||
| } | ||
|
|
||
| @Test | ||
| void testScheduledBackupService_HandlesException() throws IOException { | ||
| scheduledBackupService = new ScheduledBackupService(backupService, "octocat,github"); | ||
|
|
||
| doThrow(new IOException("Test exception")).when(backupService).backupUserRepositories("octocat"); | ||
|
|
||
| // Should continue to next user even if one fails | ||
| assertDoesNotThrow(() -> scheduledBackupService.runScheduledBackup()); | ||
|
|
||
| verify(backupService, times(1)).backupUserRepositories("octocat"); | ||
| verify(backupService, times(1)).backupUserRepositories("github"); | ||
| } | ||
|
|
||
| @Test | ||
| void testScheduledBackupService_WithEmptyStrings() throws IOException { | ||
| scheduledBackupService = new ScheduledBackupService(backupService, "octocat,,github"); | ||
|
|
||
| scheduledBackupService.runScheduledBackup(); | ||
|
|
||
| // Should filter out empty strings and only call for valid users | ||
| verify(backupService, times(1)).backupUserRepositories("octocat"); | ||
| verify(backupService, times(1)).backupUserRepositories("github"); | ||
| verifyNoMoreInteractions(backupService); | ||
| } | ||
|
|
||
| @Test | ||
| void testScheduledBackupService_WithBlankString() { | ||
| scheduledBackupService = new ScheduledBackupService(backupService, " "); | ||
|
|
||
| assertDoesNotThrow(() -> scheduledBackupService.runScheduledBackup()); | ||
|
|
||
| // Should not call backup service with blank configuration | ||
| verifyNoInteractions(backupService); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are skipped during the Docker build with -DskipTests. While this speeds up the build, it means the Docker image could contain code that hasn't been tested. Consider running tests during the build to ensure code quality, or document why tests are skipped (e.g., if tests require external dependencies not available during build).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 721d02a. Added comments documenting that tests should be run in CI/CD pipeline before building the Docker image, and that tests are skipped to speed up image creation.