65.9K
CodeProject 正在变化。 阅读更多。
Home

外部驱动器库:第 1 部分 - 处理 USB 连接设备

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (29投票s)

2017年11月5日

LGPL3

18分钟阅读

viewsIcon

65378

downloadIcon

1573

轻松读取和写入通过USB连接的任何Android手机/平板电脑/iPhone/iPad上的文件。

 

[从Github下载库]

引言

最近,我需要以编程方式处理(即读取/写入)通过USB连接的任何Android手机/平板电脑/iPhone/iPad上的文件。

这应该很容易...但是当您枚举DriveInfo.GetDrives时,这些设备不会显示为驱动器,因此无法访问它们上的文件/文件夹。

这就是我的库要解决的问题

// a0 - first Android drive
drive_root.inst.parse_folder("[a0]:/*/dcim/camera").files.First()
    .copy_sync("C:/my_camera");

背景

我创建的库旨在处理任何外部驱动器 - 任何可以通过即插即用连接/断开的设备。我将处理的问题之一(所有这些都是另一篇文章的主题)是,您可能会连接一个外部驱动器,它最终可能成为“F:驱动器”,但在稍后,同一个驱动器将成为“H:驱动器”。您是否不想有一种方法可以唯一地将文件名/文件夹名标识到特定的外部驱动器/SD卡/手机/平板电脑?

在实现代码并处理了许多Windows特有的问题后,我决定免费提供它。

处理USB驱动器

如果您稍微玩弄一下外部设备,您会注意到一件有趣的事情:外部驱动器、U盘、CD/DVD、SD卡会自动挂载:一旦插入,它们就会被识别为新驱动器。只需枚举新驱动器,然后查看它们上的文件/文件夹。

// you'll find the new drive here
foreach ( var drive in DriveInfo.GetDrives())
  Console.WriteLine(drive.Name);

除了便携式USB设备(如您的手机或平板电脑)之外,其他都如此...

它们会显示在“我的电脑”(Win-E)中,作为一种虚拟驱动器,没有驱动器号(因此,无法通过常规的DriveInfo API访问)。

经过一番搜索,我终于明白了:使用shell32.dll的API访问虚拟文件夹 - USB驱动器将显示在那里。这将会是一段艰难的旅程,但一切都是值得的!

便携设备 vs Android vs 手机/平板

该库可让您访问任何便携设备,这些设备基本上实现了shell32.dll的Windows Shell接口,该接口允许访问其“驱动器”。这可以是以下任何一种:

  • Android设备
  • Android手机/iPhone
  • Android平板电脑/iPad
  • 通过USB连接的相机
  • 其他任何“注册”自己为“我的电脑”的子项的设备,而不是作为驱动器,而是作为虚拟文件夹(可以包含虚拟子文件夹和/或文件)。

从现在开始,我将更多地提及“Android”或“手机/平板电脑”,而不是便携设备(这是一个相当模糊的术语)。

但请注意,如果它在这里,“设备和驱动器”下显示,您就可以使用我的库访问它。

同步你的手机

在向您展示API之前,让我们深入了解一些客户端代码...

假设您的任务是允许您的客户同步他所有的手机和平板电脑,并缓存相机拍摄的最后100张图片(这样即使设备未连接,他也可以访问它们)。

我的意思是 - 一旦他插入设备,只要设备解锁,同步过程就应该自动发生。

static void cache_last_files_thread() {
    var cache_root = "c:/john/buff";
    Dictionary<string, DateTime> needs_cache = new Dictionary<string, DateTime>();
    while (true) {
        Thread.Sleep(1000);
        foreach (var ad in drive_root.inst.drives.Where(d => d.type.is_android())) {
            var already_cached = needs_cache.ContainsKey(ad.unique_id) 
                        && needs_cache[ad.unique_id] > DateTime.Now;
            if (!already_cached) {
                var valid_until = DateTime.Now.AddHours(1);
                if ( !needs_cache.ContainsKey(ad.unique_id))
                    needs_cache.Add( ad.unique_id, DateTime.MinValue);
                needs_cache[ad.unique_id] = valid_until;
                cache_now(ad, cache_root);
            }
        }
    }
}
static void cache_now(IDrive ad, string root) {
    var files = ad.parse_folder("*/dcim/camera").files.OrderBy(f => f.last_write_time).ToList();
    if (files.Count > 100) files = files.GetRange(files.Count - 100, 100);
    var local_root = root + "/" + ad.unique_id;
    foreach (var f in files) 
        if ( !File.Exists(local_root + "/" + f.name))
            f.copy_sync(local_root);
}

