Directory

Directory Listing

This is a simple shell script which does a directory listing. The listing is transformed into xml. Then the xml directory is transformed into a web page using xsl. Directories are linked via anchors. The only attribute I have included are the permissions. But basically you could add any. Maybe somebody who has a computer which is browsed for mostly
statical content can use it.

#!/bin/sh
echo "<media name=\"$1\">"
ls -RQ1l $1 |             \
sed -e 's|"$|</e>|'    \
    -e 's/^\([a-z\-]\)\(.........\).*"/    <e t="\1" r="\2">/'  \
    -e 's/^"/  <d p="/'        \
    -e 's/":$/">/'            \
    -e 's|^\s*$|  </d>|'       \
    -e '/^total [0-9]*$/d'
echo "</d>"
echo "</media>"

The script above works on platforms where Quoting is allowed (ls -Q). Unfortunately SunOS doesn't seem to belong into this family so you have to use the script below. Additionally if you have files with spaces in them. Well, the script below doesn't handle it.

echo "<media name=\"$1\">"
ls -lRb $1 |\
sed -e '/:$/ s/^\(.*\):$/<d p="\1">/' \
    -e 's/^\s*$/<\/d>/' \
    -e '/^total [0-9]*$/d' |\
sed -e '/^[^<]/ s/^\(.\)/\1 /' |\
awk '{ if (substr($1,1,1) !~ /^</ ) { print "<e t=\"" $1 "\" r=\"" $2 "\">" $NF "</e>" } else {print $0}}'
echo "</d>"
echo "</media>"

And this seems to be the simplest solution at least for me. The purpose here is to get the listing and afterwards examine the files in detail. I have added the header and the link to the xsl stylesheet. Now it should be fairly complete. You can then use the IE to open the xml file directly and it should reender.

#!/bin/bash
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
echo "<?xml-stylesheet type=\"text/xsl\" href=\"lsxml.xsl\" ?>"
echo "<media name=\"$1\">"
ls -1Rpb $1 |\
sed -e '/:$/ s/^\(.*\):$/<d p="\1">/' \
    -e 's/^\s*$/<\/d>/' \
    -e '/\/$/ s/^\(.*\)\/$/<e t="d">\1<\/e>/' \
    -e '/^[^<]/ s/^\(.*\)$/<e t="-">\1<\/e>/'
echo "</d>"
echo "</media>"

And this is the xsl sheet. I have consolidated the original version which was here. And finally got to use the xsl:sort

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
<xsl:template name="dirname">
  <xsl:param name="string"/>
  <xsl:param name="separator" select="'/'"/>
  <xsl:variable name="theRest" select="substring-after($string,$separator)"/>
  <xsl:value-of select="substring-before($string, $separator)"/>
  <xsl:if test="contains($theRest, $separator)">
    <xsl:value-of select="$separator"/>
    <xsl:call-template name="dirname">
      <xsl:with-param name="string" select="$theRest"/>
    </xsl:call-template>
  </xsl:if>
</xsl:template>
 
<xsl:template match="media">
  <html>
  <body>
    <h1><xsl:value-of select="@name"/></h1>
    <xsl:apply-templates/>
  </body>
  </html>
</xsl:template>
 
<xsl:template match="d">
  <a>
    <xsl:attribute name="name">
    <xsl:value-of select="@p"/>
    </xsl:attribute>
    <h2><xsl:value-of select="@p"/></h2>
  </a>
  <table border="1">
      <tr bgcolor="#777777">
         <td colspan="7">
          <a>
            <xsl:attribute name="href">#<xsl:call-template name="dirname">
              <xsl:with-param name="string" select="@p" />
              <xsl:with-param name="char" select="'/'" />
            </xsl:call-template></xsl:attribute>Back</a>
        </td>
      </tr>
      <xsl:apply-templates>
        <xsl:sort select="@t" order="descending"/>
      </xsl:apply-templates>
  </table>
</xsl:template>
 
