Для web-сервера Apache существует множество инструкций, в том числе и на Хабре, как стилизовать стандартный листинг файлов и директорий. Однако, как сделать также для сервера nginx, в русскоязычном интернете не найти. Порывшись на просторах интернета я нашел один репозитарий, где как раз и решается этот вопрос. Но для кириллических наименований файлов и папок потребовалась небольшая работа "напильником".



Итак имеется Ubuntu 16.04 и nginx 1.10.3. Настроим красивый листинг.


Скопируем директорию проекта BetterListing к себе на сайт. Чтобы она не отображались в листинге я переименовал /betterlisting на /.html сделав ее невидимой. При этом важно не забыть поменять соответствующие ссылки в файлах проекта. Также создадим директорию ./html/icons и поместим туда иконки для известных расширений в формате "расширение файла.png". Автор BetterListing предлагает использовать иконки Faenza Icons 96x96 пикс., но можно использовать и свои. По умолчанию в проекте известны расширения "bin", "jpg", "gif", "png", "html", "css", "zip", "iso", "tiff", "ico", "psd", "pdf", "exe", "rar", "deb", "swf", "7z", "doc", "docx", "xls", "xlsx", "pptx", "ppt", "txt", "php", "js", "c", "c++", "torrent", "sql", "wmv", "avi", "mp4", "mp3", "wma", "ogg", "msg", "wav", "py", "java", "gzip", "jpeg", "raw", но их легко изменить добавив или удалив нужные в код JS, который формирует вывод иконок.


Для тех, кому лень искать иконки

За стандартный листинг в nginx отвечает модуль ngx_http_autoindex_module. Включим его для корневой директории.


location / {
               autoindex on;
               autoindex_localtime on;
               autoindex_exact_size off;
        }

Модуль ngx_http_addition_module — это фильтр, добавляющий текст до и после ответа сервера. То есть до и после тегов <html> и </html> соответственно в странице сформированной модулем ngx_http_autoindex_module. Подключим файлы top.html и bot.html из проекта BetterListing.



               add_before_body /.html/top.html;
               add_after_body /.html/bot.html;

Поскольку в файлах top.html и bot.html уже присутствуют теги <html>, <head>, </head>, <body>, </body> и </html>, то нам необходимо отфильтровать повторяющиеся из стандартного вывода. За это отвечает модуль ngx_http_sub_module — это фильтр, изменяющий в ответе одну заданную строку на другую.



               sub_filter '<html>' '';
               sub_filter '<head><title>Index of $uri</title></head>' '';
               sub_filter '<body bgcolor="white">' '';
               sub_filter '</body>' '';
               sub_filter '</html>' '';
               sub_filter_once on;

Укажем кодировку UTF-8 иначе в кириллических именах nginx выставляет неправильное количество пробелов для выравнивания.



 charset   utf-8;

В результате location / будет иметь вид:
location / {
               try_files $uri $uri/ =404;
               #Разрешаем листинг файлов и вносим изменение во внешний вид через top.html и bot.html
               add_before_body /.html/top.html;
               add_after_body /.html/bot.html;
               autoindex on;
               autoindex_localtime on;
               autoindex_exact_size off;
               #Удаляем дублирующиеся теги
               sub_filter '<html>' '';
               sub_filter '<head><title>Index of $uri</title></head>' '';
               sub_filter '<body bgcolor="white">' '';
               sub_filter '</body>' '';
               sub_filter '</html>' '';
               sub_filter_once on;
               #Кодировка страниц UTF8 для правильного выравнивания файлов и директорий с кириллическими символами
               charset   utf-8;
        }

Отредактируем ссылки в файле top.html. Также (необязательно) я вынес код JS в отдельный файл betterlisting.js и сделал загрузку bootstrap.min.css (Bootstrap v3.3.7) и jquery-3.3.1.min.js локально.


top.html
<!DOCTYPE html>

<!-- BetterListing - devCoster.com -->
<!-- Coster coster@devcoster.com -->
<!-- Version 1.0a -->

<html lang="ru-RU" prefix="og: http://ogp.me/ns#">
        <head>
                <!-- Adjust title in settings below -->
                <title>Загрузки</title>
                <meta charset="utf-8" />

                <!-- Bootstrap Core CSS-->
                <link rel="stylesheet" href="/.html/bootstrap/css/bootstrap.min.css">

                <!-- Styles -->
                <link rel="stylesheet" href="/.html/style.css">

                <!-- jQuery -->
                <script src="/.html/jquery-3.3.1.min.js"></script>

                <!-- BetterListing -->
                <script src="/.html/betterlisting.js"></script>

                <!-- Favicon -->
                <link rel="icon" href="/.html/logo.png" sizes="32x32" />
                <link rel="icon" href="/.html/logo.png" sizes="192x192" />
                <link rel="apple-touch-icon-precomposed" href="/.html/logo.png" />
                <meta name="msapplication-TileImage" content="/.html/logo.png" />

        </head>

        <body>
                <div class="wasContainer">
                        <div class="row">
                                <div class="col-xs-11 col-centered" id="mainBox">
        <!-- Start of nginx output -->