以下是亮点:

  • cache_last_files_thread中,每秒,我们枚举所有外部Android驱动器。如果这是一个新驱动器,则执行cache_now
  • 每个驱动器都有一个.unique_id(稍后将详细解释)。将唯一ID视为一个序列号,该序列号唯一标识此设备所有时间。这就是我们识别每个设备的方式。
  • 缓存时,我们有一个本地缓存目录。每个外部设备都有一个子目录,以其`.unique_id`命名。在该子目录中,我们保存该设备的缓存。这样可以轻松查看我们是否已缓存文件。

对于25行代码来说,这不算差...

驱动器根目录 (drive_root)

该接口旨在易于使用

class drive_root {  
  public static drive_root inst { get; } = new drive_root();

  IFile parse_file(string path) ;
  IFile try_parse_file(string path) ;

  IFolder parse_folder(string path) ;
  IFolder try_parse_folder(string path) ; 

  IFolder new_folder(string path) ;
  IReadOnlyList<IDrive> drives ;
  // ...
};

基本上,只需轻松访问任何便携式设备上的任何文件或文件夹

// enumerate all the pictures you've taken (a0 - first android drive)
Console.WriteLine(
  string.Join(",",drive_root.inst.parse_folder("[a0]:/*/dcim/camera").files.Select(f => f.name)));

或者,只需打印文件大小

Console.WriteLine(drive_root.inst.parse_file("[a0]:/*/dcim/camera/20171005_121557.jpg").size);

drive_root允许您轻松访问来自外部设备(如USB Android设备)或内部HDD的文件夹或文件。从那里开始,您可以枚举文件夹的文件或处理特定文件。

parse_fileparse_folder将分别返回文件/文件夹,如果不存在则抛出异常。它们的try_对应版本将在文件/文件夹不存在时返回null。

我允许访问所有驱动器(不仅是外部驱动器)的原因很简单:您可能想将文件从手机复制到硬盘,但您也可能想从硬盘复制到手机。

// Phone -> HDD
drive_root.inst.parse_file("[a0]:/*/dcim/camera/my_photo.jpg").copy("c:/john/buff");
// HDD -> Phone
drive_root.inst.parse_file("c:/john/buff").copy("[a0]:/*/dcim/camera");

最后一点,您可以互换使用“/”和“\”作为文件/文件夹分隔符。我更喜欢使用/,因为使用\总是迫使我将其转义为'\\'。

驱动器 (drive(s))

.[try_]parse_folder.[try_]parse_file中,您可以通过以下几种方式访问驱动器:

  • 它的驱动器号。例如,drive_root.inst.parse_folder("c:/windows")。我建议仅对内部HDD驱动器执行此操作。任何外部驱动器,当您插入它时,它可能会获得一个新的驱动器号。例如,东芝外部HDD,它可能被挂载为E:,几天后,可能被挂载为H:
  • 唯一ID。这对于任何便携设备都非常完美。每个设备都由某种序列号唯一标识。这允许您将文件/文件夹唯一地标识到特定设备。一旦连接了新设备,即使它具有相同的文件夹,您也知道它的内容不同。示例:drive_root.inst.parse_folder("{930002527dd2345ab}:/phone/dcim");
  • 便携设备索引 (Portable-index)。这对于测试非常有用,或者可以轻松识别第一个、第二个、第三个连接的设备。由于用户通常只有一个或最多两个USB连接的设备,因此很容易访问它们。要访问第一个连接的便携设备,请使用:drive_root.inst.parse_folder("[p0]:/phone/dcim"); 要访问第三个连接的便携设备,请使用drive_root.inst.parse_folder("[p2]:/phone/dcim");
  • Android索引 (Android-index)。这与便携设备索引类似,只是它仅指Android设备。要访问第一个连接的Android设备,请使用:drive_root.inst.parse_folder("[a0]:/phone/dcim");
  • 设备索引 (Device-index)。这是设备在设备根目录中的索引。请注意,内部HDD始终排在前面,USB连接的设备排在最后。例如,drive_root.inst.parse_folder("[d0]:/windows");(很可能等同于"c:/windows"),或drive_root.inst.parse_folder("[d1]:/pics");(很可能等同于"d:/pics")。在您了解设备顺序时(很可能用于测试)使用此方法。

