python实现Android与windows局域网文件夹同步
作者:九狼
Obsidian搭建个人笔记
最近在使用Obsidian搭建个人云笔记
尽管我使用COS
图床+gitee
实现了云备份,但是在Android
上使的Obsidian
备份有点麻烦。还好我主要是在电脑端做笔记,手机只是作为阅读工具。
所以,我写一个局域网文件夹同步工具,来解决这个问题。
传输速度很快
局域网文件互传
Windows和Android之间实现局域网内文件互传有以下几种协议
HTTP 协议
优点:
- 实现简单,客户端和服务器都有成熟的库
- 安全性较好,支持HTTPS加密
- 可以传输不同类型的数据,包括文件、文本等
缺点
:
- 传输效率比Socket等协议低
- 需要自行处理大文件分片上传和下载
Socket 协议
优点:
- 传输效率高,特别适合传输大文件
- 建立连接简单快速
缺点
:
- 需要处理粘包问题,协议较为复杂
- 没有加密,安全性差
- 需要处理网络状态变化等异常
SFTP 协议
优点:
- 安全性好,基于SSH通道传输
- 支持直接映射为本地磁盘访问
缺点
:
- 实现较复杂,需要找到可用的SFTP库
- 传输效率比Socket低
WebSocket 协议
优点:
- 传输效率高,支持双向通信
- 接口简单统一
缺点
:
- 需要处理连接状态,实现较为复杂
- 没有加密,安全性较差
综合来说,使用HTTP
或Socket
都是不错的选择
WebSocket
但是最后我选择了WebSocket
,原因是Socket
在处理接收数据的时候需要考虑缓冲区的大小和计算json
结尾标识,实现起来较为繁琐,而WebSocket
与Socket
在实现这个简单的功能时的性能差别几乎可以忽略不计,而且WebSocket
可以轻松实现按行读取数据,有效避免数据污染和丢失的问题。最关键的一点是,WebSocket
还可以轻松实现剪贴板同步
功能。
我一开始尝试使用Socket来实现这个功能,但很快就发现实现起来相当麻烦,于是换用了WebSocket
,两者在速度上没有任何差别,用WebSocket
起来舒服多了!
思路
使用Python将Windows目标文件夹压缩成zip格式,然后将其发送到Android设备。在Android设备上,接收压缩文件后,通过MD5校验确保文件的完整性。一旦确认无误,将zip文件解压到当前目录,最后删除压缩文件。整个过程既有趣又实用!
MD5校验没写,一直用着也没发现有压缩包损坏的情况(超小声)
定义json格式和功能标识码
为每个功能定义标识码
enum class SocketType(val type: String, val msg: String) { FILE_SYNC("FILE_SYNC", "文件同步"), FOLDER_SYNC("FOLDER_SYNC", "文件夹同步"), CLIPBOARD_SYNC("CLIPBOARD_SYNC", "剪贴板同步"), HEARTBEAT("HEARTBEAT", "心跳"), FILE_SENDING("FILE_SENDING", "发送中"), FOLDER_SYNCING("FOLDER_SYNCING", "文件夹同步中"), FILE_SENDEND("FILE_SENDEND", "发送完成"); }
用于文件传输过程中表示文件发送进度的模型类
data class FileSendingDot( val fileName: String, val bufferSize: Int, val total: Long, val sent: Long, val data: String )
Python服务器端实现
创建websocket服务端
使用Python
的asyncio
和websockets
模块实现了一个异步的WebSocket
服务器,通过异步事件循环来处理客户端的连接和通信。
import asyncio import websockets start_server = websockets.serve(handle_client, "", 9999) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
解析同步请求,操作本地文件夹
json_obj = json.loads(data) type_value = json_obj["type"] data_value = json_obj["data"] if type_value == "FILE_SYNC": await send_file(websocket,"FILE_SENDING", file_path)
利用循环分块读取文件并通过WebSocket发送每个数据块,同时构造消息对象封装文件信息
file_data = f.read(buffer_size) sent_size += len(file_data) # 发送数据块,包含序号和数据 send_file_data = base64.b64encode(file_data).decode() file_seading_data = { "fileName": filename, "bufferSize":buffer_size, "total": total_size, "sent": sent_size, "data": send_file_data, } msg = { "type": type, "msg": "发送中", "data": json.dumps(file_seading_data), } await ws.send(json.dumps(msg))
安卓客户端 Jetpack ComposeUI 实现
请求所有文件访问权限
va launcher = registerForActivityResult( ActivityResultContracts.StartActivityForResult()) { result -> // 权限已授权 or 权限被拒绝 } private fun checkAndRequestAllFilePermissions() { //检查权限 if (!Environment.isExternalStorageManager()) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.setData(Uri.parse("package:$packageName")) launcher.launch(intent) } }
自定义保存路径
选择文件夹
rememberLauncherForActivityResult()
创建一个ActivityResultLauncher
,用于启动并获取文件夹选择的回调结果。
val selectFolderResult = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { data -> val uri = data.data?.data if (uri != null) { intentChannel.trySend(ViewIntent.SelectFolder(uri)) } else { ToastModel("选择困难! ƪ(˘⌣˘)ʃ", ToastModel.Type.Info).showToast() } }
Uri的path
fun Uri.toFilePath(): String { val uriPath = this.path ?: return "" val path = uriPath.split(":")[1] return Environment.getExternalStorageDirectory().path + "/" + path }
okhttp实现websocket
private val client = OkHttpClient.Builder().build() //通过callbackFlow封装,实现流式API fun connect() = createSocketFlow() .onEach { LogX.i("WebSocket", "收到消息 $it") }.retry(reconnectInterval) private fun createSocketFlow(): Flow<String> = callbackFlow { val request = Request.Builder() .url("ws://192.168.0.102:9999") .build() val listener = object : WebSocketListener() { ...接收消息的回调 } socket = client.newWebSocket(request, listener) //心跳机制 launchHeartbeat() awaitClose { socket?.cancel() } }.flowOn(Dispatchers.IO) //服务端发送数据 fun send(message: String) { socket?.send(message) }
接收文件
使用 Base64.decode()
方法将 base64
数据解码成字节数组 fileData
val fileName = dot.fileName val file = File(AppSystemSetManage.fileSavePath, fileName) val fileData = Base64.decode(dot.data, Base64.DEFAULT)
- 接着就是使用IO数据流
OutputStream
加上自定义的路径
一顿操作 就得到zip文件了 - 最后解压zip到当前文件夹
接收文件
显示发送进度
从FileSendingDot对象中取出已发送数据量sent和总数据量total。 可以实时获取文件传输的进度
用drawBehind
在后面绘制矩形实现进度条占位。根据进度计算矩形宽度,实现进度填充效果。不会遮挡子组件,很简洁地实现自定义进度条。
Box( modifier = Modifier .fillMaxWidth() .drawBehind { val fraction = progress * size.width drawRoundRect( color = progressColor, size = Size(width = fraction, height = size.height), cornerRadius = CornerRadius(12.dp.toPx()), alpha = 0.9f, ) }
@Composable fun ProgressCard( modifier: Modifier = Modifier, title: String, progress: Float, onClick: () -> Unit = {} ) { val progressColor = WordsFairyTheme.colors.themeAccent //通过判断progress的值来决定是否显示加载 val load = progress > 0F val textColor = if (load) WordsFairyTheme.colors.themeUi else WordsFairyTheme.colors.textPrimary OutlinedCard( modifier = modifier, onClick = onClick, colors = CardDefaults.cardColors(WordsFairyTheme.colors.itemBackground), border = BorderStroke(1.dp, textColor) ) { Box( modifier = Modifier .fillMaxWidth() .drawBehind { val fraction = progress * size.width drawRoundRect( color = progressColor, size = Size(width = fraction, height = size.height), cornerRadius = CornerRadius(12.dp.toPx()), alpha = 0.9f, ) }, content = { Row { Title( title = title, Modifier.padding(16.dp), color = textColor ) Spacer(Modifier.weight(1f)) if (load) Title( title = "${(progress * 100).toInt()}%", Modifier.padding(16.dp), color = textColor ) } } ) } }
效果图
python代码
import asyncio import websockets import os from pathlib import Path import pyperclip import json import base64 import zipfile import math FILE_BUFFER_MIN = 1024 FILE_BUFFER_MAX = 1024 * 1024 # 1MB file_path = "E:\\xy\\FruitSugarContentDetection.zip" folder_path = "E:\\Note\\Obsidian" zip_path = "E:\\Note\\Obsidian.zip" async def send_file(ws,type, filepath): # 获取文件名 filename = os.path.basename(filepath) total_size = os.path.getsize(filepath) sent_size = 0 if total_size < FILE_BUFFER_MAX * 10: buffer_size = math.ceil(total_size / 100) else: buffer_size = FILE_BUFFER_MAX with open(filepath, "rb") as f: while sent_size < total_size: file_data = f.read(buffer_size) sent_size += len(file_data) # 发送数据块,包含序号和数据 send_file_data = base64.b64encode(file_data).decode() file_seading_data = { "fileName": filename, "bufferSize":buffer_size, "total": total_size, "sent": sent_size, "data": send_file_data, } msg = { "type": type, "msg": "发送中", "data": json.dumps(file_seading_data), } await ws.send(json.dumps(msg)) print((sent_size / total_size) * 100) # 发送结束标志 endmsg = {"type": "FILE_SENDEND", "msg": "发送完成", "data": "发送完成"} await ws.send(json.dumps(endmsg)) async def handle_client(websocket, path): # 用户连接时打印日志 print("用户连接") async for data in websocket: print(data) json_obj = json.loads(data) type_value = json_obj["type"] data_value = json_obj["data"] if type_value == "FILE_SYNC": await send_file(websocket,"FILE_SENDING", file_path) if type_value == "FOLDER_SYNC": zip_folder(folder_path, zip_path) await send_file(websocket,"FOLDER_SYNCING", zip_path) if type_value == "CLIPBOARD_SYNC": pyperclip.copy(data_value) print(data_value) if type_value == "HEARTBEAT": dictionary_data = { "type": "HEARTBEAT", "msg": "hi", "data": "", } await websocket.send(json.dumps(dictionary_data)) # 用户断开时打印日志 print("用户断开") def zip_folder(folder_path, zip_path): with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: for root, _, files in os.walk(folder_path): for file in files: file_path = os.path.join(root, file) zipf.write(file_path, arcname=os.path.relpath(file_path, folder_path)) start_server = websockets.serve(handle_client, "", 9999) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
github:https://github.com/JIULANG9/FileSync
gitee:https://gitee.com/JIULANG9/FileSync
以上就是python实现Android与windows局域网文件夹同步的详细内容,更多关于python实现Android与windows文件同步的资料请关注脚本之家其它相关文章!