From fe1c687d9a4c3efc06c905a4cf75bc54bff3dbe1 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Mon, 12 Oct 2026 20:26:50 +0900 Subject: [PATCH] fix(config): do not fail on a local disk that is not mounted motion_camera_ui_to_dict() looked the partition of a local-disk storage device up with mounted_partitions[target_dev], so applying settings while the disk was not mounted raised a KeyError and failed with HTTP 500. This happens when the disk goes away after the settings were opened, and when posting back what GET returned for a camera whose disk is not mounted. Keep the stored target dir in that case. The root directory cannot be resolved without the mount point: the UI sends it relative to the mount point when the disk was mounted at load time. Co-Authored-By: Claude Opus 5.5 --- motioneye/config.py | 21 +++++++++----- tests/test_local_disk.py | 62 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 tests/test_local_disk.py diff --git a/motioneye/config.py b/motioneye/config.py index 62762ceb8..0ea7be642 100644 --- a/motioneye/config.py +++ b/motioneye/config.py @@ -1239,14 +1239,21 @@ def motion_camera_ui_to_dict(ui, prev_config=None): elif ui['storage_device'].startswith('local-disk'): target_dev = ui['storage_device'][10:].replace('-', '/') mounted_partitions = diskctl.list_mounted_partitions() - partition = mounted_partitions[target_dev] - mount_point = partition['mount_point'] + partition = mounted_partitions.get(target_dev) + if partition: + mount_point = partition['mount_point'] + + if ui['root_directory'].startswith('/'): + ui['root_directory'] = ui['root_directory'][1:] + data['target_dir'] = os.path.normpath( + os.path.join(mount_point, ui['root_directory']) + ) - if ui['root_directory'].startswith('/'): - ui['root_directory'] = ui['root_directory'][1:] - data['target_dir'] = os.path.normpath( - os.path.join(mount_point, ui['root_directory']) - ) + else: # not mounted, root_directory cannot be resolved against it + data['target_dir'] = prev_config.get('target_dir', data['target_dir']) + logging.error( + f'could not find mounted partition for device "{target_dev}", keeping target dir "{data["target_dir"]}"' + ) # try to create the target dir try: diff --git a/tests/test_local_disk.py b/tests/test_local_disk.py new file mode 100644 index 000000000..eca29f7d6 --- /dev/null +++ b/tests/test_local_disk.py @@ -0,0 +1,62 @@ +# Copyright (c) 2013 Calin Crisan +# This file is part of motionEye. +# +# motionEye is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from motioneye import config +from tests.test_notification_settings import _ConfigCase + +_PARTITION = {'target': '/dev/sda1', 'mount_point': '/media/usb'} + + +class LocalDiskTest(_ConfigCase): + def setUp(self): + super().setUp() + self.camera['@storage_device'] = 'local-disk-dev-sda1' + self.camera['target_dir'] = '/media/usb/Camera1' + self.partitions = self._patch( + 'motioneye.controls.diskctl.list_mounted_partitions', return_value={} + ) + + def _round_trip(self): + # GET /config/1/get/ and POST the same settings back to /config/1/set/ + ui = config.motion_camera_dict_to_ui(self.camera) + return config.motion_camera_ui_to_dict(ui, self.camera) + + def test_mounted_disk_round_trips(self): + self.partitions.return_value = {'/dev/sda1': _PARTITION} + data = self._round_trip() + self.assertEqual('local-disk-dev-sda1', data['@storage_device']) + self.assertEqual('/media/usb/Camera1', data['target_dir']) + + def test_unmounted_disk_keeps_the_target_dir(self): + data = self._round_trip() + self.assertEqual('local-disk-dev-sda1', data['@storage_device']) + self.assertEqual('/media/usb/Camera1', data['target_dir']) + + def test_disk_unmounted_after_the_settings_were_opened(self): + self.partitions.return_value = {'/dev/sda1': _PARTITION} + ui = config.motion_camera_dict_to_ui(self.camera) + self.assertEqual('/Camera1', ui['root_directory']) # relative to the mount + + self.partitions.return_value = {} + data = config.motion_camera_ui_to_dict(ui, self.camera) + self.assertEqual('local-disk-dev-sda1', data['@storage_device']) + self.assertEqual('/media/usb/Camera1', data['target_dir']) + + +if __name__ == '__main__': + unittest.main()