虽然您可以随意使用以上任何一种方法,但对于外部驱动器,我建议您使用唯一ID。首先,知道Android驱动器的唯一ID非常容易。

// print the unique ID of each drive
var drives = drive_root.inst.drives.Where(d => d.type.is_android());
foreach ( var d in drives) Console.WriteLine(d.unique_id);

任何文件或文件夹,当它们返回完整路径时,它们将使用唯一ID作为其前缀。这清楚地指明了文件/文件夹的来源(它绑定到特定设备)。

假设您想缓存来自Android设备的某些文件。您将想知道缓存文件关联的确切设备。您不希望插入另一个碰巧拥有具有完全相同名称(内容完全不同)的文件的设备,并最终使用缓存的文件,而这些文件与新设备中的文件毫无关系。

因此,每个文件或文件夹,当您请求其完整路径时,都会被驱动器的唯一ID前缀。

// can output something like "{23479a234bd77ae3}:/Phone/DCIM"
Console.WriteLine( drive_root.inst.parse_folder("[a0]:/*/dcim").full_path);

最后一点,对于.[try_]parse_folder / .[try_]parse_file,如果便携设备路径以*开头,则表示:如果驱动器根目录包含单个文件夹,则将*替换为该文件夹名称

通常,每个手机/平板电脑制造商都会以他们喜欢的方式命名其驱动器的根文件夹,例如“Phone”、“Tablet”、“P8_Lite”或任何其他名称。如果您想访问第一台Android设备上的相机照片,可以这样做:

var photos = drive_root.inst.parse_folder("[a0]:/*/dcim/camera").files.;

IDrive

您可以通过几种方式访问驱动器:

  • drive_root.inst.drives - 获取所有驱动器,包括内部和外部。
  • 任何文件的.drive属性。
  • 任何文件夹的.drive属性。
  • drive_root.inst.[try_]get_drive(drive_name) - 按名称获取给定驱动器(try_将在驱动器不存在时返回null;get_drive将在驱动器不存在时抛出异常)。

通常,您会更关注文件夹和文件,而不是驱动器。因此,IDrive的接口非常简单。

public interface IDrive {
    bool is_connected();
    drive_type type { get; }
    // this is the drive path, such as "c:\" - however, 
    // for non-conventional drives, it can be a really weird path
    string root_name { get; }

    string unique_id { get; }
    string friendly_name { get; }

    IEnumerable<IFolder> folders { get; }
    IEnumerable<IFile> files { get; }

    IFile parse_file(string path);
    IFolder parse_folder(string path);

    IFile try_parse_file(string path); 
    IFolder try_parse_folder(string path); 

    // creates the full path to the folder
    IFolder create_folder(string folder);
}

