Why a "Download Non-Manager"?

I wanted to provide a download page for a Joomla website, where the users could download some files. But at the same time I wanted these files to be available as old-fashioned FTP Server downloadable files, so they could be downloaded using an FTP client program, or using a web browser in FTP mode when given a URL that starts with "ftp://xxxx". And I wanted all management of the download files (uploading new files, deleting old files, etc.) to be done via an FTP client, not via the Joomla website.

See here for a previous blog article about how I created two IIS FTP servers to make the files available for anonymous users, and to make it possible for me to manage the files using an FTP client program: Installing IIS FTP servers for an Azure VM

I had assumed that providing a web page with download links could be done using a Joomla extension, and there are indeed a dozen or more "download manager" extensions available for Joomla. But the emphasis is on the word "manager" - all of these extensions wanted to have the downloadable files completely under their control. They wanted the files to reside within the Joomla website directory structure, and all uploading and management of the files would be done using the management facilities provided by the extension.

Now I may be wrong, but after spending quite a bit of time evaluating several of these "download managers" and communicating with the authors of several others, I came to the conclusion that none of them could be used to provide a download page for files stored in the FTP server's directory structure, external to the Joomla website directory structure. Nor would they accept that these files were created and deleted by an FTP client "behind their back", and they would not automatically update the download page to reflect the new situation.

So what I ended up doing was creating my own Joomla extension, an "FTP download non-manager", and that's what I describe in this article.

What it looks like

This Joomla extension was developed specifically to provide a download page for my software development company, where there are a certain number of program distribution files made available, in a very specific structure.

Here's what the files look like in the FTP server directory:

FtpDownload Snap1

And here's what the download page generated by my "download non-manager" Joomla extension looks like:

FtpDownload Snap2

As can be seen, the generation of this download page is very customized to my specific situation. Still, I'm hoping this programming may be of some use to others.

The files in the extension

The extension is implemented as a Joomla "module". It contains the following files, which are presented without further comment.


<?xml version="1.0" encoding="utf-8"?> <extension type="module" version="3.1.0" client="site" method="upgrade"> <name>FTP Download!</name> <author>Rennie Petersen, Merlinia A/S</author> <version>1.0.0</version> <description>Provide download links to files in an FTP directory.</description> <files> <filename>mod_ftp_download.xml</filename> <filename module="mod_ftp_download">mod_ftp_download.php</filename> <filename>index.html</filename> <filename>tmpl/default.php</filename> <filename>tmpl/index.html</filename> <filename>language/da-DK/da-DK.mod_ftp_download.ini</filename> <filename>language/da-DK/index.html</filename> <filename>language/en-US/en-US.mod_ftp_download.ini</filename> <filename>language/en-US/index.html</filename> </files> <config> </config> </extension>


<?php /** * FTP Download module entry point. * * @package FTP Download * @version 1.0, January 2018 * @copyright Copyright (C) 2018, Rennie Petersen, Merlinia A/S. All rights reserved. * @license http://www.gnu.org/licenses/gpl.html GNU/GPL * * All of the processing is in the tmpl/default.php file. See there for more information. */ defined('_JEXEC') or die('Restricted access'); require JModuleHelper::getLayoutPath('mod_ftp_download'); ?>


