Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Kaaly opened this issue Mar 20, 2024 · 14 comments

Comments

@Kaaly
Copy link

Kaaly commented Mar 20, 2024

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

@ChrisTaylorDeveloper
Copy link

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.

@Kaaly
Copy link
Author

Kaaly commented Mar 28, 2024

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

@ChrisTaylorDeveloper
Copy link

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

@Kaaly
Copy link
Author

Kaaly commented Mar 28, 2024

I’m running Symfony on Windows

@ChrisTaylorDeveloper
Copy link

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

@ChrisTaylorDeveloper
Copy link

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.

@Kaaly
Copy link
Author

Kaaly commented Apr 1, 2024

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

@ChrisTaylorDeveloper
Copy link

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
Copy link

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
Copy link

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
Copy link

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

$basePath .= $this->directorySeparator;

seems to fix the problem on Windows.

@xabbuh
Copy link
Member

xabbuh commented Apr 13, 2024

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

@ChrisTaylorDeveloper
Copy link

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

@moonraking
Copy link

moonraking commented May 8, 2024

This is the same bug as #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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants