Skip to content
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

Add remote borg backup support #5206

Closed
wants to merge 14 commits into from
Closed
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
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
281 changes: 194 additions & 87 deletions Containers/borgbackup/backupscript.sh

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions Containers/borgbackup/borg_excludes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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
2 changes: 1 addition & 1 deletion Containers/mastercontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ RUN set -ex; \
wget https://getcomposer.org/installer -O - | php -- --install-dir=/usr/local/bin --filename=composer; \
chmod +x /usr/local/bin/composer; \
cd /var/www/docker-aio; \
git clone https://github.com/nextcloud-releases/all-in-one.git --depth 1 .; \
git clone https://github.com/nextcloud/all-in-one.git --depth 1 --branch enh/noid/remote-borg-backups .; \
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: revert before merging

find ./ -maxdepth 1 -mindepth 1 -not -path ./php -not -path ./community-containers -exec rm -r {} \; ; \
chown www-data:www-data -R /var/www/docker-aio; \
cd php; \
Expand Down
1 change: 1 addition & 0 deletions php/containers.json
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,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 @@ -32,15 +32,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 @@ -131,8 +133,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 @@ -424,55 +424,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 @@ -567,6 +581,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 @@ -287,6 +287,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