<?php /** * FTP Download formatting of HTML output for the web page. * * @package FTP Download * @version 1.0, January 2018 * @copyright Copyright (C) 2018, Rennie Petersen, Merlinia A/S. All rights reserved. * @license http://www.gnu.org/licenses/gpl.html GNU/GPL * * This code generates HTML <a> tags and formatted output to provide a page with download links to * files in an FTP directory structure "external" to the Joomla website directory structure. * * The basic idea is to be backward-compatible with the old-fashioned way of providing a download * facility for customers based completely on FTP. So the owner of the website installs an FTP * server facility (FileZilla, IIS FTP Server, whatever) and uploads files that are to be provided * to customers using an FTP client program. All maintenance of the collection of files is done via * the FTP client, i.e., maintenance is not web based. * * The owner of the site then makes the URL of the FTP server publicly available, so customers can * use an FTP client to download files, or can use a web browser with the "ftp://xxxx" address so * the web browser goes into FTP mode. This is the old-fashioned way to make files available to * customers as downloadable files. * * This Joomla extension module just provides an alternative and more user-friendly way to provide * a download facility for the files in the FTP directories. This code reads the FTP directories * and formats the file names as HTML <a> tags so they can be clicked on and downloaded in that * way. It also displays the date and size for each file. * * In order to make the output look a bit nicer it is all placed in an HTML table, so the dates and * sizes line up in columns. * * The processing and display of files in this code is very specific to the way Merlinia A/S wants * to display the available files. It will have to be modified for usage elsewhere. * * One minor detail: The Merlinia website runs on a Windows / IIS system, which explains the syntax * of the top directory name. PHP for Windows handles the conversion between '/' and "\". */ defined('_JEXEC') or die('Restricted access'); // URL for the FTP server. This is not used by this code - it is only used to display it as an // alternative way that the customer can download the files. $ftpUrl = 'ftp://merlinia.com'; // Directory reference for the top directory in the FTP server's directory structure, or the top // directory for the files to be made available to customers. This is the reference used by the PHP // code in this program, which runs on the server, so the reference is server-based. $topDir = 'E:\Merlinia FTP'; // This is the corresponding identifier used for web-based access to the above $topDir directory. // For Windows / IIS this is typically defined as a "virtual directory". There is presumably a // similar mechanism for Linux-based web servers, but I'm not familiar with them so I'm not sure. $webDir = 'MerliniaFTP'; // Name of the sub-directory containing the file(s) to be formatted as being the "current version" // of the main or featured product being made available to customers. This will have to be manually // changed whenever the current version changes. Set it to '' if it isn't relevant. $currentVersion = 'Merlinia OutBack 5.4'; // Array of "special" directory names. Special implies the directory is first displayed after the // other directories at the same level, and the contents are displayed in correct order, instead of // the reverse order used when the contents are assumed to be multiple builds of some program. // NB. Specify them in lowercase - that's what the isSpecial() function requires. $specialDirs = [ 'utility programs', 'documentation' ]; // Output the start of the HTML table. Width = 75% is arbitrary, intended to avoid there being too // much empty space between the columns for what is assumed to be typical usage. echo '<table style="width:75%">'; // First process and display information for the "current version" of the main or featured product, // if relevant. Zip files and non-zip files are considered to be separate types. if ($currentVersion != '') { $cvDir = $topDir . '/' . $currentVersion; if (file_exists($cvDir)) { list($zipFiles, $nonZipFiles) = scandirZipVsNonZip($cvDir); if (count($zipFiles) > 0 || count($nonZipFiles) > 0) { // Highlight the latest build(s) echoLineEmpty(); echoLine(localized_text2('DOWNLOAD_LATEST_BUILD', $currentVersion)); if (count($zipFiles) > 0) formatFilename(1, $cvDir, $zipFiles[count($zipFiles) - 1], $topDir, $webDir); if (count($nonZipFiles) > 0) formatFilename(1, $cvDir, $nonZipFiles[count($nonZipFiles) - 1], $topDir, $webDir); // Provide a link to each of the non-latest builds if (count($zipFiles) > 1 || count($nonZipFiles) > 1) { echoLineEmpty(); echoLine(localized_text2('DOWNLOAD_PRIOR_BUILDS', $currentVersion)); for($i2 = count($zipFiles) - 2; $i2 >= 0; $i2--) { // Process in reverse order formatFilename(1, $cvDir, $zipFiles[$i2], $topDir, $webDir); } for($i2 = count($nonZipFiles) - 2; $i2 >= 0; $i2--) { // Process in reverse order formatFilename(1, $cvDir, $nonZipFiles[$i2], $topDir, $webDir); } } } } } // Display download links for older versions of the main or featured product, and/or other products. // Zip files and non-zip files are considered to be separate types. $dirs = scandir($topDir); $showHeader = true; for($i1 = count($dirs) - 1; $i1 >= 2; $i1--) { // Process in reverse order $dir = $dirs[$i1]; if ($dir == $currentVersion || isSpecial($dir, $specialDirs)) continue; $showHeader = showHeader($showHeader); echoLineEmpty(); echoLine($dir); $thisDir = $topDir . '/' . $dir; list($zipFiles, $nonZipFiles) = scandirZipVsNonZip($thisDir); for($i2 = count($zipFiles) - 1; $i2 >= 0; $i2--) { // Process in reverse order formatFilename(1, $thisDir, $zipFiles[$i2], $topDir, $webDir); } for($i2 = count($nonZipFiles) - 1; $i2 >= 0; $i2--) { // Process in reverse order $fileOrDir = $nonZipFiles[$i2]; if (!isSpecial($fileOrDir, $specialDirs)) { formatFilename(1, $thisDir, $fileOrDir, $topDir, $webDir); continue; } // Special directory inside older version / legacy product directory echoLineIndented($fileOrDir); $specialDir = $thisDir . '/' . $fileOrDir; $lowFiles = scandir($specialDir); for($i3 = 2; $i3 < count($lowFiles); $i3++) { // Process in normal order formatFilename(2, $specialDir, $lowFiles[$i3], $topDir, $webDir); } } } // Finally display the files in the top-level "special" directories for($i1 = 2; $i1 < count($dirs); $i1++) { // Process in normal order $dir = $dirs[$i1]; if (!isSpecial($dir, $specialDirs)) continue; $showHeader = showHeader($showHeader); echoLineEmpty(); echoLine($dir); $thisDir = $topDir . '/' . $dir; $files = scandir($thisDir); for($i2 = 2; $i2 < count($files); $i2++) { // Process in normal order formatFilename(1, $thisDir, $files[$i2], $topDir, $webDir); } } // Some notes at the bottom of the page echoLineEmpty(); echoLineEmpty(); echoLine(localized_text1('NOTES')); echoLineIndented(localized_text1('NOTE1')); echoLineIndented(localized_text1('NOTE2')); echoLineIndented(localized_text2('NOTE3', '<a href="' . $ftpUrl . '" target="_blank">' . $ftpUrl . '</a>')); // Output the end of the HTML table echo '</table>'; return; // Function to test if a directory name is considered to be a "special" directory. function isSpecial($dirName, $specialDirs) { return in_array(strtolower($dirName), $specialDirs); } // Function to do a scandir, and then return two arrays (in an array), one for *.zip files and one // for non-zip files. The "." and ".." entries are not returned in either array. function scandirZipVsNonZip($directory) { $files = scandir($directory); $result1 = array(); $result2 = array(); for($i = 2; $i < count($files); $i++) { // First two entries are "." and ".." $file = $files[$i]; if (substr($file, -4) == ".zip") $result1[] = $file; else $result2[] = $file; } return array($result1, $result2); } // Function to show a header line for the downloads that are not for current version of main or // featured product function showHeader($showHeader) { if ($showHeader) { echoLineEmpty(); echoLineEmpty(); echoLine(localized_text1('OLDER_AND_LEGACY')); } return false; } // Function to format a filename such that it can be clicked on to provide download. This also // displays the date and size for the file. The output is formatted as three columns in the HTML // table. function formatFilename($indentation, $containingDir, $filename, $topDir, $webDir) { $filenameServer = $containingDir . '/' . $filename; $filenameWeb = str_replace(" ", '%20', str_replace($topDir, $webDir, $filenameServer)); echo '<tr>'; echo '<td>' . str_repeat('&nbsp;&nbsp;', $indentation) . '<a href="' . $filenameWeb . '" download>' . $filename . '</a> </td>'; echo '<td>' . localized_text2('DATE_EQUALS', date('Y-m-d H:i', filemtime($filenameServer))) . '</td>'; echo '<td>' . localized_text2('SIZE_EQUALS', format_size(filesize($filenameServer))) . '</td>'; echo '</tr>'; } // Function to get a localized text (multilingual facility). function localized_text1($textId) { return JText::_('MOD_FTP_DOWNLOAD_' . $textId); } // Function to get a localized text (multilingual facility). This version for when there is a text // string to be substituted into the localized text. function localized_text2($textId, $substitutionText) { return JText::sprintf('MOD_FTP_DOWNLOAD_' . $textId, $substitutionText); } // Function to format a file size. Found here: // https://stackoverflow.com/questions/3381506/how-can-i-the-size-of-an-image-file-from-the-url function format_size($size) { $sizes = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"); if ($size == 0) { return('n/a'); } else { return (round($size/pow(1000, ($i = floor(log($size, 1000)))), 2) . $sizes[$i]); } } // Function to display a line of output as a row in the HTML table, using column spanning so it can // occupy the whole row. This version displays an empty line. function echoLineEmpty() { echoLine('&nbsp;'); } // Function to display a line of output as a row in the HTML table, using column spanning so it can // occupy the whole row. This version indents the output by two spaces. function echoLineIndented($text) { echoLine('&nbsp;&nbsp;' . $text); } // Function to display a line of output as a row in the HTML table, using column spanning so it can // occupy the whole row. function echoLine($text) { echo '<tr> <td colspan="3">' . $text . '</tr> </td>'; } ?>


