1. 为什么我们需要局域网文件传输工具
每次在办公室或家里需要给同事、家人传文件时,你是不是也经历过这样的场景:翻箱倒柜找数据线,结果发现接口不匹配;用微信传大文件又慢又占空间;U盘来回插拔还担心病毒传播。这些问题在局域网环境下本不该存在——毕竟所有设备都在同一个网络里,理论上直接传输应该是最快最方便的方式。
传统解决方案要么过于复杂(比如配置Samba共享),要么功能单一(只能传文件不能传文本)。而用Python的FastAPI框架,我们完全可以在5分钟内搭建一个轻量级的Web服务,同时解决文件传输和剪贴板同步两大痛点。这个方案有以下几个不可替代的优势:
- 零配置:不需要安装任何客户端软件,打开浏览器就能用
- 跨平台:Windows、Mac、Linux、手机浏览器全兼容
- 双向传输:不仅电脑可以发文件给手机,手机也能传回电脑
- 剪贴板同步:复制的内容瞬间出现在所有设备上
- 完全私密:数据只在局域网内传输,不上传任何云端
2. FastAPI服务端核心实现
2.1 基础环境准备
首先确保你的Python版本在3.7以上(推荐3.9+),然后安装必要的依赖:
bash复制pip install fastapi uvicorn python-multipart
这里python-multipart是处理文件上传必需的依赖,而uvicorn是ASGI服务器用于运行FastAPI应用。我建议创建一个单独的虚拟环境,避免污染全局Python环境:
bash复制python -m venv filetransfer
source filetransfer/bin/activate # Linux/Mac
filetransfer\Scripts\activate # Windows
2.2 文件传输API实现
创建一个名为main.py的文件,写入以下核心代码:
python复制from fastapi import FastAPI, UploadFile, File
from fastapi.staticfiles import StaticFiles
from pathlib import Path
import uvicorn
import os
app = FastAPI()
# 存储上传文件的目录
UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
# 挂载静态文件目录用于直接访问下载
app.mount("/downloads", StaticFiles(directory=UPLOAD_DIR), name="downloads")
@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
file_path = Path(UPLOAD_DIR) / file.filename
with open(file_path, "wb") as buffer:
buffer.write(await file.read())
return {"filename": file.filename, "size": file.size}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
这段代码做了三件事:
- 创建了一个FastAPI应用实例
- 设置了一个上传目录(会自动创建)
- 实现了一个文件上传接口,接收文件并保存到本地
- 将上传目录挂载为静态文件服务,方便直接下载
关键点:使用
host="0.0.0.0"而不是默认的127.0.0.1,这样同一局域网内的其他设备才能访问到服务。
2.3 剪贴板同步功能扩展
在main.py中继续添加剪贴板功能:
python复制from fastapi import Request
from pydantic import BaseModel
class ClipboardData(BaseModel):
text: str
clipboard_content = ""
@app.post("/clipboard/")
async def update_clipboard(data: ClipboardData):
global clipboard_content
clipboard_content = data.text
return {"status": "success"}
@app.get("/clipboard/")
async def get_clipboard():
return {"content": clipboard_content}
这个实现虽然简单,但已经能满足基本需求。当你在A设备复制文本后调用/clipboard/接口更新内容,B设备通过访问同一个接口就能立即获取最新内容。
3. 前端交互界面开发
3.1 简易HTML页面
在项目根目录创建templates文件夹,然后新建index.html:
html复制<!DOCTYPE html>
<html>
<head>
<title>局域网文件传输</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.section { margin-bottom: 30px; border: 1px solid #eee; padding: 15px; border-radius: 5px; }
button { padding: 8px 15px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
#fileList { margin-top: 10px; }
#clipboardContent { width: 100%; min-height: 100px; margin-bottom: 10px; }
</style>
</head>
<body>
<h1>局域网文件传输工具</h1>
<div class="section">
<h2>文件传输</h2>
<input type="file" id="fileInput">
<button onclick="uploadFile()">上传文件</button>
<div id="fileList">
<h3>已上传文件:</h3>
<ul id="files"></ul>
</div>
</div>
<div class="section">
<h2>剪贴板同步</h2>
<textarea id="clipboardContent"></textarea>
<button onclick="updateClipboard()">同步剪贴板</button>
<button onclick="getClipboard()">获取剪贴板</button>
</div>
<script>
// 文件上传功能
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) return alert('请选择文件');
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/upload/', {
method: 'POST',
body: formData
});
const result = await response.json();
alert(`上传成功: ${result.filename}`);
refreshFileList();
} catch (error) {
alert('上传失败: ' + error.message);
}
}
// 剪贴板功能
async function updateClipboard() {
const content = document.getElementById('clipboardContent').value;
try {
await fetch('/clipboard/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: content })
});
alert('剪贴板已更新');
} catch (error) {
alert('更新失败: ' + error.message);
}
}
async function getClipboard() {
try {
const response = await fetch('/clipboard/');
const data = await response.json();
document.getElementById('clipboardContent').value = data.content;
} catch (error) {
alert('获取失败: ' + error.message);
}
}
// 刷新文件列表
async function refreshFileList() {
const response = await fetch('/downloads/');
const text = await response.text();
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(text, 'text/html');
const links = htmlDoc.querySelectorAll('a[href]');
const fileList = document.getElementById('files');
fileList.innerHTML = '';
links.forEach(link => {
if (link.href && !link.href.endsWith('/')) {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = link.href;
a.textContent = link.textContent;
a.target = '_blank';
li.appendChild(a);
fileList.appendChild(li);
}
});
}
// 初始化加载文件列表
refreshFileList();
</script>
</body>
</html>
3.2 集成前端到FastAPI
修改main.py,添加模板渲染支持:
python复制from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
@app.get("/")
async def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
现在运行服务后,访问http://localhost:8000就能看到完整的交互界面了。
4. 高级功能与优化
4.1 文件列表API优化
直接解析静态文件目录的HTML不太可靠,我们可以专门实现一个文件列表API:
python复制@app.get("/files/")
async def list_files():
files = []
for entry in os.scandir(UPLOAD_DIR):
if entry.is_file():
files.append({
"name": entry.name,
"size": entry.stat().st_size,
"url": f"/downloads/{entry.name}"
})
return {"files": files}
然后修改前端中的refreshFileList函数:
javascript复制async function refreshFileList() {
try {
const response = await fetch('/files/');
const data = await response.json();
const fileList = document.getElementById('files');
fileList.innerHTML = '';
data.files.forEach(file => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = file.url;
a.textContent = `${file.name} (${formatFileSize(file.size)})`;
a.target = '_blank';
li.appendChild(a);
fileList.appendChild(li);
});
} catch (error) {
console.error('获取文件列表失败:', error);
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
4.2 剪贴板自动同步
我们可以使用WebSocket实现剪贴板的实时同步。首先安装额外依赖:
bash复制pip install websockets
然后修改main.py:
python复制from fastapi import WebSocket
from typing import List
import asyncio
clients: List[WebSocket] = []
@app.websocket("/ws/clipboard")
async def websocket_clipboard(websocket: WebSocket):
await websocket.accept()
clients.append(websocket)
try:
while True:
data = await websocket.receive_text()
clipboard_content = data
# 广播给所有客户端
for client in clients:
try:
await client.send_text(data)
except:
clients.remove(client)
except:
clients.remove(websocket)
前端部分也需要相应修改:
javascript复制// 在页面加载时建立WebSocket连接
const socket = new WebSocket(`ws://${location.host}/ws/clipboard`);
socket.onmessage = (event) => {
document.getElementById('clipboardContent').value = event.data;
};
// 修改updateClipboard函数
async function updateClipboard() {
const content = document.getElementById('clipboardContent').value;
try {
socket.send(content);
} catch (error) {
alert('同步失败: ' + error.message);
}
}
4.3 安全增强措施
虽然是在局域网使用,但一些基本的安全措施还是必要的:
- 添加简单的认证(在
main.py中添加):
python复制from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
correct_username = "admin"
correct_password = "password" # 实际使用中应该用环境变量存储
if (credentials.username != correct_username or
credentials.password != correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/secure/")
async def secure_route(username: str = Depends(get_current_username)):
return {"message": f"Hello {username}"}
- 限制文件类型(修改上传接口):
python复制ALLOWED_EXTENSIONS = {'.txt', '.pdf', '.png', '.jpg', '.jpeg', '.gif'}
@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
file_ext = Path(file.filename).suffix.lower()
if file_ext not in ALLOWED_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"不支持的文件类型 {file_ext}"
)
# 其余上传逻辑不变...
5. 实际使用技巧与问题排查
5.1 如何在不同设备上访问
服务启动后,你需要在其他设备上访问这个服务。首先查看运行服务的电脑的局域网IP:
- Windows: 在命令提示符运行
ipconfig,找"IPv4地址" - Mac/Linux: 在终端运行
ifconfig,找"inet"地址
假设IP是192.168.1.100,那么其他设备在浏览器访问http://192.168.1.100:8000即可。
常见问题:如果其他设备无法访问,请检查:
- 运行服务的电脑防火墙是否放行了8000端口
- 所有设备是否确实在同一个局域网
- FastAPI是否使用了
host="0.0.0.0"
5.2 开机自启动配置
如果你希望这个服务在电脑开机时自动运行,可以这样配置:
Windows:
- 创建批处理文件
start_server.bat:
bat复制@echo off
cd /d "C:\path\to\your\project"
call filetransfer\Scripts\activate
python main.py
- 按Win+R,输入
shell:startup,把批处理文件放入打开的文件夹
Mac/Linux:
- 编辑crontab:
bash复制crontab -e
- 添加一行:
bash复制@reboot /path/to/your/filetransfer/bin/python /path/to/your/main.py
5.3 性能优化建议
当传输大文件或多人使用时,可以考虑以下优化:
- 增加上传大小限制(默认约100MB):
python复制app = FastAPI(
title="文件传输服务",
max_upload_size=1024 * 1024 * 1024 # 1GB
)
- 使用分块上传处理超大文件:
python复制@app.post("/upload/chunked/")
async def upload_chunked(file: UploadFile = File(...), chunk: int = 0, total: int = 1):
# 实现分块上传逻辑
pass
- 添加进度显示(前端修改):
javascript复制// 在上传函数中添加进度监听
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
console.log(`上传进度: ${percent}%`);
}
});
5.4 常见问题解决方案
问题1:上传文件时报413 Request Entity Too Large错误
解决:除了上面提到的max_upload_size设置,还需要配置uvicorn:
python复制uvicorn.run(
app,
host="0.0.0.0",
port=8000,
limit_max_requests=10000,
limit_concurrency=1000
)
问题2:剪贴板同步延迟高
解决:检查网络状况,如果使用WiFi建议切换到5GHz频段;或者降低WebSocket的心跳间隔:
python复制@app.websocket("/ws/clipboard")
async def websocket_clipboard(websocket: WebSocket):
await websocket.accept()
# 设置更短的心跳间隔
websocket._send_heartbeat = 5 # 5秒
# 其余逻辑不变...
问题3:文件列表不更新
解决:可能是浏览器缓存问题,在fetch请求中添加缓存控制:
javascript复制const response = await fetch('/files/', {
cache: 'no-store'
});
这个工具虽然简单,但已经覆盖了日常办公中80%的局域网传输需求。我在团队内部使用这个方案替代了传统的文件共享方式,特别是在需要频繁交换设计稿和文档的场景下,效率提升非常明显。根据实际使用经验,建议定期清理uploads目录下的旧文件,可以添加一个自动清理脚本:
python复制import time
from threading import Thread
def clean_old_files():
while True:
now = time.time()
for entry in os.scandir(UPLOAD_DIR):
if entry.is_file() and (now - entry.stat().st_mtime) > 86400: # 24小时
os.remove(entry.path)
time.sleep(3600) # 每小时检查一次
# 在启动服务时开启清理线程
if __name__ == "__main__":
cleaner = Thread(target=clean_old_files, daemon=True)
cleaner.start()
uvicorn.run(app, host="0.0.0.0", port=8000)
