Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Simple Backup Tool CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Java 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Build
run: mvn clean package

- name: Create test data
run: |
mkdir -p source dest
echo "test" > source/file.txt

- name: Run backup
run: java -jar target/backup-tool-1.0-SNAPSHOT.jar source dest

- name: Verify
run: |
if [ -f dest/*.zip ]; then
echo "Backup created"
ls -lh dest/
else
echo "Failed"
exit 1
fi
220 changes: 219 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,219 @@
# Backup Tool
# Backup Tool

A Java-based automated backup tool that creates compressed ZIP archives of directories with support for both one-time and scheduled backups.

## Features

- Create compressed ZIP backups of directories
- One-time backup execution
- Scheduled automatic backups (cron-like)
- Command-line interface
- Easy-to-use startup scripts
- Clean Domain-Driven Design architecture

## Prerequisites

- Java 21 or higher
- Maven 3.6 or higher

## Building the Project

```bash
mvn clean package
```

This will create an executable JAR file at `target/backup-tool-1.0-SNAPSHOT.jar`

## Usage

### Option 1: Using Startup Scripts (Recommended)

#### Linux/Mac

```bash
# Make the script executable
chmod +x backup.sh

# One-time backup
./backup.sh /path/to/source /path/to/destination

# Scheduled backup (every 5 minutes for testing)
./backup.sh /path/to/source /path/to/destination 5

# Or edit the script to set default values and run
./backup.sh
```

#### Windows

```cmd
# One-time backup
backup.bat C:\path\to\source C:\path\to\destination

# Scheduled backup (every 5 minutes for testing)
backup.bat C:\path\to\source C:\path\to\destination 5

# Or edit the script to set default values and run
backup.bat
```

### Option 2: Direct JAR Execution

```bash
# One-time backup
java -jar target/backup-tool-1.0-SNAPSHOT.jar /path/to/source /path/to/destination

# Scheduled backup (every 5 minutes)
java -jar target/backup-tool-1.0-SNAPSHOT.jar /path/to/source /path/to/destination 5
```

## Arguments

1. **source** (required) - The directory you want to backup
2. **destination** (required) - The directory where backups will be stored
3. **interval_minutes** (optional) - If provided, runs backup every N minutes. If omitted, runs once and exits.

## Examples

### One-time Backup

```bash
# Backup your Documents folder to a Backups directory
java -jar target/backup-tool-1.0-SNAPSHOT.jar ~/Documents ~/Backups
```

### Scheduled Backup (Testing - Every 5 Minutes)

```bash
# Run backup every 5 minutes (good for testing)
java -jar target/backup-tool-1.0-SNAPSHOT.jar ~/Documents ~/Backups 5
```

### Scheduled Backup (Production - Every Hour)

```bash
# Run backup every 60 minutes
java -jar target/backup-tool-1.0-SNAPSHOT.jar ~/Documents ~/Backups 60
```

### Scheduled Backup (Daily)

```bash
# Run backup every 24 hours (1440 minutes)
java -jar target/backup-tool-1.0-SNAPSHOT.jar ~/Documents ~/Backups 1440
```

## How It Works

1. The tool creates a ZIP archive of the source directory
2. The archive is stored in the destination directory with a timestamp
3. Format: `backup-<timestamp>.zip`
4. For scheduled mode, the process runs in the background at the specified interval
5. Press Ctrl+C to stop the scheduled backup service

## Project Structure

```
backup-tool/
├── src/main/java/io/github/devcavin/
│ ├── App.java # Main application entry point
│ ├── application/usecase/ # Use case layer
│ │ ├── CreateBackupJobUseCase.java
├── ListArchiveUseCase.java
│ │ ├── RunBackupJobUseCase.java
│ │ └── ListBackupJobsUseCase.java
│ ├── domain/ # Domain layer
│ │ ├── entity/
│ │ │ ├── BackupJob.java
│ │ │ └── Archive.java
│ │ ├── repository/
│ │ │ ├── BackupJobRepository.java
│ │ │ └── ArchiveRepository.java
│ │ ├── service/
│ │ │ ├── BackupService.java
│ │ │ ├── ArchiveCreator.java
│ │ │ └── ArchiveStorage.java
│ │ ├── valueobject/
│ │ │ ├── BackupName.java
│ │ │ ├── SourcePath.java
│ │ │ └── DestinationPath.java
│ │ └── enums/
│ │ └── BackupStatus.java
│ └── infrastructure/ # Infrastructure layer
│ ├── archive/local/
│ │ ├── LocalArchiveCreator.java
│ │ └── LocalArchiveStorage.java
│ ├── repository/memory/
│ │ ├── InMemoryBackupJobRepository.java
│ │ └── InMemoryArchiveRepository.java
│ └── scheduler/
│ └── ScheduledBackupService.java
├── backup.sh # Linux/Mac startup script
├── backup.bat # Windows startup script
├── pom.xml
└── README.md
```

## Customizing Defaults

Edit the startup scripts to set your preferred default values:

**backup.sh** (Linux/Mac):
```bash
DEFAULT_SOURCE="/home/user/documents"
DEFAULT_DESTINATION="/home/user/backups"
DEFAULT_INTERVAL="" # or set to number for scheduled
```

**backup.bat** (Windows):
```cmd
set DEFAULT_SOURCE=C:\Users\%USERNAME%\Documents
set DEFAULT_DESTINATION=C:\Users\%USERNAME%\Backups
set DEFAULT_INTERVAL=
```

## Running as a Background Service

### Linux (systemd)

Create a service file `/etc/systemd/system/backup-tool.service`:

```ini
[Unit]
Description=Automated Backup Service
After=network.target

[Service]
Type=simple
User=yourusername
WorkingDirectory=/path/to/backup-tool
ExecStart=/usr/bin/java -jar /path/to/backup-tool/target/backup-tool-1.0-SNAPSHOT.jar /source /destination 60
Restart=always

[Install]
WantedBy=multi-user.target
```

Then:
```bash
sudo systemctl daemon-reload
sudo systemctl enable backup-tool
sudo systemctl start backup-tool
```

### Windows (Task Scheduler)

1. Open Task Scheduler
2. Create Basic Task
3. Set trigger to "When the computer starts"
4. Action: Start a program
5. Program: `javaw.exe`
6. Arguments: `-jar C:\path\to\backup-tool.jar C:\source C:\destination 60`

## License

MIT License

## Contributing

Pull requests are welcome!
17 changes: 14 additions & 3 deletions src/main/java/io/github/devcavin/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,20 @@ public static void main(String[] args) {
System.out.println("Source: " + sourcePath);
System.out.println("Destination: " + destinationPath);

UUID jobId = createJob.execute("automated-backup", sourcePath, destinationPath);
System.out.println("Backup job created with ID: " + jobId);
UUID jobId;
try {
jobId = createJob.execute("automated-backup", sourcePath, destinationPath);
System.out.println("Backup job created with ID: " + jobId);
} catch (IllegalArgumentException e) {
System.err.println("\n=== ERROR ===");
System.err.println(e.getMessage());
System.err.println("\nPlease check:");
System.err.println(" - Source directory exists and is readable");
System.err.println(" - Source directory is not empty");
System.err.println(" - Destination directory is writable");
System.exit(1);
return; // Never reached, but keeps compiler happy
}

if (intervalMinutes > 0) {
// Scheduled mode
Expand All @@ -63,7 +75,6 @@ public static void main(String[] args) {
ScheduledBackupService scheduler = new ScheduledBackupService(runJob);
scheduler.scheduleBackup(jobId, intervalMinutes);

// Add shutdown hook to clean up gracefully
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\nShutdown signal received...");
scheduler.shutdown();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.github.devcavin.domain.valueobject;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.stream.Stream;

public class SourcePath {
private final Path path;
Expand All @@ -27,9 +29,21 @@ public SourcePath(String rawPath) {
throw new IllegalArgumentException("Source path is not a directory: %s".formatted(resolvePath));
}

if (isDirectoryEmpty(resolvePath)) {
throw new IllegalArgumentException("Source directory is empty: %s".formatted(resolvePath));
}

this.path = resolvePath;
}

private boolean isDirectoryEmpty(Path directory) {
try (Stream<Path> entries = Files.list(directory)) {
return entries.findFirst().isEmpty();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to read source directory: %s".formatted(directory), e);
}
}

public Path getPath() {
return path;
}
Expand Down Expand Up @@ -58,4 +72,4 @@ public int hashCode() {
public String toString() {
return path.toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
Expand All @@ -19,18 +20,34 @@ public Archive createArchive(BackupJob job) {

try {
Path tempFile = Files.createTempFile("backup-", ".zip");
AtomicInteger fileCount = new AtomicInteger(0);

try (Stream<Path> paths = Files.walk(source);
ZipOutputStream zos =
new ZipOutputStream(Files.newOutputStream(tempFile))) {

paths
.filter(Files::isRegularFile)
.forEach(path -> addToZip(source, path, zos));
.forEach(path -> {
addToZip(source, path, zos);
fileCount.incrementAndGet();
});
}

if (fileCount.get() == 0) {
Files.deleteIfExists(tempFile);
throw new IllegalStateException(
"No files found to backup in source directory: " + source
);
}

long size = Files.size(tempFile);

System.out.printf("Archive created: %d file(s), size: %.2f MB%n",
fileCount.get(),
size / (1024.0 * 1024.0)
);

return new Archive(
job.getId(),
tempFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

public class InMemoryArchiveRepository implements ArchiveRepository {
private final Map<UUID, Archive> store = new ConcurrentHashMap<>();

@Override
public void save(Archive archive) {
store.put(archive.getId(), archive);
Expand Down
Loading