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

最下方有demo及源码。

背景

当手机通过 USB 连接 PC (选择文件传输,也就是MTP方式) 时,会看到设备管理器中出现便携设备这一栏,如下图:
go语言 使用MTP协议 通过WPD(windows portable device)读取便携式设备信息并进行文件传输_第1张图片
打开我的电脑可以看到设备和驱动器中出现对应的设备,如下图:
go语言 使用MTP协议 通过WPD(windows portable device)读取便携式设备信息并进行文件传输_第2张图片
可以发现,在设备管理器中,便携式设备有两个,可是在我的电脑中看到的设备只有一个,还有一个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)
	}

	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可打开控制台)
go语言 使用MTP协议 通过WPD(windows portable device)读取便携式设备信息并进行文件传输_第3张图片

链接:百度网盘
提取码: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错误,应该是资源未释放

你可能感兴趣的:(golang)