File Coverage

blib/lib/App/htidx.pm
Criterion Covered Total %
statement 51 121 42.1
branch 0 28 0.0
condition 0 8 0.0
subroutine 17 24 70.8
pod 0 6 0.0
total 68 187 36.3


line stmt bran cond sub pod time code
1             package App::htidx;
2             # ABSTRACT: generate static HTML directory listings.
3 1     1   430600 use Carp;
  1         3  
  1         132  
4 1     1   8 use Cwd qw(abs_path);
  1         2  
  1         58  
5 1     1   13 use File::Basename qw(basename dirname);
  1         2  
  1         135  
6 1     1   9 use File::Spec;
  1         2  
  1         33  
7 1     1   750 use File::stat;
  1         11289  
  1         102  
8 1     1   1249 use File::Temp;
  1         26755  
  1         69  
9 1     1   511 use HTML::Tiny;
  1         4010  
  1         34  
10 1     1   6 use List::Util qw(any);
  1         4  
  1         55  
11 1     1   815 use POSIX qw(strftime ceil);
  1         6084  
  1         5  
12 1     1   1719 use URI::Escape;
  1         1446  
  1         95  
13             use constant {
14 1         78 INDEX_FILE => 'index.html',
15             SIGNATURE => '@~@ I was made by App::htidx @~@',
16             TIMEFMT => '%Y-%m-%d %H:%M:%S %Z',
17 1     1   6 };
  1         1  
18 1     1   4 use feature qw(say);
  1         1  
  1         108  
19 1     1   661 use open qw(:encoding(utf8));
  1         1005  
  1         5  
20 1     1   1491 use strict;
  1         2  
  1         16  
21 1     1   852 use utf8;
  1         205  
  1         9  
22 1     1   32 use vars qw($VERSION $H $CSS);
  1         1  
  1         44  
23 1     1   3 use warnings;
  1         6  
  1         1464  