<xsl:template match="e">
  <xsl:choose>
    <xsl:when test="@t='d'">
      <tr>
        <xsl:for-each select="@*">
          <td>
            <xsl:value-of select="."/>
          </td>
        </xsl:for-each>
        <td bgcolor="#aaaaaa">
          <a>
            <xsl:attribute name="href">#<xsl:value-of select="parent::*/@p"/>/<xsl:value-of select="text()"/></xsl:attribute>
            <xsl:value-of select="text()"/>
          </a>
        </td>
      </tr>
    </xsl:when>
    <xsl:otherwise>
      <tr>
        <xsl:for-each select="@*">
          <td>
            <xsl:value-of select="."/>
          </td>
        </xsl:for-each>
        <td>
          <xsl:value-of select="text()"/>
        </td>
      </tr>
    </xsl:otherwise>    
  </xsl:choose>
</xsl:template>
 
</xsl:stylesheet>

CSS

To improve a bit on the looks of the listing I have used a css stylesheet (shamelessly stolen from here) in a separate file. Then I have modified the xsl and there you have a visually more tasty look. Well depends on your preferences of course. Not all definitions from css are used in the xsl. So below you can see the stylesheet (I have saved it as lsxml.css as can be seen in the xsl below)

/*** COLORS ***/
body.snif {
    background: #ffffff;             /* background behind table */
}
table.snif {
    border: 1px solid #444444;       /* main table border style */
}
td.snDir {
    color: #ffffff;                  /* table header text color */
    background-color: #000000;       /* table header background color */
}
td.snDir a {
    color:white;                     /* link text color within table header */
}
tr.snHeading, td.snHeading, td.snHeading a {
    color: #dddddd;                  /* column headings text color */
    background-color: #444444;       /* column headings background color */
}
tr.snF td a {
    color: #000000;                  /* file listing link text color (filename)*/
}
tr.snF td a:hover, a.snif:hover {
    background-color: #bbbbee;       /* file listing link hover background color */
}
tr.snEven {
    background-color: #eeeeee;       /* file listing background color for even numbered rows */
}
tr.snOdd {
    background-color: #dddddd;       /* file listing background color for odd numbered rows */
}
tr.snF td {
    color: #444444;                  /* file listing text color */
}
.snCopyright * {
    color: #bbbbbb;                  /* copyright notice text color */
}
.snWhite {
    color: white;                    /* active page in paging header */
}
 
/*** FONTS ***/
.snif * {
    font-family: Tahoma, Sans-Serif;
    font-size: 10pt;
}
.snif a, a.snif {
    text-decoration: none;
}
.snif a:hover, a.snif:hover {
    text-decoration: underline;
}
.snCopyright * {
    font-size: 8pt;
}
.snifSmaller {
    font-weight: normal;
    font-size: 8pt;
}
td.snDir {
    font-weight: bold;
}
tr.snHeading, td.snHeading, td.snHeading a {
    font-weight: bold;
}
 
/*** MARGINS AND POSITIONS ***/
table.snif {
    width:100%;
}
table.snif td {
    padding-left: 10px;
    padding-right: 10px;
}
table.snif td.littlepadding {
    padding-left: 4px;
    padding-right: 0px;
}
td.snDir {
    padding-top: 3px;
    padding-bottom: 3px;
}
tr.snHeading, td.snHeading, td.snHeading a {
    padding-top: 3px;
    padding-bottom: 3px;
}
tr.snF td {
    padding-top: 2px;
    padding-bottom: 2px;
    vertical-align: top;
    padding-left: 10px;
    padding-right: 10px;
    white-space: nowrap;
}
.snif img {
    border:none;
}
.snW {
    white-space: normal;
}

And here are the modifications for the xsl. Basically the snOdd and snEven are applied to the lines to differentiate between them.

...
<xsl:template match="media">
  <head>
    <link rel="stylesheet" type="text/css" href="lsxml.css" />
  </head>
  <html>
...
<xsl:template match="e">
  <tr>
  <xsl:choose>
    <xsl:when test="(position() mod 2) = 0">
      <xsl:attribute name="class">snEven</xsl:attribute>
    </xsl:when>
    <xsl:otherwise>
      <xsl:attribute name="class">snOdd</xsl:attribute>
    </xsl:otherwise>
  </xsl:choose>
  <xsl:choose>
    <xsl:when test="@t='d'">
      <xsl:for-each select="@*">
        <td>
          <xsl:value-of select="."/>
        </td>
      </xsl:for-each>
      <td>
        <a>
          <xsl:attribute name="href">#<xsl:value-of select="parent::*/@p"/>/<xsl:value-of select="text()"/></xsl:attribute>
          <xsl:value-of select="text()"/>
        </a>
      </td>
    </xsl:when>
  <xsl:otherwise>
    <xsl:for-each select="@*">
      <td>
        <xsl:value-of select="."/>
      </td>
    </xsl:for-each>
      <td>
        <xsl:value-of select="text()"/>
      </td>
    </xsl:otherwise>    
  </xsl:choose>
  </tr>
