Video Sharing (Upload + Watch Page, No DB)

February 14, 2026

A tiny “video sharing” script you can drop on shared hosting. It lets people upload videos, generates a clean watch page, and shows a simple “latest videos” feed — all without a database.

What it does:

  • Upload videos (mp4, webm, mov).
  • Stores metadata as JSON files (title, filename, created date, views).
  • Creates a watch page via ?v=slug (SEO-friendly-ish without rewrites).
  • Counts views (simple “hit counter” per video).
  • Optional: if FFmpeg exists, auto-generates a thumbnail (falls back gracefully).

Install:

  1. Create: /tools/video-share/
  2. Create two writable folders inside it:
    • /tools/video-share/_v/ (video files)
    • /tools/video-share/_db/ (metadata JSON)
    • /tools/video-share/_thumbs/ (optional thumbs)
  3. Save the script below as: /tools/video-share/index.php
  4. Visit: /tools/video-share/

Note: This is intentionally minimal. Real public video sites need serious moderation, storage, and abuse controls. This is best for small communities, private sharing, or “demo” use.

<?php
declare(strict_types=1);

/**
 * Tiny Video Sharing (No DB) — One File
 * /tools/video-share/index.php
 *
 * - Upload video + title
 * - Stores JSON metadata per video
 * - Lists latest + watch pages
 * - Counts views
 * - Optional FFmpeg thumbnail generation (if available)
 */

header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');

$CFG = [
  'site_name' => 'Your Site',
  'tool_name' => 'Tiny Video Share',
  'desc'      => 'Lightweight video sharing: upload, browse, and watch. No database.',
  'canonical' => 'https://your-site.com/tools/video-share/', // change

  // Storage (must be writable)
  'dir_v'      => __DIR__ . '/_v',
  'dir_db'     => __DIR__ . '/_db',
  'dir_thumbs' => __DIR__ . '/_thumbs',

  // Upload limits
  'max_mb'     => 80, // adjust to your host limits
  'allowed'    => ['mp4','webm','mov'],

  // Anti-spam
  'honeypot'   => 'companyfax',
  'rate_hits'  => 3,      // uploads
  'rate_window'=> 1800,   // per 30 min per IP

  // FFmpeg thumbnail (optional)
  'ffmpeg_path' => 'ffmpeg', // or full path; leave as 'ffmpeg'
  'thumb_seconds'=> 2,
];

session_start();
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }

function ensure_dir(string $dir): bool { return is_dir($dir) || @mkdir($dir, 0755, true); }
function ip(): string { return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; }

function rl_path(string $key): string {
  return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'vs_rl_' . sha1($key) . '.json';
}
function rate_limit(string $scope, int $maxHits, int $window): array {
  $bucket = (int)floor(time() / max(1, $window));
  $key = $scope . '|' . ip() . '|' . $bucket;
  $path = rl_path($key);

  $fh = @fopen($path, 'c+');
  if (!$fh) return [true, 0];
  if (!flock($fh, LOCK_EX)) { fclose($fh); return [true, 0]; }

  $raw = '';
  $sz = @filesize($path);
  if ($sz && $sz < 4096) { rewind($fh); $raw = (string)fread($fh, $sz); }

  $st = ['reset' => time() + $window, 'count' => 0];
  if ($raw !== '') {
    $j = json_decode($raw, true);
    if (is_array($j) && isset($j['reset'],$j['count'])) { $st['reset']=(int)$j['reset']; $st['count']=(int)$j['count']; }
  }
  if (time() >= $st['reset']) { $st['reset']=time()+$window; $st['count']=0; }

  $st['count']++;
  $ok = ($st['count'] <= $maxHits);
  $retry = max(1, $st['reset'] - time());

  rewind($fh); ftruncate($fh, 0);
  fwrite($fh, json_encode($st, JSON_UNESCAPED_SLASHES));
  fflush($fh);

  flock($fh, LOCK_UN);
  fclose($fh);

  return [$ok, $retry];
}

function csrf_token(): string {
  if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16));
  return (string)$_SESSION['csrf'];
}
function csrf_ok(string $t): bool {
  return isset($_SESSION['csrf']) && hash_equals((string)$_SESSION['csrf'], $t);
}

function slugify(string $s): string {
  $s = strtolower(trim($s));
  $s = preg_replace('/[^a-z0-9\s-]/', '', $s) ?? $s;
  $s = preg_replace('/\s+/', '-', $s) ?? $s;
  $s = preg_replace('/-+/', '-', $s) ?? $s;
  $s = trim($s, '-');
  return $s !== '' ? $s : 'video';
}

