之前的版本,经过使用中测试,发现让普通使用者设置备份路径,可能有点难度。特增加了默认设置,直接读取电脑所有盘符,监控所有文件的创建和修改记录,实时备份。还增加了特殊路径忽略配置,因为有些软件生成的文件是无需备份要排除干扰的。另外还特地增加了非第三方在线编辑功能。总体功能跟新如下:
1、实时监控指定目录,或所有盘符的文件变化信息,新增、修改、删除。
2、多线程文件传输。
3、大文件分片传输,然后在服务器端自动合成。(试验了传输单个60G大文件压缩包,服务器端合成后,可正常打开)
4、一次性传输目录下所有文件。用于初始化监控时同步全部文件。
5、用户验证。(访问web接口验证用户权限)
6、文件历史版本保存。(还在考虑需不需要,需要时再做吧,升级服务器端代码就行了)
7、程序启动时,自动最小化到系统盘图标。
8、增加配置:忽略路径、忽略文件类型、只传文件类型。
9、配置文件分:个人配置,和统一配置,统一配置由服务器端获取,实现个性化监控。
10、在线编辑功能:在浏览器端查看office文件时,点击文件名可直接打开文件编辑,保存后自动上传。
这里记录一下此次重要升级,在线编辑功能的实现办法。没有考虑使用第三方的原因是,需要安装编辑服务器,这个可以后续再对接onlyoffice。我的网盘在线编辑是直接调用本地office软件进行文件编辑,原汁原味,体验更好,保存后无感知自动上传。
实现思路是在网盘python客户端实现一个web服务,接受URL传参触发文件路径比对,如果本地同路径文件存在,则直接调用office打开编辑,如果本地文件不存在,则下载保存到本地同途径,再打开编辑。保持了网络文件与本地文件一致。
WEB服务代码:
引用URL检测代码模块:
from webSrv import DocumentHandler
监控URL中的get请求。
webSrv.py详细代码:
from http.server import HTTPServer, BaseHTTPRequestHandler import json import os import base64 import configparser import requests import time from urllib.parse import unquote from urllib.parse import unquote_plus import tkinter as tk from tkinter import messagebox import subprocess # 全局变量存储文档数据 documents = [ {"id": 1, "title": "项目计划书", "content": "这是项目计划书的内容..."}, {"id": 2, "title": "技术规范文档", "content": "这里是技术规范说明..."}, {"id": 3, "title": "用户手册", "content": "用户手册详细操作指南..."} ] class DocumentHandler(BaseHTTPRequestHandler): def do_GET(self): #root.after(100 , log_message(f"11111111111111", "info")) if self.path == '/mydocuments': self.send_response(200) self.send_header('Content-type', 'application/json') self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() # 返回文档列表 self.wfile.write(json.dumps(documents, ensure_ascii=False).encode('utf-8')) #self.wfile.close() # 关闭连接 #self.rfile.close() # 关闭连接 elif self.path.startswith('/mydocuments/'): try: doc_id = int(self.path.split('/')[-1]) doc = next((d for d in documents if d["id"] == doc_id), None) if doc: self.send_response(200) self.send_header('Content-type', 'application/json') self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() self.wfile.write(json.dumps(doc, ensure_ascii=False).encode('utf-8')) else: self.send_error(404, "Document not found") except ValueError: self.send_error(400, "Invalid document ID") elif self.path.startswith('/openfile/?'): #在非控制台模式下响应异常,尝试将send_response替换为send_response_only #self.send_response(200) self.send_response_only(200) self.send_header('Content-type', 'text/html') self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() docUrl = self.path.split('?')[1] docUid = docUrl.split('***')[0] #参数传过来的当前uid,可能不是GUI登录的UID docPath = docUrl.split('***')[1] # 发送响应体 message = "<html><body><h1>" + docUid + " OpenFile:"+docPath+"</h1></body></html>" self.wfile.write(message.encode('utf-8')) guiuid = self.getUserCfg("userid") if docUid == guiuid: self.toOpenFile(docPath) else: self.showmsg(400,80,"电脑端备份软件登录的用户不是:"+docUid) return else: self.send_error(404, "Path not found") def do_POST(self): if self.path == '/mydocuments': content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) try: new_doc = json.loads(post_data.decode('utf-8')) new_doc["id"] = max([d["id"] for d in documents], default=0) + 1 documents.append(new_doc) self.send_response(201) self.send_header('Content-type', 'application/json') self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() self.wfile.write(json.dumps(new_doc, ensure_ascii=False).encode('utf-8')) except json.JSONDecodeError: self.send_error(400, "Invalid JSON") else: self.send_error(404, "Path not found") def do_OPTIONS(self): self.send_response(200) self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Content-Type') self.end_headers() def showmsg(self,wid,hig,msgx): # 创建主窗口 root = tk.Tk() root.title("提示") root.resizable(False, False) width = wid height= hig screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() x = (screen_width - width) // 2 y = (screen_height - height) // 2 root.geometry(f"{width}x{height}+{x}+{y}") #root.geometry("600x100") # 设置窗口大小,根据需要调整 root.attributes('-topmost', True) # 确保窗口在最顶层 # 创建一个标签显示消息 label = tk.Label(root, text=msgx, font=('Arial', 12), wraplength=500) label.pack(pady=20) # 在窗口中居中显示消息 # 设置3秒后关闭窗口 root.after(3000, root.destroy) # 3000毫秒后调用root.destroy()方法关闭窗口 #root.protocol("WM_DELETE_WINDOW", lambda: None) # 禁止通过点击关闭按钮关闭窗口 # 进入主事件循环 root.mainloop() #file_path=/我的E盘/temp/测试.docx def toOpenFile(self , file_path): file_path = unquote_plus(file_path) file_path = file_path.replace("\\","/") file_path = file_path.replace("//","/") file_path = file_path.replace("//","/") file_path = file_path.replace("//","/") print("网盘路径:"+file_path) locPath = self.getLocatPath(file_path) print("本地路径:"+locPath) #file_path = 'example.txt' if len(locPath)<1: self.showmsg(300,80,"本地此文件夹配置不存在") return if len(locPath) > 1 and file_path[-1]=="/": #如果是/结尾的,表示打开本地目录 if os.path.exists(locPath): self.showmsg(300,80,"打开本地文件夹中......") subprocess.Popen(['start', '', '/max', locPath], shell=True) else: self.showmsg(300,80,"本地此文件夹不存在") return if len(locPath) > 1 and os.path.exists(locPath): self.showmsg(300,80,"打开文件中......") subprocess.Popen(['start', '', '/max', locPath], shell=True) #os.startfile(locPath) else: print(">>>notfile>>>"+locPath) self.showmsg(600,80,"本地对应文件不存在:"+locPath + ",开始下载打开编辑。") self.down_file(file_path , locPath) if os.path.exists(locPath): subprocess.Popen(['start', '', '/max', locPath], shell=True) def getUserCfg(self , keyy): config = configparser.ConfigParser() config.read('config_user.ini' , encoding='utf-8') return config.get('main', keyy).strip() #入参file_path格式 #/我的C盘/1222/11111/1111/2222.doc def getLocatPath(self,file_path): locd = "" file_path = unquote_plus(file_path) if file_path.count("/") == 1: #如果是网盘根目录下的文件 os.makedirs("c:/deskDoc", exist_ok=True) return "c:/deskDoc" + file_path rfod = file_path.split('/')[1] config = configparser.ConfigParser() config.read('config_user.ini' , encoding='utf-8') loc = config.get('main', 'locdir').strip() # c:/ rmt = config.get('main', 'rmtdir').strip() if rfod.lower().strip() == rmt.lower().strip(): locd = loc + file_path[len(rmt)+1:len(file_path)] else: self.json_file = "path_config.json" if os.path.exists(self.json_file): with open(self.json_file, "r", encoding="utf-8") as f: datas = json.load(f) self.path_cfg = datas for item_data in datas: phh = item_data.get('local_path') rhh = item_data.get('remote_directory') if rfod.lower().strip() == rhh.lower().strip(): locd = phh + file_path[len(rhh)+1:len(file_path)] locd = locd.replace("\\","/") locd = locd.replace("//","/") return locd #入参url格式 #/我的C盘/1222/11111/1111/2222.doc def down_file(self , url , savedir): url = unquote_plus(url) savedir = unquote_plus(savedir) #读取个人设置 config = configparser.ConfigParser() config.read('config_user.ini' , encoding='utf-8') uid = config.get('main', 'userid').strip() # c:/ pwd = config.get('main', 'password').strip() try: pwd = base64.b64decode(pwd).decode() #密码解密 except Exception as e: pwd = "" #读取服务器配置 configs = configparser.ConfigParser() configs.read('config_srv.ini' , encoding='utf-8') self.conf_srv_ip = configs.get('main', 'srv_ip') self.conf_srv_pt = configs.get('main', 'srv_port') self.conf_srv_ur = configs.get('main', 'srv_url') self.conf_srv_ls = configs.get('main', 'srv_list') s = url.rfind("/") #相当于lastindexof fodd = url[:s] fnam = url[s:] fnam = fnam.replace("/","") durl = self.conf_srv_ip + ":" + self.conf_srv_pt + "/" + self.conf_srv_ls + "/downx.jsp?" durl = durl + "file=" + fodd + "&" durl = durl + "name=" + fnam + "" durl = durl.replace("\\","/") durl = durl.replace("//","/") savedir = savedir.replace("\\","/") savedir = savedir.replace("//","/") print("【down url】"+durl) s = savedir.rfind("/") sdir = savedir[:s] self.download_file(durl , sdir , fnam , uid , pwd) def download_file(self, url, save_dir=None,file_name=None,uid=None,pwd=None): """ 下载文件并保存到指定路径,从网络路径中query参数中获取文件名,例如https://127.0.0.1:8000/web/file?path=2025041616372016\\5ed63734774b40d181fd96e1c58133d2.pdf :param url: 文件下载URL :param save_dir: 文件保存路径 :param file_name: 文件名 """ headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7;application/json, text/javascript, */*; q=0.01', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Cookie': '132213213213213213', 'x-username': f'{uid}', 'x-userpwd': f'{pwd}', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36' } try: if file_name is None: print("not file_name") return False if save_dir is None: print("not save_dir") return False save_path = os.path.abspath(save_dir) file_path = os.path.join(save_path, file_name) if save_dir and not os.path.exists(save_dir): os.makedirs(save_dir, exist_ok=True) response = requests.get("http://"+url, stream=True, headers=headers) response.raise_for_status() with open(file_path, 'wb') as file: file.write(response.content) return True except requests.exceptions.RequestException as e: print(f"网络请求失败:{str(e)}") except IOError as e: print(f"文件操作失败:{str(e)}") except Exception as e: print(f"未知错误:{str(e)}") return False经过这段时间研究,越来越喜欢python了。生态强大资源丰富。在微服务应用上大有可为哦。
在线编辑office文件触发效果