Skip to content

Commit e6367b9

Browse files
author
Nigel
committed
Fix restore from backup failing due to read-only file system (Issue #1)
- Added mount_addons_rw() and mount_addons_ro() methods to handle addons directory mounting - Enhanced restore_file() to automatically mount/unmount addons directory during restore - Fixed path resolution in restore_backup() to correctly map addons paths - Improved error handling for filesystem operations - Updated version to 1.4.1.2 Fixes #1
1 parent e39f926 commit e6367b9

3 files changed

Lines changed: 186 additions & 4 deletions

File tree

release_notes_1.4.1.2.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# LibreELEC Backupper v1.4.1.2
2+
3+
## What's New in 1.4.1.2 (December 30, 2025)
4+
5+
### 🐛 Bug Fixes
6+
- **Fixed restore from backup failing due to read-only file system error**
7+
- Added automatic mounting/unmounting of addons directory during restore operations
8+
- Fixed path resolution for addons and repository files during restore
9+
- Restore now properly handles read-only filesystems on LibreELEC systems
10+
- Improved error handling for filesystem mount operations
11+
12+
### 🔧 Technical Improvements
13+
- Added `mount_addons_rw()` and `mount_addons_ro()` methods to handle addons directory mounting
14+
- Enhanced `restore_file()` method to detect and handle addons directory restoration
15+
- Fixed path resolution in `restore_backup()` to correctly map addons paths to `kodi_home/addons`
16+
- Improved filesystem mount detection and error reporting
17+
18+
## Issue Fixed
19+
This release fixes GitHub issue #1: "Restore from backup not working due to Read-only file system"
20+
21+
The restore process was failing when trying to restore addons because the addons directory was mounted as read-only. The addon now automatically:
22+
1. Detects when restoring addons
23+
2. Mounts the addons directory as read-write
24+
3. Performs the restore operation
25+
4. Remounts the directory as read-only
26+
27+
## Installation
28+
1. Download the latest release
29+
2. Install through Kodi's Add-on Manager
30+
3. The fix will automatically apply to all restore operations
31+
32+
## Support
33+
If you encounter any issues, please:
34+
1. Check the [Troubleshooting Guide](https://github.com/Nigel1992/service.libreelec.backupper/wiki/Troubleshooting)
35+
2. Report bugs on our [Issues Page](https://github.com/Nigel1992/service.libreelec.backupper/issues)
36+
37+
Thank you for using LibreELEC Backupper!
38+

service.libreelec.backupper/addon.xml

Lines changed: 7 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.1" provider-name="Nigel">
2+
<addon id="service.libreelec.backupper" name="LibreELEC Backupper" version="1.4.1.2" 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,12 @@
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 (2025-03-25)
17+
<news>v1.4.1.2 (2025-12-30)
18+
- Fixed restore from backup failing due to read-only file system error
19+
- Added automatic mounting/unmounting of addons directory during restore
20+
- Improved path resolution for addons and repository files during restore
21+
22+
v1.4.1 (2025-03-25)
1823
- Enhanced main menu UI with last backup and next scheduled backup information
1924
- Improved menu layout with visual separation between actions and information
2025

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

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,8 +1599,89 @@ def mount_userdata_ro(self):
15991599
xbmc.log(f"Error mounting userdata as read-only: {str(e)}", xbmc.LOGERROR)
16001600
return False
16011601

1602+
def mount_addons_rw(self):
1603+
"""Mount addons directory in read-write mode"""
1604+
try:
1605+
# Get the actual path for addons
1606+
addons_path = os.path.join(self.kodi_home, 'addons')
1607+
1608+
# Check if addons directory is already writable
1609+
test_file = os.path.join(addons_path, '.write_test')
1610+
try:
1611+
os.makedirs(addons_path, exist_ok=True)
1612+
with open(test_file, 'w') as f:
1613+
f.write('test')
1614+
os.remove(test_file)
1615+
xbmc.log("Addons directory is already writable", xbmc.LOGINFO)
1616+
return True
1617+
except (IOError, PermissionError, OSError):
1618+
xbmc.log("Addons directory is not writable, attempting to remount", xbmc.LOGINFO)
1619+
1620+
# Find the mount point that contains addons
1621+
mount_info = subprocess.run(['mount'], capture_output=True, text=True, check=True)
1622+
mount_lines = mount_info.stdout.splitlines()
1623+
1624+
addons_mount = None
1625+
for line in mount_lines:
1626+
parts = line.split()
1627+
if len(parts) >= 3 and addons_path.startswith(parts[2]):
1628+
addons_mount = parts[2]
1629+
break
1630+
1631+
if addons_mount:
1632+
xbmc.log(f"Mounting {addons_mount} as read-write", xbmc.LOGINFO)
1633+
subprocess.run(['mount', '-o', 'remount,rw', addons_mount], check=True)
1634+
1635+
# Verify it's now writable
1636+
try:
1637+
os.makedirs(addons_path, exist_ok=True)
1638+
with open(test_file, 'w') as f:
1639+
f.write('test')
1640+
os.remove(test_file)
1641+
xbmc.log("Verified addons directory is now writable", xbmc.LOGINFO)
1642+
return True
1643+
except (IOError, PermissionError, OSError):
1644+
xbmc.log("Addons directory is still not writable after remount", xbmc.LOGERROR)
1645+
return False
1646+
else:
1647+
xbmc.log(f"Could not find mount point for addons: {addons_path}", xbmc.LOGERROR)
1648+
return False
1649+
1650+
except Exception as e:
1651+
xbmc.log(f"Error mounting addons as read-write: {str(e)}", xbmc.LOGERROR)
1652+
return False
1653+
1654+
def mount_addons_ro(self):
1655+
"""Mount addons directory back in read-only mode"""
1656+
try:
1657+
# Get the actual path for addons
1658+
addons_path = os.path.join(self.kodi_home, 'addons')
1659+
1660+
# Find the mount point that contains addons
1661+
mount_info = subprocess.run(['mount'], capture_output=True, text=True, check=True)
1662+
mount_lines = mount_info.stdout.splitlines()
1663+
1664+
addons_mount = None
1665+
for line in mount_lines:
1666+
parts = line.split()
1667+
if len(parts) >= 3 and addons_path.startswith(parts[2]):
1668+
addons_mount = parts[2]
1669+
break
1670+
1671+
if addons_mount:
1672+
xbmc.log(f"Remounting {addons_mount} as read-only", xbmc.LOGINFO)
1673+
subprocess.run(['mount', '-o', 'remount,ro', addons_mount], check=True)
1674+
return True
1675+
else:
1676+
xbmc.log(f"Could not find mount point for addons: {addons_path}", xbmc.LOGERROR)
1677+
return False
1678+
1679+
except Exception as e:
1680+
xbmc.log(f"Error mounting addons as read-only: {str(e)}", xbmc.LOGERROR)
1681+
return False
1682+
16021683
def restore_file(self, zip_file, file_info, extract_path):
1603-
"""Restore a single file with special handling for config.txt and userdata"""
1684+
"""Restore a single file with special handling for config.txt, userdata, and addons"""
16041685
try:
16051686
# Handle configuration files that need /flash to be writable
16061687
if extract_path == '/flash/config.txt' or extract_path.startswith('/flash/'):
@@ -1693,6 +1774,55 @@ def restore_file(self, zip_file, file_info, extract_path):
16931774

16941775
return True, None
16951776

1777+
# Handle addons files
1778+
elif extract_path.startswith(os.path.join(self.kodi_home, 'addons')):
1779+
xbmc.log(f"Preparing to restore addon file: {extract_path}", xbmc.LOGINFO)
1780+
1781+
# Mount addons directory in read-write mode
1782+
if not self.mount_addons_rw():
1783+
xbmc.log("Failed to mount addons directory in read-write mode", xbmc.LOGERROR)
1784+
return False, "Failed to mount addons directory in read-write mode"
1785+
1786+
xbmc.log("Addons directory mounted in read-write mode", xbmc.LOGINFO)
1787+
restore_success = False
1788+
1789+
try:
1790+
# Ensure the directory exists
1791+
os.makedirs(os.path.dirname(extract_path), exist_ok=True)
1792+
1793+
# Extract the file
1794+
with zip_file.open(file_info) as source, open(extract_path, 'wb') as target:
1795+
shutil.copyfileobj(source, target)
1796+
1797+
xbmc.log(f"Addon file extracted successfully: {extract_path}", xbmc.LOGINFO)
1798+
1799+
# Ensure proper permissions (644 for files, 755 for directories)
1800+
if os.path.isdir(extract_path):
1801+
os.chmod(extract_path, 0o755)
1802+
else:
1803+
os.chmod(extract_path, 0o644)
1804+
1805+
restore_success = True
1806+
except Exception as e:
1807+
xbmc.log(f"Error during addon file restore: {str(e)}", xbmc.LOGERROR)
1808+
raise e
1809+
finally:
1810+
# Always try to remount as read-only
1811+
xbmc.log("Attempting to remount addons directory as read-only", xbmc.LOGINFO)
1812+
if not self.mount_addons_ro():
1813+
error_msg = "Warning: Failed to remount addons directory as read-only"
1814+
xbmc.log(error_msg, xbmc.LOGWARNING)
1815+
# If restore was successful but remount failed, still warn the user
1816+
if restore_success:
1817+
self.notify(error_msg)
1818+
else:
1819+
xbmc.log("Addons directory remounted as read-only", xbmc.LOGINFO)
1820+
1821+
if not restore_success:
1822+
return False, f"Failed to restore {os.path.basename(extract_path)}"
1823+
1824+
return True, None
1825+
16961826
else:
16971827
# Normal file extraction
16981828
# Ensure the directory exists
@@ -1865,8 +1995,17 @@ def restore_backup(self, backup_file=None):
18651995
if file_info.filename.startswith('userdata/'):
18661996
# Handle userdata paths correctly
18671997
extract_path = os.path.join(self.kodi_userdata, os.path.relpath(file_info.filename, 'userdata'))
1998+
elif file_info.filename.startswith('addons/'):
1999+
# Handle addons paths correctly
2000+
extract_path = os.path.join(self.kodi_home, file_info.filename)
2001+
elif file_info.filename.startswith('flash/'):
2002+
# Handle flash paths correctly
2003+
extract_path = os.path.join('/', file_info.filename)
2004+
elif file_info.filename.startswith('repo/'):
2005+
# Handle repository paths correctly (repositories are in addons directory)
2006+
extract_path = os.path.join(self.kodi_home, 'addons', os.path.relpath(file_info.filename, 'repo'))
18682007
else:
1869-
# Handle all other files
2008+
# Handle all other files (assume they're relative to root)
18702009
extract_path = os.path.join('/', file_info.filename)
18712010

18722011
# Restore the file with special handling for config.txt

0 commit comments

Comments
 (0)