MOD_FTP_DOWNLOAD_DATE_EQUALS = "date = %s" MOD_FTP_DOWNLOAD_DOWNLOAD_LATEST_BUILD = "Download the latest build of %s here:" MOD_FTP_DOWNLOAD_DOWNLOAD_PRIOR_BUILDS = "Prior builds of %s are also available for download:" MOD_FTP_DOWNLOAD_NOTES = "Notes:" MOD_FTP_DOWNLOAD_NOTE1 = "When multiple builds are listed, they are shown in reverse order, with the newer builds first." MOD_FTP_DOWNLOAD_NOTE2 = "Dates/times are local times for Denmark, and file sizes are in decimal, i.e., 1 MB is 1,000,000 bytes." MOD_FTP_DOWNLOAD_NOTE3 = "These files can also be downloaded with an FTP client or via a web browser in FTP mode: %s" MOD_FTP_DOWNLOAD_OLDER_AND_LEGACY = "Older versions and legacy products also available for download:" MOD_FTP_DOWNLOAD_SIZE_EQUALS = "size = %s"


MOD_FTP_DOWNLOAD_DATE_EQUALS = "dato = %s" MOD_FTP_DOWNLOAD_DOWNLOAD_LATEST_BUILD = "Download seneste build af %s her:" MOD_FTP_DOWNLOAD_DOWNLOAD_PRIOR_BUILDS = "Download af ældre builds af %s findes her:" MOD_FTP_DOWNLOAD_NOTES = "Notater:" MOD_FTP_DOWNLOAD_NOTE1 = "Når der findes flere builds, vises de i omvendt rækkefølge, med de nyeste builds først." MOD_FTP_DOWNLOAD_NOTE2 = "Datoer og klokkeslet er for Danmark, og filstørrelse er decimal, dvs. 1 MB er 1.000.000 byte." MOD_FTP_DOWNLOAD_NOTE3 = "Filerne kan også downloades med en FTP klient eller med en web browser i FTP mode: %s" MOD_FTP_DOWNLOAD_OLDER_AND_LEGACY = "Ældre versioner og legacy produkter findes også til download:" MOD_FTP_DOWNLOAD_SIZE_EQUALS = "størrelse = %s"

all of the index.html files

<html><body bgcolor="#FFFFFF"></body></html>

You must login to post a comment.
Loading comment... The comment will be refreshed after 00:00.

Be the first to comment.