Fileshare
A self-hosted file sharing app in a single PHP file. No framework, no Composer dependencies, no database.
Features
- Upload files into optional folder paths
- Public or private visibility per file
- Configurable expiry (1h – 30d, or never)
- Automatic expiry cleanup via a cron endpoint
- CSRF protection on all mutating actions
- Bcrypt password hashing, session fixation protection, brute-force delay
Content-Security-PolicyandX-Content-Type-Optionsheaders
Requirements
- PHP 8.2+ with the
fileinfoextension - Nginx (or Apache) — see configuration below
Installation
git clone <repo> fileshare
cd fileshare
cp .env.example .env
Edit .env:
USERNAME=admin
# Generate a bcrypt hash for your password:
# php -r "echo password_hash('yourpassword', PASSWORD_BCRYPT);"
PASSWORD=$2y$12$...
# Generate a random cron secret:
# php -r "echo bin2hex(random_bytes(32));"
CRON_SECRET=...
Create the required directories and make them writable by the PHP-FPM user:
mkdir -p uploads data
chown www-data: uploads data # replace www-data with your PHP-FPM user if different
Development server
composer serve
# → http://localhost:8000
Project structure
src/
index.php — bootstrap, constants, router
handlers.php — route handlers and view renderers
helpers.php — env loading, metadata I/O, path and output utilities
router.php — PHP built-in server router (dev only)
simple.min.css — Simple.css (local copy)
views/
login.php — login page
dashboard.php — file management dashboard
uploads/ — uploaded files (mirrors user-supplied folder structure)
data/
files.json — file metadata (path, private, expires, uploaded)
.lock — flock coordination file (created automatically)
.env — credentials and secrets (not web-accessible)
Routes
| Method | Path | Description |
|--------|------|-------------|
| GET | / | Login form (unauthenticated) or dashboard |
| POST | /login | Authenticate |
| GET | /logout | End session |
| POST | /upload | Upload a file |
| GET | /download/{path} | Download a file (403 if private and not logged in) |
| POST | /delete/{path} | Delete a file |
| POST | /toggle/{path} | Toggle public/private |
| POST | /expiry/{path} | Set or update expiry |
| GET | /cron?secret=<CRON_SECRET> | Delete expired files (403 without valid secret) |
Nginx configuration
server {
listen 443 ssl;
root /path/to/fileshare/src;
# Match the application's 50 MB limit
client_max_body_size 50M;
# HSTS — remove max-age line and re-deploy if you ever need to revert to HTTP
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Checklist before going live
- Set
client_max_body_sizeto matchMAX_UPLOAD_BYTES(50 MB)- Enable TLS and configure the
ssl_certificate/ssl_certificate_keydirectives- Add the
Strict-Transport-Securityheader only after TLS is confirmed working- Confirm
uploads/,data/, and.envare outside the Nginxroot- Confirm
uploads/anddata/are writable by the PHP-FPM user (chown www-data: uploads data)- Set
upload_max_filesize = 50Mandpost_max_size = 52Minphp.ini
Cron
Set up a system cron to trigger expiry cleanup. The CRON_SECRET from .env is required as a query parameter:
# Run expiry cleanup every hour
0 * * * * curl -s "https://example.com/cron?secret=<CRON_SECRET>"
Security notes
uploads/,data/, and.envare outside the webroot and not served by Nginx- All file paths are validated with
realpath()againstUPLOADS_DIRto prevent path traversal - Passwords are stored as bcrypt hashes; login has a 300 ms fixed delay against brute force
- Session ID is rotated on login to prevent session fixation
- The cron endpoint is protected by a secret compared with
hash_equals()to prevent timing attacks - CSRF tokens are required on all POST forms