Having troubling listing subdirectories recursively in PHP - php

I have the following code snippet. I'm trying to list all the files in a directory and make them available for users to download. This script works fine with directories that don't have sub-directories, but if I wanted to get the files in a sub-directory, it doesn't work. It only lists the directory name. I'm not sure why the is_dir is failing on me... I'm a bit baffled on that. I'm sure that there is a better way to list all the files recursively, so I'm open to any suggestions!
function getLinks ($folderName, $folderID) {
$fileArray = array();
foreach (new DirectoryIterator(<some base directory> . $folderName) as $file) {
//if its not "." or ".." continue
if (!$file->isDot()) {
if (is_dir($file)) {
$tempArray = getLinks($file . "/", $folderID);
array_merge($fileArray, $tempArray);
} else {
$fileName = $file->getFilename();
$url = getDownloadLink($folderID, $fileName);
$fileArray[] = $url;
}
}
}

Instead of using DirectoryIterator, you can use RecursiveDirectoryIterator, which provides functionality for iterating over a file structure recursively. Example from documentation:
$path = realpath('/etc');
$objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST);
foreach($objects as $name => $object){
echo "$name\n";
}
This prints a list of all files and
directories under $path (including
$path ifself). If you want to omit
directories, remove the
RecursiveIteratorIterator::SELF_FIRST
part.

You should use RecursiveDirectoryIterator, but you might also want to consider using the Finder component from Symfony2. It allows for easy on the fly filtering (by size, date, ..), including dirs or files, excluding dirs or dot-files, etc. Look at the docblocks inside the Finder.php file for instructions.

Related

Delete milions of files in a directory without listing them all at once

We have a sessions folder in outdated Magento installation,
that need to be manually cleaned from older files.
This is the current code:
private function _rrmdirContent($dir)
{
$items = array_diff(scandir($dir), array('..', '.'));
foreach ($items as $item) {
$path = $dir . DIRECTORY_SEPARATOR . $item;
is_dir($path) ? $this->_rrmdir($path) : unlink($path);
}
}
It loads a ton of resources if the file list is to long (1000 000 000 files -> 4gb memory limit exception)
Is there a way to remove the files one by one (ideally with some date created check), .. without loading them all at once?
IMHO the best way is to use DirectoryIterator, RecursiveDirectoryIterator is also available but it's quite poorly documented so I stick with DirectoryIterator.
Function to recursively delete all files in directory could look like this
function clearDirectory($path, $rmDir = false)
{
$iterator = new DirectoryIterator($path);
foreach ($iterator as $item) {
if ($item->isDot()) {
continue; // skip dot dirs
}
if ($item->isDir()) {
clearDirectory($item->getPathname(), true);
} else {
unlink($item->getPathname());
}
}
if ($rmDir) {
rmdir($iterator->getPathname());
}
}
To clear directory PATH call
clearDirectory(PATH);
If you want to delete root directory at the end, use second parameter
clearDirectory(PATH, true);
Use with caution, this will actually delete files

php mkdir folder tree from array

