История одного файлового менеджера

в 22:07, , рубрики: dolphin, php, Веб-разработка, файловый менеджер, метки: , ,

Не знаю, как вы, а я начинал изучение веба и PHP в частности путём написания бесплатных скриптов. Я написал 2 своих CMS, галерею, форум, гостевую книгу… Первым моим проектом был файловый менеджер, и бы хотел рассказать о том, через какие стадии развития он прошел и чем стал в итоге. Например, я научил его открывать папки с 500к файлов, не вылезая за memory_limit в 32 Мб с временем генерации страницы в несколько секунд.

Я подготовил небольшое демо его работы, а также выложил исходники файлового менеджера на github. Исходные тексты не слишком высокого качества, ибо в основном писалось это мной году в 2007, то есть 5 лет назад :).

2002 год. С чего всё началось. PHPFM 1.0

По сути, первым проектом, который я решил написать, был файловый менеджер. Причина была очень простая: тогда я не умел пользоваться MySQL, а с файлами уже более-менее научился :). Я назвал файловый менеджер PHPFM и даже выложил его на cgi.myweb.ru под именем aa. PHPFM, причём первые две буквы «a» я добавил, чтобы оно было в самом верху списка :). Отголоски этого до сих пор можно найти в сети, правда скачать уже ничего нельзя — сайты с «бесплатным софтом», видимо, на какой-то итерации потеряли сам архив с исходными кодами.

Реализовано всё было очень просто — тот же код работы с папками был примерно такого плана:

Говнокод!

// Да, первый мой код на PHP выглядел именно так, без htmlspecialchars, без проверок на ошибки и т.д.
$dh = opendir($dir);
while ($f = readdir($dh)) {
if ($f == '.' || $f == '..') continue;
$fullpath = $dir . '/' . $f;
$is_dir = is_dir($fullpath);
?><tr><td><input type="checkbox" name="files[]" value="<?=$f?>" /></td><td><img src="..." /></td><td><a href="..."><?=$f?></a></td></tr><?
}

Код представлял из себя классический говнокод на PHP, в котором смешивается код на PHP и HTML, ничего не экранируется и допускаются неочевидные ошибки динамической типизации в PHP (на самом деле чтение директории должно выглядеть как while (false !== ($f = readdir($dh))), потому что иначе чтение окончится на файле с именем «0»). Тем не менее, он вполне неплохо работал и генерировал ответ даже в 1 000 файлов где-то за 0.5 секунды, но зато много времени уходило рендеринг.

Недостатков у этого файлового менеджера было много: он был Web 1.0, работал не совсем корректно и вообще не мог ничего отредактировать в случае, если у пользователя, под которым запущен веб-сервер, не было прав на запись в файлы. Это, кстати, типичный подход для виртуального хостинга — доступ по FTP, из-под другого пользователя (например yuriy), нежели пользователь веб-сервера (например www-data).

2003 год. Версия вторая, улучшенная

Основной причиной, чтобы переписать первую версию файлового менеджера, было моё желание сделать его более «Web 2.0», хотя такого термина тогда ещё не существовало. Я хотел сделать возможность выбирать файлы по щелчку на файл, как в обычном виндовом проводнике, а также сделать поддержку контекстного меню. Эта версия уже кое-где сохранилась (её можно скачать по прямой ссылке, регистрироваться не обязательно). Она, кстати, до сих пор работает (по крайней мере в PHP 5.3 :), но вот верстка и JS затачивались под IE 6, поэтому не вся функциональность будет доступна в современных браузерах. Собственно, там до сих пор каким-то чудом работает открытие контекстного меню по правой кнопке, а также показ информации о выбранном файле во вкладке «Подробно» (реализовано через iframe, поскольку о существовании XMLHttpRequest мир тогда ещё не знал).

Вот пример функции удаления директории, которая в похожем виде до сих пор существует в моём файловом менеджере, почти с теми же ошибками :). В текущей версии эти ошибки не исправлены, потому что я подумываю о том, чтобы опять начать всё с нуля и написать-таки уже наконец достойную версию, которая будет работать действительно хорошо и не будет содержать очевидных ошибок безопасности, а также будет иметь хорошую структуру.

