[FINDER] SSH2 Protocol - Date filter or $file->getMTime() not working if using subfolders
Symfony version(s) affected
6.4
Description
Hi,
when using Finder with ssh2 protocol, we can't use it to filter files by last modification date and we can't use $file->getMTime();
Exemple of code :
$finder = new Finder();
$connection = ssh2_connect('HOST', 'PORT');
ssh2_auth_password($connection,'USERNAME', 'PASSWORD');
$sftp = ssh2_sftp($connection);
$finder->in('ssh2.sftp://' . intval($sftp).'/subfolder/')->files();
if ($finder->count() > 0) {
foreach ($finder as $file) {
$mTime = $file->getMTime();
}
}
We got this error :
SplFileInfo::getMTime(): stat failed for ssh2.sftp://1133/subfolder/\exemple.pdf
Error seems to come from /\ just before file name.
For exemple, this code work :
$finder = new Finder();
$connection = ssh2_connect('HOST', 'PORT');
ssh2_auth_password($connection,'USERNAME', 'PASSWORD');
$sftp = ssh2_sftp($connection);
$finder->in('ssh2.sftp://' . intval($sftp).'/subfolder/')->files();
if ($finder->count() > 0) {
foreach ($finder as $file) {
$statinfo = ssh2_sftp_stat($sftp, '/subfolder/'. $file->getFilename());;
}
}
In the same way, if we use date method to filter files directly from finder, finder return no file.
How to reproduce
Try to get last modification date from files in subfolder of a sftp
Possible Solution
Fix pathname to avoid back slash
Additional Context
No response
Full disclosure: this is my first ever review!
I cannot reproduce. When I run the example code provided by @Kaaly I get timestamp values returned by $file->getMTime() and no error.
After further testing, the problem seems to happen on Windows environment. It works fine on a Linux environment, however.
Are you connecting via ssh to Windows or running Symfony on Windows?
I’m running Symfony on Windows
OK, I will try on Windows, to re-produce your findings.
Can I assume you're using the WAMP server or are you running on Windows in some other way. I notice that the default WAMP server doesn't have the ssh extension installed.
I’m using Laragon but yes ssh is not enable by default
I get the same error on WAMP i.e. Windows. When I look for all files (should be files foo and bar) in folder subfolder of user george I get this:
SplFileInfo::getMTime(): stat failed for ssh2.sftp://237/home/george/subfolder/\foo
Another observation. If I do
foreach ($finder as $file) {
echo $file->getPathname();
}
I get double slashes on both Linux and Windows:
Windows ssh2.sftp://237/home/george/subfolder/\foo
Linux ssh2.sftp://236/home/george/subfolder//foo
There is a comment on this page which seems related https://www.php.net/manual/en/splfileinfo.getpathname.php
It seems to me that method Symfony\Component\Finder::normalizeDir is responsible for the double slash. However, even without the double slash, the error still occurs.
Removing this line Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator.php:68 which is:
$basePath .= $this->directorySeparator;
seems to fix the problem on Windows.
What is the full path returned by current() with and without this line?
Here are var_dumps of the return value of current(). I'm looking for file bar.txt in the remote folder /home/george/subfolder.
line 68 present:
class Symfony\Component\Finder\SplFileInfo#138 (4) {
private $relativePath =>
string(0) ""
private $relativePathname =>
string(7) "bar.txt"
private $pathName =>
string(46) "ssh2.sftp://235/home/george/subfolder/\bar.txt"
private $fileName =>
string(7) "bar.txt"
}
line 68 removed:
class Symfony\Component\Finder\SplFileInfo#138 (4) {
private $relativePath =>
string(0) ""
private $relativePathname =>
string(7) "bar.txt"
private $pathName =>
string(45) "ssh2.sftp://235/home/george/subfolder/bar.txt"
private $fileName =>
string(7) "bar.txt"
}
This is the same bug as https://github.com/symfony/symfony/issues/54269.
At the moment I am using the following workaround:
use ReflectionClass;
use Symfony\Component\Finder\Finder as SymfonyFinder;
use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator;
/**
* This class is a hack into the Symfony Finder class. It tries to find the base Symfony RecursiveDirectoryIterator and set
* that that is required to get php on Windows to talk in '/' rather than mixing the native '\' and sftp servers '/'.
*
* Caveats, this will not work if the base iterator gets converted somehow into an array iterator for instance by setting
* a sort order. The sort order actually iterates into an array and then sorts them, returning an ArrayIterator with already
* broken path names.
*
* @see https://github.com/symfony/symfony/issues/54269
*/
class XFinder extends SymfonyFinder
{
/** @var string The separator to use when overriding, this is public to allow us to test it. */
public string $overrideDirectorySeparator = '/';
public function getIterator()
{
$iterator = parent::getIterator();
$rootIterator = $iterator;
while(\method_exists($rootIterator, 'getInnerIterator')) {
$rootIterator = $rootIterator->getInnerIterator();
}
if ($rootIterator instanceof RecursiveDirectoryIterator) {
/* @var $rootIterator RecursiveDirectoryIterator */
$flags = $rootIterator->getFlags();
$flags |= \RecursiveDirectoryIterator::UNIX_PATHS;
$rootIterator->setFlags($flags);
}
// Now to hack the directory separator.
$class = new ReflectionClass(RecursiveDirectoryIterator::class);
$property = $class->getProperty('directorySeparator');
$property->setAccessible(true);
$property->setValue($rootIterator, $this->overrideDirectorySeparator);
return $iterator;
}
}
Then to use it there is a bit of a trick if you need to use sorting or any iterator that needs to fetch the list before iterating:
$finder1 = new XFinder();
// Only sort after the main finder1 has iterated over the files and after we have injected our hack for the
// Windows directory separator.
$finder2 = new XFinder();
$finder2
->append($finder1->in($uri))
->sortByType()->reverseSorting()
;
So you can see that we need to set the \RecursiveDirectoryIterator::UNIX_PATHS flag and we also need to update the directorySeparator as @ChrisTaylorDeveloper suggested.
Can you please test if #57895 fixes the issue for you?