Build a LAN File Sharing Server

Build a Python Flask server to share files across your local network.

Introduction

30 Second Summary

Transferring files between your devices at home can be surprisingly annoying. You end up emailing files to yourself, hunting for USB drives, or uploading everything to the cloud just to move a photo from your phone to your PC.

In this project, you will build a local network file sharing server using Python and Flask. Any device on your WiFi can open a web page to browse, upload, and download files without anything ever touching the internet.

What You'll Build

Start the server on your Ubuntu machine, open the URL on your phone, and instantly browse, download, or upload files while a live progress bar tracks every chunk.

By the end of this project, you'll have:

  • Open a URL on any device on your WiFi and see every shared file listed with its size.
  • Click a filename to download it straight to your device, or upload files of any size and watch a progress bar fill up in real time as chunks transfer.
  • Share files across your local network between phones, PCs, and other machines. No cloud accounts, no USB cables, no internet required.
  • Secret Mission: take on an optional challenge to push your file sharing server even further.

Are there any prerequisites?

This project assumes basic Python familiarity. You should be comfortable with variables, functions, and loops. No web development experience is needed.

Before We Start

Before diving into code, take a moment to lock in what you are about to build and why it matters.

Set Up Your Project

Your file sharing server runs on Flask, a Python web framework that handles browser requests and serves web pages. Before writing any server code, you need Flask installed in a clean, isolated environment.

In this step, you'll create a project folder, set up a Python virtual environment, install Flask, and build out the file structure your server needs.

In this step, get ready to:
  • Create a project folder with a Python virtual environment.
  • Install Flask and set up the project file structure.
  • Verify Flask is ready in VS Code.
Create the project folder and virtual environment

Every Python project benefits from its own virtual environment. This keeps Flask and its dependencies isolated from other Python projects on your machine.

  • Open a terminal by pressing Ctrl+Alt+T.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir lan-file-share
cd lan-file-share

Your terminal prompt now shows lan-file-share as the current directory.

  • Create a Python virtual environment and activate it by running these commands:
python3 -m venv venv
source venv/bin/activate

Your terminal prompt now starts with (venv), confirming the virtual environment is active.

What is a virtual environment?

A virtual environment is a self-contained Python installation inside your project folder. Packages you install while the environment is active only exist inside it, so they never collide with packages from other projects.

The (venv) prefix in your prompt tells you every pip and python3 command will use this isolated environment instead of your system Python.

Not seeing (venv) in your prompt?

Make sure you ran the source command, not just the python3 -m venv venv command. The first command creates the environment. The second activates it.

If you closed and reopened your terminal, run cd ~/Desktop/lan-file-share to get back to the project folder, then run source venv/bin/activate again.

help me activate my Python virtual environment on Ubuntu

Install Flask and create the project structure
  • Install Flask by running this command:
pip install Flask==3.1.3

You see output scrolling through as pip downloads and installs Flask and its dependencies. The last line reads Successfully installed followed by the package names.

Seeing a permission error or install failure?

Make sure (venv) appears in your prompt before running pip install. If it does not, activate the virtual environment first with source venv/bin/activate.

help me install Flask in my Python virtual environment

  • Create the project folders and files by running these commands:
mkdir templates shared
touch app.py templates/index.html
  • Confirm the structure was created by running:
ls

You should see app.py, shared, templates, and venv listed in the output.

What is each folder for?

  • shared/ holds the files your server will list, download, and upload.
  • templates/ holds the HTML files that Flask uses to render web pages.
  • app.py is where your Flask server code will live.
  • venv/ is the virtual environment folder Python created. You never edit files in here.

Your server needs some files in shared/ so there is something to display when you build the file browser in the next step.

  • Add a test file to the shared/ folder by running this command:
echo "Hello from LAN File Share" > shared/test-note.txt
  • Copy 1-2 more files you have handy (a photo, a PDF, any document) into the shared/ folder using the cp command, for example:
cp ~/Desktop/[[TESTFILE="name-of-a-file-on-your-Desktop"]] shared/
  • Confirm the shared folder has files by running:
ls shared/

You should see test-note.txt and any other files you copied.

Open the project in VS Code and verify Flask
  • Open your project in VS Code by running this command:
code .

VS Code opens with the lan-file-share folder loaded in the Explorer sidebar. You should see app.py, shared/, templates/, and venv/ in the sidebar.

Time to confirm Flask is installed correctly. Before you run the check, what version number do you expect to see?

  • Verify Flask is installed by running this command in your terminal:
python3 -c "import flask; print(flask.__version__)"

It prints 3.1.3. Flask is installed and ready.

