From ededbb2fb37d7751fe0b55031545f9ff4f3ed45f Mon Sep 17 00:00:00 2001 From: Daniel Lysfjord Date: Sat, 7 Feb 2026 17:10:05 +0100 Subject: [PATCH 1/6] Add option to upload to one file each day instead of syncing the contents of /conf/backup/ --- .../app/library/OPNsense/Backup/Nextcloud.php | 38 ++++++++++++++++++- .../OPNsense/Backup/NextcloudSettings.xml | 4 ++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php index 6f72ba479e..af0318226a 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php @@ -84,7 +84,13 @@ public function getConfigurationFields() "type" => "text", "label" => gettext("Directory Name without leading slash, starting from user's root"), "value" => 'OPNsense-Backup' - ) + ), + array( + "name" => "strategy", + "type" => "checkbox", + "help" => gettext("Select this one to back up to a file named config-YYYYMMDD instead of syncing contents of /conf/backup"), + "label" => gettext("Daily file instead of sync all"), + ), ); $nextcloud = new NextcloudSettings(); foreach ($fields as &$field) { @@ -138,6 +144,9 @@ public function backup() $password = (string)$nextcloud->password; $backupdir = (string)$nextcloud->backupdir; $crypto_password = (string)$nextcloud->password_encryption; + $strategy = (string)$nextcloud->strategy; + // Strategy 0 = Sync /conf/backup + // Strategy 1 = Copy /conf/config.xml to $backupdir/conf-YYYYMMDD.xml // Check if destination directory exists, create (full path) if not try { @@ -147,6 +156,33 @@ public function backup() return array(); } + // Backup strategy 1, sync /conf/config.xml to $backupdir/config-YYYYMMDD.xml + if ($strategy) { + $confdata = file_get_contents('/conf/config.xml'); + $target_filename = 'config-' . date('Ymd') . '.xml'; + // Optionally encrypt + if (!empty($crypto_password)) { + $confdata = $this->encrypt($confdata, $crypto_password); + } + try { + $this->upload_file_content( + $url, + $username, + $password, + $internal_username, + $backupdir, + $target_filename, + $confdata + ); + return array($backupdir . '/' . $target_filename); + } catch (\Exception $e) { + syslog(LOG_ERR, $e); + return array(); + } + } + + // Default strategy (0), sync /conf/backup/ + // Get list of files from local backup system $local_files = array(); $tmp_local_files = scandir('/conf/backup/'); diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml b/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml index 10a12429e9..3e24a59620 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml @@ -49,5 +49,9 @@ OPNsense-Backup The Backup Directory can only consist of alphanumeric characters, dash, underscores and slash. No leading or trailing slash. + + Y + 0 + From 5f291ff253d43214062eec719bee7751bdfbd481 Mon Sep 17 00:00:00 2001 From: Daniel Lysfjord Date: Sun, 8 Feb 2026 12:35:51 +0100 Subject: [PATCH 2/6] Base date on filemtime instead of "now" --- .../opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php index af0318226a..8253a7d47a 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php @@ -159,7 +159,9 @@ public function backup() // Backup strategy 1, sync /conf/config.xml to $backupdir/config-YYYYMMDD.xml if ($strategy) { $confdata = file_get_contents('/conf/config.xml'); - $target_filename = 'config-' . date('Ymd') . '.xml'; + $mdate = filemtime('/conf/config.xml'); + $datestring = date('Ymd', $mdate); + $target_filename = 'config-' . $datestring . '.xml'; // Optionally encrypt if (!empty($crypto_password)) { $confdata = $this->encrypt($confdata, $crypto_password); From bdd62b79e2879926387e105ea20eb665c1288239 Mon Sep 17 00:00:00 2001 From: Daniel Lysfjord Date: Wed, 11 Feb 2026 19:41:39 +0100 Subject: [PATCH 3/6] Add curl_request_nothrow --- .../app/library/OPNsense/Backup/Nextcloud.php | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php index 8253a7d47a..8f3d6e5ae6 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php @@ -380,6 +380,31 @@ public function curl_request( $error_message, $postdata = null, $headers = array("User-Agent: OPNsense Firewall") + ) { + $result = $this->curl_request_nothrow($url, $username, $password, $method, $error_message, $postdata, $headers); + $info = $result['info']; + $err = $result['err']; + $response = $result['response']; + if (!($info['http_code'] == 200 || $info['http_code'] == 207 || $info['http_code'] == 201) || $err) { + syslog(LOG_ERR, $error_message); + syslog(LOG_ERR, json_encode($info)); + throw new \Exception(); + } + return array('response' => $response, 'info' => $info); + } + + + // Add this here, since I'm fundamentally opposed to throwing exceptions + // if http codes aren't to your liking in a generic function. + // Delegate that to upper functions, where it belongs. + public function curl_request_nothrow( + $url, + $username, + $password, + $method, + $error_message, + $postdata = null, + $headers = array("User-Agent: OPNsense Firewall") ) { $curl = curl_init(); curl_setopt_array($curl, array( @@ -399,13 +424,8 @@ public function curl_request( $response = curl_exec($curl); $err = curl_error($curl); $info = curl_getinfo($curl); - if (!($info['http_code'] == 200 || $info['http_code'] == 207 || $info['http_code'] == 201) || $err) { - syslog(LOG_ERR, $error_message); - syslog(LOG_ERR, json_encode($info)); - throw new \Exception(); - } curl_close($curl); - return array('response' => $response, 'info' => $info); + return array('response' => $response, 'info' => $info, 'err' => $err); } /** From 2499042b0a9fbc39346066acf6424272d003135a Mon Sep 17 00:00:00 2001 From: Daniel Lysfjord Date: Wed, 11 Feb 2026 19:43:57 +0100 Subject: [PATCH 4/6] utilize curl_request_nothrow for uploads, so we can act on http code --- .../mvc/app/library/OPNsense/Backup/Nextcloud.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php index 8f3d6e5ae6..d62dc5d433 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php @@ -289,14 +289,21 @@ public function listFiles($url, $username, $password, $internal_username, $direc */ public function upload_file_content($url, $username, $password, $internal_username, $backupdir, $filename, $local_file_content) { - $this->curl_request( - $url . "/remote.php/dav/files/$internal_username/$backupdir/$filename", + $url = $url . "/remote.php/dav/files/$internal_username/$backupdir/$filename"; + $reply = $this->curl_request( + $url, $username, $password, 'PUT', 'cannot execute PUT', $local_file_content ); + $http_code = $reply['info']['http_code']; + // Accepted http codes for upload is 200-299 + if (!($http_code >= 200 && $http_code < 300)) { + syslog(LOG_ERR, 'Could not PUT '. $url); + throw new \Exception(); + } } /** From 1dc8cdf60b28f1a98cd014df65479dacde36eda2 Mon Sep 17 00:00:00 2001 From: Daniel Lysfjord Date: Wed, 11 Feb 2026 19:55:47 +0100 Subject: [PATCH 5/6] Finally, implement checking of remote lastmodified, compare with local mtime. Only upload if remote file is older than local --- .../app/library/OPNsense/Backup/Nextcloud.php | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php index d62dc5d433..233d6c505f 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php @@ -162,10 +162,40 @@ public function backup() $mdate = filemtime('/conf/config.xml'); $datestring = date('Ymd', $mdate); $target_filename = 'config-' . $datestring . '.xml'; + // Find the same filename @ remote + $remote_filename = $url . '/remote.php/dav/files/' . $internal_username . '/' . $backupdir . '/' . $target_filename; + try { + $reply = $this->curl_request_nothrow($remote_filename, $username, $password, 'PROPFIND', 'Cannot get remote fileinfo'); + $http_code = $reply['info']['http_code']; + if ($http_code >= 200 && $http_code < 300) { + $xml_data = $reply['response']; + if ($xml_data == NULL) { + syslog(LOG_ERR, 'Data was NULL'); + return array(); + } + $xml_data = str_replace(['children() as $response) { + if ($response->getName() == 'response') { + $lastmodifiedstr = $response->propstat->prop->getlastmodified; + $filedate = strtotime($lastmodifiedstr); + if ($filedate >= $mdate) { + syslog(LOG_ERR, 'Remote file is same or newer than local'); + return array(); + } + } + } + } + } catch (\Exception $e) { + syslog(LOG_ERR, $e); + // Exceptions are not fatal at this point + // log for information, but keep going + } // Optionally encrypt if (!empty($crypto_password)) { $confdata = $this->encrypt($confdata, $crypto_password); } + // Finally, upload some data try { $this->upload_file_content( $url, @@ -178,7 +208,7 @@ public function backup() ); return array($backupdir . '/' . $target_filename); } catch (\Exception $e) { - syslog(LOG_ERR, $e); + syslog(LOG_ERR, 'Backup to ' . $url . ' failed: ' . $e); return array(); } } From 8fdc29fe575500c726426023e7de37bf20fa2307 Mon Sep 17 00:00:00 2001 From: Daniel Lysfjord Date: Wed, 11 Feb 2026 21:15:39 +0100 Subject: [PATCH 6/6] Clean up a bit, by moving things out to its own function --- .../app/library/OPNsense/Backup/Nextcloud.php | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php index 71b261a2bc..fba7f94162 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php @@ -134,6 +134,41 @@ public function setConfiguration($conf) return $validation_messages; } + /** + * check remote file last modified tag + * @param string $remote_filename filename to check on server + * @param string $username username for login to server + * @param string $password password for authentication + * @return int unix timestamp or 0 if errors occour + */ + public function get_remote_file_lastmodified( + $remote_filename, + $username, + $password + ) { + $reply = $this->curl_request_nothrow($remote_filename, $username, $password, 'PROPFIND', 'Cannot get remote fileinfo'); + $http_code = $reply['info']['http_code']; + if ($http_code >= 200 && $http_code < 300) { + $xml_data = $reply['response']; + if ($xml_data == NULL) { + syslog(LOG_ERR, 'Data was NULL'); + return 0; + } + $xml_data = str_replace(['children() as $response) { + if ($response->getName() == 'response') { + $lastmodifiedstr = $response->propstat->prop->getlastmodified; + $filedate = strtotime($lastmodifiedstr); + return $filedate; + } + } + } + return 0; + } + + + /** * perform backup * @return array filelist @@ -175,33 +210,11 @@ public function backup() $target_filename = 'config-' . $datestring . '.xml'; // Find the same filename @ remote $remote_filename = $url . '/remote.php/dav/files/' . $internal_username . '/' . $backupdir . '/' . $target_filename; - try { - $reply = $this->curl_request_nothrow($remote_filename, $username, $password, 'PROPFIND', 'Cannot get remote fileinfo'); - $http_code = $reply['info']['http_code']; - if ($http_code >= 200 && $http_code < 300) { - $xml_data = $reply['response']; - if ($xml_data == NULL) { - syslog(LOG_ERR, 'Data was NULL'); - return array(); - } - $xml_data = str_replace(['children() as $response) { - if ($response->getName() == 'response') { - $lastmodifiedstr = $response->propstat->prop->getlastmodified; - $filedate = strtotime($lastmodifiedstr); - if ($filedate >= $mdate) { - syslog(LOG_ERR, 'Remote file is same or newer than local'); - return array(); - } - } - } - } - } catch (\Exception $e) { - syslog(LOG_ERR, $e); - // Exceptions are not fatal at this point - // log for information, but keep going + $remote_file_date = $this->get_remote_file_lastmodified($remote_filename, $username, $password); + if ($remote_file_date >= $mdate) { + return array(); } + // Optionally encrypt if (!empty($crypto_password)) { $confdata = $this->encrypt($confdata, $crypto_password);