一切都很直接。只需注意几点:

  • is_connected():
    • 保证不会抛出异常。
    • 对于所有内部HDD,返回true
    • 对于便携设备,如果驱动器通过USB连接并且设置为“通过USB传输媒体文件”,则返回true。换句话说,如果在“我的电脑”中,您可以单击此驱动器,并且它包含文件夹和文件,那么is_connected()将返回true
    • 一旦拔掉便携设备,它将返回false
    • 当便携设备被拔掉时,该驱动器将不会出现在drive_root.inst.drives中。
  • type:返回驱动器类型。这是一个枚举。您也可以询问is_portableis_adroidis_iOS
  • unique_id:返回驱动器的唯一ID。我上面已详细解释过。再说一次,将其视为驱动器的序列号。
  • friendly_name:驱动器的友好名称。这是由您的便携设备制造商提供的,可以是“Galaxy S6”、“P8 Lite”等。
  • foldersfiles:返回根目录下的文件夹和文件。对于大多数设备,文件夹将不返回任何文件,而是返回一个包含子文件夹和文件的单个根文件夹。
  • parse_fileparse_folder将分别返回文件/文件夹,如果不存在则抛出异常。它们的try_对应版本将在文件/文件夹不存在时返回null。

以下是如何查找连接的每个Android驱动器的唯一ID、友好名称以及您相机中有多少照片。

foreach ( var ad in drive_root.inst.drives.Where(d => d.type.is_android()))
  Console.Write(ad.unique_id + " - " + ad.friendly_name 
    + " , photos: " + ad.parse_folder("*/dcim/camera").files.Count());

IFolder

一旦您获得了文件夹,您就可以对其执行以下操作:

public interface IFolder {
    // guaranteed to NOT THROW
    string name { get; }
    bool exists { get; }

    string full_path { get; }
    IDrive drive { get; }
    IFolder parent { get; }
    IEnumerable<IFile> files { get; }
    IEnumerable<IFolder> child_folders { get; }
    
    void delete_async();
    void delete_sync();
}

再次,这看起来应该很熟悉。几点说明:

  • name:返回文件夹的名称,保证不抛出异常。
  • exists:如果文件夹存在,则返回true。请注意,如果您访问了IFolder,它总是存在的。但是,如果您拔掉了它的驱动器,那么exists将返回false。
  • full_path:返回文件夹的完整路径。对于便携设备,驱动器名称始终是{unique_id}。因此,文件夹的完整路径可能看起来像{2388752ea37882}:\Phone\DCIM\Camera
  • delete_syncdelete_async:分别同步删除文件夹,异步删除文件夹。
  • 没有copy:我暂时决定不为整个文件夹实现复制。您总是可以通过复制文件夹中的每个文件来实现此操作。

以下是如何查找您手机上创建的所有相册。

var dcim = drive_root.inst.parse_folder("[a0]:/*/dcim");
foreach (var f in dcim.child_folders)
  Console.WriteLine(f.full_path);

IFile

当您有一个文件时,您可以对其执行以下操作:

public interface IFile {
    // guaranteed to NOT THROW
    string name { get; }
    bool exists { get; }

    IFolder folder { get; }
    string full_path { get; }

    IDrive drive { get; }

    long size { get; }
    DateTime last_write_time { get; }

    // note: overwrites if destination exists
    void copy_sync(string dest_path);
    void copy_async(string dest_path);
    
    void delete_async();
    void delete_sync();
}

我已将接口保持在最低限度。

  • name:文件的名称。
  • exists:文件是否存在。这与IFolder完全相同 - 当其驱动器被拔掉时,它将返回false。
  • folder:返回此文件所属的文件夹。
  • full_path:返回文件的完整路径。对于便携设备,驱动器名称始终是{unique_id}。因此,文件的完整路径可能看起来像{2388752ea37882}:\Phone\DCIM\Camera\20171010_1384292.jpg
  • size:返回文件的大小(以字节为单位)。
  • last_write_time:返回文件的修改日期(最后写入时间)。
  • copy_synccopy_async:将文件同步或异步复制到另一个路径。非常重要:如果目标文件存在,它将自动覆盖它(或在不可能时抛出异常)。
  • delete_syncdelete_async:同步或异步删除文件。
  • 没有move:我暂时决定不实现移动。每个复制/删除操作都已被证明难以实现,因此我选择暂时放弃它。您可以始终将move()实现为copy() + delete()。

以下是如何将最后拍摄的照片复制到其父文件夹。

