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()