16. Live Project: Building a Reels App with EC2, S3 and RDS
16.7 Uploading Videos and Text Posts
Ha project cha heart aahe. upload.php don forms dakhavto – video aani text. Video sathi PHP size check karto, file chya content varun type olakhto (finfo), server swatah random S3 key banvto, aani putObject ne S3 madhe pathavto.
public/upload.php
<?php
// public/upload.php – GET shows two forms; POST stores a video in S3 or a text post in RDS
require __DIR__ . '/../src/bootstrap.php';
use Aws\Exception\AwsException;
$user = require_login();
$error = '';
// allow-lists: never accept raw CSS colours or unknown file types from users
$COLORS = ['#0E7C86', '#1F3A5F', '#6A1B9A', '#C62828', '#2E7D32'];
$TYPES = ['video/mp4' => 'mp4', 'video/webm' => 'webm', 'video/quicktime' => 'mov'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
$type = (string) ($_POST['type'] ?? '');
$caption = trim((string) ($_POST['caption'] ?? ''));
if (mb_strlen($caption) > 500) {
$error = 'Caption is too long (max 500)';
} elseif ($type === 'text') {
$body = trim((string) ($_POST['body_text'] ?? ''));
$color = $_POST['bg_color'] ?? '';
$color = in_array($color, $COLORS, true) ? $color : $COLORS[0];
if ($body === '' || mb_strlen($body) > 1000) {
$error = 'Text post must be 1-1000 characters';
} else {
$stmt = db()->prepare('INSERT INTO posts (user_id, post_type, caption, body_text, bg_color)
VALUES (?, \'text\', ?, ?, ?)');
$stmt->execute([$user['id'], $caption, $body, $color]);
header('Location: /');
exit;
}
} elseif ($type === 'video') {
$f = $_FILES['video'] ?? null;
if (!$f || !is_array($f) || $f['error'] !== UPLOAD_ERR_OK) {
$code = (int) ($f['error'] ?? -1);
$error = 'Upload failed (error code ' . $code . ') - check the file size';
} elseif ($f['size'] <= 0 || $f['size'] > cfg('max_video_bytes')) {
$error = 'Video must be smaller than ' . (cfg('max_video_bytes') / 1048576) . ' MB';
} else {
// trust the file CONTENT, not the name or the browser's Content-Type
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
if (!isset($TYPES[$mime])) {
$error = 'Only MP4, WebM or MOV videos are allowed';
} else {
// server-generated key: users never choose the S3 path
$key = cfg('s3_prefix') . date('Y/m/') . bin2hex(random_bytes(16))
. '.' . $TYPES[$mime];
try {
s3()->putObject([
'Bucket' => cfg('s3_bucket'),
'Key' => $key,
'SourceFile' => $f['tmp_name'],
'ContentType' => $mime,
]);
$stmt = db()->prepare('INSERT INTO posts
(user_id, post_type, caption, s3_key, mime_type, size_bytes)
VALUES (?, \'video\', ?, ?, ?, ?)');
$stmt->execute([$user['id'], $caption, $key, $mime, (int) $f['size']]);
header('Location: /');
exit;
} catch (AwsException $ex) {
error_log('S3 upload failed: ' . $ex->getAwsErrorCode() . ' '
. $ex->getMessage());
$error = 'Could not store the video, please try again';
}
}
}
} else {
$error = 'Unknown post type';
}
}
page_top('New post');
if ($error !== '') {
echo '<p class="err">' . e($error) . '</p>';
}
$token = e(csrf_token());
?>
<h2>Video reel</h2>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf" value="<?= $token ?>">
<input type="hidden" name="type" value="video">
<label>Video (MP4/WebM/MOV, max <?= (int) (cfg('max_video_bytes') / 1048576) ?> MB)
<input type="file" name="video" accept="video/mp4,video/webm,video/quicktime" required></label>
<label>Caption <input name="caption" maxlength="500"></label>
<button type="submit">Upload video</button>
</form>
<h2>Text post</h2>
<form method="post">
<input type="hidden" name="csrf" value="<?= $token ?>">
<input type="hidden" name="type" value="text">
<label>Text <textarea name="body_text" maxlength="1000" rows="4" required></textarea></label>
<label>Background
<select name="bg_color">
<?php foreach ($COLORS as $c): ?>
<option value="<?= e($c) ?>"><?= e($c) ?></option>
<?php endforeach; ?>
</select></label>
<label>Caption <input name="caption" maxlength="500"></label>
<button type="submit">Post text</button>
</form>
<p><a href="/">← Back to reels</a></p>
<?php page_bottom();
Check in upload.php |
Attack it stops |
|---|---|
csrf_check() |
Forged uploads from another site |
| Size limit (PHP + Nginx + app) | Disk/memory exhaustion (DoS) |
finfo on the file content, allow-list of MIME types |
shell.php renamed to video.mp4 |
| Random server-generated S3 key | Path tricks (../), overwriting other users' files, guessing URLs |
| Colours from an allow-list | CSS injection through bg_color |
| Prepared statements | SQL injection in captions and text |
error_log + generic message |
Leaking bucket names or AWS errors to users |
For very large videos, the SDK's Aws\S3\MultipartUploader sends the file in parts; for this project putObject with a 50 MB limit is enough.
Why this matters for security
Unrestricted file upload (अनिर्बंधित फाइल अपलोड) is a classic path to full server compromise: if an attacker can upload a .php file into the web root and open it, they run commands on your server. This design is safe by construction – files never touch the web root, they go to S3, and S3 never executes code.
Ravindra Bagale's Tip
Students check only the extension (.mp4) or the $_FILES['video']['type'] sent by the browser – an attacker can change both! We check the file's real bytes with finfo. And if you get a 413 "Request Entity Too Large", check the limit in three places: Nginx client_max_body_size, PHP upload_max_filesize/post_max_size, and the app's max_video_bytes.
Ravindra Bagale's Tip – मराठी
Students फक्त extension (.mp4) किंवा browser ने पाठवलेला $_FILES['video']['type'] check करतात – दोन्ही attacker बदलू शकतो! आपण finfo ने file चे खरे bytes तपासतो. आणि 413 "Request Entity Too Large" आला तर तीन ठिकाणी limit बघा: Nginx client_max_body_size, PHP upload_max_filesize/post_max_size, आणि app चा max_video_bytes.
Ravindra Bagale's Tip – हिंदी
Students सिर्फ़ extension (.mp4) या browser का भेजा $_FILES['video']['type'] check करते हैं – दोनों attacker बदल सकता है! हम finfo से file के असली bytes जाँचते हैं. और 413 "Request Entity Too Large" आए तो तीन जगह limit देखो: Nginx client_max_body_size, PHP upload_max_filesize/post_max_size, और app का max_video_bytes.
Lab
Upload a small MP4 and a text post, then check the object with aws s3 ls s3://ravindra-reels-media-pune/videos/ --recursive and the row with SELECT * FROM posts. Rename a text file to test.mp4 and confirm the upload is rejected.