var camera = drive_root.inst.parse_folder("[a0]:/phone/dcim/camera");
var last_file = camera.files.OrderBy(f => -f.last_write_time.Ticks).First();
last_file.copy_sync(camera.parent.full_path);

请勿在UI线程上使用。

在继续之前,请注意。许多这些函数会阻塞当前线程,有时会阻塞很长时间,我不得不说。这是shell32.dll的实现方式,我们对此无能为力(好吧,我们对此无能为力)。

预计以下情况:

  • IDrive.foldersIDrive.filesIFolder.child_foldersIFolder.files可能非常耗时,特别是对于包含大量文件的文件夹。
  • IFile.sizeIFile.last_write_time可能很耗时,尽管您通常可以不必担心它们。

该库使用起来相当直接,因此您可以轻松地异步调用操作。

Task.Run(() => foreach(var f in drive_root.inst.parse_folder("[a0]:/*/dcim/camera").files)
     f.copy_sync(somewhere));

批量复制

可怕的实现细节是,Windows在内部使用我正在使用的API实现复制(以及删除)时,“有时是同步的,有时是异步的”。您实际上无法依赖复制或删除是同步的。存在明确的情况,您希望复制是同步的。因此,我实现了IFile.copy_sync。当copy_sync返回时,它保证文件已完全复制。这意味着有时,即使文件已复制,我们也会等待稍长时间以“收到”通知。或者直言不讳地说,没有通知,我只是不断监视文件大小 - 而这并不总是最新的。

鉴于以上所有情况,这就是为什么我实现了批量复制。我能够进行一些优化,并且批量复制文件可能比单独复制每个文件快10%。

我正在使用的API实际上有时可以批量复制文件(当可能时,我将使用它),否则,我将所有文件异步复制,然后等待它们完全复制。

public static class bulk {
  static void bulk_copy_sync(string src_folder, string dest_folder
                             , Action<string,int,int> copy_complete_callback = null);
  static void bulk_copy_async(string src_folder, string dest_folder
                             , Action<string,int,int> copy_complete_callback = null);
  static void bulk_copy_sync(IEnumerable<IFile> src_files, string dest_folder
                             , Action<string,int,int> copy_complete_callback = null);
  static void bulk_copy_async(IEnumerable<IFile> src_files, string dest_folder
                             , Action<string,int,int> copy_complete_callback = null);
}

复制文件夹中的所有偶数文件。

var i = 0;
var files = drive_root.inst.parse_folder("d:/cool_pics").files.Where(f => i++ % 2 == 0);

// note: auto-creates the destination folder
bulk.bulk_copy_sync(files, "[a0]:/*/dcim/cool_pics");

// faster than:
drive_root.inst.create_folder("[a0]:/*/dcim/cool_pics");
foreach ( var f in files)
  f.copy_sync("[a0]:/*/dcim/cool_pics");

您可以使用回调,在每次文件复制完成后调用。

  • 回调参数是:源文件名、文件索引、文件总数。

例如,假设您想显示某种复制进度。

var camera = drive_root.inst.try_parse_folder("[a0]:/*/dcim/camera");
var temp = new_temp_path();
Console.WriteLine("Copying to " + temp);
bulk.bulk_copy_sync(camera.files.ToList(), temp, (f, i, c) => {
    Console.WriteLine(f + " to " + temp + "(" + (i+1) + " of " + c + ")");            
});
Console.WriteLine("Copying to " + temp + " - complete, took " 
   + (int)(DateTime.Now - start).TotalMilliseconds + " ms" );

使用库

好了,这就是全部。希望您会发现该库易于使用,并且绝对有用。从Github获取我写了一些例子来帮助您入门(请参阅console_test项目)。

我写它是因​​为我找不到一个能够抽象出处理外部驱动器所有复杂性的东西。而且,关于我使用的API(shell32.dll的FolderItem/Folder对象)的信息非常稀少,所以我不得不做大量的挖掘。

我可以告诉您,它隐藏了许多关于后台发生什么的非常丑陋的Windows细节。如果您不关心这些细节,只需使用该库并享受乐趣

否则,请和我一起在下面笑一笑!