В файле bot.html настроим свой футер, а для тегов </body > и </html > добавим пробел, чтобы ngx_http_sub_module их не отфильтровал.


bot.html
        <!-- End of nginx output -->
                                </div>
                        </div>
                        <div id="footer" class="row">
                                <!-- This footer will change depending on your settings in top.html -->
                                <p class="text-center"><a href="*URL сайта*">Загрузки</a></p>
                        </div>
                </div>
<center>
<!--LiveInternet counter-->
<script type="text/javascript"><!--
document.write("<a href='https://www.liveinternet.ru/click' "+
"target=_blank><img src='//counter.yadro.ru/hit?t14.11;r"+
escape(document.referrer)+((typeof(screen)=="undefined")?"":
";s"+screen.width+"*"+screen.height+"*"+(screen.colorDepth?
screen.colorDepth:screen.pixelDepth))+";u"+escape(document.URL)+
";"+Math.random()+
"' alt='' title='LiveInternet: показано число просмотров за 24"+
" часа, посетителей за 24 часа и за сегодня' "+
"border='0' width='88' height='31'><\/a>")
//-->
</script>
<!--/LiveInternet-->
</center>
        </body >
</html >

Теперь внесем изменения в код JS, который мы поместили в betterlisting.js. Этот скрипт отвечает за добавление иконок, фильтр файлов (поиск по странице) и переназначение вывода некоторых строк, поэтому каждый будет настраивать его под свои нужды. Стоит обратить внимание, что для корректной работы с кириллическими именами в функции filter(target) отвечающей за поиск по странице нужно изменить переменную arraySearch на decodeURIComponent( arraySearch ). Для редактирования списка известных типов файлов внесите соответствующие изменения в переменную var formats.


