Skip to content

Commit 635f8ad

Browse files
committed
Add QgsFileUtils::findClosestExistingPath to find the closest
existing folder match for a path E.g. if \a path is "/home/user/projects/2018/P4343" and "/home/user/projects" exists but no "2018" subfolder exists, then the function will return "/home/user/projects". (Suprisingly no existing Qt method for this!)
1 parent 1572314 commit 635f8ad

File tree

4 files changed

+72
-2
lines changed

4 files changed

+72
-2
lines changed

python/core/auto_generated/qgsfileutils.sip.in

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ for filenames with an '_' character.
7575
.. warning::
7676

7777
This method strips slashes from the filename, so it is safe to call with file names only, not complete paths.
78+
%End
79+
80+
static QString findClosestExistingPath( const QString &path );
81+
%Docstring
82+
Returns the top-most existing folder from ``path``. E.g. if ``path`` is "/home/user/projects/2018/P4343"
83+
and "/home/user/projects" exists but no "2018" subfolder exists, then the function will return "/home/user/projects".
84+
85+
.. versionadded:: 3.2
7886
%End
7987
};
8088

src/core/qgsfileutils.cpp

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#include "qgsfileutils.h"
1616
#include <QObject>
1717
#include <QRegularExpression>
18+
#include <QFileInfo>
19+
#include <QDir>
1820

1921
QString QgsFileUtils::representFileSize( qint64 bytes )
2022
{
@@ -29,7 +31,7 @@ QString QgsFileUtils::representFileSize( qint64 bytes )
2931
unit = i.next();
3032
bytes /= 1024.0;
3133
}
32-
return QString( "%1 %2" ).arg( QString::number( bytes ), unit );
34+
return QStringLiteral( "%1 %2" ).arg( QString::number( bytes ), unit );
3335
}
3436

3537
QStringList QgsFileUtils::extensionsFromFilter( const QString &filter )
@@ -86,8 +88,29 @@ QString QgsFileUtils::addExtensionFromFilter( const QString &fileName, const QSt
8688

8789
QString QgsFileUtils::stringToSafeFilename( const QString &string )
8890
{
89-
QRegularExpression rx( "[/\\\\\\?%\\*\\:\\|\"<>]" );
91+
QRegularExpression rx( QStringLiteral( "[/\\\\\\?%\\*\\:\\|\"<>]" ) );
9092
QString s = string;
9193
s.replace( rx, QStringLiteral( "_" ) );
9294
return s;
9395
}
96+
97+
QString QgsFileUtils::findClosestExistingPath( const QString &path )
98+
{
99+
QDir currentPath;
100+
QFileInfo fi( path );
101+
if ( fi.isFile() )
102+
currentPath = fi.dir();
103+
else
104+
currentPath = QDir( path );
105+
106+
while ( !currentPath.exists() )
107+
{
108+
const QString parentPath = QDir::cleanPath( currentPath.path() + QStringLiteral( "/.." ) );
109+
if ( parentPath.isEmpty() || parentPath == '.' )
110+
return QString();
111+
currentPath = QDir( parentPath );
112+
}
113+
114+
const QString res = QDir::cleanPath( currentPath.path() );
115+
return res == '.' ? QString() : res;
116+
}

src/core/qgsfileutils.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ class CORE_EXPORT QgsFileUtils
7878
* \warning This method strips slashes from the filename, so it is safe to call with file names only, not complete paths.
7979
*/
8080
static QString stringToSafeFilename( const QString &string );
81+
82+
/**
83+
* Returns the top-most existing folder from \a path. E.g. if \a path is "/home/user/projects/2018/P4343"
84+
* and "/home/user/projects" exists but no "2018" subfolder exists, then the function will return "/home/user/projects".
85+
*
86+
* \since QGIS 3.2
87+
*/
88+
static QString findClosestExistingPath( const QString &path );
8189
};
8290

8391
#endif // QGSFILEUTILS_H

tests/src/python/test_qgsfileutils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import qgis # NOQA
1616

17+
import tempfile
18+
import os
1719
from qgis.core import QgsFileUtils
1820
from qgis.testing import unittest
1921

@@ -61,6 +63,35 @@ def testStringToSafeFilename(self):
6163
QgsFileUtils.stringToSafeFilename('rendered map_final? rev (12-03-1017)_real/\\?%*:|"<>.tif'),
6264
'rendered map_final_ rev (12-03-1017)_real__________.tif')
6365

66+
def testFindClosestExistingPath(self):
67+
self.assertEqual(QgsFileUtils.findClosestExistingPath(''), '')
68+
self.assertEqual(QgsFileUtils.findClosestExistingPath('.'), '')
69+
self.assertEqual(QgsFileUtils.findClosestExistingPath('a_very_unlikely_path_to_really_exist/because/no_one_would_have_a_folder_called/MapInfo is the bestest/'), '')
70+
# sorry anyone not on linux!
71+
self.assertEqual(QgsFileUtils.findClosestExistingPath('/usr/youve_been_hacked/by_the_l77t_krew'), '/usr')
72+
73+
base_path = tempfile.mkdtemp()
74+
file = os.path.join(base_path, 'test.csv')
75+
with open(file, 'wt') as f:
76+
f.write('\n')
77+
78+
self.assertEqual(QgsFileUtils.findClosestExistingPath(os.path.join(base_path, 'a file name.bmp')), base_path) # non-existant file
79+
self.assertEqual(QgsFileUtils.findClosestExistingPath(file), base_path) # real file!
80+
self.assertEqual(QgsFileUtils.findClosestExistingPath(os.path.join(base_path, 'non/existant/subfolder')), base_path)
81+
82+
sub_folder1 = os.path.join(base_path, 'subfolder1')
83+
os.mkdir(sub_folder1)
84+
sub_folder2 = os.path.join(sub_folder1, 'subfolder2')
85+
os.mkdir(sub_folder2)
86+
bad_sub_folder = os.path.join(sub_folder2, 'nooo')
87+
self.assertEqual(QgsFileUtils.findClosestExistingPath(bad_sub_folder), sub_folder2)
88+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2), sub_folder2)
89+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/.'), sub_folder2)
90+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/..'), sub_folder1)
91+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../ddddddd'), sub_folder1)
92+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../subfolder2'), sub_folder2)
93+
self.assertEqual(QgsFileUtils.findClosestExistingPath(sub_folder2 + '/../subfolder2/zxcv/asfdasd'), sub_folder2)
94+
6495

6596
if __name__ == '__main__':
6697
unittest.main()

0 commit comments

Comments
 (0)