Build a LocalSend-inspired LAN file sharing app with Python and WebSockets
Welcome to another official UVF IT build guide. Today we are creating a minimal, working clone of LocalSend, the popular open-source tool for sharing files securely over your local network without an internet connection.
We will use Python with the websockets library to handle real-time communication and a simple HTTP server for file delivery. This project is designed to be completed in about 45 minutes and requires no paid APIs or external services.
First, we need to set up a clean environment for our application. Create a new directory for the project and navigate into it. This keeps our code organized and separate from other system files.
mkdir localsend-clone
cd localsend-clone
pip install websockets aiohttpNow we create the core of the receiver application. This script will listen for incoming WebSocket connections on port 8765. When a sender connects, it expects a JSON payload containing the file's name and size, which we will log to the console to verify the connection is working.
server.py
import asyncio
import json
import websockets
async def handler(websocket, path):
print(f"New connection from {websocket.remote_address}")
try:
# Wait for the metadata message from the sender
message = await websocket.recv()
data = json.loads(message)
print(f"Received metadata: {data}")
# Send an acceptance signal back to the sender
await websocket.send(json.dumps({"status": "accepted"}))
except Exception as e:
print(f"Error: {e}")
finally:
print("Connection closed")
async def main():
async with websockets.serve(handler, "0.0.0.0", 8765):
print("WebSocket server listening on ws://0.0.0.0:8765")
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())To actually transfer the file data, we need an HTTP server. We will extend our server.py to run an aiohttp server on port 8080 alongside the WebSocket server. This server will serve files from a shared directory, allowing the sender to place files there and the receiver to download them via standard HTTP requests.
server.py
import asyncio
import json
import os
import websockets
from aiohttp import web
# Ensure the shared directory exists
SHARED_DIR = "shared"
if not os.path.exists(SHARED_DIR):
os.makedirs(SHARED_DIR)
async def ws_handler(websocket, path):
print(f"New WebSocket connection from {websocket.remote_address}")
try:
message = await websocket.recv()
data = json.loads(message)
print(f"Received metadata: {data}")
await websocket.send(json.dumps({"status": "accepted"}))
except Exception as e:
print(f"WS Error: {e}")
finally:
print("WebSocket connection closed")
async def serve_file(request):
"""Serve files from the shared directory."""
filename = request.match_info.get('filename')
if not filename:
return web.Response(status=400, text="Missing filename")
# Prevent directory traversal
safe_path = os.path.join(SHARED_DIR, os.path.basename(filename))
if not os.path.exists(safe_path):
return web.Response(status=404, text="File not found")
return web.FileResponse(safe_path)
async def create_app():
app = web.Application()
app.router.add_get('/files/{filename}', serve_file)
return app
async def main():
# Start HTTP Server
app = await create_app()
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, '0.0.0.0', 8080)
await site.start()
print("HTTP file server listening on http://0.0.0.0:8080")
# Start WebSocket Server
async with websockets.serve(ws_handler, "0.0.0.0", 8765):
print("WebSocket server listening on ws://0.0.0.0:8765")
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())Now we create the sender side of the application in a new file called client.py. This script will handle the logic for connecting to the receiver's WebSocket server and transmitting the file metadata.
The client takes the receiver's IP address and the file name as command-line arguments. It constructs the WebSocket URL, connects to the server, and sends a JSON object containing the file name and its size. This metadata exchange is crucial because it allows the receiver to prepare for the incoming file before the actual data transfer begins.
client.py
import asyncio
import json
import os
import sys
import websockets
async def send_metadata(uri, filename):
"""
Connects to the receiver's WebSocket server and sends file metadata.
"""
if not os.path.exists(filename):
print(f"Error: File '{filename}' not found in current directory.")
return
file_size = os.path.getsize(filename)
metadata = {
"action": "send_metadata",
"filename": filename,
"size": file_size
}
try:
async with websockets.connect(uri) as websocket:
print(f"Connected to {uri}")
print(f"Sending metadata for '{filename}' ({file_size} bytes)...")
await websocket.send(json.dumps(metadata))
# Wait for the server's acceptance
response = await websocket.recv()
response_data = json.loads(response)
if response_data.get("status") == "accepted":
print("Server accepted the file. You can now copy the file to the shared folder or use the extended client.")
else:
print(f"Server response: {response_data}")
except ConnectionRefusedError:
print(f"Error: Could not connect to {uri}. Is the server running?")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python client.py <RECEIVER_IP> <FILENAME>")
print("Example: python client.py 192.168.1.100 test.txt")
sys.exit(1)
receiver_ip = sys.argv[1]
filename = sys.argv[2]
uri = f"ws://{receiver_ip}:8765"
asyncio.run(send_metadata(uri, filename))To test our application, we need to simulate two devices on a local network. If you only have one computer, you can run both the server and client on the same machine by using localhost or 127.0.0.1 as the IP address.
First, ensure the shared directory exists on the receiver machine. Then, start the server. In a separate terminal, run the client with the receiver's IP and a test file name. You should see the server print the received metadata, confirming that the WebSocket connection and data exchange are working correctly.
# 1. Create the shared directory on the receiver (if not exists)
mkdir -p shared
# 2. Create a dummy test file for the sender to reference
echo "Hello UVF IT! This is a test file." > test.txt
# 3. Start the Receiver (Server) in Terminal 1
# Note: On Windows, use 'python' instead of 'python3'
python3 server.py
# 4. In Terminal 2 (Sender), connect to the receiver
# If on the same machine, use 127.0.0.1. Otherwise, use the Receiver's LAN IP.
python3 client.py 127.0.0.1 test.txtIn the previous step, we only sent metadata. To make this a true file transfer tool, we need to actually move the file content. For small files (under 1MB), we can transmit the file content directly over the WebSocket connection using base64 encoding.
We will update client.py to read the file, encode it to base64, and send it in a second message. We will also update server.py to handle this new message type, decode the content, and save it to the shared folder.
client.py
import asyncio
import json
import os
import sys
import base64
import websockets
async def send_file(uri, filename):
"""
Connects to the receiver's WebSocket server, sends metadata, and then sends the file content.
"""
if not os.path.exists(filename):
print(f"Error: File '{filename}' not found in current directory.")
return
file_size = os.path.getsize(filename)
# Check if file is too large for direct WebSocket transfer (limit to 1MB for this demo)
if file_size > 1024 * 1024:
print("Error: File is too large for this minimal implementation. Please use a file under 1MB.")
return
metadata = {
"action": "send_metadata",
"filename": filename,
"size": file_size
}
try:
async with websockets.connect(uri) as websocket:
print(f"Connected to {uri}")
# Step 1: Send Metadata
print(f"Sending metadata for '{filename}' ({file_size} bytes)...")
await websocket.send(json.dumps(metadata))
# Wait for acceptance
response = await websocket.recv()
response_data = json.loads(response)
if response_data.get("status") != "accepted":
print(f"Server did not accept file: {response_data}")
return
print("Metadata accepted. Preparing file content...")
# Step 2: Send File Content
with open(filename, 'rb') as f:
file_content = f.read()
# Encode to base64 for safe transmission over JSON/WebSocket
encoded_content = base64.b64encode(file_content).decode('utf-8')
file_message = {
"action": "send_content",
"filename": filename,
"content": encoded_content
}
print("Transferring file content...")
await websocket.send(json.dumps(file_message))
# Wait for final confirmation
final_response = await websocket.recv()
final_data = json.loads(final_response)
if final_data.get("status") == "success":
print("File transferred successfully!")
else:
print(f"Transfer failed: {final_data}")
except ConnectionRefusedError:
print(f"Error: Could not connect to {uri}. Is the server running?")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python client.py <RECEIVER_IP> <FILENAME>")
print("Example: python client.py 192.168.1.100 test.txt")
sys.exit(1)
receiver_ip = sys.argv[1]
filename = sys.argv[2]
uri = f"ws://{receiver_ip}:8765"
asyncio.run(send_file(uri, filename))| Check | Command / Why it matters |
|---|---|
| WebSocket server starts successfully | Run python3 server.py and verify the output shows 'WebSocket server listening on ws://0.0.0.0:8765'. |
| Client connects and sends metadata | Run python3 client.py 127.0.0.1 test.txt and verify the server terminal prints 'Received metadata: {...}'. |
| File is saved on receiver | Check the shared folder on the receiver machine for the test.txt file after the client prints 'File transferred successfully!' |
| HTTP file download works | Open a browser on the receiver machine and navigate to http://localhost:8080/files/test.txt to verify the file is served correctly. |
You've built a minimal but functional local file transfer tool. To extend it, consider adding mDNS for device discovery, implementing proper encryption, and handling larger files with chunked transfers. Explore the LocalSend GitHub repository for advanced features and best practices.