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 @@ -code issuescode issues18151815 \ No newline at end of file +code issuescode issues18141814 \ 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