diff --git a/motioneye/controls/diskctl.py b/motioneye/controls/diskctl.py index 96b71f851..af52a1a27 100644 --- a/motioneye/controls/diskctl.py +++ b/motioneye/controls/diskctl.py @@ -41,9 +41,6 @@ def _list_mounts(): fstype = parts[2] opts = parts[3] - if not os.access(mount_point, os.W_OK): - continue - if target in seen_targets: continue # probably a bind mount @@ -60,6 +57,8 @@ def _list_mounts(): 'mount_point': mount_point, 'fstype': fstype, 'opts': opts, + # Used by the UI to mark unwritable mounts. + 'writable': os.access(mount_point, os.W_OK), } ) diff --git a/motioneye/static/js/main.js b/motioneye/static/js/main.js index a45cf24b9..5366d9a14 100644 --- a/motioneye/static/js/main.js +++ b/motioneye/static/js/main.js @@ -510,6 +510,13 @@ function initUI() { return true; }, ''); + makeCustomValidator($('#storageDeviceSelect'), function (value) { + if ($('#storageDeviceSelect option:selected').prop('disabled')) { + return i18n.gettext('The configured storage device is not writable. Recordings cannot be saved until write access is restored.'); + } + + return true; + }, true); makeCustomValidator($('#emailFromEntry'), function (value) { if (value && !value.match(emailValidRegExp)) { return i18n.gettext("enigu validan retpoŝtadreson"); @@ -2174,10 +2181,19 @@ function dict2CameraUi(dict) { label += '/part' + partition.part_no; } label += ' (' + partition.target + ')'; + if (partition.writable === false) { + label += ' [' + i18n.gettext('No write access') + ']'; + } - storageDeviceOptions[option] = true; + // Show unwritable mounts, but prevent selecting them. + $('#storageDeviceSelect').append( + $('') + .val(option) + .text(label) + .prop('disabled', partition.writable === false) + ); - $('#storageDeviceSelect').append(''); + storageDeviceOptions[option] = true; }); }); $('#storageDeviceSelect').append(''); diff --git a/motioneye/static/js/ui.js b/motioneye/static/js/ui.js index 65d9dccea..7a43e7ddc 100644 --- a/motioneye/static/js/ui.js +++ b/motioneye/static/js/ui.js @@ -339,7 +339,7 @@ function makeProgressBar($div) { /* validators */ -function makeCustomValidator($input, isValidFunc) { +function makeCustomValidator($input, isValidFunc, ignoreVisibility) { $input.each(function () { var element = this; @@ -353,8 +353,8 @@ function makeCustomValidator($input, isValidFunc) { function validate() { var strVal = element.value || ''; - /* An invisible element is considered always valid */ - var valid = !isVisible(element) || isValidFunc(strVal); + /* An invisible element is considered always valid, unless ignoreVisibility is set */ + var valid = (!ignoreVisibility && !isVisible(element)) || isValidFunc(strVal); /* Handle validators that return error messages or true */ var isValidResult = (valid === true); diff --git a/tests/test_diskctl.py b/tests/test_diskctl.py new file mode 100644 index 000000000..b9a8c0a46 --- /dev/null +++ b/tests/test_diskctl.py @@ -0,0 +1,57 @@ +# 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 unittest.mock import mock_open, patch + +from motioneye.controls import diskctl + + +class ListMountsTest(unittest.TestCase): + _PROC_MOUNTS = ( + '/dev/sda1 /media/usb ext4 rw,relatime 0 0\n' + '/dev/sdb1 /media/usb2 vfat rw,relatime 0 0\n' + '/dev/sdb1 /media/bind ext4 rw,relatime 0 0\n' # duplicate target (bind) + ) + + def test_includes_mounts_without_write_access(self): + # a drive the motion user cannot write to used to be filtered out on a + # missing os.W_OK, so it never appeared as a storage device (#3024) + with patch( + 'motioneye.controls.diskctl.open', mock_open(read_data=self._PROC_MOUNTS) + ), patch('motioneye.controls.diskctl.os.access', return_value=False): + mounts = diskctl._list_mounts() + + targets = [m['target'] for m in mounts] + self.assertIn('/dev/sda1', targets) + self.assertIn('/dev/sdb1', targets) + # ... but each is flagged as not writable so the UI can warn (#3024) + self.assertTrue(all(m['writable'] is False for m in mounts)) + + def test_deduplicates_bind_mounts(self): + with patch( + 'motioneye.controls.diskctl.open', mock_open(read_data=self._PROC_MOUNTS) + ), patch('motioneye.controls.diskctl.os.access', return_value=True): + mounts = diskctl._list_mounts() + + # the second /dev/sdb1 entry (a bind mount) is collapsed + self.assertEqual([m['target'] for m in mounts].count('/dev/sdb1'), 1) + self.assertEqual(len(mounts), 2) + self.assertTrue(all(m['writable'] is True for m in mounts)) + + +if __name__ == '__main__': + unittest.main()