</xsl:template>

Perl

use strict;
use File::Find;
use XML::Simple;
use Data::Dumper;
 
my %listing;
 
sub wanted () {
    my %onepiece;
    $onepiece{'content'}=$_;
    if ( -d "$File::Find::name" ) {
        $onepiece{'t'}='d';
    } else {
        $onepiece{'t'}='-';
    }
    push (@{$listing{'d'}{"$File::Find::dir"}{'e'}}, { %onepiece } );
}
 
my $xml = new XML::Simple ( KeyAttr=> 'p', RootName=>'media' );
$listing{'name'}='D:\\Perl';
find( \&wanted, 'D:\\Perl');
my $data=$xml->XMLout(\%listing);
print Dumper($data);

Php

Well it's not really php. The listing is done using exec and that is unix centric. Shouldn't be hard to switch for windoze thou. Or you can use some php functionality to retrieve filenames/directory structures. The problem may occur with filesizes. Now ls generates the correct sizes as strings so no problem. If you use any php function to determine the size in case of very large files this could fail. So a bit of *nix/linux with ls and a bit of Perl using preg_replace (perl regular expressions in php) and all wrapped up in a file with php extension. Well maybe in future it will be converted to real php. shellescape(arg/cmd) used for peace of mind. The output is pure xml the final formatting is done via xsl (that could be optimized). As you can see on the green header color I have used the cd example from w3school as a base for my xsl.

<?php
$directory=$_GET['dir'];
$directory.='/';
$directory=preg_replace ( '/\/+/','/', $directory );
$safedir=escapeshellarg( $directory );
$safedir=escapeshellcmd( $safedir );
$output = array();
$yearnow = "<t>".date('Y')." ";
exec("ls -l $safedir", $output, $retval);
header("Content-type: application/xml");
echo '<?xml version="1.0" encoding="ISO-8859-1" ?>'."\n";
echo '<?xml-stylesheet type="text/xsl" href="ls.xsl" ?>'."\n";
echo '<list dir="'.$directory.'">'."\n";
foreach ( $output as $line )
{
        $xmlline = preg_replace ( '/^(d|l)(.{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w+)\s+(\d+)\s+(?:(\d{2}:\d{2})|(\d{4}))\s+(.*)$/', '<d><p>$2</p><i>$3</i><u>$4</u><g>$5</g><s>$6</s><t>$10 $7 $8 $9</t><n>$11</n></d>', $line );
        $xmlline = preg_replace ( '/^(.)(.{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w+)\s+(\d+)\s+(?:(\d{2}:\d{2})|(\d{4}))\s+(.*)$/', '<f><p>$2</p><i>$3</i><u>$4</u><g>$5</g><s>$6</s><t>$10 $7 $8 $9</t><n>$11</n></f>', $xmlline );
        $xmlline = preg_replace ( '/<t>\s+/', $yearnow, $xmlline );
        $xmlline = preg_replace ( '/\s+<\/t>/', ' 00:00</t>', $xmlline );
        $xmlline = preg_replace ( '/<n>(.*?)<\/n>/', "<n>$1</n><l>$directory$1</l>", $xmlline );
        $xmlline = preg_replace ( '/<n>(.*)\s+->\s+(.*)<\/n><l>.*?<\/l>/', '<n>$1</n><l>$2</l>', $xmlline );
        echo "$xmlline\n";
}
echo "</list>\n";
?>