// плохая функция, не используйте её
function removedir($dir)
{
	$dh=opendir($dir);
	while ($file=readdir($dh))
	{
		if($file!="." && $file!="..")
		{
			$fullpath=$dir."/".$file;
			if(!is_dir($fullpath))
			{
				unlink($fullpath);
			}else
			{
				removedir($fullpath);
			}
		}
	}
	closedir($dh);
	if(rmdir($dir))
	{
		return true;
	}else
	{
		return false;
	}
}

Здесь неправильны, как минимум, 3 вещи:
1) нет проверки результата opendir
2) нет проверки на символические ссылки (а значит, если в директории будет симлинк на "/", то функция начнёт рекурсивно удалять корневую файловую систему, насколько хватит прав :))
3) неверный код чтения директории: функция остановится на файле с именем «0»

2004-2005 годы. Попытки создать третью версию

Идея с «web 2.0» мне настолько понравилась, что я решил пойти дальше, и сделать всё на фреймах, как в настоящем проводнике Windows, а также добавить наконец вид «иконки», а не списка. По сути, я скопировал большую часть поведения проводника, после чего мой JavaScript код превратился в такое мессиво, что я просто не смог это поддерживать, совсем :).

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

Текст сообщения

Господа, я решил (тоже прошло не знаю, сколько времени...) еще раз учесть все пожелания… И сделать новый PhpFM 3.0 на фреймах (по образу и подобию Проводника). +к тому, сделать поддержку вида не только списком, а еще и в виде иконок. Тот вариант, который сейчас разрабатывается, имеет очень тяжелый, но кроссбраузерный JavaScript (то есть, он работает и в Опере (в настройках яваскрипта нужно разрешить перехват нажатий на правую кнопку мыши), и в Мозилле, и ИЕ). Пока что на стадии разработки, вот то, что сделано:

1) фреймовая структура, дизайн резиновый!
2) в верхнем фрейме работает адресная строка (кнопка «Переход» декоративная!) и кнопка «Вверх»
3) в левом фрейме вид, как в Проводнике со включенным режимом «Папки». Работает не на JSHTTPRequest (планируется переделать, основываясь на нём — очень удобная штука), но все папки открываются динамически (с помощью невидимого IFRAME). Работает раскрытие и сворачивание папок — контекстного меню НЕТ!
4) в основной фрейм в виде иконок (есть в принципе еще в виде списка, но он ничего особенного из себя не представляет), рисуется яваскриптом, причем при ресайзе фрейма его содержимое перерисовывается без перезагрузки страницы. Работает выделение файлов (пока что множественное выделение не отлажено) мышью, при наведении мыши на файл через некоторое время над ним выскакивает тайтл с информацией о файле (раньше было сделано и для папок тоже). Слишком длинные имена обрезаются, при нажатии на файл или папку его имя восстанавливается до полного размера (правда, пока что без переносов строк). По двойному нажатию папка открывается, если на файл или на папку нажали два раза со значительным промежутком, выскакивает окошко с переименованием файла (не получилось у меня нормально сделать замену на <input type=text...). Для файлов и папок работает контекстное меню. Пока что еще много чего не реализовано, оцените пожалуйста хотя бы идею.

Адрес демонстрации: <нерабочий адрес, к сожалению>, чтобы узнать пароль, напишите мне ЛС, я вам пришлю пароль (мера предосторожности).

2006 год. Dolphin (Dolphin.php)

Ещё спустя какое-то время я понял, что уже более-менее набрался опыта (а также всё-таки начал что-то понимать в JavaScript) и теперь могу осилить сделать то, о чём я мечтал — полную копию Проводника, только на PHP :). Вот один из скриншотов той версии:

История одного файлового менеджера

Элементы меню были декоративные, но кнопки «назад», «вперед» и т.д. были анимированы настолько близко к Проводнику, насколько это вообще имело смысл делать в Web, и насколько у меня хватало умений. На самом деле, даже заброшенный мной PHPFM 3.0 работал настолько похоже на Проводник, что его кто-то «доработал» (дописав отсутствующую функциональность до уровня «лишь бы работало») и установил в качестве файлообменника в каком-то институте. Предыдущие версии PHPFM тоже кто-то устанавливал себе в локальную сеть в качестве файлопомойки для бухгалтерии, поскольку всё было очень похоже на Проводник и людям было очень легко в нём ориентироваться.

