diff --git a/.github/badges/code_issues.svg b/.github/badges/code_issues.svg
index 4387fdc0..8508a2c2 100644
--- a/.github/badges/code_issues.svg
+++ b/.github/badges/code_issues.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/code/+nansen/+config/+addons/@AddonManager/AddonManager.m b/code/+nansen/+config/+addons/@AddonManager/AddonManager.m
index 84a3f258..c0179a3f 100644
--- a/code/+nansen/+config/+addons/@AddonManager/AddonManager.m
+++ b/code/+nansen/+config/+addons/@AddonManager/AddonManager.m
@@ -605,6 +605,9 @@ function checkIfAddonsAreOnPath()
% Rename folder to remove main/master tag
renamedDir = fullfile(rootDir, newName);
+ if isfolder(renamedDir)
+ rmdir(renamedDir, 's')
+ end
movefile(newDir, renamedDir)
folderPath = renamedDir;
end
diff --git a/code/+nansen/+config/+dloc/@LocalRootPathManager/LocalRootPathManager.m b/code/+nansen/+config/+dloc/@LocalRootPathManager/LocalRootPathManager.m
index 0121ccf9..d29b5588 100644
--- a/code/+nansen/+config/+dloc/@LocalRootPathManager/LocalRootPathManager.m
+++ b/code/+nansen/+config/+dloc/@LocalRootPathManager/LocalRootPathManager.m
@@ -164,7 +164,7 @@ function configureLocalRootPath(obj, localRootPath, originalRootPath)
% name of the disk and the current letter assignment.
if ispc
- volumeInfo = nansen.external.fex.sysutil.listPhysicalDrives();
+ volumeInfo = nansen.external.fex.sysutil.listMountedDrives();
for i = 1:numel(data) % Loop through DataLocations
if ~isfield(data(i), 'RootPath')
@@ -245,7 +245,7 @@ function updateVolumeInfo(obj, volumeInfo)
% updateVolumeInfo(obj) updates volume info using system utilities.
% updateVolumeInfo(obj, volumeInfo) uses the provided volume info.
- import nansen.external.fex.sysutil.listPhysicalDrives
+ import nansen.external.fex.sysutil.listMountedDrives
if nargin < 2
volumeInfo = listPhysicalDrives();
end
diff --git a/code/+nansen/+dataio/+dialog/editDataLocationRootDeviceName.m b/code/+nansen/+dataio/+dialog/editDataLocationRootDeviceName.m
index 4dad4563..16e58265 100644
--- a/code/+nansen/+dataio/+dialog/editDataLocationRootDeviceName.m
+++ b/code/+nansen/+dataio/+dialog/editDataLocationRootDeviceName.m
@@ -10,13 +10,17 @@
% [ ] Is it possible to indicate that the diskname is a dropdown?
% [ ] Update dropdowns if drives are connected or disconnected
- if isunix && ~ismac
- errordlg('This feature is not implemented for linux/unix systems')
- return
+ try
+ volumeInfo = nansen.external.fex.sysutil.listMountedDrives();
+ catch MECause
+ ME = MException('NANSEN:DataIO:FailedToListDrives', ...
+ ['Failed to list mounted drives using system command. ' ...
+ 'Please report if you see this error.']);
+ ME = ME.addCause(MECause);
+ errordlg('Failed to list mounted drives using system command. See MATLAB''s command window for details.')
+ throw(ME)
end
-
- volumeInfo = nansen.external.fex.sysutil.listPhysicalDrives();
-
+
if ~isfield(dataLocationRootInfo, 'DiskType')
[dataLocationRootInfo(:).DiskType] = deal('External');
end
diff --git a/code/+nansen/+internal/+system/DiskConnectionMonitor.m b/code/+nansen/+internal/+system/DiskConnectionMonitor.m
index c6bca582..84a4d4d6 100644
--- a/code/+nansen/+internal/+system/DiskConnectionMonitor.m
+++ b/code/+nansen/+internal/+system/DiskConnectionMonitor.m
@@ -1,5 +1,10 @@
classdef DiskConnectionMonitor < handle
+ % Todo
+ % - Streamline getting drive info from nansen.external.fex.sysutil.listMountedDrives
+ % - Create event data?
+ % - Use drive instead of disk in class/method/event names
+
properties (Dependent)
TimerUpdateInterval
end
@@ -22,11 +27,15 @@
methods
function obj = DiskConnectionMonitor
- if ismac || ispc
- obj.initializeTimer()
- else
- % pass (not implemented for linux)
+
+ if ispc || (isunix && ~ismac)
+ if ~obj.checkListDrivesWorks()
+ obj.displayListDrivesNotWorkingWarning()
+ return
+ end
end
+
+ obj.initializeTimer()
end
function delete(obj)
@@ -106,7 +115,7 @@ function updateDiskList(obj, updatedVolumeList)
function checkDiskPc(obj)
%volumeList = system.
- volumeInfoTable = nansen.external.fex.sysutil.listPhysicalDrives();
+ volumeInfoTable = nansen.external.fex.sysutil.listMountedDrives();
% Convert string array to cell array of character vectors in
% order to create struct array below
@@ -130,7 +139,36 @@ function checkDiskMac(obj)
end
function checkDiskUnix(obj)
- error('Not implemented yet')
+ volumeInfoTable = nansen.external.fex.sysutil.listMountedDrives();
+
+ % Convert string array to cell array of character vectors in
+ % order to create struct array below
+ string2cellchar = @(strArray) arrayfun(@char, strArray, 'uni', false); %convertStringsToChars, cellstr
+ volumeList = struct('Name', string2cellchar(volumeInfoTable.VolumeName) );
+
+ obj.updateDiskList(volumeList)
+ end
+
+ function tf = checkListDrivesWorks(~)
+ persistent listDrivesWorks
+ if isempty(listDrivesWorks)
+ try
+ nansen.external.fex.sysutil.listMountedDrives()
+ listDrivesWorks = true;
+ catch
+ listDrivesWorks = false;
+ end
+ end
+ tf = listDrivesWorks;
+ end
+
+ function displayListDrivesNotWorkingWarning(~)
+ nansen.common.tracelesswarning(sprintf([...
+ 'Failed to list mounted drives using system command.\nIf you ', ...
+ 'want NANSEN to dynamically update when drives are ', ...
+ 'connected/disconnected, please run ', ...
+ '`nansen.external.fex.sysutil.listMountedDrives` and ', ...
+ 'report the error you are seeing.']))
end
end
diff --git a/code/+nansen/+external/+fex/+datautil/merge_tables.m b/code/external/fileexchange/+nansen/+external/+fex/+datautil/merge_tables.m
similarity index 100%
rename from code/+nansen/+external/+fex/+datautil/merge_tables.m
rename to code/external/fileexchange/+nansen/+external/+fex/+datautil/merge_tables.m
diff --git a/code/+nansen/+external/+fex/+sysutil/listPhysicalDrives.m b/code/external/fileexchange/+nansen/+external/+fex/+sysutil/listMountedDrives.m
similarity index 50%
rename from code/+nansen/+external/+fex/+sysutil/listPhysicalDrives.m
rename to code/external/fileexchange/+nansen/+external/+fex/+sysutil/listMountedDrives.m
index a5fee343..610c9726 100755
--- a/code/+nansen/+external/+fex/+sysutil/listPhysicalDrives.m
+++ b/code/external/fileexchange/+nansen/+external/+fex/+sysutil/listMountedDrives.m
@@ -1,7 +1,7 @@
-function infoTable = listPhysicalDrives()
-%listPhysicalDrives List physical drives present on system
+function infoTable = listMountedDrives()
+%listMountedDrives List mounted drives present on system
%
-% infoTable = system.listPhysicalDrives() returns a table which contains
+% infoTable = sysutil.drive.listMountedDrives() returns a table which contains
% the following variables:
%
% DeviceID : The device id (drive letter / disk number)
@@ -16,32 +16,42 @@
% PC : https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logicaldisk
% Written by Eivind Hennestad | 2022-11-24
+% Updated by Claude Sonnet 4.5 | 2025-11-13
% Todo:
-% [ ] Implement for linux systems
% [ ] Add internal, external (how to get this on pc?)
-% [ ] On mac, file system is not correct...
-% [ ] On mac, don't show hidden partitions?
-% [ ] On windows, is the serial number complete?
-% [ ] On mac, add serial number
+% [ ] On mac, file system is not correct...
+% [ ] On mac, don't show hidden partitions?
+% [ ] On mac, add serial number
% [ ] On mac, parse result when using -plist instead?
-% [ ] On windows, use 'where drivetype=3' i.e 'wmic logicaldisk where drivetype=3 get ...'
+% [ ] On linux, add serial number (use lsblk -o +UUID or blkid)
- if ismac
- [~, infoStr] = system('diskutil list physical');
- infoTable = convertListToTableMac(infoStr);
-
- elseif ispc
- [~, infoStr] = system(['wmic logicaldisk get DeviceId, ', ...
- 'VolumeName, VolumeSerialNumber, FileSystem, Size, ', ...
- 'DriveType' ] );
- infoTable = convertListToTablePc(infoStr);
-
- elseif isunix
- error('Not implemented for unix systems')
+ try
+ if ismac % Use diskutil
+ [~, infoStr] = system('diskutil list physical');
+ infoTable = convertListToTableMac(infoStr);
+
+ elseif ispc % Use PowerShell Get-Volume
+ [~, infoStr] = system(['powershell -Command "Get-Volume | ', ...
+ 'Where-Object {$_.DriveLetter} | ', ...
+ 'Select-Object DriveLetter, FileSystemLabel, FileSystem, ', ...
+ 'Size, DriveType | ', ...
+ 'ConvertTo-Csv -NoTypeInformation"']);
+ infoTable = convertListToTablePc(infoStr);
+
+ elseif isunix % Use lsblk
+ [~, infoStr] = system('lsblk -o NAME,LABEL,FSTYPE,SIZE,TYPE,MOUNTPOINT -P -b');
+ infoTable = convertListToTableLinux(infoStr);
+ end
+
+ infoTable = postprocessTable(infoTable);
+
+ catch MECause
+ ME = MException('SYSTEMUTIL:ListMountedDrives:FailedToListDrives', ...
+ 'Failed to list mounted drives using system command.');
+ ME = ME.addCause(MECause);
+ throw(ME)
end
-
- infoTable = postprocessTable(infoTable);
end
% % Local functions:
@@ -59,7 +69,7 @@
infostrCell = splitStringIntoRows(infoStr);
rowIdxRemove = strncmp(infostrCell, '/dev', 4);
-
+
% Keep track of rows belonging to same drive / device
deviceNumber = cumsum(rowIdxRemove);
deviceHeaders = infostrCell(rowIdxRemove);
@@ -94,7 +104,7 @@
expression = '\((.*)\)';
driveType = regexp(deviceHeaders, expression, 'tokens');
driveTypeColumnData = arrayfun(@(x) driveType{x}{1}{1}, deviceNumber, 'uni', 0);
-
+
colIdx = size(C, 2) + 1;
C(:, colIdx) = driveTypeColumnData;
@@ -112,32 +122,50 @@
function infoTable = convertListToTablePc(infoStr)
+ % Parse CSV output from PowerShell Get-Volume
infostrCell = splitStringIntoRows(infoStr);
- % Detect indices where rows should be split
- colStart = regexp(infostrCell{1}, '(?<=\ )\S{1}', 'start');
- colStart = [1, colStart];
+ % Remove quotes and split by comma
+ C = cellfun(@(row) strsplit(strrep(row, '"', ''), ','), ...
+ infostrCell, 'UniformOutput', false);
+ C = vertcat(C{:});
- C = splitRowsIntoColumns(infostrCell, colStart);
+ % Rename columns to match expected format
+ C{1,1} = 'DeviceID';
+ C{1,2} = 'VolumeName';
+ C{1,3} = 'FileSystem';
+ C{1,4} = 'Size';
+ C{1,5} = 'DriveType';
- %C{1,6} = 'SerialNumber'; % Shorten name
- C = strrep(C, 'VolumeSerialNumber', 'SerialNumber');
- infoTable = cell2table(C(2:end,:), 'VariableNames',C(1,:));
+ % Add colon to drive letters
+ C(2:end, 1) = cellfun(@(x) [x, ':'], C(2:end, 1), 'UniformOutput', false);
+
+ infoTable = cell2table(C(2:end,:), 'VariableNames', C(1,:));
% Compute size and add unit
- infoTable.Size = str2double( infoTable.Size );
+ infoTable.Size = str2double(infoTable.Size);
power = floor(log10(infoTable.Size)/3)*3;
infoTable.Size = infoTable.Size ./ 10.^(power);
sizeUnit = categorical(power, [3, 6, 9, 12], {'kB', 'MB', 'GB', 'TB'});
infoTable = addvars(infoTable, sizeUnit, 'NewVariableNames', 'SizeUnit');
-
+
+ % Add empty SerialNumber column (Get-Volume doesn't provide this easily)
+ serialNumber = repmat(missing, size(infoTable, 1), 1);
+ infoTable = addvars(infoTable, serialNumber, 'NewVariableNames', 'SerialNumber');
+
+ % Label drive types (handle empty values)
+ for i = 1:height(infoTable)
+ if isempty(infoTable.DriveType{i}) || strcmp(infoTable.DriveType{i}, '')
+ infoTable.DriveType{i} = '3'; % Default to Fixed for empty values
+ end
+ end
infoTable.DriveType = labelDriveTypePC(infoTable.DriveType);
end
function infoStrCell = splitStringIntoRows(infoStr)
-
+
% Split string into rows
infoStrCell = textscan( infoStr, '%s', 'delimiter', '\n' );
infoStrCell = infoStrCell{1};
@@ -147,7 +175,7 @@
end
function C = splitRowsIntoColumns(infostrCell, splitIdx)
-
+
numRows = numel(infostrCell);
numColumns = numel(splitIdx);
@@ -165,23 +193,107 @@
colIdx = splitIdx(i) : splitIdx(i+1)-1;
C(:, i) = cellfun(@(str) str(colIdx), infostrCell, 'uni', 0);
end
-
+
C = strtrim(C); % Remove trailing whitespace from all cells
end
function driveType = labelDriveTypePC(driveType)
-
- % 0 Unknown
- % 1 No Root Directory
- % 2 Removable Disk
- % 3 Local Disk
- % 4 Network Drive
- % 5 Compact Disc
- % 6 RAM Disk
-
- driveType = categorical(driveType, {'0','1','2','3','4','5','6'}, ...
- {'Unknown', 'No Root Directory', 'Removable Disk', 'Local Disk', ...
- 'Network Drive', 'Compact Disc', 'RAM Disk'});
+
+ % Map Get-Volume DriveType values to descriptive names
+ % Get-Volume returns: Unknown=0, Fixed=3, Removable=2, CD-ROM=5, Network=4
+
+ driveType = categorical(driveType, {'0','2','3','4','5'}, ...
+ {'Unknown', 'Removable', 'Fixed', 'Network', 'CD-ROM'});
+end
+
+function infoTable = convertListToTableLinux(infoStr)
+
+ % Parse lsblk output (key="value" format)
+ infostrCell = splitStringIntoRows(infoStr);
+
+ % Keep only mounted drives (has MOUNTPOINT)
+ mountedIdx = contains(infostrCell, 'MOUNTPOINT="/') | contains(infostrCell, 'MOUNTPOINT="/boot');
+ infostrCell = infostrCell(mountedIdx);
+
+ if isempty(infostrCell)
+ % Create empty table with correct structure
+ infoTable = cell2table(cell(0, 7), 'VariableNames', ...
+ {'DeviceID', 'VolumeName', 'SerialNumber', 'FileSystem', 'Size', 'SizeUnit', 'DriveType'});
+ return;
+ end
+
+ numRows = numel(infostrCell);
+ C = cell(numRows, 7);
+
+ for i = 1:numRows
+ row = infostrCell{i};
+
+ % Extract key-value pairs
+ name = extractValue(row, 'NAME');
+ label = extractValue(row, 'LABEL');
+ fstype = extractValue(row, 'FSTYPE');
+ sizeBytes = extractValue(row, 'SIZE');
+ driveType = extractValue(row, 'TYPE');
+ mountpoint = extractValue(row, 'MOUNTPOINT');
+
+ % DeviceID: /dev/name
+ C{i, 1} = ['/dev/', name];
+
+ % VolumeName: use label if available, otherwise mountpoint
+ if isempty(label)
+ C{i, 2} = mountpoint;
+ else
+ C{i, 2} = label;
+ end
+
+ % SerialNumber: not easily available with lsblk
+ C{i, 3} = '';
+
+ % FileSystem
+ C{i, 4} = fstype;
+
+ % Size (in bytes, will convert later)
+ C{i, 5} = sizeBytes;
+
+ % SizeUnit (placeholder, will be computed)
+ C{i, 6} = 'B';
+
+ % DriveType
+ if strcmp(driveType, 'disk')
+ C{i, 7} = 'Fixed';
+ elseif strcmp(driveType, 'part')
+ C{i, 7} = 'Partition';
+ elseif strcmp(driveType, 'loop')
+ C{i, 7} = 'Loop';
+ elseif strcmp(driveType, 'rom')
+ C{i, 7} = 'CD-ROM';
+ else
+ C{i, 7} = driveType;
+ end
+ end
+
+ infoTable = cell2table(C, 'VariableNames', ...
+ {'DeviceID', 'VolumeName', 'SerialNumber', 'FileSystem', 'Size', 'SizeUnit', 'DriveType'});
+
+ % Convert size to numeric and compute appropriate unit
+ infoTable.Size = str2double(infoTable.Size);
+
+ power = floor(log10(infoTable.Size)/3)*3;
+ infoTable.Size = infoTable.Size ./ 10.^(power);
+
+ sizeUnit = categorical(power, [3, 6, 9, 12], {'kB', 'MB', 'GB', 'TB'});
+ infoTable.SizeUnit = sizeUnit;
+end
+
+function value = extractValue(str, key)
+ % Extract value from KEY="VALUE" format
+ pattern = [key, '="([^"]*)"'];
+ tokens = regexp(str, pattern, 'tokens');
+ if ~isempty(tokens)
+ value = tokens{1}{1};
+ else
+ value = '';
+ end
end
function infoTable = postprocessTable(infoTable)
@@ -207,21 +319,3 @@
isEmptyCell = cellfun(@isempty, cellArray);
cellArray( isEmptyCell ) = [];
end
-
-% % % function filename = filewrite(filename, textString)
-% % %
-% % % if isempty(filename)
-% % % filename = [tempname, '.txt'];
-% % % end
-% % %
-% % % fid = fopen(filename, 'w');
-% % % fwrite(fid, textString);
-% % % fclose(fid);
-% % % end
-% % %
-% % % [~, infoStr] = system('diskutil list -plist physical');
-% % %
-% % % filename = [tempname, '.xml'];
-% % % filename = filewrite(filename, infoStr);
-% % %
-% % % convertedValue = readstruct(filename);
diff --git a/code/+nansen/+external/+fex/+sysutil/listPhysicalDrives_license.txt b/code/external/fileexchange/+nansen/+external/+fex/+sysutil/listPhysicalDrives_license.txt
similarity index 100%
rename from code/+nansen/+external/+fex/+sysutil/listPhysicalDrives_license.txt
rename to code/external/fileexchange/+nansen/+external/+fex/+sysutil/listPhysicalDrives_license.txt