Local File Transfer Tool

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.

Before You Start

Local File Transfer Architecture
LOCAL NETWORK DEVICES Send Metadata Receive Metadata Download File Serve File Sender App Python + WebSockets Receiver App Python + WebSockets WebSocket Server Port 8765 HTTP File Server Port 8080
1

Phase 1: Scaffold Project & Install Dependencies

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 aiohttp
2

Phase 2: Build the WebSocket Server (Metadata Exchange)

Now 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())
3

Phase 3: Build the HTTP File Server

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())
4

Phase 4: Build the Sender Client

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))
5

Phase 5: Run Locally & Verify Transfer

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.txt
6

Phase 6: Extend with Automatic File Copy

In 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))

Verifying the Stack

CheckCommand / Why it matters
WebSocket server starts successfullyRun python3 server.py and verify the output shows 'WebSocket server listening on ws://0.0.0.0:8765'.
Client connects and sends metadataRun python3 client.py 127.0.0.1 test.txt and verify the server terminal prints 'Received metadata: {...}'.
File is saved on receiverCheck the shared folder on the receiver machine for the test.txt file after the client prints 'File transferred successfully!'
HTTP file download worksOpen a browser on the receiver machine and navigate to http://localhost:8080/files/test.txt to verify the file is served correctly.

Next Steps

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.