function json_read(string $file): array {
  if (!is_file($file)) return [];
  $raw = @file_get_contents($file);
  if ($raw === false) return [];
  $j = json_decode($raw, true);
  return is_array($j) ? $j : [];
}
function json_write(string $file, array $data): bool {
  $out = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  if ($out === false) return false;
  return @file_put_contents($file, $out, LOCK_EX) !== false;
}

function list_videos(string $dirDb): array {
  $out = [];
  if (!is_dir($dirDb)) return $out;
  foreach (glob($dirDb . '/*.json') ?: [] as $file) {
    $j = json_read($file);
    if (!$j) continue;
    $out[] = $j;
  }
  usort($out, function($a,$b){
    return strcmp((string)($b['createdAt'] ?? ''), (string)($a['createdAt'] ?? ''));
  });
  return $out;
}

function thumb_for(array $v): string {
  $t = (string)($v['thumb'] ?? '');
  if ($t !== '') return $t;
  return '';
}

function try_make_thumb(array $CFG, string $videoPath, string $thumbPath): bool {
  // generate a JPG thumbnail using ffmpeg (if available)
  $ff = trim((string)$CFG['ffmpeg_path']);
  if ($ff === '') return false;

  $sec = max(0, (int)$CFG['thumb_seconds']);
  $cmd = escapeshellcmd($ff)
    . ' -y -ss ' . escapeshellarg((string)$sec)
    . ' -i ' . escapeshellarg($videoPath)
    . ' -vframes 1 -q:v 4 '
    . escapeshellarg($thumbPath)
    . ' 2>/dev/null';

  @exec($cmd, $o, $code);
  return $code === 0 && is_file($thumbPath) && filesize($thumbPath) > 1000;
}

// ensure dirs
ensure_dir($CFG['dir_v']);
ensure_dir($CFG['dir_db']);
ensure_dir($CFG['dir_thumbs']);

