Skip to content

Commit f0aeb16

Browse files
author
Nigel
committed
v1.4.1.5 - Test release with comprehensive fixes
- Fixed NFS remote browsing with improved user feedback and validation - Fixed SMB backup creation by correcting path format handling - Fixed View Backups and Restore Backup operations to show backup lists - Enhanced get_all_backups method to support all remote protocols - Improved local backup path validation to prevent NFS mount issues - Fixed backup listing to show proper dates, sizes, and selection interface
1 parent e6fd26d commit f0aeb16

5 files changed

Lines changed: 260 additions & 77 deletions

File tree

release_notes_1.4.1.5.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# LibreELEC Backupper v1.4.1.5 - Test Release (2026-01-05)
2+
3+
## Fixed Issues
4+
5+
### NFS Remote Backup Issues
6+
- **Fixed NFS browsing**: Improved user feedback and validation when setting NFS paths
7+
- **Enhanced error handling**: Better validation of NFS path formats with clear error messages
8+
- **Improved connection testing**: More detailed NFS connection test results
9+
10+
### SMB Remote Backup Issues
11+
- **Fixed SMB path handling**: Corrected path format conversion from Kodi's browser to internal storage
12+
- **Fixed backup creation**: SMB backups now properly create files instead of failing silently
13+
- **Improved URL construction**: Proper SMB URL formatting for all operations
14+
15+
### Local Backup Issues
16+
- **Enhanced path validation**: Local backup paths containing invalid characters (like ':') are now properly detected and reset
17+
- **Better NFS mount support**: Improved handling of local paths pointing to NFS mounts
18+
19+
### User Interface Issues
20+
- **Fixed View Backups**: Now shows a proper list of available backups instead of remote browser dialogues
21+
- **Fixed Restore Backup**: Displays backup selection interface with dates and sizes
22+
- **Enhanced backup listing**: Shows backup creation dates, file sizes, and proper selection dialogs
23+
24+
### Remote Protocol Support
25+
- **Complete remote protocol support**: `get_all_backups()` now works with NFS, SMB, FTP, SFTP, and WebDAV
26+
- **Improved error handling**: Better error messages and fallback behavior for all protocols
27+
28+
## Technical Changes
29+
30+
### Core Files Modified
31+
- `resources/lib/backup_utils.py`: Enhanced backup listing, path validation, SMB URL handling
32+
- `resources/lib/remote_browser.py`: Improved NFS browsing, SMB path conversion, user feedback
33+
- `addon.py`: Complete rewrite of backup viewing/restoration interface
34+
- `addon.xml`: Version bump and changelog update
35+
36+
### Key Improvements
37+
1. **Backup Discovery**: All remote protocols now properly list available backups
38+
2. **Path Validation**: Prevents invalid paths from causing silent failures
39+
3. **User Experience**: Clear feedback and proper dialog flows
40+
4. **Error Handling**: Comprehensive error handling for all operations
41+
42+
## Testing
43+
44+
Please test the following scenarios:
45+
1. **NFS Remote Backups**: Browse, test connection, create backups
46+
2. **SMB Remote Backups**: Path setting, backup creation, file listing
47+
3. **Local Backups**: Path validation, NFS mount compatibility
48+
4. **View/Restore Operations**: Proper backup listing and selection
49+
5. **Settings**: Remote type switching and path validation
50+
51+
## Known Issues
52+
- WebDAV timestamp sorting may not work perfectly
53+
- FTP/SFTP may have limited timestamp information in backup lists
54+
55+
## Installation
56+
Install as a standard Kodi addon zip file.
57+
58+
---
59+
*This is a test release. Please report any issues found during testing.*

service.libreelec.backupper/addon.py

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,109 @@ def log(message, level=xbmc.LOGINFO):
2222