2007 год. Смена дизайна из-за боязни реакции копирастов из Microsoft :)

Я активно разрабатывал «дельфина» в период с 2006 по 2007 год, и в какой-то момент до меня дошло, что нехорошо копировать чужой дизайн, и что на меня теоретически напасть копирасты, если я продолжу брать элементы интерфейса из Проводника :). Поэтому я решил сменить дизайн, в результате чего файловый менеджер до сих пор находится в недоделанном состоянии, поскольку я не до конца отловил все баги, связанные с его сменой. Собственно, я им особо и не занимался.

Сейчас файловый менеджер выглядит примерно вот так:
История одного файлового менеджера

Оптимизация производительности

Я, честно говоря, отношусь к той категории людей, которым важна производительность. Я очень люблю, когда всё работает быстро, и для своего файлового менеджера я всё время пытался придумать новые способы ускорить его работу. Основная вещь, которую я хотел ускорить, было открытие списка файлов — самая часто используемая функция в файловом менеджере. Вот, какие идеи для увеличения скорости работы я придумывал:

1. Минимизация количества операций, совершаемых в цикле чтения директории

Описание

Код:

<?php
function d_filelist_fast($dir)
{
	setreadable($dir,true);
	if(!(@$dh=opendir($dir)) && !(@$ftp_list=d_ftplist($dir))) return false;
	
	if($dh)
	{
		$dirs = $files = $fsizes = array();
		/* chdir($dir); */
		
		while(false!==(@$file=readdir($dh)))
		{
			if($file=='.' || $file=='..') continue;
			if(is_dir($dir.'/'.$file)) $dirs[]=$dir.'/'.$file;
			else $files[]=$dir.'/'.$file;
			$fsizes[$dir.'/'.$file] = filesize($dir.'/'.$file);
		}
	
		closedir($dh);
	}else return $ftp_list;
	
	return array('dirs'=>$dirs,'files'=>$files,'fsizes'=>$fsizes);
}

Оценка производительности на папке в 50 000 файлов:

488 ms генерация
33.51 Мб памяти
246 Кб в gzip ( + 500 ms на загрузку )
305 ms в браузере
= 1300 мс

+ нет подгрузок данных (вообще)
— самый медленный вариант
— требуется очень много памяти

2. Анализ производительности при открытии папки i386 под Windows дал понять, что вызов stat() под Windows — очень дорогая операция. Если разбить результат на страницы и вызывать stat() только для файлов на текущей странице, можно сильно сэкономить. Я также пытался экономить на спичках, генерируя код «на лету» и скармливая его eval, поэтому код получился очень сложный, но довольно быстрый.

Описание

Код:

/*

extremely complicated :), extremely fast (on huge directories) and extremely customizable sorted filelist :)

$dir    -- directory from which to get filelist
$params -- array with optional parameters (see beginning of function for details)

RETURN: array(

	'pages' => array(
		$pagemin => array(
			'files' => array('field1' => $list1, ..., 'fieldN' => $listN),
			'dirs' 	=> ... (array of the same format)
		),
	
		... (the intermediate pages)
	
		$pagemax => array(
			'files' => ...,
			'dirs'  => ...
		)
	),
	
	'pages_num' => ...,
	'items_num' => ...
)

where

field1, ..., fieldN   -- requested fields (default 'name' and 'size')
$list1, ..., $listN   -- a list of values (array('value1', 'value2', ...,'valueN')),

$pagemin and $pagemax -- the page numbers of the specified range (default 1) 
pages_num             -- filtered number of pages (returns 1 if you do not ask not to split to pages)
items_num             -- filtered number of files + total number of folders

EXAMPLE:

$res = d_filelist_exteme('/home/yourock');
print_r($res);

this will result in:

Array
(
[pages] => Array
(
	[1] => Array
	(
		[files] => Array
		(
			[name] => Array (
				[0] => file1
				[1] => file3
				[2] => file20
			)

			[size] => Array (
				[0] => 1000
				[1] => 2000
				[2] => 300000
			)
		)

		[dirs] => Array
		(
			[name] => Array (
				[0] => dir1
			)

			[size] => Array (
				[0] => 512
			)
		)
	)
)

[pages_num] => 1

[items_num] => 4
)

*/
/* TODO: move it to config.php in some time */
define('LIGHT_PERPAGE', 30);

