• 【无标题】


    编程解决一切烦恼

    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起来舒服多了!

    我最近开发了一个笔录加密共享App 也是使用了WebSocket
    下载地址:https://www.wordsfairy.cloud/introduce/

    请添加图片描述

    思路

    使用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", "发送完成");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    用于文件传输过程中表示文件发送进度的模型类

    data class FileSendingDot(
        val fileName: String,
        val bufferSize: Int,
        val total: Long,
        val sent: Long,
        val data: String
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    解析同步请求,操作本地文件夹
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    利用循环分块读取文件并通过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))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    安卓客户端 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)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    自定义保存路径

    选择文件夹

    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()
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Uri的path

    fun Uri.toFilePath(): String {
    
        val uriPath = this.path ?: return ""
        val path = uriPath.split(":")[1]
        return Environment.getExternalStorageDirectory().path + "/" + path
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    请添加图片描述

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    接收文件

    使用 Base64.decode() 方法将 base64 数据解码成字节数组 fileData

    val fileName = dot.fileName
    val file = File(AppSystemSetManage.fileSavePath, fileName)
    val fileData = Base64.decode(dot.data, Base64.DEFAULT)
    
    • 1
    • 2
    • 3
    • 接着就是使用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,
                )
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    @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
                            )
                    }
                }
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    效果图

    请添加图片描述

    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()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    源码

    github: https://github.com/JIULANG9/FileSync

    gitee: https://gitee.com/JIULANG9/FileSync

  • 相关阅读:
    基于springboot的ShardingSphere5.2.1的分库分表的解决方案之mysql主从的配置(五)
    分别使用BP/RBF/GRNN神经网络识别航迹异常matlab仿真
    BSN长话短说之十一:为什么NFT会成为文化数字化的核心
    数据结构简答题综合
    功能测试、自动化测试、性能测试的区别
    优化理论笔记
    图片处理后再保存为图片到文件夹中,文件夹下文件名不变改格式保存
    Android Studio adb WiFi插件的使用
    G1D15-fraud-APT-汇报-基础模型与LR相关内容总结-KG-cs224w colab1-ctf rce41-44
    Vue核心 内置指令 自定义指令
  • 原文地址:https://blog.csdn.net/jiulang9/article/details/133252519