2323
class BackupBrowser:
2424
"""GUI for browsing and restoring backups"""
25-
25+
2626
def __init__(self):
2727
self.backup_utils = BackupManager()
2828
self.remote_browser = RemoteBrowser()
29-
30-
def show_backups(self):
31-
"""Display a list of available backups"""
32-
# Use RemoteBrowser to show the browse dialog
33-
selected_file = self.remote_browser.browse_remote(mode='restore')
34-
if selected_file:
35-
success, message = self.backup_utils.restore_backup(selected_file)
36-
if not success:
37-
xbmcgui.Dialog().ok(ADDON_NAME, f"Failed to restore backup: {message}")
29+
30+
def show_backups(self, mode='view'):
31+
"""Display a list of available backups for selection
32+
mode: 'view' for viewing/listing, 'restore' for selecting to restore"""
33+
# Get list of available backups
34+
backups = self.backup_utils.get_all_backups()
35+
if not backups:
36+
xbmcgui.Dialog().ok(ADDON_NAME, "No backup files found")
37+
return
38+
39+
# Create backup options with detailed information
40+
backup_options = []
41+
for backup in backups:
42+
try:
43+
backup_name = os.path.basename(backup)
44+
45+
# Get backup date from filename (format: backup_items_timestamp.zip)
46+
# Extract timestamp from filename
47+
try:
48+
# Split by underscore and find the timestamp part
49+
parts = backup_name.replace('.zip', '').split('_')
50+
if len(parts) >= 3:
51+
# Find the timestamp (should be the last part that's all digits)
52+
timestamp_part = None
53+
for part in reversed(parts):
54+
if part.isdigit() and len(part) == 14: # YYYYMMDDHHMMSS format
55+
timestamp_part = part
56+
break
57+
58+
if timestamp_part:
59+
# Parse timestamp: YYYYMMDDHHMMSS
60+
year = int(timestamp_part[0:4])
61+
month = int(timestamp_part[4:6])
62+
day = int(timestamp_part[6:8])
63+
hour = int(timestamp_part[8:10])
64+
minute = int(timestamp_part[10:12])
65+
second = int(timestamp_part[12:14])
66+
67+
from datetime import datetime
68+
backup_date = datetime(year, month, day, hour, minute, second).strftime("%Y-%m-%d %H:%M:%S")
69+
else:
70+
backup_date = "Unknown date"
71+
else:
72+
backup_date = "Unknown date"
73+
except:
74+
backup_date = "Unknown date"
75+
76+
# Get backup size
77+
try:
78+
if self.backup_utils.location_type == 0: # Local
79+
backup_size = os.path.getsize(backup)
80+
else:
81+
# For remote backups, size info may not be available
82+
backup_size = 0
83+
backup_size_formatted = self.backup_utils.format_size(backup_size)
84+
except:
85+
backup_size_formatted = "Unknown size"
86+
87+
# Create display string
88+
display_name = f"{backup_date} - {backup_name}"
89+
if backup_size_formatted != "Unknown size":
90+
display_name += f" ({backup_size_formatted})"
91+
92+
backup_options.append((display_name, backup))
93+
except Exception as e:
94+
xbmc.log(f"Error processing backup {backup}: {str(e)}", xbmc.LOGERROR)
95+
continue
96+
97+
if not backup_options:
98+
xbmcgui.Dialog().ok(ADDON_NAME, "No valid backup files found")
99+
return
100+
101+
# Show dialog to select backup
102+
dialog = xbmcgui.Dialog()
103+
title = "Select backup to restore" if mode == 'restore' else "Available backups"
104+
selected = dialog.select(title, [opt[0] for opt in backup_options])
105+
106+
if selected == -1: # User cancelled
107+
return
108+
109+
selected_backup = backup_options[selected][1]
110+
111+
if mode == 'restore':
112+
# Confirm restore
113+
confirmed = dialog.yesno(
114+
ADDON_NAME,
115+
f"Restore backup: {os.path.basename(selected_backup)}?",
116+
"This will overwrite existing files. Continue?"
117+
)
118+
119+
if confirmed:
120+
success, message = self.backup_utils.restore_backup(selected_backup)
121+
if success:
122+
dialog.ok(ADDON_NAME, "Backup restored successfully")
123+
else:
124+
dialog.ok(ADDON_NAME, f"Failed to restore backup: {message}")
125+
else:
126+
# For view mode, just show backup info
127+
dialog.ok(ADDON_NAME, f"Backup: {os.path.basename(selected_backup)}")
38128

39129
def show_main_menu():
40130
"""Show the main menu with options"""
@@ -61,7 +151,7 @@ def show_main_menu():
61151
xbmcgui.Dialog().ok(ADDON_NAME, f"Backup failed: {message}")
62152
elif selected == 1: # Restore Backup
63153
browser = BackupBrowser()
64-
browser.show_backups()
154+
browser.show_backups(mode='restore')
65155
elif selected == 2: # Settings
66156
ADDON.openSettings()
67157

@@ -121,10 +211,10 @@ def main():
121211
backup()
122212
elif args == 'restore':
123213
browser = BackupBrowser()
124-
browser.show_backups()
214+
browser.show_backups(mode='restore')
125215
elif args == 'view':
126216
browser = BackupBrowser()
127-
browser.show_backups()
217+
browser.show_backups(mode='view')
128218
elif args == 'browse_remote':
129219
browser = RemoteBrowser()
130220
browser.browse_remote()
@@ -184,4 +274,4 @@ def main():
184274

185275
if __name__ == '__main__':
186276
# This file is only used for script functionality, not service
187-
main()
277+
main()