function d_filelist_extreme($dir, $params=array())
{
	/* set defaults: $key = ''; is equal to default value for $params['key'] */
	
	$fields = array('name', 'size'); // name, chmod or any field of stat(): (size, mtime, uid, gid, ...)
    
	$filt=''; // filename filter
	$sort='name'; // what field to sort (see $fields)
	$order='asc'; // sorting order: "asc" (ascending) or "desc" (descending)
	
	$fast=true; // use some optimizations (e.g. can allow to get some range from a filelist of 5000 files in 0.1 sec)
	$maxit=defined('JS_MAX_ITEMS') ? JS_MAX_ITEMS : 200; // how many items is enough to enable optimization?
	
	$split=true; // split result to pages and return only results for pages from $pagemin to $pagemax (including both)
	$pagemin=1; 
	$pagemax=1; // see description for $split
	$perpage=LIGHT_PERPAGE; // how many files per page
	
	$ftp=true; // try to get filelist through FTP also (can affect performance)
	
	/* read parameters, overwriting default values */
	extract($params,EXTR_OVERWRITE);
	
	if($sort!='name')
	{
		/*return d_error('Not supported yet');*/
		$fast = false;
		
		if(array_search('mode', $fields) === false) $fields[] = 'mode';
	}
	if($pagemax < $pagemin) $pagemax = $pagemin;
	if(array_search($sort, $fields)===false) $fields[] = $sort;
	if($order != 'asc') $order = 'desc';
	$filt = strtolower($filt);
	
	/* check required fields */
	$st = stat(__FILE__);
	
	foreach($fields as $k=>$v)
	{
		if($v == 'name' || $v=='chmod') continue;
		if(!isset($st[$v]))
		{
			$keys = array_filter(array_keys($st), 'is_string');
			return d_error('Unknown field: '.$v.'. Use the following: name, chmod, '.implode(', ', $keys));
		}
	}
	
	setreadable($dir, true);
	
	if(!@$dh = opendir($dir))
	{
		if(!$ftp) return d_error('Directory not readable');
		
		if(!@$ftp_list=d_ftplist($dir)) return d_error('Directory not readable');
	}
	
	if($dh)
	{
	    $it = array(); /* items */
		if(!$filt)
		{
			while(false!==(@$f=readdir($dh)))
			{
				if($f=='.' || $f=='..') continue;
				$it[] = $f;
			}
		}else
		{
			while(false!==(@$f=readdir($dh)))
			{
				if($f=='.' || $f=='..') continue;
				if(strpos(strtolower($f),$filt)!==false) $it[] = $f;
			}
		}
		closedir($dh);
		
		if(!$split) $perpage = sizeof($it);
		
		$old_dir = getcwd();
		chdir($dir);
		
		$l = sizeof($it);
		if($l < $maxit) $fast = false;
		
		/* $fast means do not sort "folders first" */
		
		if($fast) /* $sort = 'name' and $split = true */
		{
		    if($order=='asc') sort($it);
			else rsort($it);
		}else
		{
		    $dirs = $files = array();
		    
		    if($sort=='name')
		    {
			    for($i = 0; $i < $l; $i++)
			    {
			        if(is_dir($it[$i])) $dirs[]  = $it[$i];
			        else                $files[] = $it[$i];
			    }
		    	
		    	if($order=='asc')
				{
					sort($dirs);
					sort($files);
					$it = array_merge($dirs, $files);
				}else
				{
					rsort($files);
					rsort($dirs);
					$it = array_merge($files, $dirs);
				}
		    }
		}
		
		/* array_display($it); */
		
	    $res = array('pages_num' => ceil($l / $perpage), 'items_num' => $l);
	    $all = $pages = array();
		
		/* fix invalid page range, if it is required */
		
		if($pagemin > $res['pages_num'])
		{
			$pagemin = //max(1, $pagemin + ($pagemax - $res['pages_num']) );
			$pagemax = $res['pages_num'];
		}
	    
	    $cd = $cf = $ci =  ''; /* Code for Directories, Code for Files and Code for Items */ 
	    foreach($fields as $v)
	    {
	        $t = '[''.$v.''][] = ';
	        
	        if      ($v == 'name')  $t .= '$n';
	        else if ($v == 'chmod') $t .= 'decoct($s['mode'] & 0777)';
	        else                    $t .= '$s[''.$v.'']';
	        
	        if($sort == 'name')
	        {
		        $cd .= '$pages[$page]['dirs']'.$t.";n";
		        $cf .= '$pages[$page]['files']'.$t.";n";
	        }else
	        {
	        	$ci .= '$all'.$t.";n";
	        }
	    }
	    
	    /* we cannot optimize sorting, if sorted field is not name:
		   we will have to call expensive stat() for every file, not only
		   $perpage files.
		*/
	    
	    if($sort == 'name')
	    {
		    eval('
		    for($i = 0; $i < '.$l.'; $i++)
		    {
		        $page = floor($i / '.$perpage.') + 1;
		        if( $page < '.$pagemin.' || $page > '.$pagemax.' ) continue;
		        
		        $n = $it[$i];
		        $s = stat($n);
		        
		        // is a directory?
		        if(($s['mode'] & 0x4000) == 0x4000)
		        {
		            '.$cd.'
		        }else
		        {
		            '.$cf.'
		        }
		    }
		    ');
	    }
	    
	    if($sort!='name')
	    {
	    	eval('
		    for($i = 0; $i < '.$l.'; $i++)
		    {
		        $n = $it[$i];
		        $s = stat($n);
		        '.$ci.'
		    }
		    ');
	    	
	    	$code = 'array_multisort($all[''.$sort.''], SORT_NUMERIC, SORT_'.strtoupper($order);
	    	
	    	foreach($fields as $v)
	    	{
	    		if($v != $sort) $code .= ', $all[''.$v.'']';
	    	}
	    	
	    	$code .= ');';
	    	
	    	/* code is evalled to prevent games with links to arrays */
	    	
	    	eval($code);
	    	
	    	$pages = array();
	    	
	    	$cf = $cd = '';
	    	
	    	foreach($fields as $k => $v)
	    	{
	    		$cf .= '$pages[$page]['files'][''.$v.''][] = $all[''.$v.''][$i];';
	    		$cd .= '$pages[$page]['dirs'][''.$v.''][] = $all[''.$v.''][$i];';
	    	}
	    	
	    	eval('
	    	for($i = 0; $i < $l; $i++)
	    	{
	    		$page = floor($i / '.$perpage.') + 1;
	    		if( $page < '.$pagemin.' || $page > '.$pagemax.' ) continue;
	    		
	    		if(($all['mode'][$i] & 0x4000) == 0x4000)
		        {
		            '.$cd.'
		        }else
		        {
		            '.$cf.'
		        }
	    	}
	    	');
	    }
	    
	    if($res) $res['pages'] = $pages;
	    
	    chdir($old_dir);
	    
	    return $res;
	}else
	{
		extract($ftp_list);
		
		if($fields !== array('name', 'size') || $sort!='name') return d_error('Custom fields and sorting not by name are not currently supported in FTP mode');
		
		$files = array_map('basename', $files);
		$dirs  = array_map('basename', $dirs);
		
		$fl = sizeof($files);
		$dl = sizeof($dirs);
		
		if($filt)
		{
			$files_c = $dirs_c = array();
			
			for($i=0; $i<$fl; $i++) if(strpos(strtolower($files[$i]),$filt)!==false) $files_c[]=$files[$i];
			for($i=0; $i<$dl; $i++) if(strpos(strtolower($dirs[$i]),$filt)!==false) $dirs_c[]=$dirs[$i];
			
			$dirs = $dirs_c;
			$files = $files_c;
			
			$fl = sizeof($files);
			$dl = sizeof($dirs);
		}
		
		if(!$split) $perpage = $fl + $dl;
		
		$pages = array();
		
		for($i=0,$arr='files'; $arr=='files'||$i<$dl; $i++)
		{
		    if($arr=='files' && $i>=$fl)
		    {
			    $arr='dirs';
			    $i=0;
		    }
		    
		    $page = floor($i / $perpage) + 1;
		    if( $page < $pagemin || $page > $pagemax ) continue;
		    
		    $pages[$page][$arr]['name'][] = ${$arr}[$i];
		    $pages[$page][$arr]['size'][] = @$fsizes[$dir.'/'.${$arr}[$i]];
		}
	    
	    return array('items_num' => ($fl+$dl), 'pages_num' => ceil(($fl+$dl) / $perpage), 'pages' => $pages);
	}
}

Анализ производительности:

169 ms генерация
13.48 Мб памяти
2 Кб в gzip
7 мс на запрос в браузере
= 200 мс

+ быстрый старт
— требуется очень много памяти
— долгая и ресурсоёмкая подгрузка данных: сканирование папки заново

3. Идея следующей оптимизации пришла мне в голову, когда я написал свою СУБД, о которой я уже рассказывал здесь на Хабре (да, тот сумасшедший — это я :)). У предыдущего механизма было всё хорошо, кроме того, что нужно было делать разделение на страницы, и для каждой страницы мне приходилось обходить директорию заново. Поэтому я решил написать код, который бы кешировал структуру директории в таком виде, чтобы можно было быстро выбирать диапазоны файлов, например чтобы можно быстро получить файлы с 1000 по 1100.

Описание

/*

the cached version of filelist especially made for JS GGGR

returns:

array(

'items' => array(
	start: array(
		'name' => ...,
		'size' => ...,
		'is_dir' => true|false,
	),
	...
	start+length-1: array(
		'name' => ...,
		'size' => ...,
		'is_dir' => true|false,
	),
),

'cnt' => N

)

*/

function d_filelist_cached($dir, $start, $length, $filter = '')
{
	//sleep(1);

	$tmpdir = is_callable('get_tmp_dir') ? get_tmp_dir() : '/tmp';
	$cache_prefix = $tmpdir.'/dolphin'.md5(__FILE__);
	
	$cache_dat = $cache_prefix.'.dat';
	$cache_idx = $cache_prefix.'.idx';
	
	$new = false;
	
	if(!file_exists($cache_dat) || filemtime($dir) > filemtime($cache_dat))
	{
		$new = true;
		
		$fp = fopen($cache_dat, 'w+b');
		$ifp = fopen($cache_idx, 'w+b');
	}else
	{
		$fp = fopen($cache_dat, 'r+b');
		$ifp = fopen($cache_idx, 'r+b');

		list(, $l) = unpack('n', fread($fp, 2));
		$cached_dir = fread($fp, $l);
		
		list(, $l) = unpack('n', fread($fp, 2));
		$cached_filter = fread($fp, $l);
		
		if($cached_dir != $dir || $cached_filter != $filter)
		{
			ftruncate($fp, 0);
			ftruncate($ifp, 0);
			
			fseek($fp, 0, SEEK_SET);
			fseek($ifp, 0, SEEK_SET);
			
			$new = true;
		}
	}
	
	$items = array();
	$cnt   = 0;
	
	$old_cwd = getcwd();
	
	try
	{
		if(!@chdir($dir)) throw new Exception('Could not chdir to the folder');

		if($new)
		{
			fwrite($fp, pack('n', strlen($dir)).$dir);
			fwrite($fp, pack('n', strlen($filter)).$filter);
			
			$pos = ftell($fp);
			
			$dh = opendir('.');

			if(!$dh) throw new Exception('Could not open directory for reading');
			
			$filter = strtolower($filter);
			
			while( false !== ($f = readdir($dh)) )
			{
				if($f == '.' || $f == '..') continue;
				if(strlen($filter) && !substr_count(strtolower($f), $filter)) continue;
				
				fwrite($ifp, pack('NN', $pos, strlen($f)));
				fwrite($fp, $f);
				
				$pos += strlen($f);
				
				// ftell is not as fast as it should be, sadly
			}
			
			fflush($ifp);
			fflush($fp);
		}
		
		fseek($ifp, $start * 8 /* length(pack('NN')) */);
		
		$first = true;
		$curr_idx = $start;
		
		while( $curr_idx < $start + $length )
		{
			list(, $pos, $l) = unpack('N2', fread($ifp, 8));

			if($first)
			{
				fseek($fp, $pos);
				$first = false;
			}
			
			$f = fread($fp, $l);
			
			if(!strlen($f)) break;
			
			if(strlen($f) != $l)
			{
				throw new Exception('Consistency error');
			}
			
			$items[$curr_idx++] = array(
				'name' => $f,
				'size' => filesize($f),
				'is_dir' => is_dir($f),
			);
		}
		
		
		$cnt = filesize($cache_idx) / 8;
		
		
	}catch(Exception $e)
	{
		fclose($fp);
		fclose($ifp);

		unlink($cache_dat);
		unlink($cache_idx);
		
		@chdir($old_cwd);

		return is_callable('d_error') ? d_error($e->getMessage()) : false;
	}
	
	if($cnt < 500 && $length >= 500)
	{
		usort($items, 'd_filelist_cached_usort_cmp');
	}
	
	@chdir($old_cwd);
	
	return array( 'items' => $items, 'cnt' => $cnt );
}

function d_filelist_cached_usort_cmp($it1, $it2)
{
	return strcmp( $it1['name'], $it2['name'] );
}

Анализ производительности:

460 мс генерация
1.18 Мб памяти
2 Кб в gzip
8 мс на запрос в браузере
= 500 мс

+ легкая и быстрая подгрузка
+ не требуется много памяти
— медленный старт
— хранение промежуточных файлов значительного размера (800 Кб на 50 000 файлов при размере файла директории в 1.6 Мб)

4. Если вы читали код выше, вы наверное заметили, что код сначала был очень простой, потом стал очень сложным (до такой степени, что его невозможно поддерживать), потом стал проще… И конечно же, самая простая идея мне почему-то пришла в голову последней: самый быстрый код — это тот, который делает как можно меньше, и для PHP это очень актуально. Можно заметить, что в именах файлов и директорий никогда не может присутствовать символ "/", поэтому список файлов можно составить в виде строки:

function d_filelist_simple($dir)
{
	$dh = opendir($dir);
	if (!$dh) return d_error("Cannot open $dir");
	// use as little memory as possible using strings
	$files = '';
	// assuming that first two entries are always "." and ".." or (in case of root dir) it is only "."
	// we can read first two entries separately and skip check for "." and ".." in main cycle for 
	// maximum possible performance
	for ($i = 0; $i < 2; $i++) {
		$f = readdir($dh);
		if ($f === false) return array('res' => '', 'cnt' => 0);
		if ($f === "." || $f === "..") continue;
		$files .= "$f/";
	}
	while (false !== ($f = readdir($dh))) $files .= "$f/";
	closedir($dh);
	return array('res' => $files, 'cnt' => substr_count($files, '/'));

}

Поскольку в PHP конкатенация является O(1), этот код на PHP, я думаю, работает так быстро, насколько это вообще возможно (в данной ситуации) и потребляет минимально возможное количество оперативной памяти: меньше (без сжатия) сделать очень тяжело, поскольку нужно как-то разделять имена файлов.
Такой код, на самом деле, позволяет открыть папку в 500к файлов, потребляя меньше 32 Мб, причём это с учётом json_encode. Поскольку мы составляем очень простую структуру данных — просто строку, время работы json_encode тоже будет очень небольшим, как и затраты на парсинг со стороны браузера. После получения строки делается split('/') и мы получаем полный список файлов в папке. После этого можно вторым AJAX-запросом попросить информацию только для видимых файлов (можно в ответ также включить первые несколько сотен файлов). Таким образом мы решаем все проблемы, которые у нас были до этого: долгую генерацию списка файлов, неэкономное потребление памяти и неудобства со скроллингом больших списков.

Анализ прозводительности этой схемы (50 000 файлов)

48 мс генерация
1.92 Мб памяти
107 Кб в gzip ( + 200 ms на загрузку )
56 ms в браузере
= 300 ms

+ быстрый старт
+ нет подгрузок данных для имен файлов
+ низкие затраты CPU
— тратит много трафика
— тратит много памяти в браузере

Ссылки

Видео с демонстрацией работы последней версии моего файлового менеджера (версия не лишена проблем): www.youtube.com/watch?v=XSvY9joxQqI
Исходники: github.com/YuriyNasretdinov/Dolphin.php
Ветка форума с уцелевшей старой версией: forum.dklab.ru/viewtopic.php?t=9504

Автор: youROCK


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js