Backend Development

Generating Dynamic Urls For Downloading Files in PHP

Generating Dynamic Urls For Downloading Files in PHP

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:

dynamic files download

 

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:

generating dynamic urls with php

generating dynamic urls with php

generating dynamic urls with php

4.4 8 votes
Article Rating

What's your reaction?

Excited
8
Happy
1
Not Sure
1
Confused
0

You may also like

Subscribe
Notify of
guest

12 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
John Baxter
John Baxter
5 years ago

Excellent work. Flawless!

Giants
Giants
5 years ago

Hi !
Error in yout tuto, you said, create file : fetch-link.php
But it’s fetch_link.php

😉

Axel
Axel
4 years ago

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 🙂

Asad mirahni
Asad mirahni
4 years ago

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 »

Alessandro
Alessandro
4 years ago

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

Sika
Sika
4 years ago

Excellent job deserves thanks

zoldos
3 years ago

Works great! Thanks!!

Last edited 3 years ago by zoldos
zoldos
3 years ago

I’m having a couple issues. One, the download timer seems broken. I changed it to 2 hours for my project, but 3 or 4 hours later, on the same IP, I am still allowed to download the file! Also, how can I store the downloads *outside* of the root folder for better security? I’ve seen this done in other download scripts. Thanks! I also contacted you via your contact form.