AutoIngest/AutoIngest/Engine/SDCardMonitor.cs

208 lines
7.8 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace AutoIngest.Engine
{
/// <summary>
/// Mass-storage device source: enumerates SD cards and USB sticks that appear as drive
/// letters. Implements <see cref="IDeviceSource"/> so the import coordinator can poll it
/// alongside the MTP source. Each connected drive is surfaced as a <see cref="DeviceIdentity"/>
/// whose <see cref="DeviceIdentity.Id"/> 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
/// <see cref="DeviceImporter"/>; this class is now only responsible for detection and for
/// producing an <see cref="IPhotoProvider"/> per drive.
/// </summary>
public class SDCardMonitor : IDeviceSource
{
static readonly HashSet<char> 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<DeviceIdentity> GetConnectedDevices()
{
foreach (var ch in GetCandidateLetters())
{
var identity = TryIdentify(ch);
if (identity != null) yield return identity;
}
}
static IEnumerable<char> 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; }
}
/// <summary>
/// Photo provider for a drive-letter root. Enumerates via the shared mass-storage
/// discovery logic in <see cref="DeviceImporter"/>; removal is File.Delete (the
/// importer has already copied the file to its destination before calling this).
/// </summary>
sealed class MassStoragePhotoProvider : IPhotoProvider
{
readonly string _root;
public MassStoragePhotoProvider(string root) { _root = root; }
public List<string> 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);
}
}