symfony icon indicating copy to clipboard operation
symfony copied to clipboard

[FINDER] SSH2 Protocol - Date filter or $file->getMTime() not working if using subfolders

Open Kaaly opened this issue 2 years ago • 14 comments

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

Kaaly avatar Mar 20 '24 14:03 Kaaly

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.

ChrisTaylorDeveloper avatar Mar 27 '24 19:03 ChrisTaylorDeveloper

After further testing, the problem seems to happen on Windows environment. It works fine on a Linux environment, however.

Kaaly avatar Mar 28 '24 07:03 Kaaly

Are you connecting via ssh to Windows or running Symfony on Windows?

ChrisTaylorDeveloper avatar Mar 28 '24 07:03 ChrisTaylorDeveloper

I’m running Symfony on Windows

Kaaly avatar Mar 28 '24 18:03 Kaaly

OK, I will try on Windows, to re-produce your findings.

ChrisTaylorDeveloper avatar Mar 28 '24 18:03 ChrisTaylorDeveloper

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.

ChrisTaylorDeveloper avatar Apr 01 '24 18:04 ChrisTaylorDeveloper

I’m using Laragon but yes ssh is not enable by default

Kaaly avatar Apr 01 '24 21:04 Kaaly

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

ChrisTaylorDeveloper avatar Apr 03 '24 11:04 ChrisTaylorDeveloper

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

ChrisTaylorDeveloper avatar Apr 04 '24 09:04 ChrisTaylorDeveloper

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.

ChrisTaylorDeveloper avatar Apr 10 '24 17:04 ChrisTaylorDeveloper

Removing this line Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator.php:68 which is:

$basePath .= $this->directorySeparator;

seems to fix the problem on Windows.

ChrisTaylorDeveloper avatar Apr 12 '24 17:04 ChrisTaylorDeveloper

What is the full path returned by current() with and without this line?

xabbuh avatar Apr 13 '24 07:04 xabbuh

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"
}

ChrisTaylorDeveloper avatar Apr 13 '24 17:04 ChrisTaylorDeveloper

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.

moonraking avatar May 08 '24 15:05 moonraking

Can you please test if #57895 fixes the issue for you?

xabbuh avatar Jul 31 '24 12:07 xabbuh