service.libreelec.backupper/addon.xml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="service.libreelec.backupper" name="LibreELEC Backupper" version="1.4.1.4" provider-name="Nigel">
2+
<addon id="service.libreelec.backupper" name="LibreELEC Backupper" version="1.4.1.5" provider-name="Nigel">
33
<requires>
44
<import addon="xbmc.python" version="3.0.0"/>
55
<import addon="script.module.requests" version="2.22.0"/>
@@ -14,7 +14,15 @@
1414
<license>GPL-2.0-or-later</license>
1515
<forum>https://forum.libreelec.tv/</forum>
1616
<source>https://github.com/Nigel1992/service.libreelec.backupper</source>
17-
<news>v1.4.1.4 (2026-01-04) - FIX RELEASE
17+
<news>v1.4.1.5 (2026-01-05) - TEST RELEASE
18+
- Fixed NFS remote browsing with improved user feedback and validation
19+
- Fixed SMB backup creation by correcting path format handling
20+
- Fixed View Backups and Restore Backup operations to show backup lists instead of remote browser dialogues
21+
- Enhanced get_all_backups method to support all remote protocols (NFS, SMB, FTP, SFTP, WebDAV)
22+
- Improved local backup path validation to prevent issues with NFS mount paths
23+
- Fixed backup listing to show proper dates, sizes, and selection interface
24+
25+
v1.4.1.4 (2026-01-04) - FIX RELEASE
1826
- Fixed NFS mounting with nolock option for better compatibility
1927
- Fixed SMB browsing path conversion (smb://server/share -> server:/share)
2028
- Fixed local backup path handling for NFS mounts

service.libreelec.backupper/resources/lib/backup_utils.py

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ def update_backup_location(self):
6464
if not self.backup_dir:
6565
self.backup_dir = "/storage/backup" # Default location
6666

67-
# Validate that local path doesn't contain network protocols
68-
if self.backup_dir and (self.backup_dir.startswith(('nfs:', 'smb:', 'ftp:', 'sftp:', 'http:', 'https:')) or '://' in self.backup_dir):
69-
xbmc.log(f"Invalid local path detected (contains network protocol): {self.backup_dir}", xbmc.LOGWARNING)
67+
# Validate that local path doesn't contain network protocols or remote path formats
68+
if self.backup_dir and (self.backup_dir.startswith(('nfs:', 'smb:', 'ftp:', 'sftp:', 'http:', 'https:')) or '://' in self.backup_dir or ':' in self.backup_dir):
69+
xbmc.log(f"Invalid local path detected (contains network protocol or remote path format): {self.backup_dir}", xbmc.LOGWARNING)
7070
# Reset to default if invalid
7171
self.backup_dir = "/storage/backup"
7272
self.addon.setSetting('backup_location', self.backup_dir)
@@ -145,13 +145,16 @@ def connect_remote(self):
145145
if self.remote_type == 0: # SMB
146146
# Use Kodi's built-in SMB support via xbmcvfs
147147
# Construct SMB URL properly
148+
# remote_path is in format: server/share
149+
# Convert to: smb://server/share
150+
smb_path = self.remote_path.replace(':', '/') # Just in case there's still a colon
148151
if self.remote_username and self.remote_password:
149-
remote_url = f"smb://{self.remote_username}:{urllib.parse.quote(self.remote_password)}@{self.remote_path}"
152+
remote_url = f"smb://{self.remote_username}:{urllib.parse.quote(self.remote_password)}@{smb_path}"
150153
elif self.remote_username:
151-
remote_url = f"smb://{self.remote_username}@{self.remote_path}"
154+
remote_url = f"smb://{self.remote_username}@{smb_path}"
152155
else:
153-
remote_url = f"smb://{self.remote_path}"
154-
156+
remote_url = f"smb://{smb_path}"
157+
155158
self.remote_connection = remote_url
156159
# Test connection by trying to list directory
157160
dirs, files = xbmcvfs.listdir(remote_url)
@@ -1287,60 +1290,66 @@ def create_backup(self, backup_name=None):
12871290
def get_all_backups(self):
12881291
"""Get list of all available backup files"""
12891292
self.update_backup_location()
1290-
1293+
12911294
if self.location_type == 0: # Local
12921295
backup_pattern = os.path.join(self.backup_dir, 'backup_*.zip')
12931296
return sorted(glob.glob(backup_pattern))
12941297
else: # Remote
12951298
try:
12961299
# Connect to remote location
1297-
if self.location_type == 1: # WebDAV
1298-
if not self.webdav:
1299-
self.webdav = self.connect_webdav()
1300-
if not self.webdav:
1301-
return []
1302-
1303-
# List files from WebDAV
1304-
xbmc.log(f"Listing WebDAV files from: {self.webdav_url}", xbmc.LOGINFO)
1305-
xbmc.log(f"Using WebDAV credentials: username={self.webdav_username}, password=****************", xbmc.LOGINFO)
1306-
1307-
try:
1308-
response = self.webdav.list(self.webdav_path)
1309-
xbmc.log(f"WebDAV PROPFIND response status: {response.status_code}", xbmc.LOGINFO)
1310-
xbmc.log(f"WebDAV response headers: {response.headers}", xbmc.LOGINFO)
1311-
xbmc.log(f"WebDAV response text: {response.text}", xbmc.LOGINFO)
1312-
1313-
if response.status_code == 207: # Multi-status
1314-
files = []
1315-
# Parse XML response
1316-
for line in response.text.split('\n'):
1317-
if '<D:href>' in line:
1318-
href = line.strip().replace('<D:href>', '').replace('</D:href>', '')
1319-
if href.endswith('.zip'):
1320-
files.append(href)
1321-
elif '<D:displayname>' in line:
1322-
name = line.strip().replace('<D:displayname>', '').replace('</D:displayname>', '')
1323-
if name.endswith('.zip'):
1324-
xbmc.log(f"Found backup file: {name}", xbmc.LOGINFO)
1325-
files.append(name)
1326-
1327-
# Remove duplicates and sort
1328-
files = sorted(list(set(files)))
1329-
xbmc.log(f"Final list of backup files found: {files}", xbmc.LOGINFO)
1330-
xbmc.log(f"Found {len(files)} backup files via WebDAV", xbmc.LOGINFO)
1331-
return files
1332-
else:
1333-
xbmc.log(f"WebDAV PROPFIND failed with status: {response.status_code}", xbmc.LOGERROR)
1334-
return []
1335-
1336-
except Exception as e:
1337-
xbmc.log(f"Error listing WebDAV files: {str(e)}", xbmc.LOGERROR)
1338-
return []
1300+
if not self.connect_remote():
1301+
xbmc.log("Failed to connect to remote location for listing backups", xbmc.LOGERROR)
1302+
return []
1303+
1304+
# List files based on remote type
1305+
files = self.list_remote_files()
1306+
1307+
# Filter for backup files (backup_*.zip)
1308+
backup_files = [f for f in files if f.startswith('backup_') and f.endswith('.zip')]
1309+
1310+
# Sort by modification time (newest first) if possible
1311+
try:
1312+
backup_files_with_time = []
1313+
for f in backup_files:
1314+
# Get file info if available
1315+
try:
1316+
if self.remote_type == 0: # SMB
1317+
# For SMB, we can't easily get timestamps via xbmcvfs
1318+
backup_files_with_time.append((f, 0))
1319+
elif self.remote_type == 1: # NFS
1320+
stat = os.stat(os.path.join(self.remote_connection, f))
1321+
backup_files_with_time.append((f, stat.st_mtime))
1322+
elif self.remote_type == 2: # FTP
1323+
# FTP doesn't provide easy timestamp access
1324+
backup_files_with_time.append((f, 0))
1325+
elif self.remote_type == 3: # SFTP
1326+
stat = self.remote_connection.stat(f)
1327+
backup_files_with_time.append((f, stat.st_mtime))
1328+
elif self.remote_type == 4: # WebDAV
1329+
# WebDAV timestamps are complex, use 0
1330+
backup_files_with_time.append((f, 0))
1331+
except:
1332+
backup_files_with_time.append((f, 0))
1333+
1334+
# Sort by timestamp (newest first)
1335+
backup_files_with_time.sort(key=lambda x: x[1], reverse=True)
1336+
backup_files = [f[0] for f in backup_files_with_time]
1337+
except Exception as e:
1338+
xbmc.log(f"Error sorting backup files by time: {str(e)}", xbmc.LOGWARNING)
1339+
# Fall back to alphabetical sorting
1340+
backup_files.sort(reverse=True)
1341+
1342+
xbmc.log(f"Found {len(backup_files)} backup files: {backup_files}", xbmc.LOGINFO)
1343+
return backup_files
1344+
13391345
except Exception as e:
13401346
xbmc.log(f"Error getting remote backups: {str(e)}", xbmc.LOGERROR)
13411347
import traceback
13421348
xbmc.log(f"Traceback: {traceback.format_exc()}", xbmc.LOGERROR)
13431349
return []
1350+
finally:
1351+
# Disconnect from remote location
1352+
self.disconnect_remote()
13441353

13451354
def show_rotation_warning(self):
13461355
"""Show warning dialog when enabling backup rotation"""

0 commit comments

Comments
 (0)