betterlisting.js
            //
            // Configure BetterListing here:
            var websiteName = 'Загрузки';
            var websiteURL = '*URL сайта*';
            // End of normal settings.
            //

            $(document).ready(function(){

            // Working on nginx HTML and applying settings.
            var text = $("h1").text();
            var array = text.split('/');
            var last = array[array.length-2];
            var dirStructure = $("a").text();
            var dirStructure = document.getElementsByTagName('a')[0].href;
            var dir = text.substring(10);
            var currentDir = last.charAt(0).toUpperCase() + last.slice(1);
            var dirTrun;

            // Truncate long folder names.
            if (currentDir.length > 19){
                var currentDir = currentDir.substring(0, 18) + '...';
            }

            // Updating page title.
            document.title = websiteName;

            // Add back button.
            $("h1").html('<p><img alt="логотип" style="margin-top: 7px; margin-left: 0px; padding: -1px; border-radius: 5px; border: 1px solid #aaa;" src="/.html/logo2.png"></p>');

            if (dir.length > 90) {
                dirTrun = dir.replace(/(.{90})/g, "$1\n")
            } else {
                dirTrun = dir.substring(0, dir.length - 1);
            }

            // Add subtitle and back arrow.
            $("h1").append('<h4><a href="' + dirStructure + '"></a><b>Загрузить: </b>' + dirTrun.slice(0) + '</h4>');

            // Add search box.
            $("h1").prepend('<form id="custom-search-form" class="form-inline pull-right"><div class="btn-group"><input id="searchBox" placeholder="Найти на странице" type="search" class="form-control"> <span id="searchclear" class="glyphicon glyphicon-remove-circle"></span></div></form>');

            // Add parent directory bit.
            $("a").eq(1).html('Parent Directory');

            // Add titles.
            $("pre").prepend('<div class="header">Name                                                   Time                 Size</div>');

            // Establish supported formats.
            var list = new Array();
            var formats = ["bin", "jpg", "gif", "png", "html", "css", "zip", "iso", "tiff", "ico", "psd", "pdf", "exe", "rar", "deb", "swf", "7z", "doc", "docx", "xls", "xlsx", "pptx", "ppt", "txt", "php", "js", "c", "c++", "torrent", "sql", "wmv", "avi", "mp4", "mp3", "wma", "ogg", "msg", "wav", "py", "java", "gzip", "jpeg", "raw"];

            // Run when text is entered in the search box.
            $('#custom-search-form').on('input',function(e){
                e.preventDefault();
                var target = $('#searchBox').val();
                filter(target);
            });

            // Instant search.
            function filter(target){
                var parent_directory = 'parent directory';
                $('pre a').each(function(){
                    var arraySearch = $(this).attr('href');
                    var arraySearch = decodeURIComponent( arraySearch )
                    // Check the href data for searched term. Using href because the link label truncates if the file or folder name is too long.
                    // Special handling for 'Parent Directory' as the href data doesn't contain that word.
                    if (arraySearch.toLowerCase().indexOf(target.toLowerCase()) > -1 || (($(this).text() == 'Parent Directory') && (parent_directory.indexOf(target.toLowerCase()) > -1))){
                        $(this).show();
                        $($(this)[0].nextSibling).css('display', 'inline');
                    } else {
                        $(this).hide();
                        if($($(this)[0].nextSibling).hasClass('hideMe')) {
                            $($(this)[0].nextSibling).css('display', 'none');
                        } else {
                            $($(this)[0].nextSibling).wrap('<span class="hideMe" style="display:none"></style>');
                        }
                    }
                });
            }

            // Runs when clear button is hit.
            $("#searchclear").click(function(){
                $("#searchBox").val('');
                filter('');
            });

            // Scan all files in the directory, check the extensions and show the right MIME-type image.
            $('pre a').each(function(){
                var found = 0;
                var arraySplit = $(this).attr('href').split(".");
                var fileExt = arraySplit[arraySplit.length - 1];

                for (var i = 0; i < formats.length; i++) {
                    if (fileExt.toLowerCase() == formats[i].toLowerCase()) {
                        var found = 1;
                        var oldText = $(this).text();
                        $(this).html('<img class="icons" src="/.html/icons/' + formats[i] + '.png"></img></a>' + oldText);
                        return;
                    }
                }

                // Add an icon for the go-back link.
                if ($(this).text().indexOf("Parent Directory") >= 0) {
                    var found = 1;
                    var oldText = $(this).text();
                    $(this).html('<img class="icons" src="/.html/icons/home.png">' + oldText);
                    return;
                }

                // Check for folders as they don't have extensions.
                if ($(this).attr('href').substr($(this).attr('href').length - 1) == '/') {
                    var found = 1;
                    var oldText = $(this).text();
                    $(this).html('<img class="icons" src="/.html/icons/folder.png">' + oldText.substring(0, oldText.length - 1));

                    // Fix for annoying jQuery behaviour where inserted spaces are treated as new elements -- which breaks my search.
                    var string = ' ' + $($(this)[0].nextSibling).text();

                    // Copy the original meta-data string, append a space char and save it over the old string.
                    $($(this)[0].nextSibling).remove();
                    $(this).after(string);
                    return;
                }

                // File format not supported by Better Listings, so let's load a generic icon.
                if (found == 0){
                    var oldText = $(this).text();
                    $(this).html('<img class="icons" src="/.html/icons/error.png">' + oldText);
                    return;
                }
            });
        });

В файле style.css нужно подключить моноширинные шрифты с поддержкой кириллицы, которой нет в оригинальном проекте BetterListing и соответственно назначить их для класса .pre. Здесь же можно внести свои "украшалки" для листинга.


style.css
@import url('https://fonts.googleapis.com/css?family=Cousine&subset=cyrillic');
a {
    color: #4C4C4C;
}

body{
    color: #4C4C4C;
    font-family: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
        background: #F9F9F9 url(index.png) repeat;
}

pre{
    font-family: 'Cousine', monospace;
    background-color: #FFFFFF;
    border: 1px solid #ccc;
    width:  750px;
    padding:  9.5px 0px 9.5px 0px;
        margin-top: 20px;
        text-align: left;
}

#footer{
    margin-top: 20px;
}
#footer a{
    color: #686868;
}

hr{
    margin-bottom: 0px;
}

h4 {
    color: #aaa;
    margin-top: 10px;
        font-size: 40%;
}

.header {
    color: #686868;
    margin:3px 5px 6px 5px;
}

.icons {
    margin: 2px 5px 3px 5px;
     height: 4%;
     width: 4%;
}

.col-centered{
    float: none;
    margin: 0 auto;
}

#mainBox{
    width:766px;
}

.text-center{
    margin-bottom: 20px;
}

#custom-search-form{
    font-size: 15px;
    margin-top: 7px;
}

#searchIcon{
    padding: 0px 0px 0px 5px;
    color: #686868;
}

.form-group-sm .form-control + .form-control-feedback, .input-group-sm + .form-control-feedback, .input-sm + .form-control-feedback {
    width: 30px;
    height: 30px;
    line-height: 30px;
}

.form-group-sm .form-control {
    height: 30px;
    padding: 5px 10px;
    font-size: 12px;
    line-height: 1.5;
    border-radius: 4px;
}

