python实现Android与windows局域网文件夹同步

 更新时间:2023年09月28日 09:13:04   作者:九狼  
这篇文章主要给大家详细介绍了python实现Android与windows局域网文件夹同步,文中有详细的代码示例和图文介绍,具有一定的参考价值,需要的朋友可以参考下

Obsidian搭建个人笔记

最近在使用Obsidian搭建个人云笔记

尽管我使用COS图床+gitee实现了云备份,但是在Android上使的Obsidian备份有点麻烦。还好我主要是在电脑端做笔记,手机只是作为阅读工具。

所以,我写一个局域网文件夹同步工具,来解决这个问题。

传输速度很快

局域网文件互传

Windows和Android之间实现局域网内文件互传有以下几种协议

HTTP 协议

优点:

  • 实现简单,客户端和服务器都有成熟的库
  • 安全性较好,支持HTTPS加密
  • 可以传输不同类型的数据,包括文件、文本等

缺点:

  • 传输效率比Socket等协议低
  • 需要自行处理大文件分片上传和下载

Socket 协议

优点:

  • 传输效率高,特别适合传输大文件
  • 建立连接简单快速

缺点:

  • 需要处理粘包问题,协议较为复杂
  • 没有加密,安全性差
  • 需要处理网络状态变化等异常

SFTP 协议

优点:

  • 安全性好,基于SSH通道传输
  • 支持直接映射为本地磁盘访问

缺点:

  • 实现较复杂,需要找到可用的SFTP库
  • 传输效率比Socket低

WebSocket 协议

优点:

  • 传输效率高,支持双向通信
  • 接口简单统一

缺点:

  • 需要处理连接状态,实现较为复杂
  • 没有加密,安全性较差

综合来说,使用HTTPSocket都是不错的选择

WebSocket

但是最后我选择了WebSocket,原因是Socket在处理接收数据的时候需要考虑缓冲区的大小和计算json结尾标识,实现起来较为繁琐,而WebSocketSocket在实现这个简单的功能时的性能差别几乎可以忽略不计,而且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服务端

使用Pythonasynciowebsockets模块实现了一个异步的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文件同步的资料请关注脚本之家其它相关文章!

相关文章

  • Windows64x下VScode下载过程

    Windows64x下VScode下载过程

    这篇文章主要介绍了Windows64x下VScode下载,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • python下函数参数的传递(参数带星号的说明)

    python下函数参数的传递(参数带星号的说明)

    python中函数参数的传递是通过赋值来传递的。
    2010-09-09
  • Flask学习之全局异常处理详解

    Flask学习之全局异常处理详解

    Flask是一个基于Python的Web框架,它提供了全局异常处理的机制来捕获和处理应用程序中的异常,下面就带大家深入了解一下Flask是如何实现异常处理的,希望对大家有所帮助
    2023-06-06
  • 深入了解Python中描述器的使用

    深入了解Python中描述器的使用

    Python描述器是Python编程语言中的一个重要特性,它提供了一种灵活且强大的机制来控制属性访问行为。在本文中,我们将详细介绍Python描述器的概念、实现方式以及如何使用Python描述器来增强我们的Python程序
    2023-03-03
  • python实现批量提取指定文件夹下同类型文件

    python实现批量提取指定文件夹下同类型文件

    这篇文章主要为大家详细介绍了python实现批量提取指定文件夹下同类型文件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-04-04
  • python使用smtplib模块通过gmail实现邮件发送的方法

    python使用smtplib模块通过gmail实现邮件发送的方法

    这篇文章主要介绍了python使用smtplib模块通过gmail实现邮件发送的方法,涉及Python使用smtplib模块发送邮件的相关技巧,非常简单实用,需要的朋友可以参考下
    2015-05-05
  • Python-GUI wxPython之自动化数据生成器的项目实战

    Python-GUI wxPython之自动化数据生成器的项目实战

    本文主要介绍了Python-GUI wxPython之自动化数据生成器实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • 跟老齐学Python之开始真正编程

    跟老齐学Python之开始真正编程

    通过对四则运算的学习,已经初步接触了Python中内容,但是到目前为止,还不能算编程,只能算会用一些指令(或者叫做命令)来做点简单的工作。列位稍安勿躁,下面我们就学习如何编写一个真正的程序。
    2014-09-09
  • Python使用tkinter加载png、jpg等图片

    Python使用tkinter加载png、jpg等图片

    这篇文章主要介绍了Python使用tkinter加载png、jpg等图片,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • Python变量的定义和运算符的使用

    Python变量的定义和运算符的使用

    这篇文章主要介绍了Python变量的定义和运算符的使用,Python和C/Java不同,在定义变量的时候不需要显示的指定变量的类型,在赋值的时候自动就会确定类型,需要的朋友可以参考下
    2023-05-05

最新评论