shell32:有趣的部分

我在内部使用的API是shell32.dll的Folder/FolderItem shell对象。我访问“我的电脑”对象,枚举其子文件夹,然后忽略驻留在内部HDD上的对象。剩下的是便携式驱动器。

使用对话框的API

微软可能没有认真考虑shell32.dll库,并且将其与Windows资源管理器(或Win7+上的“我的电脑”,或Win10上的“此电脑”)紧密耦合。您将看到的第一个问题是,有时在使用API时,您会看到对话框或进度对话框 - 例如,您在Windows资源管理器中复制大文件夹时通常看到的进度对话框。

但是,后来我说,Folder.CopyHereFolder.MoveHere(我内部使用的)有一个额外的参数options,我可以用它来关闭UI。太棒了!但不幸的是,它被完全忽略了……糟糕……

所以,我剩下要做的就是:创建一个线程,该线程不断监视来自我们进程的对话框窗口(幸运的是,对话框和进度对话框是在我们的进程中创建的)。一旦找到一个,我就将其移出屏幕。起初,我想隐藏它,但是对话框会愉快地忽略隐藏。

万一您不想要这样,只需执行drive_root.inst.auto_close_win_dialogs = false;。此时,您将注意到显示的进度对话框。

当复制/移动文件并且文件已存在于目标位置时,您会看到的另一个UI是这个小家伙。

为了避免这种情况,我的库会检查目标文件是否存在,如果存在,则自动删除它,然后执行复制。

令人敬畏的删除

这也涉及一个对话框,但它很有趣,值得单独一节……

出于我无法理解的原因,在使用FolderItem.Delete API时,无论您删除什么,都会遇到:

根本无法将其关闭。Google上的大多数答案都涉及在对话框显示时立即发送Enter键。在我几乎放弃这一点之后,我找到了一个很好的解决方法:而不是FolderItem.Delete,只需将文件从便携式驱动器移动到HDD,然后在那​​里使用System.IO.File.Delete删除它。移动时,没有烦人的对话框 - 问题解决了。

同步/异步灵活性 ([A]Synchronous Flexibility)

当使用Folder.CopyHereFolder.MoveHere时,API有时是同步的,有时是异步的。在我的测试中,似乎Portable到HDD是同步的。Portable到Portable以及HDD到Portable是异步的。

无论哪种方式,我们显然不能依赖它们来知道操作何时完成。因此,我的监视方式(在copy_sync上)是通过检查目标文件的大小,直到它与源文件匹配为止。

FolderItems“集合”

进行批量复制操作时,如果我能让shell32 API一次性复制所有内容,那显然会有所帮助。

Folder.CopyHere的第一个参数可以是FolderItem(单个文件或文件夹),也可以是FolderItems(它是一个文件和/或文件夹的集合)。

获取FolderItems的唯一方法是枚举Folder - 因此,它将返回其所有文件及其所有子文件夹。遗憾的是,您无法添加或删除任何内容。换句话说,您要么一次性复制文件夹的所有子项,要么逐个文件复制。

有一个解决这个问题的方法……或者我曾经这么认为。您可以将从Folder获得的FolderItems对象转换为FolderItems3 COM接口。该接口有一个Filter函数,该函数可以对它包含的所有项执行过滤。

过滤器的第二个参数是一个查找规范,您可以使用“;”来指定要复制的多个文件。因此,您可以说:

items.Filter(flags, "file1;file2;file3...")

这将允许我们一次性从文件夹中复制一组特定文件,从而实现简单的批量复制。遗憾的是,这仅在源文件夹来自HDD时才有效。如果源文件夹来自便携设备,则应用于项对象的任何过滤器都将返回0个项(除了“*.*”,它返回所有原始项)。

希望微软将来会修复此问题,我的库会尝试使用items.Filter。如果无效(items.Filter返回0),它将逐个文件复制。
 

故障排除

希望您不需要来到这里,但万一出了什么问题……这基本上意味着我们不识别您的便携设备,即使您在“我的电脑”中看到了它。