$notice = '';
$error  = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (!csrf_ok((string)($_POST['csrf'] ?? ''))) {
    $error = 'Security check failed. Refresh and try again.';
  } else {
    [$ok, $retry] = rate_limit('video_upload', (int)$CFG['rate_hits'], (int)$CFG['rate_window']);
    if (!$ok) $error = 'Rate limited. Try again in ' . $retry . 's.';

    $hp = (string)($_POST[(string)$CFG['honeypot']] ?? '');
    if ($error === '' && $hp !== '') $error = 'Spam blocked.';

    $title = trim((string)($_POST['title'] ?? ''));
    if ($error === '' && (mb_strlen($title) < 3 || mb_strlen($title) > 120)) $error = 'Title must be 3–120 chars.';

    $f = $_FILES['video'] ?? null;
    if ($error === '' && (!is_array($f) || (int)($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK)) {
      $error = 'Upload failed.';
    }

    if ($error === '' && is_array($f)) {
      $sz = (int)($f['size'] ?? 0);
      $maxBytes = (int)$CFG['max_mb'] * 1024 * 1024;
      if ($sz <= 0 || $sz > $maxBytes) $error = 'File too large (max ' . (int)$CFG['max_mb'] . 'MB).';

      $name = (string)($f['name'] ?? '');
      $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
      if ($error === '' && ($ext === '' || !in_array($ext, (array)$CFG['allowed'], true))) {
        $error = 'Unsupported file type.';
      }

      if ($error === '') {
        $base = slugify($title);
        $id = gmdate('Ymd_His') . '_' . bin2hex(random_bytes(6));
        $slug = $base . '-' . substr($id, -6);

        $fileName = $slug . '.' . $ext;
        $destVideo = rtrim($CFG['dir_v'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;

        if (!@move_uploaded_file((string)$f['tmp_name'], $destVideo)) {
          $error = 'Could not store video (permissions?).';
        } else {
          $thumbName = '';
          $thumbRel  = '';
          $thumbFile = rtrim($CFG['dir_thumbs'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $slug . '.jpg';

          if (try_make_thumb($CFG, $destVideo, $thumbFile)) {
            $thumbName = basename($thumbFile);
            $thumbRel  = '_thumbs/' . $thumbName;
          }

          $meta = [
            'id'        => $id,
            'slug'      => $slug,
            'title'     => $title,
            'file'      => '_v/' . $fileName,
            'thumb'     => $thumbRel,
            'views'     => 0,
            'createdAt' => gmdate('c'),
          ];

          $metaFile = rtrim($CFG['dir_db'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $slug . '.json';
          json_write($metaFile, $meta);

          header('Location: ?v=' . rawurlencode($slug));
          exit;
        }
      }
    }
  }
}

// route
$slug = (string)($_GET['v'] ?? '');
$slug = preg_replace('/[^a-z0-9\-]/', '', strtolower($slug)) ?? '';
$video = [];
if ($slug !== '') {
  $video = json_read(rtrim($CFG['dir_db'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $slug . '.json');
  if ($video) {
    // increment views (simple)
    $video['views'] = (int)($video['views'] ?? 0) + 1;
    json_write(rtrim($CFG['dir_db'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $slug . '.json', $video);
  }
}

$all = list_videos($CFG['dir_db']);

function current_url_base(): string {
  $proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
  $host = $_SERVER['HTTP_HOST'] ?? '';
  $path = $_SERVER['SCRIPT_NAME'] ?? '';
  return $proto . $host . $path;
}

$pageTitle = $video ? ((string)$video['title'] . ' — ' . $CFG['tool_name']) : ($CFG['tool_name']);
$desc = $CFG['desc'];
$canon = $video ? (current_url_base() . '?v=' . rawurlencode((string)$video['slug'])) : (string)$CFG['canonical'];

?><!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title><?php echo h($pageTitle); ?></title>
  <meta name="description" content="<?php echo h($desc); ?>" />
  <link rel="canonical" href="<?php echo h($canon); ?>" />
  <meta name="robots" content="index,follow,max-snippet:-1,max-image-preview:large,max-video-preview:-1" />

  <style>
    :root{
      --bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
      --text:#e8eef8; --muted:rgba(232,238,248,.72);
      --line:rgba(255,255,255,.10); --accent:#2a8cff;
      --shadow:0 18px 55px rgba(0,0,0,.45); --r:16px;
    }
    *{box-sizing:border-box}
    body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
    a{color:#8ad1ff;text-decoration:none}
    a:hover{text-decoration:underline}
    .wrap{max-width:1050px;margin:0 auto;padding:18px}
    .card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin:12px 0;box-shadow:var(--shadow)}
    .muted{color:var(--muted)}
    .top{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center}
    .pill{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(42,140,255,.14);border:1px solid rgba(42,140,255,.25);font-weight:900;font-size:12px}
    .btn{display:inline-flex;align-items:center;gap:8px;border:0;border-radius:12px;padding:10px 12px;background:var(--accent);color:#06101a;font-weight:900;cursor:pointer}
    input,textarea{
      width:100%;padding:10px 12px;border-radius:12px;border:1px solid var(--line);
      background:var(--panel2);color:var(--text);outline:none
    }
    .grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:12px}
    .c7{grid-column:span 7}
    .c5{grid-column:span 5}
    @media(max-width:980px){.c7,.c5{grid-column:span 12}}
    .thumb{
      width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:14px;
      border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.04)
    }
    .vwrap{
      border-radius:16px;border:1px solid rgba(255,255,255,.10);overflow:hidden;background:#000
    }
    video{display:block;width:100%;height:auto;max-height:70vh}
    .hr{height:1px;background:var(--line);margin:12px 0}
    .item{display:grid;grid-template-columns:180px 1fr;gap:12px;align-items:center}
    @media(max-width:720px){.item{grid-template-columns:1fr}}
  </style>
</head>
<body>
<div class="wrap">

  <div class="card">
    <div class="top">
      <div>
        <div class="pill">No DB</div>
        <h1 style="margin:10px 0 6px;font-size:22px"><?php echo h((string)$CFG['tool_name']); ?></h1>
        <div class="muted"><?php echo h((string)$CFG['desc']); ?></div>
      </div>
      <div style="display:flex;gap:10px;flex-wrap:wrap">
        <a class="btn" href="./">🏠 Home</a>
        <a class="btn" href="#upload">⬆️ Upload</a>
      </div>
    </div>
    <?php if ($error): ?><div class="hr"></div><div class="muted" style="color:#ffb3b3"><b><?php echo h($error); ?></b></div><?php endif; ?>
    <?php if ($notice): ?><div class="hr"></div><div class="muted"><b><?php echo h($notice); ?></b></div><?php endif; ?>
  </div>

  <?php if ($video): ?>
    <div class="card">
      <h2 style="margin:0 0 8px"><?php echo h((string)$video['title']); ?></h2>
      <div class="muted" style="font-size:13px">Views: <b><?php echo (int)($video['views'] ?? 0); ?></b> • Uploaded: <?php echo h((string)($video['createdAt'] ?? '')); ?></div>
      <div class="hr"></div>
      <div class="vwrap">
        <video controls playsinline preload="metadata">
          <source src="<?php echo h((string)$video['file']); ?>" type="<?php
            $ext = strtolower(pathinfo((string)$video['file'], PATHINFO_EXTENSION));
            echo $ext === 'webm' ? 'video/webm' : 'video/mp4';
          ?>">
          Your browser does not support the video tag.
        </video>
      </div>
      <div class="muted" style="margin-top:10px;font-size:13px">
        Share link: <code><?php echo h(current_url_base() . '?v=' . (string)$video['slug']); ?></code>
      </div>
    </div>
  <?php endif; ?>

  <div class="grid">
    <div class="card c7">
      <h2 style="margin:0 0 10px;font-size:18px">Latest videos</h2>

      <?php if (!$all): ?>
        <div class="muted">No uploads yet.</div>
      <?php else: ?>
        <?php foreach (array_slice($all, 0, 20) as $v): ?>
          <div class="item" style="margin:12px 0">
            <?php
              $t = thumb_for($v);
              if ($t !== '' && is_file(__DIR__ . '/' . $t)) {
                echo '<a href="?v='.h((string)$v['slug']).'"><img class="thumb" src="'.h($t).'" alt=""></a>';
              } else {
                echo '<a href="?v='.h((string)$v['slug']).'"><div class="thumb" style="display:flex;align-items:center;justify-content:center;color:rgba(232,238,248,.55);font-weight:900">No thumbnail</div></a>';
              }
            ?>
            <div>
              <div style="font-weight:900">
                <a href="?v=<?php echo h((string)$v['slug']); ?>"><?php echo h((string)$v['title']); ?></a>
              </div>
              <div class="muted" style="font-size:13px;margin-top:4px">
                <?php echo h((string)($v['createdAt'] ?? '')); ?> • Views: <?php echo (int)($v['views'] ?? 0); ?>
              </div>
            </div>
          </div>
          <div class="hr"></div>
        <?php endforeach; ?>
      <?php endif; ?>
    </div>

    <div class="card c5" id="upload">
      <h2 style="margin:0 0 8px;font-size:18px">Upload a video</h2>
      <div class="muted" style="font-size:13px;line-height:1.4">
        Allowed: <b><?php echo h(implode(', ', (array)$CFG['allowed'])); ?></b> • Max: <b><?php echo (int)$CFG['max_mb']; ?>MB</b><br>
        This is a tiny demo—use moderation if you open it publicly.
      </div>

      <form method="post" enctype="multipart/form-data" style="margin-top:12px;display:grid;gap:10px">
        <input type="hidden" name="csrf" value="<?php echo h(csrf_token()); ?>">
        <div>
          <div class="muted" style="font-size:12px;margin-bottom:6px">Title</div>
          <input name="title" required maxlength="120" placeholder="Short title..." />
        </div>
        <div>
          <div class="muted" style="font-size:12px;margin-bottom:6px">Video file</div>
          <input type="file" name="video" required accept="video/*" />
        </div>

        <!-- honeypot -->
        <div style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden">
          <label>Leave blank <input name="<?php echo h((string)$CFG['honeypot']); ?>" autocomplete="off"></label>
        </div>

        <button class="btn" type="submit">⬆️ Upload</button>
      </form>
    </div>
  </div>

  <div class="card">
    <div class="muted" style="font-size:13px;line-height:1.5">
      <b>Want to expand it?</b>
      <ul style="margin:8px 0 0; padding-left:18px">
        <li>Add an approval queue before videos go public.</li>
        <li>Limit uploads by IP/day or require a “share token”.</li>
        <li>Generate smaller transcoded MP4s (FFmpeg) for compatibility.</li>
      </ul>
    </div>
  </div>

</div>
</body>
</html>

Comments (2)

  • admin
    Mar 25, 2026 3:27 AM
    Biggest risk is that the filesystem basically becomes the database, so upload handling and output escaping have to be tight. For uploads, you’d want strict server-side validation, random filenames, size limits, and rules that prevent anything executable from running in the upload folder. For watch pages, the main issue is XSS if titles, descriptions, or filenames are printed without proper escaping. For access control without a DB, you can still protect admin/upload actions with sessions, passwords, signed tokens, or even basic private upload areas, but once you need real multi-user permissions, a database-backed auth system is usually the safer move. So basically: databaseless can be lightweight and fine for small projects, but only if the upload flow, page output, and admin protection are locked down properly.
  • Anonymous
    Mar 20, 2026 6:23 PM
    What are the security implications of a databaseless video sharing script on shared hosting, particularly regarding file upload validation, potential XSS vulnerabilities in the generated watch pages, and how would you implement proper access controls without database-driven user authentication?

← Back to all scripts