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






4.76/5 (29投票s)
轻松读取和写入通过USB连接的任何Android手机/平板电脑/iPhone/iPad上的文件。
- 引言
- 背景
- 处理USB“驱动器”
- 便携设备 vs Android vs 手机/平板
- 同步你的手机
- 驱动器根目录 (drive_root)
- 驱动器 (drive(s))
- IDrive
- IFolder
- IFile
- 批量复制
- 使用库
- shell32:有趣的部分
- 故障排除
- 另一种实现方式
- 历史
引言
最近,我需要以编程方式处理(即读取/写入)通过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_file
和parse_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_portable
、is_adroid
、is_iOS
。unique_id
:返回驱动器的唯一ID。我上面已详细解释过。再说一次,将其视为驱动器的序列号。friendly_name
:驱动器的友好名称。这是由您的便携设备制造商提供的,可以是“Galaxy S6”、“P8 Lite”等。folders
和files
:返回根目录下的文件夹和文件。对于大多数设备,文件夹将不返回任何文件,而是返回一个包含子文件夹和文件的单个根文件夹。parse_file
和parse_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_sync
和delete_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_sync
和copy_async
:将文件同步或异步复制到另一个路径。非常重要:如果目标文件存在,它将自动覆盖它(或在不可能时抛出异常)。delete_sync
和delete_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.folders
、IDrive.files
、IFolder.child_folders
、IFolder.files
可能非常耗时,特别是对于包含大量文件的文件夹。IFile.size
、IFile.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.CopyHere
和Folder.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.CopyHere
或Folder.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日 - 添加了“另一种实现方式”部分。