Build a Local File Vault with Streamlit
Build a database-backed file manager app accessible across your local network.
Introduction
30 Second Summary
Everyone has files scattered across their devices. What if you could drop them all into one app and pull them up from your phone, tablet, or laptop without any cloud service?
In this project, you will build a File Vault web app with Streamlit that runs on your computer and is accessible from any device on your home network. You will upload files through your browser, track their details in a SQLite database, and browse, preview, or download everything from a clean dashboard.
What You'll Build
Drag a PDF into your browser on your laptop, walk to the couch, and pull it up on your phone to read it inline.
By the end of this project, you'll have:
- Upload and browse PDFs, images, audio, and text files through a drag-and-drop interface. Every file's name, type, size, and upload date is tracked in a searchable table.
- Preview files inline right in your browser. View images, play audio, read PDFs, and display text contents without leaving the app.
- Access your vault from any device on your local WiFi. Open it on your phone and tap large, touch-friendly buttons to upload, browse, or download files.
- Secret Mission: Add a category tagging system to organize and filter your files.
Are there any prerequisites?
No. This project walks you through installing everything you need, including Streamlit and a virtual environment. You just need Python and VS Code already on your computer.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Project
Before building the File Vault, you need a clean workspace. Your project needs its own folder with an isolated Python environment so Streamlit and its dependencies stay separate from the rest of your system.
In this step, you will create a project folder, set up a virtual environment, install Streamlit, and get a basic app running in your browser.
In this step, get ready to:
- Create a Python virtual environment and install Streamlit with PDF support.
- Configure the Streamlit server for large file uploads and network access.
- Create a basic app file and confirm it runs in the browser.
Create a virtual environment and install Streamlit
A virtual environment is a self-contained Python workspace. Packages you install inside it only exist in that folder, so installing Streamlit here will not affect any other Python projects on your machine.
- Press the Super key on your keyboard to open the search bar.
- Type Visual Studio Code and press Enter to open it.
- Open a terminal inside VS Code by pressing Ctrl+` (the backtick key above Tab).
- Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir file-vault
cd file-vault
Your terminal prompt now shows file-vault as the current directory.
- Create and activate a Python virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate
(venv) now appears at the start of your terminal prompt. This confirms the virtual environment is active.
What is a virtual environment?
A virtual environment is a self-contained Python workspace stored in the venv folder. Every package you install while it is active lives only inside that folder.
If you ever delete the venv folder, all the installed packages go with it. Your system Python stays untouched.
- Install Streamlit with PDF support by running:
pip install "streamlit[pdf]"
The install takes a moment. When it finishes, you should see Successfully installed followed by a list of package names at the end of the output.
Seeing 'command not found' for pip?
- Make sure your virtual environment is active. You should see (venv) at the start of your prompt. If not, run source venv/bin/activate and try the install again.
- help me fix a pip command not found error in my virtual environment
Before you run the next command, what do you think will happen when Streamlit launches for the first time?
- Verify that Streamlit is working by running:
streamlit hello
What does this command do?
streamlit hello starts a local web server and opens the Streamlit demo app in your browser. Your terminal stays busy while the server runs, so you will not be able to type new commands until you stop it.
A new browser tab opens showing the Streamlit demo app with interactive examples. Your terminal displays the local URL where the app is running.
- Switch back to your terminal and stop the demo by pressing Ctrl+C.
Next, open the file-vault folder in VS Code so you can see and edit project files in the Explorer sidebar.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to Desktop, select the file-vault folder, and click Open.
VS Code reloads with file-vault as your workspace. You should see the venv folder in the Explorer sidebar on the left.
- Open a terminal by pressing Ctrl+`.
- Reactivate your virtual environment by running:
source venv/bin/activate
(venv) appears in your prompt again. Opening a new terminal deactivates the virtual environment, so you need to reactivate it each time.
Streamlit's default upload limit is 200MB, but your File Vault needs to handle files up to 600MB. You also want the app accessible from other devices on your network. A configuration file handles both.
- Create the Streamlit configuration folder by running:
mkdir .streamlit
The .streamlit folder appears in the VS Code Explorer sidebar.
- Right-click the .streamlit folder in the Explorer sidebar and select New File....
- Type config.toml as the file name and press Enter.
- Paste the following configuration into config.toml:
[server]
maxUploadSize = 700
address = "0.0.0.0"
port = 8501
What does this config do?
- maxUploadSize = 700 raises the file upload limit from the default 200MB to 700MB, so your large PDFs can be uploaded.
- address = "0.0.0.0" makes the app listen on all network interfaces. This lets other devices on your WiFi access the vault.
- port = 8501 sets the port number for the web server.
- Save config.toml by pressing Ctrl+S.
- Confirm the file was created by running:
ls .streamlit/
You should see config.toml in the output.
✔️ Awesome, I've got everything!
Great. Double check that you have saved .streamlit/config.toml.
ⓧ I'd like to double check the full code
Here is the complete config.toml file. Make sure yours matches exactly.
[server]
maxUploadSize = 700
address = "0.0.0.0"
port = 8501
Create the app skeleton
With Streamlit installed and configured, the last piece is the app file itself. This creates a minimal page with a title and prepares the folder where uploaded files will be stored.
- Right-click the file-vault folder name at the top of the Explorer sidebar and select New File....
- Type app.py and press Enter.
- Paste the following code into app.py:
import streamlit as st
import os
# Directory where uploaded files will be stored
UPLOAD_DIR = "uploads"
# Configure the browser tab title and use the full page width
st.set_page_config(page_title="File Vault", layout="wide")
# Create the uploads directory if it doesn't exist yet
os.makedirs(UPLOAD_DIR, exist_ok=True)
# Display the app heading and description
st.title("File Vault")
st.write("Upload, browse, preview, and download your files from any device.")
What does this code do?
- UPLOAD_DIR = "uploads" defines the folder name where uploaded files will be saved on disk.
- st.set_page_config() sets the browser tab title to "File Vault" and switches to a wide layout that uses the full page width.
- os.makedirs(UPLOAD_DIR, exist_ok=True) creates the uploads/ folder automatically when the app starts. If the folder already exists, it does nothing.
- st.title() and st.write() display the heading and description at the top of the page.
- Save app.py by pressing Ctrl+S.
Before you run the app, what do you expect to see in the browser?
- Start the app by running:
streamlit run app.py
What does this command do?
streamlit run app.py starts a local web server and opens your app in a new browser tab. Just like streamlit hello, your terminal stays busy while the server runs.
A browser tab opens showing "File Vault" as the heading and the description text below it. Your terminal displays the local and network URLs where the app is running.
App not loading in the browser?
- Make sure your virtual environment is active. You should see (venv) in your prompt before running the streamlit command.
- Check that you saved app.py before running the command. Unsaved changes will not be picked up by Streamlit.
- help me debug why my Streamlit app is not loading
✔️ Awesome, I've got everything!
Great. Double check that you have saved app.py and that the app is running in your browser.
ⓧ I'd like to double check the full code
Here is the complete app.py file. Make sure yours matches exactly.
import streamlit as st
import os
UPLOAD_DIR = "uploads"
st.set_page_config(page_title="File Vault", layout="wide")
os.makedirs(UPLOAD_DIR, exist_ok=True)
st.title("File Vault")
st.write("Upload, browse, preview, and download your files from any device.")
Your project workspace is ready and the app skeleton is running. Next up, you will build the upload system and start filling your vault with files.
Build the Upload and File List
Your project folder is ready and Streamlit is running. Now it's time to build the core feature: a way to upload files into your vault and see what's inside.
A file vault that cannot accept files is not much of a vault. In this step, you will add a drag-and-drop file uploader and a live file list so you have something tangible to interact with right away.
In this step, get ready to:
- Add a file upload widget that accepts PDFs, images, audio, and text files.
- Display a list of all uploaded files with their name, size, and type.
- Observe the limitations of a file-only approach to set up the need for a database.
Add the file uploader widget
Streamlit provides a built-in st.file_uploader widget that gives you a drag-and-drop upload area for free. You just tell it which file types to accept, and it handles the rest.
- Switch back to app.py in VS Code.
- Find the line that says st.write("Upload, browse, preview, and download your files from any device."), then copy the following code below it:
st.header("Upload Files")
uploaded_files = st.file_uploader(
"Choose files to upload",
type=["pdf", "png", "jpg", "jpeg", "gif", "bmp", "webp", "mp3", "wav", "ogg", "flac", "txt", "csv", "log", "md"],
accept_multiple_files=True,
key="file_uploader",
)
# Save each uploaded file to the uploads/ folder
if uploaded_files:
for uploaded_file in uploaded_files:
file_path = os.path.join(UPLOAD_DIR, uploaded_file.name)
# Skip files that already exist on disk
if os.path.exists(file_path):
st.warning(f"'{uploaded_file.name}' already exists. Skipping.")
continue
# Write the file bytes to disk
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
st.success(f"Uploaded: {uploaded_file.name}")
What does this code do?
st.file_uploader creates a drag-and-drop upload area in your browser. The type list restricts which file extensions the widget accepts, and accept_multiple_files=True lets the user upload several files at once.
When files are uploaded, the loop saves each one to the uploads/ folder on disk. The os.path.exists() check prevents overwriting a file that was already uploaded.
- Save app.py.
- Switch to your browser. Streamlit detects the saved file and reruns the app automatically.
- You should see an Upload Files heading with a drag-and-drop area underneath it.
- Drag any image or text file from your computer into the upload area (or click Browse files to pick one).
- You should see a green success message that says Uploaded: <your filename>.
- Confirm the file landed on disk by running this in your terminal:
ls uploads/
You should see your uploaded file listed in the output.
Don't see the upload widget?
- Make sure you saved app.py. Streamlit only reruns when the file is saved to disk.
- Check that you pasted the new code below the existing st.write() line, not inside it.
- If the browser tab closed, open a new tab and navigate to http://localhost:8501.
Still stuck? Help me debug why my Streamlit file uploader widget is not showing up.
Display uploaded files as a list
The file is on disk, but the page does not show what is in your vault yet. You need a file list that scans the uploads/ folder and displays each file's name, size, and type.
- In app.py, add the following code at the very bottom of the file:
# --- File List Section ---
st.header("Your Files")
if os.path.exists(UPLOAD_DIR):
files_on_disk = os.listdir(UPLOAD_DIR)
if files_on_disk:
# Display each file's name, size, and extension
for fname in files_on_disk:
fpath = os.path.join(UPLOAD_DIR, fname)
fsize = os.path.getsize(fpath)
ext = os.path.splitext(fname)[1]
st.write(f"{fname} | {round(fsize / 1024, 1)} KB | {ext}")
else:
st.info("No files uploaded yet.")
What does this code do?
os.listdir() scans the uploads/ folder and returns a list of every filename inside it. The loop then grabs each file's size with os.path.getsize() and its extension with os.path.splitext().
st.write() prints each file as a single line of text showing the filename, size in KB, and file extension. This gives you a quick inventory of everything in the vault.
- Save app.py and switch to your browser.
- Below the upload widget, you should now see a Your Files heading with your uploaded file listed, showing its name, size in KB, and extension.
File list is empty?
- Make sure you uploaded at least one file in the previous substep. Run ls uploads/ to confirm a file is on disk.
- Check that the UPLOAD_DIR variable at the top of app.py is set to "uploads". The folder name must match exactly.
Need more help? Help me figure out why my file list is not showing uploaded files.
✔️ Awesome, I've got everything!
Great. Make sure you have saved app.py.
ⓧ I'd like to double check the full code
Your app.py should contain, in order, the following sections. Scroll up and confirm each one is present:
- The import lines, UPLOAD_DIR, st.set_page_config, os.makedirs, st.title, and st.write from Step 1.
- The Upload Files section: st.header("Upload Files"), the st.file_uploader call, and the save loop.
- The Your Files section: st.header("Your Files"), the os.listdir() loop, and the st.write() line for each file.
Test the persistence gap
Your vault can accept files and list them. But is it really tracking your files well enough to be useful long-term?
- Before you test, think about this: what information do you see about each file in the list? What is missing that a real file manager would show?
- Refresh the browser page (press F5 or Cmd+R / Ctrl+R).
The file list still shows because it reads directly from the uploads/ folder. The files survive a refresh.
But look closely at what the list actually tells you. There are no upload dates. There is no way to search files. There is no structured metadata you could filter or sort by.
Why is the on-disk folder not enough?
A plain folder gives you filenames and sizes, but nothing else. Once you have dozens of files, you need timestamps, categories, and search to keep things manageable.
A database solves this by storing structured metadata alongside each file. In the next step, you will add a SQLite database to close this gap.
Your vault can accept and list files, which is a great start. Next up, you will add a SQLite database to store real metadata like upload dates and file types so you can build a proper file browser.
Add SQLite for Persistent Metadata
Your file vault can accept uploads and list what's on disk. But that list is bare bones. There are no upload dates, no structured types, and no way to search or sort.
A SQLite database solves all of these problems. You will create a dedicated database module that stores metadata about every uploaded file, then wire it into your app so the file list becomes a proper, data-driven table.
In this step, get ready to:
- Create a database.py module with functions to initialize, insert, query, and delete file records.
- Wire the database into your upload flow so every file gets a metadata record.
- Replace the plain text file list with a structured dataframe showing filename, type, size, and upload date.
Create the database module
Python includes a built-in sqlite3 module, so you do not need to install anything. Your database will be a single file called file_vault.db that lives right in your project folder.
- In VS Code, create a new file called database.py in your file-vault folder (the same folder as app.py).
- Paste the following code into database.py:
import sqlite3
import os
from datetime import datetime
# Path to the SQLite database file
DB_PATH = "file_vault.db"
def get_connection():
# Connect to the database and enable column-name access
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
# Create the files table if it doesn't already exist
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
file_type TEXT NOT NULL,
file_size INTEGER NOT NULL,
upload_date TEXT NOT NULL,
file_path TEXT NOT NULL
)
""")
conn.commit()
conn.close()
What does this code do?
- DB_PATH sets the filename for your SQLite database. The file gets created automatically the first time sqlite3.connect() runs.
- get_connection() opens a connection to the database and sets row_factory = sqlite3.Row so you can access columns by name (like row["filename"]) instead of by index number.
- init_db() creates a files table with columns for the file's ID, name, type, size, upload date, and disk path. The IF NOT EXISTS clause means it is safe to call this every time the app starts.
- Save database.py.
- Test that the module works by running this command in your terminal:
python3 -c "from database import init_db; init_db(); print('Database ready')"
- You should see Database ready printed in the terminal.
- Confirm the database file was created by running:
ls *.db
You should see file_vault.db listed.
Seeing ModuleNotFoundError?
- Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
- Check that database.py is saved inside the file-vault folder, not inside uploads/ or .streamlit/.
Help me debug a ModuleNotFoundError when importing database.py in my Streamlit project.
Now add the functions that create, read, and delete file records. Paste this code below the init_db() function in database.py:
def insert_file(filename, file_type, file_size, file_path):
# Insert a new file record with the current timestamp
conn = get_connection()
cursor = conn.cursor()
cursor.execute(
"INSERT INTO files (filename, file_type, file_size, upload_date, file_path) VALUES (?, ?, ?, ?, ?)",
(filename, file_type, file_size, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), file_path),
)
conn.commit()
conn.close()
def get_all_files():
# Return all file records, newest first
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM files ORDER BY upload_date DESC")
rows = cursor.fetchall()
conn.close()
return rows
What does this code do?
- insert_file() saves a new record into the files table. It uses ? placeholders to safely insert values, and stamps each record with the current date and time.
- get_all_files() queries every record in the table, sorted newest first. This is what will power your file list.
- Save database.py. You will test these functions when you wire them into the app in the next substep.
Finally, add two more helper functions below get_all_files() in database.py. These will be used in a later step for previewing and deleting individual files:
def get_file_by_id(file_id):
# Look up a single file record by its database ID
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM files WHERE id = ?", (file_id,))
row = cursor.fetchone()
conn.close()
return row
def delete_file(file_id):
# Remove the file from disk and delete its database record
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT file_path FROM files WHERE id = ?", (file_id,))
row = cursor.fetchone()
if row and os.path.exists(row["file_path"]):
os.remove(row["file_path"])
cursor.execute("DELETE FROM files WHERE id = ?", (file_id,))
conn.commit()
conn.close()
What does this code do?
- get_file_by_id() fetches a single file record using its database ID. You will use this when the user selects a file to preview.
- delete_file() does double duty. It deletes the actual file from the uploads/ folder on disk, then removes the matching record from the database.
- Save database.py. These functions power the preview and delete features you will build in Step 4.
✔️ Awesome, I've got everything!
Great. Double check you have saved database.py.
ⓧ I'd like to double check the full code
Your database.py should contain all three code blocks you pasted above, in order:
- Imports, DB_PATH, get_connection(), and init_db().
- insert_file() and get_all_files().
- get_file_by_id() and delete_file().
Scroll up and confirm each block is present in your file, in that order.
Wire the database into the upload flow
The database module is ready. Now you need to connect it to your app so that every uploaded file gets a metadata record.
- In app.py, find the import lines at the top of the file. Find this line:
import os
- Add this new import directly below it, so the line reads:
import os
from database import init_db, insert_file, get_all_files
- Next, find this line:
os.makedirs(UPLOAD_DIR, exist_ok=True)
- Add init_db() on a new line directly above it, so the two lines look like this:
init_db()
os.makedirs(UPLOAD_DIR, exist_ok=True)
Why call init_db() on every app start?
Streamlit reruns your entire script on every interaction. Calling init_db() each time is safe because CREATE TABLE IF NOT EXISTS only creates the table the first time. On every subsequent run, it does nothing.
- Now find the line inside the upload loop that says:
st.success(f"Uploaded: {uploaded_file.name}")
- Add the following insert_file() call directly above that line, between the file write and the success message:
# Record the file's metadata in the database
insert_file(
filename=uploaded_file.name,
file_type=uploaded_file.type,
file_size=uploaded_file.size,
file_path=file_path,
)
What does this code do?
Every time a file is saved to the uploads/ folder, insert_file() writes a new row into the database with the file's name, MIME type, size in bytes, and disk path. The function automatically stamps the row with the current date and time.
- Save app.py.
- Switch to your browser and refresh the app.
- Upload a new test file (any small image or text file with a name you have not uploaded before).
You should see the green "Uploaded" success message. The file's metadata is now in the database, but the display still uses the old os.listdir() code. You will fix that next.
Seeing an ImportError?
- Make sure you saved both database.py and app.py.
- Confirm database.py is in the same folder as app.py (both inside file-vault/).
- Check the import line for typos. It should read from database import init_db, insert_file, get_all_files exactly.
Help me fix an ImportError in my Streamlit File Vault app when importing from database.py.
Display files in a Streamlit dataframe
The database is recording metadata, but the display still reads from the filesystem. Time to replace it with a proper data-driven table.
- In app.py, scroll down to the "Your Files" section. Find this block of code:
st.header("Your Files")
if os.path.exists(UPLOAD_DIR):
files_on_disk = os.listdir(UPLOAD_DIR)
if files_on_disk:
for fname in files_on_disk:
fpath = os.path.join(UPLOAD_DIR, fname)
fsize = os.path.getsize(fpath)
ext = os.path.splitext(fname)[1]
st.write(f"{fname} | {round(fsize / 1024, 1)} KB | {ext}")
else:
st.info("No files uploaded yet.")
- Delete that entire block and replace it with the following code:
st.header("Your Files")
# Query file records from the database
files = get_all_files()
if not files:
st.info("No files uploaded yet. Use the uploader above to add files.")
else:
# Build a list of dictionaries for the dataframe
file_data = []
for f in files:
size_mb = round(f["file_size"] / (1024 * 1024), 2)
file_data.append(
{
"ID": f["id"],
"Filename": f["filename"],
"Type": f["file_type"],
"Size (MB)": size_mb,
"Uploaded": f["upload_date"],
}
)
st.dataframe(file_data, use_container_width=True)
What changed?
Instead of scanning the filesystem with os.listdir(), the app now queries the database with get_all_files(). Each record is formatted into a dictionary with human-readable columns.
st.dataframe() renders the list as an interactive table that you can sort and scroll through. The use_container_width=True setting makes the table stretch to fill the page width.
What about my old test files?
Files you uploaded in the previous step exist on disk in uploads/, but they were uploaded before the database existed. They do not have database records, so they will not appear in the new table. Upload a fresh file to see it show up.
- Save app.py.
Before you check your browser, think about what you expect to see. If you uploaded a file in the previous substep, will it appear in the table? What columns should show up?
- Switch to your browser and refresh the app.
- Upload a new test file (any image or text file).
You should see the file appear in a clean table with columns for ID, Filename, Type, Size (MB), and Uploaded. Refresh the page. The file is still there because the database persists it.
Table not showing any files?
- Files from Step 2 do not have database records. Upload a brand new file to test.
- If you see a duplicate warning, use a file with a name you have not uploaded before.
- Check that you deleted the old os.listdir() block completely and replaced it with the new code.
Help me debug my Streamlit File Vault dataframe not displaying uploaded files.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py.
ⓧ I'd like to double check the full code
Your app.py should contain these sections in order. Scroll through and confirm each is present:
- Imports: streamlit, os, and from database import init_db, insert_file, get_all_files.
- UPLOAD_DIR and st.set_page_config().
- init_db() and os.makedirs().
- Title and description lines.
- Upload section with st.file_uploader, file write, insert_file() call, and success message.
- Your Files section with get_all_files(), the file_data loop, and st.dataframe().
Your vault now stores and displays file metadata in a real database. Next up, you will add the ability to preview and download files directly from the browser.
Add File Preview and Download
In the last step, you upgraded your File Vault from a plain file list to a proper database-backed table showing filenames, types, sizes, and upload dates.
A file list you can only stare at is frustrating. You can see your files listed, but you cannot open them, preview their contents, or get them back. In this step, you will add the ability to select any file, preview it inline, download it, and delete it.
In this step, get ready to:
- Build a file selector dropdown that lets you pick any uploaded file.
- Add inline previews for images, audio, PDFs, and text files.
- Add download and delete buttons for managing your vault.
Build the file selector and preview area
Your database.py already has two functions you have not used yet: get_file_by_id() to fetch a single file record, and delete_file() to remove a file from both disk and database. You need to import them before you can use them.
- In app.py, find the import line at the top of the file:
from database import init_db, insert_file, get_all_files
- Add get_file_by_id and delete_file to the end of the import, so the line now looks like this:
from database import init_db, insert_file, get_all_files, get_file_by_id, delete_file
Now you can build the preview section. The idea is to let the user pick a file from a dropdown, then render the right kind of preview based on the file's MIME type (like image/png or application/pdf).
- In app.py, scroll down to the else: block and find this line:
st.dataframe(file_data, use_container_width=True)
- Add the file selector and preview logic below that line by copying this code:
# --- File Preview Section ---
st.header("Preview & Download")
file_options = {f"[{f['id']}] {f['filename']}": f["id"] for f in files}
selected = st.selectbox("Select a file to preview", options=list(file_options.keys()))
if selected:
file_id = file_options[selected]
file_record = get_file_by_id(file_id)
if file_record and os.path.exists(file_record["file_path"]):
file_path = file_record["file_path"]
file_type = file_record["file_type"]
filename = file_record["filename"]
# Preview based on type
if file_type and file_type.startswith("image/"):
st.image(file_path, caption=filename, width="stretch")
elif file_type and file_type.startswith("audio/"):
st.audio(file_path, format=file_type)
elif file_type == "application/pdf":
st.pdf(file_path, height=600)
elif file_type and file_type.startswith("text/"):
with open(file_path, "r", errors="replace") as text_file:
st.code(text_file.read(), language=None)
else:
st.info(f"Preview not available for type: {file_type}")
What does this code do?
- file_options builds a dictionary that maps a display label like [1] photo.png to the file's database ID. This lets the selectbox show human-readable names while giving you the ID to look up.
- st.selectbox() creates a dropdown populated with every file in the vault.
- The if/elif chain checks the file's MIME type and calls the matching Streamlit widget: st.image() for images, st.audio() for audio, st.pdf() for PDFs, and st.code() for text files.
- Save app.py.
- Switch to your browser and refresh the page.
- You should see a new Preview & Download header with a dropdown listing your uploaded files.
- Select a file from the dropdown. If you pick an image, it displays inline. If you pick a text file, its contents appear in a code block.
Not seeing the preview section?
- Make sure the new code is indented inside the else: block, at the same level as st.dataframe(). If it is outside the else: block, it will run even when no files exist and crash.
- Check that your import line now includes get_file_by_id. A missing import causes a NameError when you select a file.
- If you have no files uploaded yet, the preview section only appears after you upload at least one file.
Why isn't my file preview showing up in the Streamlit app?
Add download and delete buttons
Previewing is great, but you also need a way to get files out of the vault and onto your device. You also need a way to remove files you no longer want.
- In app.py, find the last line of the preview code you just added. It is the st.info(f"Preview not available for type: {file_type}") line inside the final else: of the type-check chain.
- Add the download and delete buttons below that line by copying this code:
# Download and delete buttons
col1, col2 = st.columns(2)
with col1:
with open(file_path, "rb") as dl_file:
st.download_button(
label="Download",
data=dl_file.read(),
file_name=filename,
mime=file_type,
use_container_width=True,
)
with col2:
if st.button("Delete", use_container_width=True):
delete_file(file_id)
st.success(f"Deleted: {filename}")
st.rerun()
else:
st.error("File not found on disk. It may have been moved or deleted.")
What does this code do?
- st.columns(2) creates two side-by-side columns so the Download and Delete buttons sit next to each other.
- st.download_button() reads the file bytes from disk and serves them as a browser download when clicked.
- The Delete button calls delete_file(), which removes the file from both the uploads/ folder and the SQLite database. Then st.rerun() refreshes the entire page so the file disappears from the list immediately.
- The final else: handles the edge case where a file record exists in the database but someone manually deleted the file from disk.
Before you test, do you think clicking the Download button will trigger a browser download, or will it open the file in a new tab?
- Save app.py and refresh your browser.
- Select a file from the dropdown. You should see the preview and two buttons below it: Download and Delete.
- Click Download. Your browser downloads the file to your Downloads folder.
- Click Delete on a test file you do not need. The page reruns and the file vanishes from both the table and the dropdown.
Download button not working?
- Make sure the download/delete code is indented at the same level as the preview if/elif chain (3 levels of indentation, or 12 spaces). It must be inside the if file_record and os.path.exists(...): block.
- If clicking Delete shows an error about delete_file not being defined, scroll back up and double check your import line includes delete_file.
My Streamlit download or delete button is not working. Help me debug it.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py.
ⓧ I'd like to double check the full code
Your app.py is now too long to show in one block. Scroll up through your file and confirm these sections appear in order:
- The import line at the top includes init_db, insert_file, get_all_files, get_file_by_id, and delete_file.
- The Upload Files section with st.file_uploader() and the upload loop (unchanged from last step).
- The Your Files section with st.dataframe() (unchanged from last step).
- The Preview & Download section with st.selectbox(), the type-based preview chain, and the download/delete buttons in two columns.
database.py is unchanged from the previous step.
Your File Vault can now upload, list, preview, download, and delete files. In the next step, you will make the app accessible from your phone and any other device on your local network.
Go Mobile with Touch-Friendly CSS
Your File Vault can upload, preview, and download files from your desktop browser. That is a solid file manager, but only if you are sitting at your computer.
What about pulling up your vault from your phone on the couch, or from a tablet in another room? In this step, you will open the app on a mobile device, discover that the default Streamlit buttons are painfully small for finger taps, and inject custom CSS to fix it.
In this step, get ready to:
- Find your computer's local IP address and open the app on a mobile device.
- Inject touch-friendly CSS to make buttons and controls comfortable for finger taps.
Find your computer's local IP address
Back in Step 1, you configured the Streamlit server to listen on all network interfaces by setting address = "0.0.0.0" in .streamlit/config.toml. That means any device on your local WiFi can reach the app if it knows your computer's IP address.
- Find your local IP address by running this command in the terminal:
hostname -I
- Note the first IP address in the output (it will look something like 192.168.1.105).
Before you open the app on your phone, think about this: the app was designed and tested on a desktop browser with a mouse pointer. Do you think it will feel just as comfortable to use on a small touchscreen?
- Grab your phone or tablet and make sure it is connected to the same WiFi network as your computer.
- Open your phone's browser.
- Type the following URL into the address bar, replacing the IP with the one you noted:
http://[[LOCAL_IP="your local IP address"]]:8501
The app loads on your phone. Try tapping the Download or Delete buttons. They are tiny. Hitting the right button with your finger requires way too much precision.
The sidebar collapse icon is also almost impossible to tap. This is the gap we will fix next.
Can't load the app on your phone?
- Make sure your phone and computer are connected to the same WiFi network.
- Double-check the IP address you typed. Run hostname -I again and compare.
- If you have a firewall enabled on your computer, you may need to allow traffic on port 8501.
Help me debug why my phone can't reach my Streamlit app on the local network.
Inject touch-friendly CSS
Streamlit does not have a built-in mobile styling system. However, you can inject raw CSS into the page using st.markdown() with unsafe_allow_html=True. This lets you target Streamlit's internal CSS classes and resize controls for touchscreens.
- In app.py, find this line:
st.set_page_config(page_title="File Vault", layout="wide")
- Add the following CSS injection block directly below that line by copying this code:
# Inject touch-friendly CSS
st.markdown(
"""
<style>
.stButton > button {
min-height: 3rem;
font-size: 1.1rem;
padding: 0.5rem 1.5rem;
}
[data-testid="collapsedControl"] svg {
height: 3rem;
width: 3rem;
}
</style>
""",
unsafe_allow_html=True,
)
What does this CSS do?
- .stButton > button targets every Streamlit button on the page. min-height: 3rem and generous padding make each button large enough for a comfortable finger tap.
- [data-testid="collapsedControl"] svg targets the sidebar toggle icon. Sizing it to 3rem makes it easy to tap on a phone screen.
- Save app.py.
- Switch to your phone and refresh the page.
The buttons are now visibly larger. Tap Download or Delete and feel the difference. The sidebar collapse icon is also easy to hit now.
Buttons still look small on your phone?
- Make sure you saved app.py. Streamlit auto-reloads on save, so unsaved changes will not appear.
- Check that the st.markdown() block is placed right after st.set_page_config() and before init_db().
- Do a hard refresh on your phone browser (pull down to refresh, or close and reopen the tab) to clear any cached styles.
Help me figure out why my custom CSS is not applying in Streamlit.
✔️ Awesome, I've got everything!
Great. Make sure you have saved app.py and confirmed the larger buttons on your mobile device.
ⓧ I'd like to double check the full code
The only change in this step is the CSS injection block added to app.py. Scroll up and confirm the st.markdown() block with the <style> tag is present directly after st.set_page_config() and before init_db(). It should contain styles for .stButton > button and [data-testid="collapsedControl"] svg.
No other files changed in this step. database.py and .streamlit/config.toml remain the same as before.
Your File Vault is fully functional. You can upload files from any device on your network, browse them in a structured table, preview them inline, and download or delete them with comfortable, finger-friendly buttons. Next up, the Secret Mission challenges you to add a category tagging system to organize your growing collection of files.
Secret mission
Add Category Tags
Your File Vault works, but with dozens of files it becomes one long, unsorted list. Can you add a category tagging system so you can label files on upload and filter the vault by category from the sidebar?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- file-vault/ project directory containing all project code, virtual environment, and configuration.
- file_vault.db SQLite database file storing file metadata.
- uploads/ folder containing all files you uploaded through the app.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep using the File Vault from your devices.
- Leave the terminal running streamlit run app.py and access the vault anytime from any device on your local network.
- Your uploaded files stay in the uploads/ folder and your metadata stays in file_vault.db.
- Streamlit is installed inside the venv/ virtual environment, so it does not affect any other Python projects on your machine.
Pause - I'll come back to this later
Shut down the running server to free up your terminal. Your files and database are preserved on disk and will be there next time you start the app.
- Switch back to the terminal where Streamlit is running.
- Press Ctrl+C to stop the Streamlit server.
- Deactivate your virtual environment by running:
deactivate
When you want to come back, navigate to the project folder, reactivate the virtual environment, and start the app again:
cd ~/Desktop/file-vault
source venv/bin/activate
streamlit run app.py
Delete - I don't want to use this again
Remove all project files, uploaded content, the database, and the virtual environment. This cannot be undone.
- Switch back to the terminal where Streamlit is running.
- Press Ctrl+C to stop the Streamlit server.
- Deactivate your virtual environment by running:
deactivate
- Navigate to your Desktop and delete the entire project directory by running:
cd ~/Desktop
rm -rf file-vault
- Confirm the folder is gone by running:
ls
You should no longer see file-vault in the output. Everything has been removed.
Nice Work!
Nice Work!
You just built a fully functional File Vault web app from scratch with Python and Streamlit. You can upload files from your phone, preview them in the browser, and manage everything through a database-backed dashboard that works across your entire local network.
You've learned how to:
- Build a complete Streamlit web app that handles file uploads, inline previews for PDFs, images, audio, and text files, and file downloads.
- Use SQLite to create a persistent database that stores file metadata and survives browser refreshes and app restarts.
- Style an app with custom CSS for touch-friendly mobile use and make it accessible from any device on your local network.
- Secret Mission: Added a category tagging and filtering system to organize and search files by purpose.
Ready to quiz yourself?