I'm trying to create a folder tree from an array, taken from a string.
$folders = str_split(564);
564 can actually be any number. The goal is to create a folder structure like /5/6/4
I've managed to create all folders in a single location, using code inspired from another thread -
for ($i=0;$i<count($folders);$i++) {
for ($j=0;$j<count($folders[$i]);$j++) {
$path .= $folders[$i][$j] . "/";
mkdir("$path");
}
unset($path);
}
but this way I get all folders in the same containing path.
Furthermore, how can I create these folders in a specific location on disk? Not that familiar with advanced php, sorry :(
Thank you.
This is pretty simple.
Do a for each loop through the folder array and create a string which appends on each loop the next sub-folder:
<?php
$folders = str_split(564);
$pathToCreateFolder = '';
foreach($folders as $folder) {
$pathToCreateFolder .= DIRECTORY_SEPARATOR . $folder;
mkdir($folder);
}
You may also add the base path, where the folders should be created to initial $pathToCreateFolder.
Here you'll find a demo: http://codepad.org/aUerytTd
Or you do it as Michael mentioned in comments, with just one line:
mkdir(implode(DIRECTORY_SEPARATOR, $folders), 0777, TRUE);
The TRUE flag allows mkdir to create folders recursivley. And the implode put the directory parts together like 5/6/4. The DIRECTORY_SEPARATOR is a PHP constant for the slash (/) on unix machines or backslash (\) on windows.
Why not just do:
<?php
$directories = str_split(564);
$path = implode(DIRECTORY_SEPARATOR, $directories);
mkdir($path, 0777, true);
Don't know what you're really trying to do, but here are some hints.
There are recursive mkdir:
if(!file_exists($dir)) // check if directory is not created
{
#mkdir($dir, 0755, true); // create it recursively
}
Path you want can be made in two function calls and prefixed by some start path:
$path = 'some/path/to/cache';
$cache_node_id = 4515;
$path = $path.'/'.join('/', str_split($cache_node_id));
Resulting path can be used to create folder with the code above
So here we come to a pair of functions/methods
function getPath($node_id, $path = 'default_path')
{
return $path.'/'.join('/', str_split($node_id))
}
function createPath($node_id, $path = 'default_path');
{
$path = getPath($node_id, $path);
if(!file_exists($path)) // check if directory is not created
{
#mkdir($path, 0755, true); // create it recursively
}
}
With these you can easily create such folders everywhere you desire and get them by your number.
As mentioned earlier, the solution I got from a friend was
$folders = str_split(564);
mkdir(implode('/',$folders),0777,true);
Also, to add a location defined in a variable, I used
$folders = str_split($idimg);
mkdir($path_defined_earlier. implode('/',$folders),0777,true);
So thanks for all the answers, seems like this was the correct way to handle this.
Now the issue is that I need to the created path, so how can I store it in a variable? Sorry if this breaches any rules, if I need to create a new thread I'll do it...

php write subdirectories' contents into separate text files

I am trying to list files in subdirectories and write these lists into separate text files.
I managed to get the directory and subdirectory listings and even to write all the files into a text file.
I just don't seem to manage to burst out of loops I am creating. I either end up with a single text file or the second+ files include all preceeding subdirectories content as well.
What I need to achieve is:
dir A/AA/a1.txt,a2.txt >> AA.log
dir A/BB/b1.txt,b2.txt >> BB.log
etc.
Hope this makes sense.
I've found the recursiveDirectoryIterator method as described in PHP SPL RecursiveDirectoryIterator RecursiveIteratorIterator retrieving the full tree being great help. I then use a for and a foreach loop to iterate through the directories, to write the text files, but I cannot break them into multiple files.
Most likely you are not filtering out the directories . and .. .
$maindir=opendir('A');
if (!$maindir) die('Cant open directory A');
while (true) {
$dir=readdir($maindir);
if (!$dir) break;
if ($dir=='.') continue;
if ($dir=='..') continue;
if (!is_dir("A/$dir")) continue;
$subdir=opendir("A/$dir");
if (!$subdir) continue;
$fd=fopen("$dir.log",'wb');
if (!$fd) continue;
while (true) {
$file=readdir($subdir);
if (!$file) break;
if (!is_file($file)) continue;
fwrite($fd,file_get_contents("A/$dir/$file");
}
fclose($fd);
}
I thought I'd demonstrate a different way, as this seems like a nice place to use glob.
// Where to start recursing, no trailing slash
$start_folder = './test';
// Where to output files
$output_folder = $start_folder;
chdir($start_folder);
function glob_each_dir ($start_folder, $callback) {
$search_pattern = $start_folder . DIRECTORY_SEPARATOR . '*';
// Get just the folders in an array
$folders = glob($search_pattern, GLOB_ONLYDIR);
// Get just the files: there isn't an ONLYFILES option yet so just diff the
// entire folder contents against the previous array of folders
$files = array_diff(glob($search_pattern), $folders);
// Apply the callback function to the array of files
$callback($start_folder, $files);
if (!empty($folders)) {
// Call this function for every folder found
foreach ($folders as $folder) {
glob_each_dir($folder, $callback);
}
}
}
glob_each_dir('.', function ($folder_name, Array $filelist) {
// Generate a filename from the folder, changing / or \ into _
$output_filename = $_GLOBALS['output_folder']
. trim(strtr(str_replace(__DIR__, '', realpath($folder_name)), DIRECTORY_SEPARATOR, '_'), '_')
. '.txt';
file_put_contents($output_filename, implode(PHP_EOL, $filelist));
});

Is there an easy way to read filenames in a directory and add to an array?

I have a directory: Audio/ and in that will be mp3 files only. I'm wanting to automate the process of creating links to those files. Is there a way to read a directory and add filenames within that directory to an array?
It'd be doubly cool if we could do an associative array, and have the key be the file name minus the .mp3 tag.
Any ideas?
To elaborate: I actual have several Audio/ folders and each folder contains mp3s of a different event. The event details are being pulled from a database and populating a table. That's why I'm duplicating code, because right now in each Audio/ folder, I'm having to define the filenames for the download links and define the filenames for the mp3 player.
Thank you! This will greatly simplify my code as right now I'm repeating tons of code over and over!
The SPL way is with DirectoryIterator:
$files = array();
foreach (new DirectoryIterator('/path/to/files/') as $fileInfo) {
if($fileInfo->isDot() || !$fileInfo->isFile()) continue;
$files[] = $fileInfo->getFilename();
}
And for completeness : you could use glob as well :
$files = array_filter(glob('/path/to/files/*'), 'is_file');
This will return all files (but not the folders), you can adapt it as needed.
To get just the filenames (instead of files with complete path), just add :
$files = array_map('basename', $files);
Yes: use scandir(). If you just want the name of the file without the extension, use basename() on each element in the array you received from scandir().
This should be able to do what you're looking for:
// Read files
$files = scandir($dirName);
// Filter out non-files ('.' or '..')
$files = array_filter($files, 'is_file');
// Create associative array ('filename' => 'filename.mp3')
$files = array_combine(array_map('basename', $files), $files);
Sure...I think this should work...
$files[] = array();
$dir = opendir("/path/to/Audio") or die("Unable to open folder");
while ($file = readdir($dir)) {
$cleanfile = basename($file);
$files[$cleanfile] = $file;
}
closedir($dir);
I imagine that should work...
$results = array();
$handler = opendir($directory);
while ($file = readdir($handler)) {
if ($file != "." && $file != "..") {
$results[] = $file;
}
}
closedir($handler);
this should work, if you want any files to be excluded from the array, just add them to the if statement, same for file extensions

PHP (folder) File Listing in Alphabetical Order?

I'm not sure how simple this would be, but I'm using a script which displays the files from a specific folder, however I'd like them to be displayed in alphabetical order, would it be hard to do this? Here's the code I'm using:
if ($handle = opendir($mainframe->getCfg( 'absolute_path' ) ."/images/store/")) {
while (false !== ($file = readdir($handle))) {
if ($file != "." && $file != "..") {
if (($file != "index.html")&&($file != "index.php")&&($file != "Thumbs.db")) {
$strExt = end(explode(".", $file));
if ($strExt == 'jpg') {
$Link = 'index.php?option=com_shop&task=deleteFile&file[]='.$file;
$thelist .= '<tr class="row0"><td nowrap="nowrap">'.$file.'</td>'."\n";
$thelist .= '<td align="center" class="order"><img src="/administrator/images/publish_x.png" width="16" height="16" alt="delete"></td></tr>'."\n";
}
}
}
}
closedir($handle);
}
echo $thelist;
:)
Instead of using readdir you could simply use scandir (documentation) which sorts alphabetically by default.
The return value of scandir is an array instead of a string, so your code would have to be adjusted slightly, to iterate over the array instead of checking for the final null return value. Also, scandir takes a string with the directory path instead of a file handle as input, the new version would look something like this:
foreach(scandir($mainframe->getCfg( 'absolute_path' ) ."/images/store/") as $file) {
// rest of the loop could remain unchanged
}
That code looks pretty messy. You can separate the directory traversing logic with the presentation. A much more concise version (in my opinion):
<?php
// Head of page
$it = new DirectoryIterator($mainframe->getCfg('absolute_path') . '/images/store/'));
foreach ($it as $file) {
if (preg_match('#\.jpe?g$#', $file->getFilename()))
$files[] = $file->getFilename();
}
sort($files);
// Further down
foreach ($files as $file)
// display links to delete file.
?>
You don't even need to worry about opening or closing the handle, and since you're checking the filename with a regular expression, you don't need any of the explode or conditional checks.
I like Glob
It makes directory reading a snap as it returns an array that's easily sortable:
<?php
$files = glob("*.txt");
sort($files);
foreach ($files as $filename) {
echo "$filename size " . filesize($filename) . "\n";
}
?>
If you're using Joomla1.5 you should be using the defined constant JPATH_BASE instead of
$mainframe->getCfg( 'absolute_path' )
If this is a Joomla extension that you will distribute, don't use scandir() as it is PHP5 only.
The best thing to do is to use the Joomla API. It has a classes for directory and file access that is layered to do this over different networks and protocols. So the file system can be over FTP for example, and the classes can be extended for any network/protocol.
jimport( 'joomla.filesystem.folder' );
$files = JFolder::files(JPATH_BASE."/images/store/");
sort($files);
foreach($files as $file) {
// do your filtering and other task
}
You can also pass a regular expression as the second parameter to JFolder::files() that filters the files you receive.
You also don't want to use URL literals like /administrator/ since they can be changed.
use the JURI methods like:
JURI::base();
If you want to make sure of the Joomla CSS classes in the tables, for:
'<tr class="row0">'
use:
'<tr class="row'.($i&1).'">'
where $i is the number of iterations. This gives you a sequence of alternating 0s and 1s.
if we have PHP built in functions, always use it, they are faster.
use glob instead of traversing folders, if it fits for your needs.
$folder_names = array();
$folder_names = glob( '*', GLOB_ONLYDIR + GLOB_MARK + GLOB_NOSORT );
returs everything in the current directory, use chdir() before calling it
remove the GLOB_ONLYDIR to include files too ( . would be only files )
GLOB_MARK is for adding a slash to folders names
Remove GLOB_NOSORT not to sort the array

Categories