最下方有demo及源码。
当手机通过 USB 连接 PC (选择文件传输,也就是MTP方式) 时,会看到设备管理器中出现便携设备这一栏,如下图:
打开我的电脑可以看到设备和驱动器中出现对应的设备,如下图:
可以发现,在设备管理器中,便携式设备有两个,可是在我的电脑中看到的设备只有一个,还有一个Nokia的设备显示不出来。这就是为什么我要使用WPD:用于读取和传输我的电脑中所看不见的设备(都是老设备,windows phone,塞班等)的一些信息, 如图片/视频等,相当于自己构建一个小小的文件系统,拥有展示文件列表和传输文件的能力。
现为统一技术栈,需要使用go来实现微软的WPD。
这是微软提供的一个库,可以通过MTP方式读取到一些设备信息,如:设备名、生产商、设备型号等信息。上github搜索了一下,发现了一个库可以使用 github.com/rlj1202/go-wpd , 貌似是一个韩国人写的。
由于需要在项目中使用C++代码,需要本机有gcc的环境,具体如何配置环境就不在本篇赘述了。很贴心的,github的作者将C++的代码库放上去了。
枚举设备基本信息:
func deviceEnumerate() {
gowpd.Initialize()
mng, err := gowpd.CreatePortableDeviceManager()
if err != nil {
panic(err)
}
deviceIDs, err := mng.GetDevices()
if err != nil {
panic(err)
}
for i, deviceID := range deviceIDs {
friendlyName, err := mng.GetDeviceFriendlyName(deviceID)
if err != nil {
panic(err)
}
manufacturer, err := mng.GetDeviceManufacturer(deviceID)
if err != nil {
panic(err)
}
description, err := mng.GetDeviceDescription(deviceID)
if err != nil {
panic(err)
}
log.Printf("[%d]:\n", i)
log.Printf("\tName: %s\n", friendlyName)
log.Printf("\tManufacturer: %s\n", manufacturer)
log.Printf("\tDescription: %s\n", description)
gowpd.FreeDeviceID(deviceID)
}
gowpd.Uninitialize()
}
得到content的objectID
func RecursiveEnumerate(parentObjectID string, content *gowpd.IPortableDeviceContent) {
enum, err := content.EnumObjects(parentObjectID)
if err != nil {
panic(err)
}
objectIDs := make([]string, 0)
for {
tmp, err := enum.Next(10)
if err != nil {
panic(err)
}
if len(tmp) == 0 {
break
}
objectIDs = append(objectIDs, tmp...)
}
for _, objectID := range objectIDs {
log.Println(objectID)
}
for _, objectID := range objectIDs {
RecursiveEnumerate(objectID, content)
}
}
func contentEnumerate() {
gowpd.Initialize()
mng, err := gowpd.CreatePortableDeviceManager()
if err != nil {
panic(err)
}
pClientInfo, err := gowpd.CreatePortableDeviceValues()
if err != nil {
panic(err)
}
pClientInfo.SetStringValue(gowpd.WPD_CLIENT_NAME, "libgowpd")
pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_MAJOR_VERSION, 1)
pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_MINOR_VERSION, 0)
pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_REVISION, 2)
deviceIDs, err := mng.GetDevices()
if err != nil {
panic(err)
}
for _, deviceID := range deviceIDs {
device, err := gowpd.CreatePortableDevice()
if err != nil {
panic(err)
}
err = device.Open(deviceID, pClientInfo)
if err != nil {
panic(err)
}
content, err := device.Content()
if err != nil {
panic(err)
}
RecursiveEnumerate(gowpd.WPD_DEVICE_OBJECT_ID, content)
gowpd.FreeDeviceID(deviceID)
}
gowpd.Uninitialize()
}
文件传输 Device to PC
func Example_transferToPC() {
gowpd.Initialize()
mng, err := gowpd.CreatePortableDeviceManager()
if err != nil {
panic(err)
}
deviceIDs, err := mng.GetDevices()
if err != nil {
panic(err)
}
clientInfo, err := gowpd.CreatePortableDeviceValues()
if err != nil {
panic(err)
}
clientInfo.SetStringValue(gowpd.WPD_CLIENT_NAME, "libgowpd")
clientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_MAJOR_VERSION, 1)
clientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_MINOR_VERSION, 0)
clientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_REVISION, 2)
// object ID which will be transferred to PC.
targetObjectID := "F:\\test.txt" // 这边是模拟的一个,通过枚举content得到的ID
// location where file will be transferred into.
targetDestination := "E:\\test.txt"
for _, id := range deviceIDs {
portableDevice, err := gowpd.CreatePortableDevice()
if err != nil {
panic(err)
}
portableDevice.Open(id, clientInfo)
content, err := portableDevice.Content()
if err != nil {
panic(err)
}
resources, err := content.Transfer()
if err != nil {
panic(err)
}
objectDataStream, optimalTransferSize, err := resources.GetStream(targetObjectID, gowpd.WPD_RESOURCE_DEFAULT, gowpd.STGM_READ)
if err != nil {
panic(err)
}
pFinalFileStream, err := gowpd.SHCreateStreamOnFile(targetDestination, gowpd.STGM_CREATE|gowpd.STGM_WRITE)
if err != nil {
panic(err)
}
totalBytesWritten, err := gowpd.StreamCopy(pFinalFileStream, objectDataStream, optimalTransferSize)
if err != nil {
panic(err)
}
err = pFinalFileStream.Commit(0)
if err != nil {
panic(err)
}
log.Printf("Total bytes written: %d\n", totalBytesWritten)
gowpd.FreeDeviceID(id)
portableDevice.Release()
}
mng.Release()
gowpd.Uninitialize()
// Output:
}
文件传输 PC to Device
func Example_transferToDevice() {
gowpd.Initialize()
mng, err := gowpd.CreatePortableDeviceManager()
if err != nil {
panic(err)
}
deviceIDs, err := mng.GetDevices()
if err != nil {
panic(err)
}
pClientInfo, err := gowpd.CreatePortableDeviceValues()
if err != nil {
panic(err)
}
pClientInfo.SetStringValue(gowpd.WPD_CLIENT_NAME, "libgowpd")
pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_MAJOR_VERSION, 1)
pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_MINOR_VERSION, 0)
pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_REVISION, 2)
targetDeviceFriendlyName := "SANDISK "
// objectId where the file will be transferred under.
targetObjectID := "F:\\" // 这边是模拟的一个,通过枚举content得到的ID
for _, id := range deviceIDs {
friendlyName, err := mng.GetDeviceFriendlyName(id)
if err != nil {
panic(err)
}
if friendlyName != targetDeviceFriendlyName {
gowpd.FreeDeviceID(id)
continue
}
pPortableDevice, err := gowpd.CreatePortableDevice()
if err != nil {
panic(err)
}
// Establish a connection
err = pPortableDevice.Open(id, pClientInfo)
if err != nil {
panic(err)
}
// path to selected file to transfer to device.
filePath := "E:\\RedLaboratory\\Media\\Picture\\result.png"
filePath = "E:\\test.md"
// open file as IStream.
pFileStream, err := gowpd.SHCreateStreamOnFile(filePath, 0)
if err != nil {
panic(err)
}
// acquire properties needed to transfer file to device
pObjectProperties, err := gowpd.GetRequiredPropertiesForContentType(gowpd.WPD_CONTENT_TYPE_IMAGE, targetObjectID, filePath, pFileStream)
if err != nil {
panic(err)
}
// get stream to device
content, err := pPortableDevice.Content()
if err != nil {
panic(err)
}
pTempStream, cbTransferSize, err := content.CreateObjectWithPropertiesAndData(pObjectProperties)
if err != nil {
panic(err)
}
// convert pTempStream to PortableDeviceDataStream to use more method e.g newly created object id.
_pFinalObjectDataStream, err := pTempStream.QueryInterface(gowpd.IID_IPortableDeviceDataStream)
if err != nil {
panic(err)
}
pFinalObjectDataStream := (*gowpd.IPortableDeviceDataStream)(_pFinalObjectDataStream)
// copy data from pFileStream to pFinalObjectDataStream
cbBytesWritten, err := gowpd.StreamCopy((*gowpd.IStream)(_pFinalObjectDataStream), pFileStream, cbTransferSize)
if err != nil {
panic(err)
}
// call commit method to notice device that transferring data is finished.
err = pFinalObjectDataStream.Commit(0)
if err != nil {
panic(err)
}
newlyCreatedObjectID, err := pFinalObjectDataStream.GetObjectID()
if err != nil {
panic(err)
}
log.Printf("\"%s\" has been transferred to device successfully: %d\n", newlyCreatedObjectID, cbBytesWritten)
// transferring is finished. release the deviceID.
gowpd.FreeDeviceID(id)
// release device interface too.
pPortableDevice.Release()
}
for _, id := range deviceIDs {
gowpd.FreeDeviceID(id)
}
gowpd.Uninitialize()
}
先说结论
1. 这个go的库缺少了device的Close,资源释放的不干净。
如何发现的: 由于工作需要,现有一个比较老的windows phone手机,但这个手机通过MTP方式连接PC,只能够有一个地方能访问该手机的文件系统(例:通过文件资源管理器进行了文件的查看,就不能通过Zune来查看文件内容)。但是在访问的时候,由于资源没有释放干净,导致了该进程运行时,其他进程无法访问手机文件系统。
2. 传输过程中,有的手机传输了一个文件后,就不能再继续传输了,应该是哪里资源还有问题,这个暂时还未解决。
以上是源码中原本就包含的一些例子,在这提供一个自己写的小demo。使用vue搭的页面,用go作为服务,实现文件列表的展示以及文件的导出。demo使用过程中可能遇到:请求服务无反应的状况,手动重启ginServer.exe,然后重启项目即可,目前默认ginServer监听的端口号为7860。(CmdOrCtrl+Alt+Y可打开控制台)
链接:百度网盘
提取码:er6m
源码链接:https://gitee.com/Grassto/WPD-FileSystem.git
望解决上述问题的大佬私聊我。
上述遇到的问题2(传输过程中,有的手机传输了一个文件后,就不能再继续传输了,应该是哪里资源还有问题)解决了,是在导出过程中,通过重新创建protableDevice资源实现的。源码已更新。
问题2,发现是打开的IStream未进行释放,修改了源码进行资源的释放,添加了IStream.Release方法,解决了该问题。
顺带提一下,当进行文件传输的时候,resources.GetStream 若是返回E_FAIL错误,很有可能是由于没有文件的访问权限。返回0x800700AA错误,应该是资源未释放