using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace AutoIngest.Engine { /// /// Mass-storage device source: enumerates SD cards and USB sticks that appear as drive /// letters. Implements so the import coordinator can poll it /// alongside the MTP source. Each connected drive is surfaced as a /// whose is built from the volume serial number returned by /// GetVolumeInformationW (stable across reboots and drive-letter reassignment) so the device /// registry recognizes the same physical card no matter which letter it gets next time. /// /// The photo-discovery and import logic that used to live here has moved to /// ; this class is now only responsible for detection and for /// producing an per drive. /// public class SDCardMonitor : IDeviceSource { static readonly HashSet IGNORE_DRIVES = new() { 'C' }; static readonly string[] SD_KEYWORDS = { "SD", "CARD", "CAMERA", "NO NAME", "UNTITLED", "CANON", "NIKON", "SONY", "FUJI", "GOPRO", "EOS", "LUMIX", "CYBERSHOT", "ALPHA", "PENTAX", "OLYMPUS", "LEICA", "HUAWEI", "SAMSUNG", "PIXEL", "BLACKMAGIC", "DJI", "INSTA360", "MEMORY", "FLASH" }; static readonly string[] DCIM_PROBES = { "DCIM", "Pictures", "photos", "PICTURES" }; public string SourceName => "SD/USB"; public IEnumerable GetConnectedDevices() { foreach (var ch in GetCandidateLetters()) { var identity = TryIdentify(ch); if (identity != null) yield return identity; } } static IEnumerable GetCandidateLetters() { return Enumerable.Range(0, 26) .Select(i => (char)('A' + i)) .Where(ch => !IGNORE_DRIVES.Contains(ch)) .Where(ch => { string root = $"{ch}:\\"; if (!Directory.Exists(root)) return false; try { var dt = Win32.GetDriveType(root); if (dt == DriveType.DRIVE_REMOVABLE) return true; if (dt == DriveType.DRIVE_FIXED) return LooksLikeSDCard(ch, out _, out _, out _); } catch (UnauthorizedAccessException) { } catch (IOException) { } return false; }); } static DeviceIdentity? TryIdentify(char ch) { string root = $"{ch}:\\"; string label = ""; uint serial = 0; bool isRemovable; try { var dt = Win32.GetDriveType(root); isRemovable = dt == DriveType.DRIVE_REMOVABLE; // LooksLikeSDCard reads label + serial in one shot for fixed drives; for // removable drives we still want the label/serial for the registry id, so do // the read here too. bool looksLikeCamera = false; if (!isRemovable) looksLikeCamera = LooksLikeSDCard(ch, out label, out serial, out _); else ReadVolumeInfo(root, out label, out serial, out _); if (!isRemovable && !looksLikeCamera) return null; } catch (UnauthorizedAccessException) { return null; } catch (IOException) { return null; } string kind = isRemovable ? "USB" : "SD"; string id; if (serial != 0) { id = $"MS:{serial:X8}:{(label ?? "").ToUpperInvariant()}"; } else { // No serial — rare, but some virtual/removable drives don't expose one. Fall // back to a weak id (label + volume size) and flag it so the UI can warn. long sizeBytes = TryGetDriveSize(root); id = $"WEAK:{(label ?? "").ToUpperInvariant()}:{sizeBytes}"; kind = "USB-weak"; } string display = string.IsNullOrWhiteSpace(label) ? $"{ch}:\\ ({kind})" : $"{label} ({ch}:)"; return new DeviceIdentity { Id = id, DisplayName = display, Kind = kind, Photos = new MassStoragePhotoProvider(root) }; } static bool LooksLikeSDCard(char ch, out string label, out uint serial, out string fsName) { label = ""; serial = 0; fsName = ""; string root = $"{ch}:\\"; // Volume-label keyword heuristic, but also surface label/serial for the caller. ReadVolumeInfo(root, out label, out serial, out fsName); if (DCIM_PROBES.Any(p => Directory.Exists(Path.Combine(root, p)))) return true; string upper = (label ?? "").ToUpperInvariant(); return SD_KEYWORDS.Any(kw => upper.Contains(kw)); } static void ReadVolumeInfo(string root, out string label, out uint serial, out string fsName) { label = ""; serial = 0; fsName = ""; var volName = new System.Text.StringBuilder(1024); var fsBuf = new System.Text.StringBuilder(1024); if (Win32.GetVolumeInformationW(root, volName, 1024, out serial, out _, out _, fsBuf, 1024)) { label = volName.ToString(); fsName = fsBuf.ToString(); } } static long TryGetDriveSize(string root) { try { var drive = new DriveInfo(root); return drive.TotalSize; } catch (UnauthorizedAccessException) { return 0; } catch (IOException) { return 0; } catch (ArgumentException) { return 0; } } /// /// Photo provider for a drive-letter root. Enumerates via the shared mass-storage /// discovery logic in ; removal is File.Delete (the /// importer has already copied the file to its destination before calling this). /// sealed class MassStoragePhotoProvider : IPhotoProvider { readonly string _root; public MassStoragePhotoProvider(string root) { _root = root; } public List EnumeratePhotos() => DeviceImporter.FindPhotosOnRoot(_root); public bool RemoveSource(string path) { try { if (File.Exists(path)) { File.Delete(path); return true; } } catch (UnauthorizedAccessException) { } catch (IOException) { } return false; } } } public enum DriveType : uint { DRIVE_UNKNOWN = 0, DRIVE_NO_ROOT_DIR = 1, DRIVE_REMOVABLE = 2, DRIVE_FIXED = 3, DRIVE_REMOTE = 4, DRIVE_CDROM = 5, DRIVE_RAMDISK = 6 } public static class Win32 { [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern DriveType GetDriveType(string lpRootPathName); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool GetVolumeInformationW( string rootPathName, System.Text.StringBuilder volumeNameBuffer, int volumeNameSize, out uint volumeSerialNumber, out uint maximumComponentLength, out uint fileSystemFlags, System.Text.StringBuilder fileSystemNameBuffer, int fileSystemNameSize); } }