python實現(xiàn)Android與windows局域網(wǎng)文件夾同步
Obsidian搭建個人筆記
最近在使用Obsidian搭建個人云筆記
盡管我使用COS
圖床+gitee
實現(xiàn)了云備份,但是在Android
上使的Obsidian
備份有點麻煩。還好我主要是在電腦端做筆記,手機只是作為閱讀工具。
所以,我寫一個局域網(wǎng)文件夾同步工具,來解決這個問題。
傳輸速度很快
局域網(wǎng)文件互傳
Windows和Android之間實現(xiàn)局域網(wǎng)內(nèi)文件互傳有以下幾種協(xié)議
HTTP 協(xié)議
優(yōu)點:
- 實現(xiàn)簡單,客戶端和服務(wù)器都有成熟的庫
- 安全性較好,支持HTTPS加密
- 可以傳輸不同類型的數(shù)據(jù),包括文件、文本等
缺點
:
- 傳輸效率比Socket等協(xié)議低
- 需要自行處理大文件分片上傳和下載
Socket 協(xié)議
優(yōu)點:
- 傳輸效率高,特別適合傳輸大文件
- 建立連接簡單快速
缺點
:
- 需要處理粘包問題,協(xié)議較為復(fù)雜
- 沒有加密,安全性差
- 需要處理網(wǎng)絡(luò)狀態(tài)變化等異常
SFTP 協(xié)議
優(yōu)點:
- 安全性好,基于SSH通道傳輸
- 支持直接映射為本地磁盤訪問
缺點
:
- 實現(xiàn)較復(fù)雜,需要找到可用的SFTP庫
- 傳輸效率比Socket低
WebSocket 協(xié)議
優(yōu)點:
- 傳輸效率高,支持雙向通信
- 接口簡單統(tǒng)一
缺點
:
- 需要處理連接狀態(tài),實現(xiàn)較為復(fù)雜
- 沒有加密,安全性較差
綜合來說,使用HTTP
或Socket
都是不錯的選擇
WebSocket
但是最后我選擇了WebSocket
,原因是Socket
在處理接收數(shù)據(jù)的時候需要考慮緩沖區(qū)的大小和計算json
結(jié)尾標識,實現(xiàn)起來較為繁瑣,而WebSocket
與Socket
在實現(xiàn)這個簡單的功能時的性能差別幾乎可以忽略不計,而且WebSocket
可以輕松實現(xiàn)按行讀取數(shù)據(jù),有效避免數(shù)據(jù)污染和丟失的問題。最關(guān)鍵的一點是,WebSocket
還可以輕松實現(xiàn)剪貼板同步
功能。
我一開始嘗試使用Socket來實現(xiàn)這個功能,但很快就發(fā)現(xiàn)實現(xiàn)起來相當麻煩,于是換用了WebSocket
,兩者在速度上沒有任何差別,用WebSocket
起來舒服多了!
思路
使用Python將Windows目標文件夾壓縮成zip格式,然后將其發(fā)送到Android設(shè)備。在Android設(shè)備上,接收壓縮文件后,通過MD5校驗確保文件的完整性。一旦確認無誤,將zip文件解壓到當前目錄,最后刪除壓縮文件。整個過程既有趣又實用!
MD5校驗沒寫,一直用著也沒發(fā)現(xiàn)有壓縮包損壞的情況(超小聲)
定義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", "發(fā)送中"), FOLDER_SYNCING("FOLDER_SYNCING", "文件夾同步中"), FILE_SENDEND("FILE_SENDEND", "發(fā)送完成"); }
用于文件傳輸過程中表示文件發(fā)送進度的模型類
data class FileSendingDot( val fileName: String, val bufferSize: Int, val total: Long, val sent: Long, val data: String )
Python服務(wù)器端實現(xiàn)
創(chuàng)建websocket服務(wù)端
使用Python
的asyncio
和websockets
模塊實現(xiàn)了一個異步的WebSocket
服務(wù)器,通過異步事件循環(huán)來處理客戶端的連接和通信。
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)
利用循環(huán)分塊讀取文件并通過WebSocket發(fā)送每個數(shù)據(jù)塊,同時構(gòu)造消息對象封裝文件信息
file_data = f.read(buffer_size) sent_size += len(file_data) # 發(fā)送數(shù)據(jù)塊,包含序號和數(shù)據(jù) 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": "發(fā)送中", "data": json.dumps(file_seading_data), } await ws.send(json.dumps(msg))
安卓客戶端 Jetpack ComposeUI 實現(xiàn)
請求所有文件訪問權(quán)限
va launcher = registerForActivityResult( ActivityResultContracts.StartActivityForResult()) { result -> // 權(quán)限已授權(quán) or 權(quán)限被拒絕 } private fun checkAndRequestAllFilePermissions() { //檢查權(quán)限 if (!Environment.isExternalStorageManager()) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.setData(Uri.parse("package:$packageName")) launcher.launch(intent) } }
自定義保存路徑
選擇文件夾
rememberLauncherForActivityResult()
創(chuàng)建一個ActivityResultLauncher
,用于啟動并獲取文件夾選擇的回調(diào)結(jié)果。
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實現(xiàn)websocket
private val client = OkHttpClient.Builder().build() //通過callbackFlow封裝,實現(xiàn)流式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() { ...接收消息的回調(diào) } socket = client.newWebSocket(request, listener) //心跳機制 launchHeartbeat() awaitClose { socket?.cancel() } }.flowOn(Dispatchers.IO) //服務(wù)端發(fā)送數(shù)據(jù) fun send(message: String) { socket?.send(message) }
接收文件
使用 Base64.decode()
方法將 base64
數(shù)據(jù)解碼成字節(jié)數(shù)組 fileData
val fileName = dot.fileName val file = File(AppSystemSetManage.fileSavePath, fileName) val fileData = Base64.decode(dot.data, Base64.DEFAULT)
- 接著就是使用IO數(shù)據(jù)流
OutputStream
加上自定義的路徑
一頓操作 就得到zip文件了 - 最后解壓zip到當前文件夾
接收文件
顯示發(fā)送進度
從FileSendingDot對象中取出已發(fā)送數(shù)據(jù)量sent和總數(shù)據(jù)量total。 可以實時獲取文件傳輸?shù)倪M度
用drawBehind
在后面繪制矩形實現(xiàn)進度條占位。根據(jù)進度計算矩形寬度,實現(xiàn)進度填充效果。不會遮擋子組件,很簡潔地實現(xiàn)自定義進度條。
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) # 發(fā)送數(shù)據(jù)塊,包含序號和數(shù)據(jù) 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": "發(fā)送中", "data": json.dumps(file_seading_data), } await ws.send(json.dumps(msg)) print((sent_size / total_size) * 100) # 發(fā)送結(jié)束標志 endmsg = {"type": "FILE_SENDEND", "msg": "發(fā)送完成", "data": "發(fā)送完成"} 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實現(xiàn)Android與windows局域網(wǎng)文件夾同步的詳細內(nèi)容,更多關(guān)于python實現(xiàn)Android與windows文件同步的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
pyqt5實現(xiàn)繪制ui,列表窗口,滾動窗口顯示圖片的方法
今天小編就為大家分享一篇pyqt5實現(xiàn)繪制ui,列表窗口,滾動窗口顯示圖片的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-06-06Python Django項目和應(yīng)用的創(chuàng)建詳解
這篇文章主要為大家介紹了Python Django項目和應(yīng)用的創(chuàng)建,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2021-11-11