input.form-control,input.form-control:focus {
    border-color: #AAA;
    box-shadow: none;
   -webkit-box-shadow: none;
   -moz-box-shadow: none;
   -moz-transition: none;
   -webkit-transition: none;
}

#searchclear {
    position: absolute;
    right: 10px;
    top: 11px;
    height: 14px;
    margin: auto;
    font-size: 14px;
    cursor: pointer;
    color: #AAA;
}

В результате листинг будет выглядеть, как-то так:



Ссылку на рабочий вариант могу указать по запросу в комментариях.

Комментарии (22)


  1. berezuev
    13.04.2018 17:26

    Выглядит хорошо, но… Зачем?
    Если вы по http видите список файлов, значит что-то пошло не так.


    1. Revertis
      13.04.2018 18:25

      У меня знакомый качает торренты на подкроватный ноутбук, а в LAN открыл nginx и браузером ходит по содержимому :)


      1. eri
        15.04.2018 12:47

        расскажите ему про webdav ;)


        1. Revertis
          15.04.2018 13:21

          Да проще самбу поднять. У меня через неё организовано.


    1. UksusoFF
      13.04.2018 18:51

      Вы это серьезно? У касперского/длинка/китайских регистраторов/mirror.yandex.ru и еще у кучи вендоров и опенсорсных проектов кого тоже что-то пошло не так?


      1. berezuev
        13.04.2018 19:11

        Если бы не mirror.yandex.ru среди ваших примеров, я бы подумал, что вы так шутите)
        Касперский, Длинк и китайские регистраторы — неудачные примеры.


        1. UksusoFF
          13.04.2018 20:06

          1. bondbig
            14.04.2018 16:46

            да любой дистр линукса


  1. SlavikF
    13.04.2018 17:59

    Я недавно узнал, что у Nginx есть модуль, который позволяет загрузить директорию, как Zip архив. Полезная штука, но простого и понятного туториала по нему ещё не видел.


    1. UksusoFF
      13.04.2018 18:52

      А для апача не встречалось?


      1. SlavikF
        13.04.2018 18:53

        Для Апача не нашёл ничего аналогичного


      1. berezuev
        13.04.2018 20:32

        Можно попробовать по этой инструкции запускать bash-скрипт, наподобие такого:

        #!/bin/bash
        echo "Content-type: application/zip"
        echo `zip -r0q - ./files_to_be_achived`

        Писал на коленке, без тестирования.

        В принципе, этот вариант можно и в nginx провернуть.


  1. denvist
    13.04.2018 20:45

    Подобное, но на php — h5ai, демо.


    1. UksusoFF
      13.04.2018 22:59

      Оно с русскими буквами не очень дружит.


  1. Fox_exe
    13.04.2018 23:41

    Я тоже этим както озадачился… Написал за вечер на PHP: fox-exe.ru/Files/php_nginx_autoindex (Сам сайт работает на чуть дополненной версии этого-же скрипта)


  1. EvgenyMorozov
    14.04.2018 14:45

    А есть подобная штука, чтобы работала как стандартный Проводник Винды? Чтобы можно было фотографии смотреть превьюшками, бродить по директорями, искать по имени файла?


    1. bondbig
      14.04.2018 16:49

      Да миллион их, искать по ключевым словам “web file browser”
      Пример: https://demo.filerun.co/?username=admin&password=admin


  1. VBart
    14.04.2018 14:45

    Довольно странное решение, учитывая, что autoindex в nginx может выдавать данные в формате JSON или XML. Чтобы затем получить из этого красивую страничку, нужен либо xslt на стороне сервера, либо тот же JavaScript на стороне клиента.


  1. ZigFisher
    14.04.2018 16:32

    Спасибо, интересное решение. У самого то-же есть пару проектов, связанных с прошивками под различные устройства, и есть желание причесать стандартный вывод информации.

    Ссылку на рабочий вариант могу указать по запросу в комментариях.
    Да, покажите пожалуйста рабочий пример на базе вашей разработки. Спасибо.


    1. Dasuber Автор
      14.04.2018 16:59

      1. ZigFisher
        15.04.2018 13:31

        Сделал себе такой-же листинг для отображения списка модифицированных и своих прошивок по IP камерам — IPCam. У меня, конечно, не такое разнообразие по типам файлов чем в примерах, но в любом случае интереснее выглядит чем ничего. Спасибо, получилось именно то, что и хотел.


  1. symbix
    14.04.2018 18:51
    +3

    OMG, зачем так сложно?


    autoindex on;
    autoindex_format xml;
    xslt_stylesheet /path/to/my-fancy-index.xslt;

    XSLT можно даже особо не изучать, достаточно погуглить "nginx index xslt" и допилить готовое.