go语言 使用MTP协议 通过WPD(windows portable device)读取便携式设备信息并进行文件传输

最下方有demo及源码。

背景

当手机通过 USB 连接 PC (选择文件传输,也就是MTP方式) 时,会看到设备管理器中出现便携设备这一栏,如下图:
便携式设备
打开我的电脑可以看到设备和驱动器中出现对应的设备,如下图:
设备和驱动器
可以发现,在设备管理器中,便携式设备有两个,可是在我的电脑中看到的设备只有一个,还有一个Nokia的设备显示不出来。这就是为什么我要使用WPD:用于读取和传输我的电脑中所看不见的设备(都是老设备,windows phone,塞班等)的一些信息, 如图片/视频等,相当于自己构建一个小小的文件系统,拥有展示文件列表和传输文件的能力。
现为统一技术栈,需要使用go来实现微软的WPD。

应用技术 WPD(windows portable device)

这是微软提供的一个库,可以通过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)
	}
	//for _, id := range deviceIDs {
	//	fmt.Println(string(gowpd.PnpToByte(id)))
	//}

	pClientInfo, err := gowpd.CreatePortableDeviceValues()
	if err != nil {
		panic(err)
	}
	pClientInfo.SetStringValue(gowpd.WPD_CLIENT_NAME, "grassto")
	pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_MAJOR_VERSION, 1)
	pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_MINOR_VERSION, 0)
	pClientInfo.SetUnsignedIntegerValue(gowpd.WPD_CLIENT_REVISION, 2)

	targetDeviceFriendlyName := "M1852"
	// objectId where the file will be transferred under.
	targetObjectID := "s10001" // gowpd.WPD_DEVICE_OBJECT_ID

	for _, id := range deviceIDs {
		friendlyName, err := mng.GetDeviceFriendlyName(id)
		if err != nil {
			panic(err)
		}

		if friendlyName[:len(friendlyName)-1] != 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:\\test\\1.txt"

		// open file as IStream.
		pFileStream, err := gowpd.SHCreateStreamOnFile(filePath, 0)
		if err != nil {
			panic(err)
		}


		pObjectProperties, err := manualIPV(targetObjectID, filePath)
		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)
		// cbBytesWritten, err := gowpd.StreamCopy(pTempStream, 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()
}

func manualIPV(targetObjectID, filePath string) (*gowpd.IPortableDeviceValues, error) {
	srcFileInfo, err := os.Stat(filePath)
	if err != nil {
		return nil, err
	}
	pObjectProperties, err := gowpd.CreatePortableDeviceValues()
	if pObjectProperties == nil {
		return nil, gowpd.E_UNEXPECTED
	}
	if err != nil {
		return nil, err
	}
	err = pObjectProperties.SetStringValue(gowpd.WPD_OBJECT_PARENT_ID, targetObjectID)
	if err != nil {
		return nil, err
	}
	err = pObjectProperties.SetUnsignedLargeIntegerValue(gowpd.WPD_OBJECT_SIZE, uint64(srcFileInfo.Size()))
	if err != nil {
		return nil, err
	}
	originalFileName := filepath.Base(filePath)
	ext := filepath.Ext(filePath)
	err = pObjectProperties.SetStringValue(gowpd.WPD_OBJECT_ORIGINAL_FILE_NAME, originalFileName)
	if err != nil {
		return nil, err
	}
	err = pObjectProperties.SetStringValue(gowpd.WPD_OBJECT_NAME, originalFileName[:len(originalFileName)-len(ext)])
	if err != nil {
		return nil, err
	}

	return pObjectProperties, nil
}

过程中遇到的一些问题

先说结论
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

望解决上述问题的大佬私聊我。

----------------------------------- 2019.11.13更新 ----------------------------------

上述遇到的问题2(传输过程中,有的手机传输了一个文件后,就不能再继续传输了,应该是哪里资源还有问题)解决了,是在导出过程中,通过重新创建protableDevice资源实现的。源码已更新。

----------------------------------- 2019.11.15更新 ----------------------------------

问题2,发现是打开的IStream未进行释放,修改了源码进行资源的释放,添加了IStream.Release方法,解决了该问题。
顺带提一下,当进行文件传输的时候,resources.GetStream 若是返回E_FAIL错误,很有可能是由于没有文件的访问权限。返回0x800700AA错误,应该是资源未释放

----------------------------------- 2020.12.09更新 ----------------------------------

这个库还是有些问题的,今天评论区说从PC写文件到设备,提供的示例使用不了,的确是有问题的。手动构造了一个IPortableDeviceValues对象,可以成功写文件。

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值