Seeing ModuleNotFoundError?

This means python3 is not using your virtual environment. Check that (venv) appears in your terminal prompt. If it does not, run source venv/bin/activate from inside the lan-file-share folder and try the check again.

If VS Code opened a new terminal that is not in the right directory, run cd ~/Desktop/lan-file-share first, then activate.

help me fix ModuleNotFoundError for Flask in my virtual environment

Your project folder is ready and Flask is locked in. Next up, you'll write the server code that turns this folder into a working file browser.

Build the File Browser

Your project structure is ready with Flask installed and test files waiting in the shared/ folder. Right now, the only way to access those files is by opening the folder directly on your machine.

In this step, you'll build a web page that lists every file in your shared folder with its size and a download link. This is the foundation everything else builds on.

In this step, get ready to:
  • Write a Flask app that reads the shared/ folder and serves a file listing.
  • Build an HTML template with download links and mobile-friendly styling.
  • Run the server and confirm your test files appear in the browser.
Write the Flask server in app.py

Your server needs to do two things: scan the shared/ folder and list what's inside, and serve files when someone clicks a download link.

  • In VS Code, open app.py from the Explorer sidebar on the left.
  • Add the imports, app configuration, and a size-formatting helper function by pasting this code:
import os
from flask import Flask, request, send_from_directory, render_template
from werkzeug.utils import secure_filename

app = Flask(__name__)

# Point Flask to the shared/ folder for storing files
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'shared')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER


# Convert raw byte count to a human-readable size string
def format_size(size_bytes):
    if size_bytes < 1024:
        return f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.1f} KB"
    elif size_bytes < 1024 * 1024 * 1024:
        return f"{size_bytes / (1024 * 1024):.1f} MB"
    else:
        return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"

What does this code do?

  • The import lines bring in Flask for the web server, os for reading files from disk, and secure_filename for safely handling filenames. The request and secure_filename imports are not used yet. You'll need them in the next step when you add file uploads.
  • UPLOAD_FOLDER builds an absolute path to your shared/ directory so Flask knows where to find files regardless of where you run the script from.
  • os.makedirs(UPLOAD_FOLDER, exist_ok=True) creates the folder if it does not already exist, preventing a crash on first run.
  • format_size() converts raw byte counts (like 1048576) into human-readable strings (like 1.0 MB).
  • Add the route handlers and server entry point below the format_size function in app.py:
# Home page: list all files in the shared folder
@app.route('/')
def index():
    files = []
    for filename in os.listdir(app.config['UPLOAD_FOLDER']):
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        if os.path.isfile(filepath):
            size = os.path.getsize(filepath)
            files.append({
                'name': filename,
                'size': format_size(size),
            })
    files.sort(key=lambda x: x['name'].lower())
    return render_template('index.html', files=files)


# Download route: serve individual files
@app.route('/download/<filename>')
def download_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)


if __name__ == '__main__':
    app.run(debug=True)

What does this code do?

  • The / route scans every file in your shared/ folder, collects each file's name and formatted size, sorts alphabetically, and passes the list to the HTML template.
  • The /download/<filename> route serves the actual file when someone clicks a download link. Flask captures the filename from the URL and passes it to send_from_directory.
  • app.run(debug=True) starts the development server with auto-reload. Any changes you save to your code take effect without restarting manually.
  • Save app.py.
  • Confirm the server starts by running this command in your terminal:
python3 app.py

You should see Flask's startup message showing http://127.0.0.1:5000. You will also see a warning about the development server. That is expected for local use.

  • Press Ctrl+C in the terminal to stop the server for now.

Seeing a syntax error?

  • Check for missing colons at the end of def, if, and elif lines.
  • Make sure the format_size function and the @app.route decorators are at the same indentation level (no extra spaces).
  • Confirm your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
  • Help me fix a syntax error in my Flask app.py file.

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py.

ⓧ I'd like to double check the full code

Scroll up and confirm your app.py has these two sections in order:

  • The imports, app creation, UPLOAD_FOLDER config, and the format_size function from the first code block.
  • The index() route, the download_file() route, and the if __name__ == '__main__' block from the second code block.
Build the HTML template