24              
25              
26             $VERSION = '0.02';
27              
28             $H = HTML::Tiny->new(mode => 'html');
29              
30             undef $/;
31             $CSS = ;
32              
33             sub main {
34 0 0   0 0   help() if (any { '-h' eq $_ || '--help' eq $_ } @_);
  0 0   0      
35              
36 0   0       my $dir = abs_path(shift(@_) || '.');
37              
38 0 0         croak("Error: $dir does not exist") unless (-e $dir);
39 0 0         croak("Error: $dir is not a directory") unless (-d $dir);
40              
41 0           mkindex($dir, 1);
42              
43 0           say STDERR 'htidx: done';
44              
45 0           return 0;
46             }
47              
48             sub help {
49 0     0 0   say "Usage: $0 DIRECTORY";
50 0           exit;
51             }
52              
53             sub mkindex {
54 0     0 0   my ($dir, $toplevel) = @_;
55              
56 0           printf STDERR "htidx: generating directory listing for %s...\n", $dir;
57              
58 0           my $index = File::Spec->catfile($dir, INDEX_FILE);
59              
60 0           opendir(my $dh, $dir);
61              
62 0           my @entries = sort grep { 0 != index($_, '.') } readdir($dh);
  0            
63              
64 0           closedir($dh);
65              
66 0           my (@dirs, @files);
67 0           foreach my $entry (sort(@entries)) {
68 0           my $path = File::Spec->catfile($dir, $entry);
69              
70 0 0 0       if (-d $path || (-l $path && -d readlink($path))) {
    0 0        
71 0           push(@dirs, $entry);
72              
73             } elsif (lc($entry) ne lc(INDEX_FILE)) {
74 0           push(@files, $entry);
75              
76             }
77             }
78              
79 0           my $mkhtml = scalar(grep { /^index\./i } @files) < 1;
  0            
80              
81 0 0         if (-e $index) {
82 0           open(my $fh, $index);
83              
84 0           while (!$fh->eof) {
85 0 0         if (index($fh->getline, SIGNATURE) >= 0) {
86 0           $mkhtml = 1;
87 0           last;
88             }
89             }
90              
91 0           $fh->close;
92             }
93              
94 0 0         mkhtml($index, $dir, $toplevel, \@dirs, \@files) if ($mkhtml);
95              
96 0           map { mkindex(File::Spec->catfile($dir, $_)) } @dirs;
  0            
97             }
98              
99             sub mkhtml {
100 0     0 0   my ($index, $dir, $toplevel, $dref, $fref) = @_;
101              
102 0           my $tmpfile = File::Temp::tempnam($dir, '.mkindex');
103              
104 0 0         open(my $fh, '>', $index) || die("$index: $!");
105              
106 0           my $title = sprintf('Directory listing of %s/', basename($dir));
107              
108 0           $fh->say('');
109 0           $fh->say(sprintf('', he(SIGNATURE)));
110 0           $fh->say($H->open('html', {lang => 'en'}));
111              
112 0           $fh->say($H->head([
113             $H->meta({charset => 'UTF-8'}),
114             $H->title(he($title)),
115             $H->meta({name => 'generator', content => sprintf('%s v%s', __PACKAGE__, $VERSION)}),
116             $H->meta({name => 'viewport', content => 'width=device-width'}),
117             $H->style(he($CSS)),
118             ]));
119              
120 0           $fh->say($H->open('body', { class => 'htidx-body' }));
121              
122 0           $fh->say($H->h1($title));
123              
124 0           $fh->say($H->open('table'));
125              
126 0           $fh->say($H->thead($H->tr([map { $H->th($_) } ('Name', 'Last Modified', 'Size') ])));
  0            
127              
128 0           $fh->say($H->open('tbody'));
129              
130 0 0         $fh->say($H->tr(
131             { class => 'htidx-directory' },
132             [
133             $H->td($H->a(
134             { href => '../'},
135             'Parent Directory'
136             )),
137             $H->td(strftime(TIMEFMT, localtime(stat(File::Spec->catfile(dirname($dir)))->mtime))),
138             $H->td('-')
139             ]
140             )) unless ($toplevel);
141              
142 0           foreach my $entry (@{$dref}) {
  0            
143             $fh->say($H->tr(
144             { class => 'htidx-directory' },
145 0           [ map { $H->td($_) } (
  0            
146             $H->a(
147             { href => uri_escape($entry).'/'},
148             he($entry.'/')
149             ),
150             strftime(TIMEFMT, localtime(stat(File::Spec->catfile($dir, $entry))->mtime)),
151             '-',
152             ) ]
153             ));
154             }
155              
156 0           foreach my $entry (@{$fref}) {
  0            
157 0           my $stat = stat(File::Spec->catfile($dir, $entry));
158              
159             $fh->say($H->tr(
160             { class => 'htidx-file' },
161 0           [ map { $H->td($_) } (
  0            
162             $H->a(
163             { href => uri_escape($entry) },
164             he($entry)
165             ),
166             strftime(TIMEFMT, localtime($stat->mtime)),
167             fsize($stat->size),
168             ) ]
169             ));
170             }
171              
172 0           map { $fh->say($H->close($_)) } qw(tbody table body html);
  0            
173              
174 0           $fh->close;
175              
176 0           rename($tmpfile, $index);
177             }
178              
179             sub fsize {
180 0     0 0   my $size = shift;
181 0 0         if ($size < 1000) {
    0          
    0          
182 0           return '1K';
183              
184             } elsif ($size < 1000 * 1000) {
185 0           return sprintf('%uK', ceil($size / 1000));
186              
187             } elsif ($size < 1000 * 1000 * 1000) {
188 0           return sprintf('%uM', ceil($size / (1000 * 1000)));
189            
190             } else {
191 0           return sprintf('%uG', ceil($size / (1000 * 1000 * 1000)));
192              
193             }
194             }
195              
196 0     0 0   sub he { $H->entity_encode(@_) }
197              
198             1;
199              
200             =pod
201              
202             =encoding UTF-8
203              
204             =head1 NAME
205              
206             App::htidx - generate static HTML directory listings.
207              
208             =head1 VERSION
209              
210             version 0.02
211              
212             =head1 SYNOPSIS
213              
214             Run C on the command line:
215              
216             htidx DIRECTORY
217              
218             Or in in your Perl scripts:
219              
220             use App::htidx;
221              
222             App::htidx::main($DIRECTORY);
223              
224             =head1 INTRODUCTION
225              
226             C generates static HTML directory listings for a directory tree.
227             This is useful in scenarios where you are using a static hosting service (such
228             as GitHub Pages) which doesn't auto-index directories which don't contain an
229             C.
230              
231             =head1 DIRECTORY INDEX FILES
232              
233             C will create an C file in each directory, unless one
234             or more files matching the pattern C exist.
235              
236             If C exists and was previously created by C then it will
237             be overwritten, otherwise it will be left as-is.
238              
239             =head1 AUTHOR
240              
241             Gavin Brown
242              
243             =head1 COPYRIGHT AND LICENSE
244              
245             This software is copyright (c) 2024 by Gavin Brown.
246              
247             This is free software; you can redistribute it and/or modify it under
248             the same terms as the Perl 5 programming language system itself.
249              
250             =cut
251              
252             __DATA__