无法识别的驱动器或无效/错误的唯一ID

这基本上意味着我们无法解析驱动器的根路径。长话短说,它应该看起来或多或少是这样的:

::{20D04FE0-3AFA-1069-A2D8-06002B30309D}\\\\\\\\\\\\?\\usb#vid_13d2&pid_0307#557a9de7#{6ad27878-a6fa-4155-ba85-f38f491d4f33}

某些设备可能具有稍微不同的根路径,例如:

::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\\\\\\\\\\?\\\\activesyncwpdenumerator#umb#2&306b293b&2&aceecamez1500windowsmobile5#{6ac27878-a6fa-4155-ba85-f98f491d4f33}

如果是这种情况,我只需要更新代码,并正确解析该路径。

请在console_test项目中运行以下代码:

foreach ( var p in usb_util.get_all_portable_paths())
    Console.WriteLine(p);
foreach ( var p in usb_util.get_all_usb_pnp_device_ids())
    Console.WriteLine(p);
foreach ( var p in usb_util.get_all_usb_dependent_ids())
    Console.WriteLine(p);

然后在这里留下评论,附上结果。我应该能够正确解析设备根路径,并使其适用于您的特定设备。

无法将唯一ID与USB设备ID匹配

有时(例如,对于较旧的WinCE设备),将USB设备ID与唯一ID匹配并不直接。我们需要这个的原因是,以便我们知道何时拔掉驱动器(即断开连接)。

在这种情况下,请在console_test项目中运行以下代码:

usb_util.monitor_usb_devices("Win32_USBHub");

然后,插入设备,等待几秒钟,然后拔掉设备。如果屏幕上显示任何内容,请写下评论并附上结果。

如果什么都没有显示,请将上面的代码修改为:

usb_util.monitor_usb_devices("Win32_USBControllerDevice");

……并做同样的事情。谢谢!

 

iPhone

iPhone仅允许访问您设备上的照片。这几乎是您想要的。请注意,您将无法像Android那样轻松地解析文件夹,例如在"[a0]:/*/dcim"中,您拥有所有相册,而您拍摄的照片位于"[a0]:/*/dcim/camera"

幸运的是,您可以简单地从"[i0]:/*/dcim"枚举所有文件夹。它们可能是100Apple、101Apple等,但这并不总是如此。

从这些文件夹中,只需枚举所有文件。您可以轻松地按日期排序,瞧 - 您拥有iPhone拍摄的所有照片!

另一种实现方式

最近一位Reddit用户向我指出,还有另一种处理便携设备的方法 - 即WPD。嗯,整个接口都是COM,为了将其转换为C#,您需要导入2个COM DLL。但这并不是最大的问题。似乎有一些封送处理问题,因此使用此实现远非易事(只需查看评论)。已经有一个WPD .NET包装器的实现,您可以在此处找到它。它确实存在上述封送处理问题

就个人而言,此时我没有使用WPD - 似乎这会带来相当多的问题。我可能会重新实现portable_*类来使用WPD,但到目前为止,我没有看到任何令人满意的理由这样做。

历史

  • 1.0,2017年11月5日,初始版本
  • 1.1,2017年11月7日,all_drives -> drives,删除了external_drives,添加了一些小功能。
  • 1.1b,2017年11月8日,修复了Dirk Bahle发现的问题,增加了测试便携设备路径和PNPDeviceIDs的简单方法。
  • 1.2,2017年11月11日,根据Dirk Bahle的建议,添加了示例。添加了IDrive.is_available()。添加了try_parse_file、try_parse_folder。
  • 1.2.4,2017年11月18日,添加/测试了iPhone识别,添加了bulk_copy回调。
  • 1.2.5,2017年11月26日,
    • 处理shell32的进度对话框,这样用户就无法取消文件复制。
    • 添加了“无法将唯一ID与USB设备ID匹配”部分。
    • 添加了“请勿在UI线程上使用”部分。
  • 1.2.5b,2017年11月30日 - 添加了“另一种实现方式”部分。

 

 

© . All rights reserved.