Flask looks for templates in the templates/ folder next to your app. The index.html file uses Jinja templating to loop through the file list your server passes in.

  • Open templates/index.html in VS Code from the Explorer sidebar.
  • Add the document head with all the CSS styling by pasting this code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LAN File Share</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f0f2f5; color: #333; padding: 24px; max-width: 800px; margin: 0 auto; }
        h1 { margin-bottom: 24px; color: #1a1a2e; font-size: 1.8em; }
        h2 { margin-bottom: 12px; color: #1a1a2e; font-size: 1.2em; }
        .file-list { list-style: none; }
        .file-item { background: #fff; padding: 14px 18px; margin-bottom: 4px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; }
        .file-item a { color: #4a90d9; text-decoration: none; font-weight: 500; word-break: break-all; }
        .file-item a:hover { text-decoration: underline; }
        .file-size { color: #999; font-size: 0.85em; margin-left: 16px; white-space: nowrap; }
        .empty { text-align: center; color: #999; padding: 40px; background: #fff; border-radius: 8px; }
    </style>
</head>

What does this CSS do?

  • The viewport meta tag makes the page scale properly on mobile screens like phones and tablets.
  • max-width: 800px on the body keeps the layout readable on wide monitors instead of stretching across the full screen.
  • .file-item uses display: flex to put each file's name and size side by side in a card-like row.
  • word-break: break-all on the download links makes long filenames wrap instead of overflowing off the page.
  • Add the page body below the </head> tag in index.html:
<body>
    <h1>LAN File Share</h1>
    <h2>Shared Files</h2>
    {% if files %}
    <ul class="file-list">
        {% for file in files %}
        <li class="file-item">
            <a href="/download/{{ file.name }}" download>{{ file.name }}</a>
            <span class="file-size">{{ file.size }}</span>
        </li>
        {% endfor %}
    </ul>
    {% else %}
    <p class="empty">No files shared yet. Upload something!</p>
    {% endif %}
</body>
</html>

What does this template do?

  • {% if files %} checks whether the server passed any files to the template. If the folder is empty, the page shows a "No files shared yet" message instead.
  • {% for file in files %} loops through each file and creates a list item with the file's name as a download link and its size as a label.
  • The download attribute on the <a> tag tells the browser to download the file instead of trying to open it in the browser tab.
  • Save templates/index.html.

Seeing a blank page or an error?

  • Make sure your file is saved as templates/index.html (inside the templates folder), not in the project root.
  • Check that the Jinja tags use curly braces with percent signs ({% %}) for logic and double curly braces ({{ }}) for outputting values.
  • My Flask template is not rendering correctly. Help me debug.

✔️ Awesome, I've got everything!

Great. Double check you have saved templates/index.html.

ⓧ I'd like to double check the full code

Scroll up and confirm your index.html has both sections in order:

  • The <head> section with all the CSS styles from the first code block, ending with </head>.
  • The <body> with the Jinja {% if files %} and {% for file in files %} loops from the second code block, ending with </html>.
Run the server and test it

Both files are ready. Before you run the server, think about what you expect to see. How many files did you put in the shared/ folder earlier?

  • Start the server by running this command in your terminal:
python3 app.py
  • Open your browser and go to this URL:
http://127.0.0.1:5000

You should see your test files listed with their human-readable sizes. Each filename is a blue, clickable link.

  • Click any filename to test the download. The file should save to your machine.

Why does the terminal show a warning?

Flask's built-in server prints a warning about not using it in production. That is completely normal. For a local network file sharing tool among your own devices, the development server works perfectly.

Don't see your files?

  • Make sure your test files are directly inside the shared/ folder in your project directory, not in a subfolder within it.
  • Check that the server is still running in your terminal. If you see an error, stop it with Ctrl+C and re-run python3 app.py.
  • Verify the index.html file is inside the templates/ folder, not the project root. Flask only looks for templates in that specific folder.
  • My Flask file browser loads but does not show any files. Help me debug.

Your file browser is live and downloads are working. Next up, you'll add the ability to upload files and make the server accessible to other devices on your network.

Add File Uploads and Go Network-Wide

Your file browser can display and download files, but only from the same machine it runs on. Right now it is read-only and locked to localhost.

In this step you will add an upload route so any device can send files to the server. You will also change how the server listens for connections so every device on your local network can reach it.

In this step, get ready to:
  • Add an upload route and form so users can send files to the server.
  • Discover why the server is invisible to other devices on the network.
  • Fix the network binding so any device on your LAN can browse and upload files.
Add an upload route to app.py

Your server needs a new route that accepts file uploads via HTTP POST requests. After saving the file, the route redirects the user back to the home page so they see the updated file list.

  • In app.py, find the Flask import line at the top of the file:
from flask import Flask, request, send_from_directory, render_template
  • Update it to include redirect and url_for, so the line now looks like this:
from flask import Flask, request, send_from_directory, render_template, redirect, url_for
  • Next, scroll down to the bottom of app.py and find the download_file function.
  • Add the upload route below it, before the if __name__ block:
# Accept uploaded files via POST and save to the shared folder
@app.route('/upload', methods=['POST'])
def upload_file():
    f = request.files.get('file')
    if f:
        # Sanitize the filename to prevent path traversal attacks
        safe_name = secure_filename(f.filename)
        f.save(os.path.join(app.config['UPLOAD_FOLDER'], safe_name))
    return redirect(url_for('index'))

What does this code do?

request.files.get('file') grabs the uploaded file from the incoming HTTP request. The name 'file' must match the name attribute on the HTML file input you will add next.

secure_filename() strips dangerous characters from the filename. Without it, a crafted name like ../../../etc/passwd could write files outside your shared/ folder.

redirect(url_for('index')) sends the browser back to the home page after the upload finishes, so the new file appears in the list immediately.

  • Save app.py.
Add an upload form to index.html

The server can accept uploads now, but the web page has no way for users to select a file. You need an HTML form that sends the file to your new /upload route.

  • Open templates/index.html in VS Code.
  • Inside the <style> block, find the .empty rule and add this new CSS rule above it:
/* Style the upload area with a dashed border */
.upload-area { background: #fff; border: 2px dashed #ccc; border-radius: 12px; padding: 40px; text-align: center; margin-bottom: 24px; }
  • Now find the <h1>LAN File Share</h1> line in the <body>.
  • Add this upload form between the <h1> and the <h2>Shared Files</h2>:
<!-- Upload form that sends a file to the /upload route -->
<div class="upload-area">
    <form action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file">
        <button type="submit">Upload</button>
    </form>
</div>

What does this form do?

enctype="multipart/form-data" tells the browser to send the file as binary data instead of plain text. Without this attribute, the server receives the filename but not the actual file contents.

The name="file" on the input matches the request.files.get('file') call in your Flask route. These two names must match for the upload to work.

  • Save templates/index.html.

✔️ Awesome, I've got everything!

Great. Double check you have saved templates/index.html before moving on.

ⓧ I'd like to double check the full code

Your templates/index.html should contain all the code from Step 2 (the file list template with CSS) plus the two additions you just made. Scroll up and confirm:

  • The .upload-area CSS rule is inside the <style> block.
  • The upload form <div class="upload-area"> sits between the <h1> and <h2>Shared Files</h2>.
Test uploads on localhost

Both files are saved. Time to see if the upload works on your own machine before trying it from another device.

  • Stop your server if it is still running by pressing Ctrl+C in the terminal.
  • Start the server again by running:
python3 app.py
  • Open http://127.0.0.1:5000 in your browser.

You should see a dashed upload area above the file list. Click the file input, select any small file from your computer, and click Upload. The page reloads and the new file appears in the list with its size.

File not appearing after upload?

  • Make sure you saved both app.py and templates/index.html before restarting the server.
  • Check that the form's action="/upload" matches the route decorator @app.route('/upload', methods=['POST']) in app.py.
  • Confirm the input has name="file" and the form has enctype="multipart/form-data". Missing either one causes a silent failure.

Why isn't my file upload working in Flask?

Try accessing the server from another device

Uploading works on your own machine. The next test is whether another device on your network can reach the server.

  • Grab your phone, a Windows PC, or any other device connected to the same WiFi network as your Ubuntu machine.
  • Open a browser on that device and navigate to http://127.0.0.1:5000.

Before you try, do you think this will load the page? Why or why not?

The page does not load. 127.0.0.1 always refers to the device you are on. On your phone, it tries to reach a server running on the phone itself, not your Ubuntu machine. This is exactly the gap you need to fix.

Why can't other devices connect?

Flask's dev server defaults to listening on 127.0.0.1 (the loopback interface). That means it only accepts connections from the local machine. Even if you knew your Ubuntu machine's actual IP address and typed it into the other device's browser, the connection would be refused because Flask is not listening on that network interface.

Fix the network binding and go network-wide

To make the server reachable from any device on your LAN, you need to tell Flask to listen on all network interfaces instead of just the loopback.

  • In app.py, scroll down to the very last line and find:
    app.run(debug=True)
  • Replace it with this line so the server listens on all interfaces on port 5000:
    app.run(host='0.0.0.0', port=5000, debug=True)

What does 0.0.0.0 mean?

host='0.0.0.0' tells the server to accept connections on every network interface your machine has. That includes the loopback (127.0.0.1), your WiFi adapter, and any Ethernet connections.

With this change, any device that can reach your machine's IP address on port 5000 will be able to load the file sharing page.

  • Save app.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py before restarting the server.

ⓧ I'd like to double check the full code

Your app.py should contain all the code from Step 2 plus the two changes you made in this step. Scroll up and confirm:

  • The Flask import line includes redirect and url_for.
  • The upload_file route is present below download_file.
  • The last line reads app.run(host='0.0.0.0', port=5000, debug=True).

Now you need your machine's LAN IP address so other devices know where to connect.

  • Stop the running server with Ctrl+C.
  • Find your machine's LAN IP address by running:
hostname -I

This prints all IP addresses assigned to your machine's network interfaces. The first address is usually your LAN IP, something like 192.168.1.42. Note this address down.

  • Restart the server by running:
python3 app.py
  • On your phone or another device on the same network, open a browser.
  • Navigate to http:// followed by the LAN IP you noted, then :5000. For example: http://192.168.1.42:5000.

Do you think the page will load this time?

The file sharing page loads on the other device. You can see all your shared files, download them, and upload new ones. Try uploading a file from the other device and watch it appear in the list.

Page not loading from the other device?

  • Make sure both devices are on the same WiFi network. A phone on mobile data cannot reach your LAN.
  • Double check you typed your machine's IP (from hostname -I), not 127.0.0.1.
  • Confirm your app.run line says host='0.0.0.0' and that you restarted the server after saving.
  • If you have a firewall running, you may need to allow incoming connections on port 5000.

Help me debug why another device on my LAN can't connect to my Flask server.

Your file sharing server is now live on your local network. Any device on the same WiFi can browse, download, and upload files. Next up, you will tackle the biggest limitation: uploading large files with visible progress so the browser does not sit there silently.

Handle Large Files with a Progress Bar

Your file sharing server works. Any device on the network can browse files, download them, and upload new ones. But there is a gap in the upload experience that shows up the moment someone tries to share a larger file.

In this step, you will replace the basic HTML form upload with a chunked upload system. The browser will slice files into 5 MB pieces, send each piece to the server one at a time, and update a progress bar after every successful chunk.

In this step, get ready to:
  • Replace the basic upload route with a chunked upload endpoint.
  • Add JavaScript chunked upload logic with a real-time progress bar.
  • Test large file uploads from both localhost and LAN devices.
Try uploading a large file

Before changing any code, experience the problem yourself. You need a file that is at least 50 MB to see the gap clearly.

  • If you do not already have a large test file, create one by running this command in a separate terminal:
dd if=/dev/zero of=~/Desktop/testfile-50mb bs=1M count=50
  • Make sure your server is still running. If it is not, start it by running this command from the lan-file-share folder:
python3 app.py
  • Open http://127.0.0.1:5000 in your browser.

Before you upload the 50 MB file, predict what will happen. Will you see any progress indicator? How will you know if the upload is working or frozen?

  • Click the file input, select your 50 MB test file, and click the Upload button.

The browser just sits there. The button does nothing visually. There is no spinner, no percentage, and no indication that bytes are actually transferring. After several seconds (or longer on slower networks), the page suddenly refreshes and the file appears.

Why is there no progress feedback?

The basic HTML form sends the entire file in a single HTTP request. The browser has no built-in way to report upload progress for a standard form submission. The page is locked until the server finishes receiving every byte and sends back the redirect response.

That blind upload experience is exactly what you will fix now. Stop the server with Ctrl+C in your terminal.

Replace the upload route with chunked transfers

The fix has two sides. The server needs a new endpoint that accepts one small chunk at a time instead of the whole file. The browser needs JavaScript that slices the file and sends those chunks sequentially. You will update the server first.

  • In app.py, find the import lines at the top of the file:
import os
from flask import Flask, request, send_from_directory, render_template, redirect, url_for
from werkzeug.utils import secure_filename
  • Replace those three lines with the following. You are adding import socket and swapping redirect and url_for for jsonify:
import os
import socket
from flask import Flask, request, send_from_directory, jsonify, render_template
from werkzeug.utils import secure_filename
  • Next, add the get_local_ip() function below the format_size() function by pasting this code:
# Detect this machine's LAN IP address
def get_local_ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('10.254.254.254', 1))
        ip = s.getsockname()[0]
    except Exception:
        ip = '127.0.0.1'
    finally:
        s.close()
    return ip

What does get_local_ip() do?

This function uses a UDP socket trick to find your machine's LAN IP address. It creates a socket and asks the operating system which local interface it would use to reach a private IP address. No data is actually sent.

The result is the IP address other devices on your network can use to reach this machine. If anything goes wrong, it falls back to 127.0.0.1.

  • Save app.py and verify the server still starts without errors by running:
python3 app.py

The server should start on port 5000 with no import errors. Stop it again with Ctrl+C.

Seeing an ImportError?

Double check that the Flask import line includes jsonify and does not include redirect or url_for. Make sure import socket is on its own line below import os.

Help me fix my Flask imports

Now replace the old upload route with the new chunked endpoint. The old route received the entire file in one request. The new route receives one 5 MB chunk at a time, appends it to a temporary file, and moves the completed file into place when the last chunk arrives.

  • In app.py, find the old upload route:
@app.route('/upload', methods=['POST'])
def upload_file():
    f = request.files.get('file')
    if f:
        safe_name = secure_filename(f.filename)
        f.save(os.path.join(app.config['UPLOAD_FOLDER'], safe_name))
    return redirect(url_for('index'))
  • Delete that entire route and replace it with the following chunked upload endpoint:
# Accept one chunk at a time and assemble the full file
@app.route('/upload_chunk', methods=['POST'])
def upload_chunk():
    chunk = request.files.get('chunk')
    filename = request.form.get('filename')
    chunk_index = int(request.form.get('chunkIndex', 0))
    total_chunks = int(request.form.get('totalChunks', 1))

    if not chunk or not filename:
        return jsonify({'error': 'Missing chunk or filename'}), 400

    safe_name = secure_filename(filename)
    if not safe_name:
        return jsonify({'error': 'Invalid filename'}), 400

    temp_path = os.path.join(app.config['UPLOAD_FOLDER'], f".{safe_name}.part")
    final_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_name)

    mode = 'ab' if chunk_index > 0 else 'wb'
    with open(temp_path, mode) as f:
        f.write(chunk.read())

    if chunk_index + 1 == total_chunks:
        os.replace(temp_path, final_path)
        return jsonify({'status': 'complete', 'filename': safe_name})

    return jsonify({'status': 'partial', 'chunksReceived': chunk_index + 1})

What does this route do?

Each request carries one chunk of the file plus metadata: the original filename, the chunk index (which piece this is), and the total number of chunks. The route uses secure_filename to sanitize the filename, then appends the chunk data to a temporary .part file.

When the final chunk arrives, os.replace() atomically moves the temp file to its final name. This prevents half-uploaded files from appearing in the file list.

  • Finally, scroll to the bottom of app.py and find the if __name__ block:
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
  • Replace it with the following, which prints the LAN IP at startup so you do not have to run hostname -I separately:
if __name__ == '__main__':
    local_ip = get_local_ip()
    print(f"\n  LAN File Share running!")
    print(f"  Local:   http://127.0.0.1:5000")
    print(f"  Network: http://{local_ip}:5000\n")
    app.run(host='0.0.0.0', port=5000, debug=True)
  • Save app.py and start the server:
python3 app.py

You should see your LAN IP printed in the terminal output, along with the local and network URLs. Stop the server with Ctrl+C for now. You still need to update the frontend before the chunked upload will work.

Server crashes or shows a NameError?

Make sure the get_local_ip() function is defined above the if __name__ block (not inside it). Also check that import socket is at the top of the file.

Help me fix the server startup error

✔️ Awesome, I've got everything!

Great. Make sure you have saved app.py before moving on.

ⓧ I'd like to double check the full code

Scroll up through your app.py and confirm each of these sections is present, in order:

  • The updated import lines at the top (with socket and jsonify).
  • The format_size() function (unchanged from before).
  • The new get_local_ip() function.
  • The index() and download_file() routes (unchanged).
  • The new upload_chunk() route (replacing the old /upload route).
  • The updated if __name__ block with the LAN IP printout.
Add the chunked upload UI with a progress bar

The server is ready to accept chunks. Now you need the browser to slice files and send them. You will replace the basic HTML form with a JavaScript-driven upload area that includes a visible progress bar.

  • In templates/index.html, find the .upload-area CSS rule. It currently looks like this:
.upload-area { background: #fff; border: 2px dashed #ccc; border-radius: 12px; padding: 40px; text-align: center; margin-bottom: 24px; }
  • Replace that single line with the expanded version below, which adds cursor: pointer and a hover transition:
        .upload-area {
            background: #fff;
            border: 2px dashed #ccc;
            border-radius: 12px;
            padding: 40px;
            text-align: center;
            margin-bottom: 24px;
            cursor: pointer;
            transition: border-color 0.2s, background 0.2s;
        }
        .upload-area:hover {
            border-color: #4a90d9;
            background: #f8faff;
        }
  • Directly below the .upload-area:hover rule you just added, paste the following styles for the hidden file input, the clickable label, and the progress bar:
        .upload-area input[type="file"] {
            display: none;
        }
        .upload-area label {
            cursor: pointer;
            font-size: 1.1em;
            color: #666;
        }
        .progress-container {
            display: none;
            margin-top: 20px;
        }
        .progress-bar {
            width: 100%;
            height: 24px;
            background: #e8e8e8;
            border-radius: 12px;
            overflow: hidden;
        }
        .progress-fill {
            height: 100%;
            background: #4a90d9;
            border-radius: 12px;
            transition: width 0.2s;
            width: 0%;
        }
        .progress-text {
            margin-top: 8px;
            font-size: 0.9em;
            color: #666;
        }

What do these styles do?

The file input is hidden with display: none so the upload area looks clean. The <label> element acts as the clickable trigger instead. When clicked, it opens the file picker because it is linked to the input via the for attribute.

The progress bar starts hidden (display: none). JavaScript will show it during uploads and set the width of .progress-fill to reflect the upload percentage.

  • Now replace the upload form HTML. Find the <div class="upload-area"> section in the body:
    <div class="upload-area">
        <form action="/upload" method="post" enctype="multipart/form-data">
            <input type="file" name="file">
            <button type="submit">Upload</button>
        </form>
    </div>
  • Replace that entire block with the following upload area that includes the progress bar HTML:
    <div class="upload-area" id="uploadArea">
        <input type="file" id="fileInput" multiple>
        <label for="fileInput">Click to select files to upload</label>
        <div class="progress-container" id="progressContainer">
            <div class="progress-bar">
                <div class="progress-fill" id="progressFill"></div>
            </div>
            <p class="progress-text" id="progressText">Uploading...</p>
        </div>
    </div>
  • Save templates/index.html and start the server:
python3 app.py
  • Open http://127.0.0.1:5000 in your browser.

You should see a clean upload area with the text "Click to select files to upload" instead of the old form. Clicking it opens the file picker. The upload will not actually work yet because the JavaScript logic is not in place.

Stop the server with Ctrl+C. Time to add the JavaScript that slices files into chunks and sends them to your new endpoint.

  • In templates/index.html, add a <script> tag just before the closing </body> tag. Start with the setup variables and the file input listener:
    <script>
        // 5 MB chunk size for splitting files
        var CHUNK_SIZE = 5 * 1024 * 1024;
        var fileInput = document.getElementById('fileInput');
        var progressContainer = document.getElementById('progressContainer');
        var progressFill = document.getElementById('progressFill');
        var progressText = document.getElementById('progressText');

        // When the user picks files, start uploading
        fileInput.addEventListener('change', function () {
            var files = Array.from(this.files);
            if (files.length > 0) {
                uploadFiles(files);
            }
        });

        // Upload each file one at a time
        async function uploadFiles(files) {
            for (var i = 0; i < files.length; i++) {
                await uploadFile(files[i]);
            }
            window.location.reload();
        }

What does this code do?

CHUNK_SIZE sets each piece to 5 MB. This is a practical balance: small enough for responsive progress updates, large enough to avoid excessive HTTP overhead.

The change event listener fires when the user selects files. The uploadFiles() function processes each file sequentially. After all files finish, window.location.reload() refreshes the page so the new files appear in the list.

  • Still inside the same <script> tag, add the uploadFile() function that handles slicing and sending chunks. Paste this directly after the uploadFiles() function, before the closing </script> tag:
        // Slice a single file into chunks and send each to the server
        async function uploadFile(file) {
            var totalChunks = Math.ceil(file.size / CHUNK_SIZE);
            progressContainer.style.display = 'block';
            progressFill.style.width = '0%';
            progressText.textContent = 'Uploading ' + file.name + '...';

            for (var i = 0; i < totalChunks; i++) {
                var start = i * CHUNK_SIZE;
                var end = Math.min(start + CHUNK_SIZE, file.size);
                var chunk = file.slice(start, end);

                var formData = new FormData();
                formData.append('chunk', chunk, file.name);
                formData.append('filename', file.name);
                formData.append('chunkIndex', i);
                formData.append('totalChunks', totalChunks);

                var response = await fetch('/upload_chunk', {
                    method: 'POST',
                    body: formData,
                });

                if (!response.ok) {
                    progressText.textContent = 'Error uploading ' + file.name;
                    return;
                }

                var percent = Math.round(((i + 1) / totalChunks) * 100);
                progressFill.style.width = percent + '%';
                progressText.textContent = 'Uploading ' + file.name + '... ' + percent + '%';
            }

            progressText.textContent = file.name + ' uploaded!';
        }
    </script>

How does the chunked upload work?

file.slice(start, end) cuts a portion of the file without loading the whole thing into memory. Each slice becomes a FormData payload sent to your /upload_chunk endpoint via fetch().

After each successful chunk response, the progress bar width updates. A 50 MB file produces 10 chunks (50 / 5 = 10), so the bar jumps 10% after each one. A 500 MB file produces 100 chunks, giving smooth 1% increments.

  • Save templates/index.html.

Not sure where the script tag goes?

The entire <script>...</script> block should be the last thing before </body>, after the file list section. Make sure the opening <script> from the first code block and the closing </script> from the second code block form one continuous block.

Help me place the JavaScript correctly

✔️ Awesome, I've got everything!

Great. Make sure you have saved templates/index.html before testing.

ⓧ I'd like to double check the full code

Scroll up through your templates/index.html and confirm each of these sections is present, in order:

  • The expanded .upload-area CSS rule with cursor: pointer and transition.
  • The .upload-area:hover rule.
  • CSS rules for .upload-area input[type="file"], .upload-area label, .progress-container, .progress-bar, .progress-fill, and .progress-text.
  • The new <div class="upload-area" id="uploadArea"> block with the hidden file input, label, and progress bar HTML.
  • A <script> tag before </body> containing CHUNK_SIZE, the fileInput listener, uploadFiles(), and uploadFile().
Test with a large file
  • Start the server by running:
python3 app.py
  • Open http://127.0.0.1:5000 in your browser.

Before you upload the 50 MB test file again, predict what will be different this time. Will you see progress? When will the file appear in the list?

  • Click the upload area, select your 50 MB (or larger) test file, and watch.

The progress bar appears and fills up chunk by chunk. For a 50 MB file, you see it jump in 10% increments (10 chunks of 5 MB each). When it reaches 100%, the page reloads and the file appears in the shared files list.

  • Now test from another device. Check the terminal for your network URL (printed when the server started). Open that URL from your phone or another computer on the same network.
  • Upload a file from that device and confirm it appears in the shared list.

Progress bar not showing up?

Open your browser's developer tools (F12 or Ctrl+Shift+I) and check the Console tab for JavaScript errors. A typo in a variable name like progressFill or progressContainer will cause the script to fail silently.

Also confirm that the id attributes in your HTML (fileInput, progressContainer, progressFill, progressText) exactly match the names in the JavaScript getElementById() calls.

Help me debug my chunked upload

Your file sharing server now handles files of any size with real-time progress feedback. Any device on your LAN can browse, download, and upload files through the browser. Next up, take on an optional Secret Mission to push your server even further.

Secret mission

Add Drag-and-Drop and File Deletion

Your file sharing server works, but uploading means clicking a button and browsing for files every time. And once a file is shared, it lives there forever. Can you make the upload area accept drag-and-drop from your file manager, and add a delete button next to every file?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files, stop the server for now, or delete everything.

Resources you used:

  • The lan-file-share project folder on your Desktop (contains app.py, templates/, shared/, and the venv virtual environment with Flask installed).
  • The Flask development server process (if it is still running in your terminal).

Keep everything running

No action needed. Choose this if you are still actively using the file sharing server or plan to extend it further.

  • Your server keeps running as long as the terminal stays open. You can restart it any time by activating the virtual environment and running python3 app.py.
  • Any files in the shared/ folder remain on your machine and are accessible through the web UI whenever the server is running.

Pause - I'll come back to this later

Stop the running server to free up the terminal, but keep all your project files so you can pick up where you left off.

  • In the terminal where the server is running, press Ctrl+C to stop the Flask server.
  • Deactivate the virtual environment by running:
deactivate
  • To resume later, open a terminal, navigate to your lan-file-share folder, run source venv/bin/activate, and then python3 app.py.

Delete - I don't want to use this again

Remove the entire project folder, including all code, the virtual environment, and any shared files.

  • If the server is still running, press Ctrl+C in the terminal to stop it.
  • Deactivate the virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf lan-file-share
  • Confirm the folder is gone by running:
ls ~/Desktop
  • You should no longer see lan-file-share in the output. Everything is cleaned up.

Nice Work!

Nice Work!

Nice work! You just built a fully functional file sharing server that any device on your local network can use through a browser.

No cloud services, no USB drives, no accounts to sign up for. Just your Ubuntu machine, a Python script, and a clean web UI that handles files of any size.

You've learned how to:

  • Build a Flask web server from scratch that serves a file browser with human-readable sizes and working download links.
  • Make the server accessible to any device on your LAN by binding to all network interfaces with host='0.0.0.0'.
  • Implement chunked file uploads with a real-time progress bar, removing any file size limit by slicing files client-side with JavaScript.
  • Completed an optional Secret Mission to push your skills further.

Ready to quiz yourself?