And this is the transformation.

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
<xsl:template match="/">
<html>
  <body>
    <h2><xsl:value-of select="list/@dir"/></h2>
    <table border="1">
    <tr bgcolor="#9acd32">
    <th align="left">Permission</th>
    <th align="left">Subdirs</th>
    <th align="left">Owner</th>
    <th align="left">Group</th>
    <th align="left">Size</th>
    <th align="left">Date/Time</th>
    <th align="left">Name</th>
    </tr>
    <xsl:for-each select="list/d">
      <tr>
      <td><xsl:value-of select="p"/></td>
      <td><xsl:value-of select="i"/></td>
      <td><xsl:value-of select="u"/></td>
      <td><xsl:value-of select="g"/></td>
      <td><xsl:value-of select="s"/></td>
      <td><xsl:value-of select="t"/></td>
      <td><a>
        <xsl:attribute name="href">
          ?dir=<xsl:value-of select="l"/>
        </xsl:attribute>
        <xsl:value-of select="n"/>
      </a></td>
      </tr>
    </xsl:for-each>
    <xsl:for-each select="list/f">
      <tr>
      <td><xsl:value-of select="p"/></td>
      <td><xsl:value-of select="i"/></td>
      <td><xsl:value-of select="u"/></td>
      <td><xsl:value-of select="g"/></td>
      <td><xsl:value-of select="s"/></td>
      <td><xsl:value-of select="t"/></td>
      <td><xsl:value-of select="n"/></td>
      </tr>
    </xsl:for-each>
    </table>
  </body>
</html>
</xsl:template>
 
</xsl:stylesheet>

Now the script evolved a bit. The xml and xsl have been moved to template strings. The xsl has got sorting. It's quite compact and easy to understand. See for yourself. And it is invoked something like this: h**p://www.hat.is/it/lsxml.php?t=app&d=/a/long/long/way/to/SF/&s=n&o=a

<?php
 
// map from month name to number
$month2num = array (
 'Jan' => 1, 'Feb' => 2 , 'Mar' => 3 , 'Apr' => 4,
 'May' => 5, 'Jun' => 6 , 'Jul' => 7 , 'Aug' => 8,
 'Sep' => 9, 'Oct' => 10, 'Nov' => 11, 'Dec' => 12
);
 
// define templates for xml output
$template_xml_head = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n<?xml-stylesheet type=\"text/xsl\" href=\"?t=xsl&s={SORT}&o={ORDER}\" ?>\n<list d=\"{DIRECTORY}\">\n";
$template_xml_entry = "\t<e><y>{DIR}</y><p>{PRIVILEGES}</p><i>{SUBDIRS}</i><u>{OWNER}</u><g>{GROUP}</g><s>{SIZE}</s><t>{YEAR} {MONTH} {DAY} {TIME}</t><n>{NAME}</n><l>{LINK}</l></e>\n";
$template_xml_tail = '</list>';
// define templates for xsl output
$template_xsl_head = '<?xml version="1.0" encoding="ISO-8859-1"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:template match="/"><html><body><table border="1"><tr bgcolor="#aaaaaa"><td colspan="7"><xsl:value-of select="list/@d"/></td></tr><tr bgcolor="#9acd32">';
$template_xsl_th = '<th align="{ALIGN}"><a><xsl:attribute name="href"><xsl:text>?t=app&amp;d=</xsl:text><xsl:value-of select="list/@d"/>    <xsl:text>&amp;s={SHORTHAND}</xsl:text><xsl:text>&amp;o={ORDER}</xsl:text></xsl:attribute>{NAME}</a></th>';
$template_xsl_tail = '</tr><xsl:for-each select="list/e"><xsl:sort select="{SORT}" order="{ORDER}" /><tr><td><xsl:value-of select="p"/></td><td><xsl:value-of select="i"/></td><td><xsl:value-of select="u"/></td><td><xsl:value-of select="g"/></td><td align="right"><xsl:value-of select="s"/></td><td><xsl:value-of select="t"/></td><xsl:choose><xsl:when test="y=\'d\' or y=\'l\'"><td><a><xsl:attribute name="href">?t=app&amp;d=<xsl:value-of select="l"/></xsl:attribute><xsl:value-of select="n"/></a></td></xsl:when><xsl:otherwise><td><xsl:value-of select="n"/></td></xsl:otherwise></xsl:choose></tr></xsl:for-each></table></body></html></xsl:template></xsl:stylesheet>';
 
// get the sort order
$sort='n';
$sortag=$_GET['s'];
if ( preg_match ( '/^([piugstn])/', $sortag, $matches ) ) {
    $sort=$matches[1];
}
 
