尧图精选

基于Python与WebDAV构建跨平台游戏存档云同步工具

🕒 发布时间:2026/9/1 15:45:25 📁 来源:尧图网络
大家好我是专注于分享实用开发工具和解决方案的技术博主。今天我们来聊聊一个让无数游戏玩家头疼也让开发者感到棘手的问题游戏存档的跨平台、跨设备同步。你是否遇到过在台式机上辛苦通关的游戏想在笔记本上继续却找不到存档的尴尬或者在 Steam Deck 这类掌机上玩的游戏想在大屏幕上继续却束手无策特别是随着 SteamOS、Winlator 这类非传统 Windows 环境的普及存档管理变得更加复杂。本文将手把手带你从零构建一个轻量级、可扩展的游戏存档云同步工具它不仅支持 Windows、macOS、Linux 三大桌面平台还能覆盖 SteamOS、甚至通过 Winlator 运行 Android 游戏的复杂场景实现真正的“一次存档随处游戏”。1. 背景与核心概念为什么我们需要自建存档同步工具游戏存档本质上是游戏程序在运行时生成的一系列数据文件用于记录玩家的进度、物品、角色状态等信息。传统的同步方式依赖游戏厂商提供的云服务如 Steam Cloud但这存在几个显著痛点平台割裂Steam Cloud 只对 Steam 平台有效。Epic、GOG、独立游戏启动器、甚至直接运行的绿色版游戏其存档往往散落在本地磁盘的各个角落。设备限制在台式机、笔记本、Steam Deck 之间手动拷贝存档繁琐且易出错。特殊环境支持差SteamOS基于 Arch Linux的文件路径与 Windows 截然不同Winlator一个在 Android 上运行 Windows 程序的模拟器内部的“Windows”环境更是与宿主 Android 系统隔离存档导出导入极为麻烦。容量与选择性厂商云服务可能有容量限制且无法选择性地同步部分存档。因此一个自建的、通用的存档同步工具的核心价值在于将存档文件的管理权从游戏平台手中夺回通过自定义的规则和云端存储实现任意游戏、任意平台、任意设备间的自由同步。我们的工具将围绕以下几个核心概念构建存档路径规则库一个配置文件记录了不同游戏通过游戏名称或ID标识其存档在各大操作系统Win/macOS/Linux/SteamOS上的默认存储路径。这是工具的“大脑”。文件同步引擎负责对比本地存档与云端存档的差异并进行上传、下载、合并需谨慎处理等操作。我们将采用“单向同步为主冲突提示为辅”的策略。云存储后端工具不绑定特定云服务而是设计成可插拔的。我们将首先实现基于WebDAV协议的通用后端因为它被许多网盘如坚果云、Nextcloud支持且配置简单。平台适配层专门处理 SteamOS 的只读文件系统挑战、Winlator 内部路径到 Android 外部存储的映射等特殊场景。2. 环境准备与版本说明本项目将使用Python作为开发语言因其跨平台特性极佳库生态丰富。我们将尽量使用标准库和轻量级第三方库。基础环境Python 3.8推荐 3.10 或更高版本以获得更好的类型提示支持pipPython 包管理器主要第三方库requests用于进行 WebDAV 的 HTTP 请求。watchdog用于监控本地存档文件夹的变化实现实时/定时同步。pyyaml用于读写 YAML 格式的配置文件规则库、用户设置。click用于构建命令行界面CLI让工具更易用。send2trash在删除冲突备份文件时先发送到回收站避免误操作。你可以通过以下命令一次性安装所有依赖pip install requests watchdog pyyaml click send2trash项目结构预览在开始编码前我们先规划好项目目录这有助于理解后续的代码模块。game_save_sync_tool/ ├── main.py # 主程序入口 ├── config.yaml # 用户配置文件云存储信息、同步设置 ├── game_rules.yaml # 游戏存档路径规则库 ├── core/ │ ├── __init__.py │ ├── sync_engine.py # 同步引擎核心逻辑 │ ├── cloud_webdav.py # WebDAV 云存储后端实现 │ └── path_resolver.py # 跨平台路径解析器 ├── utils/ │ ├── __init__.py │ ├── file_utils.py # 文件操作工具函数 │ └── logger.py # 日志模块 └── platforms/ ├── __init__.py ├── steamos.py # SteamOS 特殊处理 └── winlator.py # Winlator 特殊处理3. 核心模块设计与原理拆解3.1 游戏规则库 (game_rules.yaml)这是工具的“知识库”采用 YAML 格式便于阅读和编辑。其核心结构是“游戏名”到“各平台路径”的映射。# game_rules.yaml The Witcher 3: windows: | %USERPROFILE%\Documents\The Witcher 3\gamesaves\ %USERPROFILE%\Documents\The Witcher 3\gamesaves\modsaves\ linux: | ~/.local/share/Steam/steamapps/compatdata/292030/pfx/drive_c/users/steamuser/Documents/The Witcher 3/gamesaves/ steamos: | /home/deck/.local/share/Steam/steamapps/compatdata/292030/pfx/drive_c/users/steamuser/Documents/The Witcher 3/gamesaves/ # macOS 或其他平台路径... Hollow Knight: windows: %USERPROFILE%\AppData\LocalLow\Team Cherry\Hollow Knight\ linux: ~/.config/unity3d/Team Cherry/Hollow Knight/ steamos: /home/deck/.config/unity3d/Team Cherry/Hollow Knight/ # 通用规则使用变量如 {user} 代表用户名 General_Rule: windows_saved_games: %USERPROFILE%\Saved Games\{game_name}\ linux_local_share: ~/.local/share/{game_name}/原理path_resolver.py模块会读取此文件根据当前运行的操作系统类型解析%USERPROFILE%、~等环境变量并将{game_name}、{user}等占位符替换为实际值最终生成绝对路径。3.2 云存储后端抽象与 WebDAV 实现我们定义一个抽象的CloudStorage类规定必须实现upload,download,list,delete等方法。这样未来可以轻松扩展支持 S3、SFTP 等。# core/cloud_base.py from abc import ABC, abstractmethod from typing import List, Optional class CloudStorage(ABC): abstractmethod def upload(self, local_path: str, remote_path: str) - bool: 上传本地文件到云端 pass abstractmethod def download(self, remote_path: str, local_path: str) - bool: 从云端下载文件到本地 pass abstractmethod def list_files(self, remote_prefix: str) - List[str]: 列出云端指定前缀的文件列表 pass abstractmethod def delete(self, remote_path: str) - bool: 删除云端文件 pass基于此我们实现WebDAVStorage# core/cloud_webdav.py import os import requests from requests.auth import HTTPBasicAuth from urllib.parse import urljoin from .cloud_base import CloudStorage class WebDAVStorage(CloudStorage): def __init__(self, base_url: str, username: str, password: str): self.base_url base_url.rstrip(/) / self.auth HTTPBasicAuth(username, password) self.session requests.Session() self.session.auth self.auth def upload(self, local_path: str, remote_path: str) - bool: url urljoin(self.base_url, remote_path) # 确保远程目录存在 dir_path os.path.dirname(remote_path) self._ensure_remote_dir(dir_path) with open(local_path, rb) as f: response self.session.put(url, dataf) return response.status_code in [200, 201, 204] def _ensure_remote_dir(self, remote_dir: str): 递归创建远程目录WebDAV MKCOL 方法 if not remote_dir: return parts remote_dir.strip(/).split(/) current_path for part in parts: current_path part / url urljoin(self.base_url, current_path) resp self.session.request(MKCOL, url) # 207 或 409 表示目录已存在或其他状态可忽略 if resp.status_code not in [201, 207, 409]: # 尝试创建父目录失败可能不影响后续操作记录日志 pass def download(self, remote_path: str, local_path: str) - bool: url urljoin(self.base_url, remote_path) response self.session.get(url) if response.status_code 200: os.makedirs(os.path.dirname(local_path), exist_okTrue) with open(local_path, wb) as f: f.write(response.content) return True return False def list_files(self, remote_prefix: str) - List[str]: # 简化实现使用 PROPFIND 进行深度搜索这里返回简单列表 # 实际实现需解析 WebDAV 的 XML 响应 url urljoin(self.base_url, remote_prefix) headers {Depth: 1} response self.session.request(PROPFIND, url, headersheaders) # 解析 response.text (XML) 获取文件列表 # 此处为示例省略详细 XML 解析代码 return [] # 应返回文件路径列表 def delete(self, remote_path: str) - bool: url urljoin(self.base_url, remote_path) response self.session.delete(url) return response.status_code in [200, 204]关键点WebDAV 协议通过 HTTP 方法PUT上传GET下载DELETE删除MKCOL创建目录PROPFIND列目录实现文件管理。我们使用requests库模拟这些操作。3.3 同步引擎策略 (sync_engine.py)同步的核心是决策。我们采用一个简单的“时间戳优先”策略并处理冲突。# core/sync_engine.py import os import shutil from datetime import datetime from send2trash import send2trash from .cloud_webdav import WebDAVStorage class SyncEngine: def __init__(self, cloud_storage: WebDAVStorage, local_base: str, remote_base: str): self.cloud cloud_storage self.local_base local_base self.remote_base remote_base def sync_game(self, game_name: str, local_save_path: str): 同步单个游戏 remote_path_prefix f{self.remote_base}/{game_name}/ # 1. 确保本地路径存在 if not os.path.exists(local_save_path): print(f本地存档路径不存在: {local_save_path}) return # 2. 遍历本地存档文件 for root, dirs, files in os.walk(local_save_path): for file in files: local_file_full os.path.join(root, file) # 计算相对于本地存档根目录的相对路径 rel_path os.path.relpath(local_file_full, local_save_path) remote_file_path remote_path_prefix rel_path.replace(os.sep, /) # 3. 决策上传、下载还是冲突 self._sync_file(local_file_full, remote_file_path) def _sync_file(self, local_path: str, remote_path: str): 同步单个文件的核心逻辑 local_mtime os.path.getmtime(local_path) # 尝试获取云端文件信息可通过 HEAD 请求获取最后修改时间 # 此处简化假设我们能从 list_files 或一个元数据接口获取时间 remote_exists self._check_remote_exists(remote_path) remote_mtime self._get_remote_mtime(remote_path) if remote_exists else 0 if not remote_exists: # 场景1云端不存在直接上传 print(f[上传] {local_path} - {remote_path}) self.cloud.upload(local_path, remote_path) elif local_mtime remote_mtime 1: # 本地更新留1秒容差 # 场景2本地文件比云端新上传覆盖 print(f[覆盖上传] {local_path} - {remote_path} (本地较新)) # 可选先将云端旧文件备份 backup_path remote_path f.backup_{int(remote_mtime)} self.cloud.upload(local_path, backup_path) # 实际上传备份 self.cloud.upload(local_path, remote_path) elif remote_mtime local_mtime 1: # 场景3云端文件比本地新下载覆盖 print(f[下载] {remote_path} - {local_path} (云端较新)) # 备份本地旧文件到回收站 local_backup local_path f.backup_{int(local_mtime)} shutil.copy2(local_path, local_backup) send2trash(local_backup) # 移到回收站更安全 self.cloud.download(remote_path, local_path) else: # 场景4时间戳接近视为同步 print(f[已同步] {local_path}) def _check_remote_exists(self, remote_path: str) - bool: # 简化实现尝试发起 HEAD 请求 # 实际应使用更高效的方式如缓存的文件列表 return False # 示例占位 def _get_remote_mtime(self, remote_path: str) - float: # 简化实现WebDAV 可通过 PROPFIND 获取 getlastmodified 属性 return 0.0 # 示例占位策略解析我们以文件的最后修改时间 (mtime) 作为同步依据。这能解决大部分问题但对于频繁跨设备游玩的玩家可能会遇到“冲突”即两个设备几乎同时修改了存档。上述代码在覆盖前进行了备份更完善的方案可以引入“冲突文件”概念将其重命名为xxx.conflict并提示用户手动解决。4. 完整实战案例构建并运行你的同步工具4.1 创建项目结构与配置文件首先按照之前规划的目录结构创建文件夹和文件。 创建用户配置文件config.yaml# config.yaml cloud: type: webdav webdav: base_url: https://dav.jianguoyun.com/dav/ # 示例坚果云WebDAV地址 username: your_emailexample.com password: your_application_password # 强烈建议使用应用密码非账户密码 remote_base_path: GameSaves # 云端存储的根文件夹 sync: local_base_path: ~/game_saves_sync # 本地暂存或工作区可选 rules_file: game_rules.yaml # 同步模式manual手动 watch监控 interval间隔单位秒 mode: manual interval: 300 # 冲突处理策略newer_wins新的覆盖旧的 backup备份旧的 manual手动 conflict_strategy: backup platform_overrides: steamos: # SteamOS 可能需要对某些路径进行特殊挂载或权限处理 compatdata_root: /home/deck/.local/share/Steam/steamapps/compatdata/ winlator: # Winlator 内部Windows路径到Android外部存储的映射 # 例如Z:\ 可能对应 /storage/emulated/0/ drive_z_mount: /storage/emulated/0/4.2 编写主程序入口 (main.py)主程序负责解析配置、初始化组件、并提供命令行接口。# main.py import os import yaml import click from pathlib import Path from core.cloud_webdav import WebDAVStorage from core.sync_engine import SyncEngine from core.path_resolver import PathResolver import sys def load_config(config_pathconfig.yaml): with open(config_path, r, encodingutf-8) as f: return yaml.safe_load(f) click.group() def cli(): 游戏存档云同步工具 pass cli.command() click.option(--game, -g, help指定要同步的游戏名称在规则库中定义) click.option(--all, -a, is_flagTrue, help同步规则库中所有游戏) def sync(game, all): 执行同步操作 config load_config() cloud_cfg config[cloud][webdav] # 1. 初始化云存储 cloud WebDAVStorage( base_urlcloud_cfg[base_url], usernamecloud_cfg[username], passwordcloud_cfg[password] ) # 2. 初始化同步引擎 engine SyncEngine( cloud_storagecloud, local_baseconfig[sync][local_base_path], remote_basecloud_cfg[remote_base_path] ) # 3. 初始化路径解析器 resolver PathResolver(config[platform_overrides]) # 4. 加载游戏规则 with open(config[sync][rules_file], r, encodingutf-8) as f: game_rules yaml.safe_load(f) games_to_sync [] if game: if game in game_rules: games_to_sync [game] else: click.echo(f错误未找到游戏 {game} 的规则。) sys.exit(1) elif all: games_to_sync [g for g in game_rules.keys() if not g.startswith(General_Rule)] else: click.echo(请使用 --game 指定游戏或 --all 同步所有游戏。) return # 5. 遍历并同步 for game_name in games_to_sync: click.echo(f\n 开始同步游戏{game_name} ) rule game_rules[game_name] # 解析出当前平台的实际路径 local_path resolver.resolve(rule) if local_path and os.path.exists(local_path): engine.sync_game(game_name, local_path) else: click.echo(f 警告解析的路径不存在或无法访问 - {local_path}) cli.command() def list_games(): 列出规则库中定义的所有游戏 config load_config() with open(config[sync][rules_file], r, encodingutf-8) as f: game_rules yaml.safe_load(f) click.echo(已配置的游戏存档规则) for game in game_rules: if not game.startswith(General_Rule): click.echo(f - {game}) if __name__ __main__: cli()4.3 实现路径解析器 (core/path_resolver.py)这是处理跨平台路径差异的核心。# core/path_resolver.py import os import platform import re class PathResolver: def __init__(self, platform_overridesNone): self.system platform.system().lower() # windows, linux, darwin self.overrides platform_overrides or {} # 处理 SteamOS (通常是 Linux 但我们可以通过环境变量或路径判断) if self.system linux and steamos in os.uname().version.lower(): self.system steamos # 处理 Winlator 环境可通过检查特定环境变量判断 if WINLATOR in os.environ: self.system winlator def resolve(self, rule_dict): 根据规则字典解析出当前系统的实际路径 # 1. 获取当前系统对应的路径模板 path_template rule_dict.get(self.system) if not path_template: # 尝试回退到通用规则 if self.system linux and steamos in rule_dict: path_template rule_dict.get(steamos) elif self.system winlator: # Winlator 内部是 Windows 环境但需要映射到 Android path_template rule_dict.get(windows) else: return None # 2. 展开环境变量 (如 %USERPROFILE%, ~) path os.path.expanduser(path_template) path os.path.expandvars(path) # 3. 应用平台特定的覆盖规则如路径重映射 if self.system winlator and winlator in self.overrides: # 将 Windows 路径 (如 Z:\path\to\save) 映射到 Android 路径 drive_z self.overrides[winlator].get(drive_z_mount, ) if drive_z and path.startswith(Z:\\): path path.replace(Z:\\, drive_z).replace(\\, /) # 4. 确保路径分隔符正确 if self.system in [linux, darwin, steamos]: path path.replace(\\, /) else: path path.replace(/, \\) return os.path.normpath(path)4.4 运行与验证配置你的云存储修改config.yaml填入你的 WebDAV 服务信息如坚果云。编辑游戏规则在game_rules.yaml中添加你常玩游戏的存档路径。可以通过搜索引擎查询“[游戏名] save file location PC”来找到路径。安装依赖并运行cd /path/to/your/game_save_sync_tool pip install -r requirements.txt # 如果你创建了此文件 # 或者直接安装pip install requests watchdog pyyaml click send2trash使用命令行工具列出所有已配置游戏python main.py list-games同步特定游戏python main.py sync --game Hollow Knight同步所有游戏python main.py sync --all首次运行结果示例 开始同步游戏Hollow Knight [上传] C:\Users\YourName\AppData\LocalLow\Team Cherry\Hollow Knight\user1.dat - https://dav.jianguoyun.com/dav/GameSaves/Hollow Knight/user1.dat [上传] ... (其他存档文件) 同步完成4.5 进阶添加文件监控自动同步利用watchdog库我们可以实现当存档文件发生变化时自动触发同步。# utils/watcher.py (新建文件) import time from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from core.sync_engine import SyncEngine class SaveFileEventHandler(FileSystemEventHandler): def __init__(self, sync_engine: SyncEngine, game_name: str, watch_path: str): self.engine sync_engine self.game_name game_name self.watch_path watch_path self.debounce_time 5 # 防抖5秒内变化只同步一次 self.last_sync 0 def on_modified(self, event): if not event.is_directory: current_time time.time() if current_time - self.last_sync self.debounce_time: print(f检测到存档变化: {event.src_path}) # 这里可以优化为只同步变化的文件而非整个游戏 self.engine.sync_game(self.game_name, self.watch_path) self.last_sync current_time def start_watching(game_name, local_save_path, sync_engine): event_handler SaveFileEventHandler(sync_engine, game_name, local_save_path) observer Observer() observer.schedule(event_handler, local_save_path, recursiveTrue) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join()在主程序中添加一个watch命令来调用它。5. 常见问题与排查思路问题现象可能原因排查步骤与解决方案同步失败报 SSL 证书错误1. 系统时间不正确。2. 使用了自签名证书的 WebDAV 服务。1. 校准系统时间。2. 在WebDAVStorage初始化Session时设置verifyFalse(仅限测试环境生产环境有安全风险)。上传/下载时卡住或超时1. 网络连接不稳定。2. 云端存储空间已满。3. 单个存档文件过大如超过 100MB。1. 检查网络尝试重试。2. 登录云盘网页端检查剩余空间。3. 考虑在同步前压缩大文件或实现分块传输。解析的存档路径不存在1. 游戏规则中的路径错误。2. 游戏尚未在本地运行过未生成存档目录。3. Steam Play (Proton) 的游戏其compatdata下的路径可能因 Proton 版本不同而变化。1. 仔细核对game_rules.yaml使用绝对路径或正确环境变量。2. 先运行一次游戏生成存档目录。3. 到Steam/steamapps/compatdata/下找到对应游戏 AppID 的文件夹检查内部pfx路径。在 SteamOS 上权限被拒绝SteamOS 的/home/deck目录是用户可写的但某些系统目录只读。确保你的存档路径在用户主目录下如/home/deck/.local/share/...。如果工具需要写系统文件可能需要通过sudo运行但极其不推荐应调整路径到用户区。Winlator 内路径映射无效Winlator 的驱动器映射 (Z:\) 可能因版本或配置不同而变化。1. 在 Winlator 内部打开文件管理器确认Z:\对应的实际 Android 路径。2. 更新config.yaml中platform_overrides.winlator.drive_z_mount的值。冲突文件过多在多设备间频繁、快速地切换游戏并保存。1. 调整debounce_time减少同步频率。2. 采用更智能的冲突解决策略如三向合并对文本格式存档或保留两个版本让用户选择。3. 养成“退出游戏后再同步”的习惯。6. 最佳实践与工程建议安全第一使用应用专用密码永远不要在配置文件中直接使用你的网盘主账户密码。像坚果云、Dropbox 等都支持生成“应用密码”或“访问令牌”权限仅限于 WebDAV即使泄露也不会危及主账户。规则库的维护开源与共享考虑将你的game_rules.yaml托管到 GitHub Gist 或一个公共仓库让社区共同维护形成强大的游戏存档路径数据库。版本控制对game_rules.yaml使用 Git 管理方便回溯和合并更新。同步策略优化增量同步对于大文件可以计算 MD5 或 SHA1 哈希只有哈希值不同时才传输整个文件。压缩传输在传输前对存档文件夹进行压缩如.tar.gz尤其适用于大量小文件能显著提升速度。选择性同步在规则中支持include和exclude模式例如只同步*.sav文件忽略*.tmp或*.log文件。错误处理与日志为工具添加详细的日志系统记录每次同步的操作、成功/失败、文件大小、耗时等信息。这便于后期排查问题。实现重试机制对于网络波动造成的失败自动重试 2-3 次。添加--dry-run参数模拟运行并显示将要执行的操作而不实际修改任何文件。图形化界面 (GUI) 拓展对于非技术用户可以使用PyQt、Tkinter或Flet框架包裹核心同步逻辑提供一个图形界面方便选择游戏、查看同步状态、解决冲突。支持更多云后端抽象化让我们可以轻松添加新后端。例如实现S3Storage用于 AWS S3 或兼容 S3 的服务如 MinIO实现SFTPStorage用于通过 SSH 同步到自己的服务器。生产环境部署可以将此工具打包成 Docker 镜像在任何支持 Docker 的设备包括 NAS上运行作为常驻服务。在 Steam Deck 上可以将其配置为一个开机自启动的 systemd 服务或添加到 Steam 库作为“非Steam游戏”来方便启动。通过本文我们从需求分析、架构设计、模块编码到实战运行完整地构建了一个高度可定制、跨平台的游戏存档云同步工具。它不仅解决了多设备游戏存档管理的痛点更提供了一个灵活的框架你可以根据自己的需求不断扩展它例如添加对更多云服务的支持、实现更复杂的冲突合并算法、或者为其开发一个漂亮的图形界面。技术服务于需求希望这个项目能成为你游戏之旅的得力助手让你在任何设备上都能无缝衔接你的游戏世界。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →