Want to create a download server for movies or you want to allow your users to buy digital downloads but you don’t want to expose the real path for the file so the solution is to create dynamic urls.
Dynamic urls is a url that changed every time when the user want to download a certain file, instead of giving the user the actual file path. This helps protecting the file sources from being stolen as this may be a paid file and also decrease the heavy load on the server.
Imagine you own a website that sell digital books, of course the process is the user pay for the book first then he can download the file by sending some kind of email message with the download link. To download the file the system generate a dynamic url for that file and for that user.
Dynamic urls takes the form of a normal link and ends and with a long token string, this token is the download identifier and this marks the url as dynamic and it has an expiration time and in some websites is attached with the device IP.
In this tutorial i will create a simple php script to show the process of dynamic url generation, we will create two tables. The first table is the table that holds the actual file names, and the second table is a table that holds the dynamic urls.
Database
Let’s create a new database and name it whatever you want then execute those queries:
CREATE TABLE `files` ( `id` int(11) NOT NULL AUTO_INCREMENT, `filename` varchar(255) NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
CREATE TABLE `file_urls` ( `id` int(11) NOT NULL AUTO_INCREMENT, `file_id` int(11) NOT NULL, `token_identifier` varchar(400) NOT NULL, `time_before_expire` varchar(50) NOT NULL, `ip_address` varchar(50) NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `token_identifier` (`token_identifier`), KEY `file_id` (`file_id`), CONSTRAINT `file_urls_ibfk_1` FOREIGN KEY (`file_id`) REFERENCES `files` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
As you see the files table only holds the filename and created_at fields. I assume that the files stored in a fixed location in the project.
The file_urls table contains the file_id, token_identifier and is unique, time_before_expire and the ip_address. time_before_expire field this is the expiration time for the url after that time that url is useless and inaccessible. The token_identifier must be unique per download.
Preparing Code
Create a new folder in your server root directory name it whatever you want, in my case i called it downloader with those files:
downloader
– files/
– scripts/
—- scripts.js
—- includes.php
—- index.php
—- download.php
—- fetch_link.php
—- download-box.php
—- d.php
Before starting to create the actual code try to add some files in the files/ directory and also insert them into the database like this:
Let’s open the includes.php and insert the below code:
<?php define("DB_HOST", "localhost"); define("DB_NAME", "downloader"); define("DB_USER", "root"); define("DB_PASSWORD", "root"); define("PROJECT_URL", "http://localhost/downloader/"); define("TIME_BEFORE_EXPIRE", 8); // 8 hours define("FILES_DIRECTORY", "./files"); try { $db_connection = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME, DB_USER, DB_PASSWORD); $db_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch(PDOException $e) { exit($e->getMessage()); } function getFiles() { global $db_connection; $query = $db_connection->prepare("SELECT * FROM files"); $query->execute(); $results = $query->setFetchMode(PDO::FETCH_OBJ); $files = []; foreach ($query->fetchAll() as $row) { $files[] = $row; } return $files; } function getFile($id) { global $db_connection; $query = $db_connection->prepare("SELECT * FROM files where id='{$id}'"); $query->execute(); $results = $query->setFetchMode(PDO::FETCH_OBJ); $file = false; foreach ($query->fetchAll() as $row) { $file = $row; } return $file; } function checkId() { if(!isset($_GET['id']) || empty($_GET['id']) || !is_numeric($_GET['id'])) { exit('<div class="alert alert-danger">missing file id</div>'); } $file = getFile($_GET['id']); if(!$file) { exit('<div class="alert alert-danger">file not found, may be it\'s deleted!</div>'); } return $file; } function getFileUrl($file_id) { global $db_connection; $query = $db_connection->prepare("SELECT * FROM file_urls where file_id='{$file_id}' and ip_address='".$_SERVER['REMOTE_ADDR']."' and time_before_expire >= '".time()."'"); $query->execute(); $results = $query->setFetchMode(PDO::FETCH_OBJ); $file_url = false; foreach ($query->fetchAll() as $row) { $file_url = $row; } return $file_url; } function getFileUrlByToken($token_identifier) { global $db_connection; $query = $db_connection->prepare("SELECT * FROM file_urls where token_identifier='{$token_identifier}'"); $query->execute(); $results = $query->setFetchMode(PDO::FETCH_OBJ); $file_url = false; foreach ($query->fetchAll() as $row) { $file_url = $row; } return $file_url; } function randStr($length = 60) { return substr(str_shuffle(str_repeat($x='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length/strlen($x)) )),1,$length); } function insertFileUrl($file_id) { global $db_connection; $data = [ 'file_id' => $file_id, 'token_identifier' => randStr(), 'time_before_expire' => (time() + TIME_BEFORE_EXPIRE*60*60), // 8 hours later 'ip_address' => $_SERVER['REMOTE_ADDR'] ]; $sql = "INSERT INTO file_urls (file_id, token_identifier, time_before_expire, ip_address) VALUES (:file_id, :token_identifier, :time_before_expire, :ip_address)"; $query = $db_connection->prepare($sql); $query->execute($data); return $data; }
The includes.php contains database config and some constants such as the expiration time and the files location and also i have added the code that connects to the database using PHP PDO.
The file contains some helper functions that we will need across the project like getFiles() function for displaying the files, and getFile($id) to retrieve a file by id, etc.
Displaying files
The first thing we need to do is to display the files to user so it can select a file and download it. So open index.php and modify it with this code:
index.php
<?php include_once './includes.php'; ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Downloader</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous" /> <style> .bottom-spacing { margin-bottom: 10px; } </style> </head> <body> <div class="container"> <h2>Downloads</h2> <p>Click on any of the download links below</p> <?php foreach (getFiles() as $row): ?> <div class="row bottom-spacing"> <div class="col-md-6"><strong><?= $row->filename ?></strong></div> <div class="col-md-6"><a href="./download.php?id=<?= $row->id ?>" class="btn btn-success center-block">Download</a></div> </div> <?php endforeach ?> </div> </body> </html>
In this simple script i iterate over the files returned by getFiles() function and display download links, in each link the user will be redirected to ./download.php.
Downloading files
Now open download.php and insert this code
<?php include_once './includes.php'; ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Download</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous" /> <style> .download-box { border: 2px solid #ccc; box-shadow: 0 0 1px 1px #ccc inset; padding: 10px; } #download-counter { font-weight: bold; color: blue; font-size: 15px; } </style> </head> <body> <div class="container"> <div class="row"> <?php $file = checkId(); ?> <?php if(($data = getFileUrl($file->id))) { ?> <div class="col-md-12"> <p class="text-center">Use this direct link</p> <div class="text-center download-wrapper"> <?php $token_identifier = $data->token_identifier; $time_before_expire = round(($data->time_before_expire - time())/(60*60)); include_once './download-box.php'; ?> </div> </div> <?php } else { ?> <div class="col-md-12"> <p class="text-center">Please wait until we generate your download link</p> <div class="text-center" id="download-counter"></div> <div class="text-center download-wrapper"></div> </div> <?php } ?> </div> </div> <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script> <script> var file_id = '<?= $file->id ?>'; var project_url = '<?= PROJECT_URL ?>'; </script> <script src="./scripts/scripts.js" type="text/javascript"></script> </body> </html>
In the above code first i check the file using checkId() function then i check also if their are a file url for this device IP that it is not expired using getFileUrl() function in this snippet:
<?php if(($data = getFileUrl($file->id))) { ?> <div class="col-md-12"> <p class="text-center">Use this direct link</p> .... .... ....
If there is a working file url we display it in the download box along with the remaining time to expire. Otherwise we display some sort of counter that counts from 10 downward and on reaching 0 it displays the download url using ajax.
download-box.php
<div class="remaining-time"> <p>This link will be available for this ip the next <?= $time_before_expire ?> hours</p> </div> <div class="download-box"> <a href="<?= PROJECT_URL . "d.php?t=" . $token_identifier ?>"><?= PROJECT_URL . "d.php?t=" . $token_identifier ?></a> </div>
The download box displays the remaining time in hours for the link to terminate and actual download url with the token identifier.
scripts/scripts.js
var seconds = 10; $("#download-counter").text(seconds); var counter = setInterval(function() { curr_counter = parseInt($("#download-counter").text()); --curr_counter; if(curr_counter == 0) { clearInterval(counter); fetchDownloadLink(file_id); } else { $("#download-counter").text(curr_counter); } }, 1000); function fetchDownloadLink(file_id) { $.ajax({ url: project_url + 'fetch_link.php?file_id=' + file_id, method: "GET", success: function(response) { $("#download-counter").remove(); $(".download-wrapper").html(response); } }); }
The above javascript code displays a counter to user that decrements on every second and on reaching 0 it calls fetchDownloadLink() function which sends an ajax request to fetch the new download link.
fetch-link.php
<?php include_once './includes.php'; if(!isset($_GET['file_id']) || !is_numeric($_GET['file_id'])) exit('missing file id'); // insert new url $insertedData = insertFileUrl($_GET['file_id']); // return response $token_identifier = $insertedData['token_identifier']; $time_before_expire = TIME_BEFORE_EXPIRE; include_once './download-box.php'; exit;
Here we just insert a new url using insertFileUrl() function which uses another function to generate a random token string called randStr(). Finally we return the download box with the new url.
d.php
<?php include_once './includes.php'; if(!isset($_GET['t']) || empty($_GET['t'])) { exit('Download identifier missing'); } $fileUrl = getFileUrlByToken($_GET['t']); // if download not exist if(!$fileUrl) { exit('There are no download that match this identifier'); } // if download expired if($fileUrl->time_before_expire < time()) { exit('This download is expired'); } // download $file = getFile($fileUrl->file_id); if(!empty($file->filename) && file_exists('./files/' . $file->filename)) { $file = urldecode($file->filename); $filepath = "./files/" . $file; header('Content-Description: File Transfer'); // header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="'.basename($filepath).'"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($filepath)); flush(); // Flush system output buffer readfile($filepath); exit; }
The above code allows downloading the file using php header() function. At the beginning of the function we retrieve the file by token using getFileUrlByToken() function.
next we check if there is a file that match this identifier and also we check if the url expiration time not ended. Finally we check if the file already exits and download it using this snippet:
if(!empty($file->filename) && file_exists('./files/' . $file->filename)) { $file = urldecode($file->filename); $filepath = "./files/" . $file; header('Content-Description: File Transfer'); // header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="'.basename($filepath).'"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($filepath)); flush(); // Flush system output buffer readfile($filepath); exit; }
Now try to run the code, you should see something like in those screenshots:
Excellent work. Flawless!
thanks
Hi !
Error in yout tuto, you said, create file : fetch-link.php
But it’s fetch_link.php
😉
Thank you for this script 🙂
Can you or anybody update the code to a higher version of PHP?
Currently version 8 is out.
On newer version the download from bigger files wouldn’t work 🙁
Thank you 🙂
Thank you bro. but how can i make the link to redirect the user to my website before downloading the video for example when a video is been uploaded in a blog section and the generate button is been clicked,a link will be automatically generated for the video being uploaded and once the link is been shared to facebook or telegram then any body that clicks on the link will be automaticallylautomatically be redirected to my website together with a page where the download button is for him or her to download the video. I need the full source code… Read more »
Thanks a lot, i think this is easy but i am busy right now for making another tutorial
Great tutorial
Compliments
One question:
is it possible, similar to what you did for the download,
do it to watch videos?
Thanks a lot in anticipation
May be in future lesson
Excellent job deserves thanks