// get the  order ascending/descending
$order='a';
$ordertag=$_GET['o'];
if ( preg_match ( '/^([d])/', $ordertag, $matches ) ) {
    $order=$matches[1];
}
 
function fillTemplate ( $tpl, $arr )
{
    $output = $tpl;
    foreach ($arr as $key => $value) {
        $output = preg_replace ( $key, $value, $output );
    }
    return $output;
}
 
// type denotes the type of data requested from server. app means run the application, xsl means return xsl stylesheet
// return xsl data
if ( $_GET['t'] == 'xsl' ) {
    $fullorder = 'descending';
    if ( $order == 'a' ) {
        $fullorder = 'ascending';
    }
    header("Content-type: application/xsl");
    echo fillTemplate ( $template_xsl_head, array() );
    foreach ( array(
        array ( 'Permission', 'p', 'left' ),
        array ( 'Subdirs'   , 's', 'left' ),
        array ( 'Owner'     , 'u', 'left' ),
        array ( 'Group'     , 'g', 'left' ),
        array ( 'Size'      , 's', 'right'),
        array ( 'Date/Time' , 't', 'left' ),
        array ( 'Name'      , 'n', 'left' )
    ) as $th ) {
        echo fillTemplate ( $template_xsl_th, array(
            "/{NAME}/"      => $th[0], // name
            "/{SHORTHAND}/" => $th[1], // shorthand
            "/{ORDER}/"     => $order == 'a' ? 'd' : 'a',
            "/{ALIGN}/"     => $th[2]  // align
        ) );
    }
    echo fillTemplate ( $template_xsl_tail, array(
        "/{SORT}/"      => $sort,
        "/{ORDER}/"     => $fullorder,
    ) );
}
else if ( $_GET['t'] == 'app' ) {
    $directory=$_GET['d'];
    // prepare the safe directory for the ls command
    $directory='/'.$directory.'/';
    $directory=preg_replace ( '@/+@','/', $directory );
    // remove relative paths
    $directory=preg_replace ( '@/\.\./@','/', $directory );
    $directory=preg_replace ( '@/\./@','/', $directory );
    $safedir=escapeshellarg( $directory );
    $output = array();
    $yearnow = date('Y');
    $monthnow = date('n');
    exec("ls -l $safedir", $output, $retval);
    header("Content-type: application/xml");
    echo fillTemplate ( $template_xml_head, array(
        "/{SORT}/"      => $sort,
        "/{DIRECTORY}/" => $directory,
        "/{ORDER}/"     => $order,
    ) );
    foreach ( $output as $line )
    {
        preg_match ( '/^(.)(.{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w+)\s+(\d+)\s+(?:(\d{2}:\d{2})|(\d{4}))\s+(.*)$/', $line, $matches );
        // replace month string with month number
        $matches[7] = $month2num[$matches[7]];
        // correct the date-time
        if ( strlen ( $matches[10] ) == 0 ) {
            $matches[10] = $yearnow;
            if ( $matches[7] > $monthnow ) {
                $matches[10] = $yearnow-1;
            }
        }
        else {
            $matches[9] = '00:00';
        }
        $matches[12] = $directory.$matches[11];
        if ( preg_match ( '/(.*?)\s+->\s+(.*)/', $matches[11], $submatches ) ) {
            $matches[11] = $submatches[1];
            $matches[12] = $submatches[2];
        }
        echo fillTemplate ( $template_xml_entry, array(
            "/{DIR}/"        => $matches[1],
            "/{PRIVILEGES}/" => $matches[2],
            "/{SUBDIRS}/"    => $matches[3],
            "/{OWNER}/"      => $matches[4],
            "/{GROUP}/"      => $matches[5],
            "/{SIZE}/"       => $matches[6],
            "/{MONTH}/"      => $matches[7],
            "/{DAY}/"        => $matches[8],
            "/{TIME}/"       => $matches[9],
            "/{YEAR}/"       => $matches[10],
            "/{NAME}/"       => $matches[11],
            "/{LINK}/"       => $matches[12]
        ) );
    }
    echo fillTemplate ( $template_xml_tail, array() );
}
?>

ToDo

  • Add thumbnails for pictures and video
  • Change the php to real php and make it "exec ls" independent
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-Share Alike 2.5 License.