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

5 1 vote
Article Rating
Share this: