208 lines
7.8 KiB
C#
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);
|
|
}
|
|
}
|