Skip to content

Commit

Permalink
Add remote borg backup support (#4804)
Browse files Browse the repository at this point in the history
Signed-off-by: Tim Diels <[email protected]>
Signed-off-by: Simon L. <[email protected]>
Co-authored-by: Simon L. <[email protected]>
  • Loading branch information
timdiels and szaimen authored Nov 7, 2024
1 parent 34a264d commit 3eeda1e
Show file tree
Hide file tree
Showing 12 changed files with 399 additions and 156 deletions.
4 changes: 3 additions & 1 deletion Containers/borgbackup/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ RUN set -ex; \
rsync \
fuse \
py3-llfuse \
jq
jq \
openssh-client

VOLUME /root

COPY --chmod=770 *.sh /
COPY borg_excludes /

ENTRYPOINT ["/start.sh"]
# hadolint ignore=DL3002
Expand Down
286 changes: 199 additions & 87 deletions Containers/borgbackup/backupscript.sh

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Containers/borgbackup/borg_excludes
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# These patterns need to be kept in sync with rsync and find excludes in backupscript.sh,
# which use a different syntax (patterns appear in 3 places in total)
nextcloud_aio_volumes/nextcloud_aio_apache/caddy/
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/caddy/
nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/nextcloud.log*
nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/audit.log
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/certs/
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_running
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/session_date_file
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/session/
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg*
18 changes: 15 additions & 3 deletions Containers/borgbackup/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Variables
export MOUNT_DIR="/mnt/borgbackup"
export BORG_BACKUP_DIRECTORY="$MOUNT_DIR/borg"
export BORG_BACKUP_DIRECTORY="$MOUNT_DIR/borg" # necessary even when remote to store the aio-lockfile

# Validate BORG_PASSWORD
if [ -z "$BORG_PASSWORD" ] && [ -z "$BACKUP_RESTORE_PASSWORD" ]; then
Expand All @@ -18,6 +18,18 @@ else
fi
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
if [ -n "$BORG_REMOTE_REPO" ]; then
export BORG_REPO="$BORG_REMOTE_REPO"

# Location to create the borg ssh pub/priv key
export BORGBACKUP_KEY="/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg"

# Accept any host key the first time connecting to the remote. Strictly speaking should be provided by user but you'd
# have to be very unlucky to get MitM'ed on your first connection.
export BORG_RSH="ssh -o StrictHostKeyChecking=accept-new -i $BORGBACKUP_KEY"
else
export BORG_REPO="$BORG_BACKUP_DIRECTORY"
fi

# Validate BORG_MODE
if [ "$BORG_MODE" != backup ] && [ "$BORG_MODE" != restore ] && [ "$BORG_MODE" != check ] && [ "$BORG_MODE" != "check-repair" ] && [ "$BORG_MODE" != test ]; then
Expand All @@ -36,8 +48,8 @@ fi
rm -f "/nextcloud_aio_volumes/nextcloud_aio_database_dump/backup-is-running"

# Get a list of all available borg archives
if borg list "$BORG_BACKUP_DIRECTORY" &>/dev/null; then
borg list "$BORG_BACKUP_DIRECTORY" | grep "nextcloud-aio" | awk -F " " '{print $1","$3,$4}' > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list"
if borg list &>/dev/null; then
borg list | grep "nextcloud-aio" | awk -F " " '{print $1","$3,$4}' > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list"
else
echo "" > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list"
fi
Expand Down
1 change: 1 addition & 0 deletions php/containers.json
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@
"image": "nextcloud/aio-borgbackup",
"init": true,
"environment": [
"BORG_REMOTE_REPO=%BORGBACKUP_REMOTE_REPO%",
"BORG_PASSWORD=%BORGBACKUP_PASSWORD%",
"BORG_MODE=%BORGBACKUP_MODE%",
"SELECTED_RESTORE_TIME=%SELECTED_RESTORE_TIME%",
Expand Down
2 changes: 2 additions & 0 deletions php/public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
'domain' => $configurationManager->GetDomain(),
'apache_port' => $configurationManager->GetApachePort(),
'borg_backup_host_location' => $configurationManager->GetBorgBackupHostLocation(),
'borg_remote_repo' => $configurationManager->GetBorgRemoteRepo(),
'borg_public_key' => $configurationManager->GetBorgPublicKey(),
'nextcloud_password' => $configurationManager->GetAndGenerateSecret('NEXTCLOUD_PASSWORD'),
'containers' => (new \AIO\ContainerDefinitionFetcher($container->get(\AIO\Data\ConfigurationManager::class), $container))->FetchDefinition(),
'borgbackup_password' => $configurationManager->GetAndGenerateSecret('BORGBACKUP_PASSWORD'),
Expand Down
14 changes: 8 additions & 6 deletions php/src/Controller/ConfigurationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ public function SetConfig(Request $request, Response $response, array $args) : R
$this->configurationManager->ChangeMasterPassword($currentMasterPassword, $newMasterPassword);
}

if (isset($request->getParsedBody()['borg_backup_host_location'])) {
if (isset($request->getParsedBody()['borg_backup_host_location']) || isset($request->getParsedBody()['borg_remote_repo'])) {
$location = $request->getParsedBody()['borg_backup_host_location'] ?? '';
$this->configurationManager->SetBorgBackupHostLocation($location);
$borgRemoteRepo = $request->getParsedBody()['borg_remote_repo'] ?? '';
$this->configurationManager->SetBorgLocationVars($location, $borgRemoteRepo);
}

if (isset($request->getParsedBody()['borg_restore_host_location']) || isset($request->getParsedBody()['borg_restore_password'])) {
if (isset($request->getParsedBody()['borg_restore_host_location']) || isset($request->getParsedBody()['borg_restore_remote_repo']) || isset($request->getParsedBody()['borg_restore_password'])) {
$restoreLocation = $request->getParsedBody()['borg_restore_host_location'] ?? '';
$borgRemoteRepo = $request->getParsedBody()['borg_restore_remote_repo'] ?? '';
$borgPassword = $request->getParsedBody()['borg_restore_password'] ?? '';
$this->configurationManager->SetBorgRestoreHostLocationAndPassword($restoreLocation, $borgPassword);
$this->configurationManager->SetBorgRestoreLocationVarsAndPassword($restoreLocation, $borgRemoteRepo, $borgPassword);
}

if (isset($request->getParsedBody()['daily_backup_time'])) {
Expand Down Expand Up @@ -132,8 +134,8 @@ public function SetConfig(Request $request, Response $response, array $args) : R
$this->configurationManager->SetCollaboraDictionaries($collaboraDictionaries);
}

if (isset($request->getParsedBody()['delete_borg_backup_host_location'])) {
$this->configurationManager->DeleteBorgBackupHostLocation();
if (isset($request->getParsedBody()['delete_borg_backup_location_vars'])) {
$this->configurationManager->DeleteBorgBackupLocationVars();
}

return $response->withStatus(201)->withHeader('Location', '/');
Expand Down
89 changes: 60 additions & 29 deletions php/src/Data/ConfigurationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -439,55 +439,69 @@ public function GetAIOURL() : string {
/**
* @throws InvalidSettingConfigurationException
*/
public function SetBorgBackupHostLocation(string $location) : void {
$isValidPath = false;
if (str_starts_with($location, '/') && !str_ends_with($location, '/')) {
$isValidPath = true;
} elseif ($location === 'nextcloud_aio_backupdir') {
$isValidPath = true;
}

if (!$isValidPath) {
throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!");
}

public function SetBorgLocationVars(string $location, string $repo) : void {
$this->ValidateBorgLocationVars($location, $repo);

$config = $this->GetConfig();
$config['borg_backup_host_location'] = $location;
$config['borg_remote_repo'] = $repo;
$this->WriteConfig($config);
}

public function DeleteBorgBackupHostLocation() : void {
private function ValidateBorgLocationVars(string $location, string $repo) : void {
if ($location === '' && $repo === '') {
throw new InvalidSettingConfigurationException("Please enter a path or a remote repo url!");
} elseif ($location !== '' && $repo !== '') {
throw new InvalidSettingConfigurationException("Location and remote repo url are mutually exclusive!");
}

if ($location !== '') {
$isValidPath = false;
if (str_starts_with($location, '/') && !str_ends_with($location, '/')) {
$isValidPath = true;
} elseif ($location === 'nextcloud_aio_backupdir') {
$isValidPath = true;
}

if (!$isValidPath) {
throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!");
}
} else {
$this->ValidateBorgRemoteRepo($repo);
}
}

private function ValidateBorgRemoteRepo(string $repo) : void {
$commonMsg = "For valid urls, see the remote examples at https://borgbackup.readthedocs.io/en/stable/usage/general.html#repository-urls";
if ($repo === "") {
// Ok, remote repo is optional
} elseif (!str_contains($repo, "@")) {
throw new InvalidSettingConfigurationException("The remote repo must contain '@'. $commonMsg");
} elseif (!str_contains($repo, ":")) {
throw new InvalidSettingConfigurationException("The remote repo must contain ':'. $commonMsg");
}
}

public function DeleteBorgBackupLocationVars() : void {
$config = $this->GetConfig();
$config['borg_backup_host_location'] = '';
$config['borg_remote_repo'] = '';
$this->WriteConfig($config);
}

/**
/**
* @throws InvalidSettingConfigurationException
*/
public function SetBorgRestoreHostLocationAndPassword(string $location, string $password) : void {
if ($location === '') {
throw new InvalidSettingConfigurationException("Please enter a path!");
}

$isValidPath = false;
if (str_starts_with($location, '/') && !str_ends_with($location, '/')) {
$isValidPath = true;
} elseif ($location === 'nextcloud_aio_backupdir') {
$isValidPath = true;
}

if (!$isValidPath) {
throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!");
}
public function SetBorgRestoreLocationVarsAndPassword(string $location, string $repo, string $password) : void {
$this->ValidateBorgLocationVars($location, $repo);

if ($password === '') {
throw new InvalidSettingConfigurationException("Please enter the password!");
}

$config = $this->GetConfig();
$config['borg_backup_host_location'] = $location;
$config['borg_remote_repo'] = $repo;
$config['borg_restore_password'] = $password;
$config['instance_restore_attempt'] = 1;
$this->WriteConfig($config);
Expand Down Expand Up @@ -582,6 +596,23 @@ public function GetBorgBackupHostLocation() : string {
return $config['borg_backup_host_location'];
}

public function GetBorgRemoteRepo() : string {
$config = $this->GetConfig();
if(!isset($config['borg_remote_repo'])) {
$config['borg_remote_repo'] = '';
}

return $config['borg_remote_repo'];
}

public function GetBorgPublicKey() : string {
if (!file_exists(DataConst::GetBackupPublicKey())) {
return "";
}

return trim(file_get_contents(DataConst::GetBackupPublicKey()));
}

public function GetBorgRestorePassword() : string {
$config = $this->GetConfig();
if(!isset($config['borg_restore_password'])) {
Expand Down
4 changes: 4 additions & 0 deletions php/src/Data/DataConst.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public static function GetConfigFile() : string {
return self::GetDataDirectory() . '/configuration.json';
}

public static function GetBackupPublicKey() : string {
return self::GetDataDirectory() . '/id_borg.pub';
}

public static function GetBackupSecretFile() : string {
return self::GetDataDirectory() . '/backupsecret';
}
Expand Down
2 changes: 2 additions & 0 deletions php/src/Docker/DockerActionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ public function CreateContainer(Container $container) : void {
$replacements[1] = $this->configurationManager->GetBaseDN();
} elseif ($out[1] === 'AIO_TOKEN') {
$replacements[1] = $this->configurationManager->GetToken();
} elseif ($out[1] === 'BORGBACKUP_REMOTE_REPO') {
$replacements[1] = $this->configurationManager->GetBorgRemoteRepo();
} elseif ($out[1] === 'BORGBACKUP_MODE') {
$replacements[1] = $this->configurationManager->GetBackupMode();
} elseif ($out[1] === 'AIO_URL') {
Expand Down
Loading

0 comments on commit 3eeda1e

Please sign in to comment.