QGIS API Documentation 3.32.0-Lima (311a8cb8a6)
qgsfileutils.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsfileutils.cpp
3 ---------------------
4 begin : November 2017
5 copyright : (C) 2017 by Etienne Trimaille
6 email : etienne.trimaille at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15#include "qgsfileutils.h"
16#include "qgis.h"
17#include "qgsexception.h"
18#include "qgsconfig.h"
19#include "qgsproviderregistry.h"
20#include "qgsprovidermetadata.h"
21
22#include <QObject>
23#include <QRegularExpression>
24#include <QFileInfo>
25#include <QDir>
26#include <QSet>
27#include <QDirIterator>
28
29#ifdef Q_OS_UNIX
30// For getrlimit()
31#include <sys/resource.h>
32#include <sys/time.h>
33#endif
34
35#ifdef _MSC_VER
36#include <Windows.h>
37#include <ShlObj.h>
38#pragma comment(lib,"Shell32.lib")
39#endif
40
41QString QgsFileUtils::representFileSize( qint64 bytes )
42{
43 QStringList list;
44 list << QObject::tr( "KB" ) << QObject::tr( "MB" ) << QObject::tr( "GB" ) << QObject::tr( "TB" );
45
46 QStringListIterator i( list );
47 QString unit = QObject::tr( "B" );
48
49 double fileSize = bytes;
50 while ( fileSize >= 1024.0 && i.hasNext() )
51 {
52 fileSize /= 1024.0;
53 unit = i.next();
54 }
55 return QStringLiteral( "%1 %2" ).arg( QString::number( fileSize, 'f', bytes >= 1048576 ? 2 : 0 ), unit );
56}
57
58QStringList QgsFileUtils::extensionsFromFilter( const QString &filter )
59{
60 const thread_local QRegularExpression rx( QStringLiteral( "\\*\\.([a-zA-Z0-9]+)" ) );
61 QStringList extensions;
62 QRegularExpressionMatchIterator matches = rx.globalMatch( filter );
63
64 while ( matches.hasNext() )
65 {
66 const QRegularExpressionMatch match = matches.next();
67 if ( match.hasMatch() )
68 {
69 QStringList newExtensions = match.capturedTexts();
70 newExtensions.pop_front(); // remove whole match
71 extensions.append( newExtensions );
72 }
73 }
74 return extensions;
75}
76
77QString QgsFileUtils::wildcardsFromFilter( const QString &filter )
78{
79 const thread_local QRegularExpression globPatternsRx( QStringLiteral( ".*\\((.*?)\\)$" ) );
80 const QRegularExpressionMatch matches = globPatternsRx.match( filter );
81 if ( matches.hasMatch() )
82 return matches.captured( 1 );
83 else
84 return QString();
85}
86
87bool QgsFileUtils::fileMatchesFilter( const QString &fileName, const QString &filter )
88{
89 QFileInfo fi( fileName );
90 const QString name = fi.fileName();
91 const QStringList parts = filter.split( QStringLiteral( ";;" ) );
92 for ( const QString &part : parts )
93 {
94#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
95 const QStringList globPatterns = wildcardsFromFilter( part ).split( ' ', QString::SkipEmptyParts );
96#else
97 const QStringList globPatterns = wildcardsFromFilter( part ).split( ' ', Qt::SkipEmptyParts );
98#endif
99 for ( const QString &glob : globPatterns )
100 {
101 const QString re = QRegularExpression::wildcardToRegularExpression( glob );
102
103 const QRegularExpression globRx( re );
104 if ( globRx.match( name ).hasMatch() )
105 return true;
106 }
107 }
108 return false;
109}
110
111QString QgsFileUtils::ensureFileNameHasExtension( const QString &f, const QStringList &extensions )
112{
113 if ( extensions.empty() || f.isEmpty() )
114 return f;
115
116 QString fileName = f;
117 bool hasExt = false;
118 for ( const QString &extension : std::as_const( extensions ) )
119 {
120 const QString extWithDot = extension.startsWith( '.' ) ? extension : '.' + extension;
121 if ( fileName.endsWith( extWithDot, Qt::CaseInsensitive ) )
122 {
123 hasExt = true;
124 break;
125 }
126 }
127
128 if ( !hasExt )
129 {
130 const QString extension = extensions.at( 0 );
131 const QString extWithDot = extension.startsWith( '.' ) ? extension : '.' + extension;
132 fileName += extWithDot;
133 }
134
135 return fileName;
136}
137
138QString QgsFileUtils::addExtensionFromFilter( const QString &fileName, const QString &filter )
139{
140 const QStringList extensions = extensionsFromFilter( filter );
141 return ensureFileNameHasExtension( fileName, extensions );
142}
143
144QString QgsFileUtils::stringToSafeFilename( const QString &string )
145{
146 const thread_local QRegularExpression rx( QStringLiteral( "[/\\\\\\?%\\*\\:\\|\"<>]" ) );
147 QString s = string;
148 s.replace( rx, QStringLiteral( "_" ) );
149 return s;
150}
151
152QString QgsFileUtils::findClosestExistingPath( const QString &path )
153{
154 if ( path.isEmpty() )
155 return QString();
156
157 QDir currentPath;
158 QFileInfo fi( path );
159 if ( fi.isFile() )
160 currentPath = fi.dir();
161 else
162 currentPath = QDir( path );
163
164 QSet< QString > visited;
165 while ( !currentPath.exists() )
166 {
167 const QString parentPath = QDir::cleanPath( currentPath.absolutePath() + QStringLiteral( "/.." ) );
168 if ( visited.contains( parentPath ) )
169 return QString(); // break circular links
170
171 if ( parentPath.isEmpty() || parentPath == QLatin1String( "." ) )
172 return QString();
173 currentPath = QDir( parentPath );
174 visited << parentPath;
175 }
176
177 const QString res = QDir::cleanPath( currentPath.absolutePath() );
178
179 if ( res == QDir::currentPath() )
180 return QString(); // avoid default to binary folder if a filename alone is specified
181
182 return res == QLatin1String( "." ) ? QString() : res;
183}
184
185QStringList QgsFileUtils::findFile( const QString &file, const QString &basePath, int maxClimbs, int searchCeilling, const QString &currentDir )
186{
187 int depth = 0;
188 QString originalFolder;
189 QDir folder;
190 const QString fileName( basePath.isEmpty() ? QFileInfo( file ).fileName() : file );
191 const QString baseFolder( basePath.isEmpty() ? QFileInfo( file ).path() : basePath );
192
193 if ( QFileInfo( baseFolder ).isDir() )
194 {
195 folder = QDir( baseFolder ) ;
196 originalFolder = folder.absolutePath();
197 }
198 else // invalid folder or file path
199 {
200 folder = QDir( QFileInfo( baseFolder ).absolutePath() );
201 originalFolder = folder.absolutePath();
202 }
203
204 QStringList searchedFolder = QStringList();
205 QString existingBase;
206 QString backupDirectory = QDir::currentPath();
207 QStringList foundFiles;
208
209 if ( !currentDir.isEmpty() && backupDirectory != currentDir && QDir( currentDir ).exists() )
210 QDir::setCurrent( currentDir );
211
212 // find the nearest existing folder
213 while ( !folder.exists() && folder.absolutePath().count( '/' ) > searchCeilling )
214 {
215
216 existingBase = folder.path();
217 if ( !folder.cdUp() )
218 folder = QFileInfo( existingBase ).absoluteDir(); // using fileinfo to move up one level
219
220 depth += 1;
221
222 if ( depth > ( maxClimbs + 4 ) ) //break early when no folders can be found
223 break;
224 }
225 bool folderExists = folder.exists();
226
227 if ( depth > maxClimbs )
228 maxClimbs = depth;
229
230 if ( folder.absolutePath().count( '/' ) < searchCeilling )
231 searchCeilling = folder.absolutePath().count( '/' ) - 1;
232
233 while ( depth <= maxClimbs && folderExists && folder.absolutePath().count( '/' ) >= searchCeilling )
234 {
235
236 QDirIterator localFinder( folder.path(), QStringList() << fileName, QDir::Files, QDirIterator::NoIteratorFlags );
237 searchedFolder.append( folder.absolutePath() );
238 if ( localFinder.hasNext() )
239 {
240 foundFiles << localFinder.next();
241 return foundFiles;
242 }
243
244
245 const QFileInfoList subdirs = folder.entryInfoList( QDir::AllDirs );
246 for ( const QFileInfo &subdir : subdirs )
247 {
248 if ( ! searchedFolder.contains( subdir.absolutePath() ) )
249 {
250 QDirIterator subDirFinder( subdir.path(), QStringList() << fileName, QDir::Files, QDirIterator::Subdirectories );
251 if ( subDirFinder.hasNext() )
252 {
253 QString possibleFile = subDirFinder.next();
254 if ( !subDirFinder.hasNext() )
255 {
256 foundFiles << possibleFile;
257 return foundFiles;
258 }
259
260 foundFiles << possibleFile;
261 while ( subDirFinder.hasNext() )
262 {
263 foundFiles << subDirFinder.next();
264 }
265 return foundFiles;
266 }
267 }
268 }
269 depth += 1;
270
271 if ( depth > maxClimbs )
272 break;
273
274 folderExists = folder.cdUp();
275 }
276
277 if ( QDir::currentPath() == currentDir && currentDir != backupDirectory )
278 QDir::setCurrent( backupDirectory );
279
280 return foundFiles;
281}
282
283#ifdef _MSC_VER
284std::unique_ptr< wchar_t[] > pathToWChar( const QString &path )
285{
286 const QString nativePath = QDir::toNativeSeparators( path );
287
288 std::unique_ptr< wchar_t[] > pathArray( new wchar_t[static_cast< uint>( nativePath.length() + 1 )] );
289 nativePath.toWCharArray( pathArray.get() );
290 pathArray[static_cast< size_t >( nativePath.length() )] = 0;
291 return pathArray;
292}
293
294
295void fileAttributesOld( HANDLE handle, DWORD &fileAttributes, bool &hasFileAttributes )
296{
297 hasFileAttributes = false;
298 BY_HANDLE_FILE_INFORMATION info;
299 if ( GetFileInformationByHandle( handle, &info ) )
300 {
301 hasFileAttributes = true;
302 fileAttributes = info.dwFileAttributes;
303 }
304}
305
306// File attributes for Windows starting from version 8.
307void fileAttributesNew( HANDLE handle, DWORD &fileAttributes, bool &hasFileAttributes )
308{
309 hasFileAttributes = false;
310#if WINVER >= 0x0602
311 _FILE_BASIC_INFO infoEx;
312 if ( GetFileInformationByHandleEx(
313 handle,
314 FileBasicInfo,
315 &infoEx, sizeof( infoEx ) ) )
316 {
317 hasFileAttributes = true;
318 fileAttributes = infoEx.FileAttributes;
319 }
320 else
321 {
322 // GetFileInformationByHandleEx() is observed to fail for FAT32, QTBUG-74759
323 fileAttributesOld( handle, fileAttributes, hasFileAttributes );
324 }
325#else
326 fileAttributesOld( handle, fileAttributes, hasFileAttributes );
327#endif
328}
329
330bool pathIsLikelyCloudStorage( QString path )
331{
332 // For OneDrive detection need the attributes of a file from the path, not the directory itself.
333 // So just grab the first file in the path.
334 QDirIterator dirIt( path, QDir::Files );
335 if ( dirIt.hasNext() )
336 {
337 path = dirIt.next();
338 }
339
340 std::unique_ptr< wchar_t[] > pathArray = pathToWChar( path );
341 const HANDLE handle = CreateFileW( pathArray.get(), 0, FILE_SHARE_READ,
342 nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr );
343 if ( handle != INVALID_HANDLE_VALUE )
344 {
345 bool hasFileAttributes = false;
346 DWORD attributes = 0;
347 fileAttributesNew( handle, attributes, hasFileAttributes );
348 CloseHandle( handle );
349 if ( hasFileAttributes )
350 {
351 /* From the Win32 API documentation:
352 *
353 * FILE_ATTRIBUTE_RECALL_ON_OPEN:
354 * When this attribute is set, it means that the file or directory has no physical representation
355 * on the local system; the item is virtual. Opening the item will be more expensive than normal,
356 * e.g. it will cause at least some of it to be fetched from a remote store
357 *
358 * FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS
359 * When this attribute is set, it means that the file or directory is not fully present locally.
360 * For a file that means that not all of its data is on local storage (e.g. it may be sparse with
361 * some data still in remote storage).
362 */
363 return ( attributes & FILE_ATTRIBUTE_RECALL_ON_OPEN )
364 || ( attributes & FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS );
365 }
366 }
367 return false;
368}
369#endif
370
372{
373#ifdef _MSC_VER
374 auto pathType = [ = ]( const QString & path ) -> Qgis::DriveType
375 {
376 std::unique_ptr< wchar_t[] > pathArray = pathToWChar( path );
377 const UINT type = GetDriveTypeW( pathArray.get() );
378 switch ( type )
379 {
380 case DRIVE_UNKNOWN:
382
383 case DRIVE_NO_ROOT_DIR:
385
386 case DRIVE_REMOVABLE:
388
389 case DRIVE_FIXED:
391
392 case DRIVE_REMOTE:
394
395 case DRIVE_CDROM:
397
398 case DRIVE_RAMDISK:
400 }
401
403
404 };
405
406 const QString originalPath = QDir::cleanPath( path );
407 QString currentPath = originalPath;
408 QString prevPath;
409 while ( currentPath != prevPath )
410 {
411 if ( pathIsLikelyCloudStorage( currentPath ) )
413
414 prevPath = currentPath;
415 currentPath = QFileInfo( currentPath ).path();
416
417 const Qgis::DriveType type = pathType( currentPath );
418 if ( type != Qgis::DriveType::Unknown && type != Qgis::DriveType::Invalid )
419 return type;
420 }
422
423#else
424 ( void )path;
425 throw QgsNotSupportedException( QStringLiteral( "Determining drive type is not supported on this platform" ) );
426#endif
427}
428
429bool QgsFileUtils::pathIsSlowDevice( const QString &path )
430{
431#ifdef ENABLE_TESTS
432 if ( path.contains( QLatin1String( "fake_slow_path_for_unit_tests" ) ) )
433 return true;
434#endif
435
436 try
437 {
438 const Qgis::DriveType type = driveType( path );
439 switch ( type )
440 {
445 return false;
446
451 return true;
452 }
453 }
454 catch ( QgsNotSupportedException & )
455 {
456
457 }
458 return false;
459}
460
461QSet<QString> QgsFileUtils::sidecarFilesForPath( const QString &path )
462{
463 QSet< QString > res;
464 const QStringList providers = QgsProviderRegistry::instance()->providerList();
465 for ( const QString &provider : providers )
466 {
469 {
470 const QStringList possibleSidecars = metadata->sidecarFilesForUri( path );
471 for ( const QString &possibleSidecar : possibleSidecars )
472 {
473 if ( QFile::exists( possibleSidecar ) )
474 res.insert( possibleSidecar );
475 }
476 }
477 }
478 return res;
479}
480
481bool QgsFileUtils::renameDataset( const QString &oldPath, const QString &newPath, QString &error, Qgis::FileOperationFlags flags )
482{
483 if ( !QFile::exists( oldPath ) )
484 {
485 error = QObject::tr( "File does not exist" );
486 return false;
487 }
488
489 const QFileInfo oldPathInfo( oldPath );
490 QSet< QString > sidecars = sidecarFilesForPath( oldPath );
492 {
493 const QString qmdPath = oldPathInfo.dir().filePath( oldPathInfo.completeBaseName() + QStringLiteral( ".qmd" ) );
494 if ( QFile::exists( qmdPath ) )
495 sidecars.insert( qmdPath );
496 }
498 {
499 const QString qmlPath = oldPathInfo.dir().filePath( oldPathInfo.completeBaseName() + QStringLiteral( ".qml" ) );
500 if ( QFile::exists( qmlPath ) )
501 sidecars.insert( qmlPath );
502 }
503
504 const QFileInfo newPathInfo( newPath );
505
506 bool res = true;
507 QStringList errors;
508 errors.reserve( sidecars.size() );
509 // first check if all sidecars CAN be renamed -- we don't want to get partly through the rename and then find a clash
510 for ( const QString &sidecar : std::as_const( sidecars ) )
511 {
512 const QFileInfo sidecarInfo( sidecar );
513 const QString newSidecarName = newPathInfo.dir().filePath( newPathInfo.completeBaseName() + '.' + sidecarInfo.suffix() );
514 if ( newSidecarName != sidecar && QFile::exists( newSidecarName ) )
515 {
516 res = false;
517 errors.append( QDir::toNativeSeparators( newSidecarName ) );
518 }
519 }
520 if ( !res )
521 {
522 error = QObject::tr( "Destination files already exist %1" ).arg( errors.join( QLatin1String( ", " ) ) );
523 return false;
524 }
525
526 if ( !QFile::rename( oldPath, newPath ) )
527 {
528 error = QObject::tr( "Could not rename %1" ).arg( QDir::toNativeSeparators( oldPath ) );
529 return false;
530 }
531
532 for ( const QString &sidecar : std::as_const( sidecars ) )
533 {
534 const QFileInfo sidecarInfo( sidecar );
535 const QString newSidecarName = newPathInfo.dir().filePath( newPathInfo.completeBaseName() + '.' + sidecarInfo.suffix() );
536 if ( newSidecarName == sidecar )
537 continue;
538
539 if ( !QFile::rename( sidecar, newSidecarName ) )
540 {
541 errors.append( QDir::toNativeSeparators( sidecar ) );
542 res = false;
543 }
544 }
545 if ( !res )
546 {
547 error = QObject::tr( "Could not rename %1" ).arg( errors.join( QLatin1String( ", " ) ) );
548 }
549
550 return res;
551}
552
554{
555#ifdef Q_OS_UNIX
556 struct rlimit rescLimit;
557 if ( getrlimit( RLIMIT_NOFILE, &rescLimit ) == 0 )
558 {
559 return rescLimit.rlim_cur;
560 }
561#endif
562 return -1;
563}
564
566{
567#ifdef Q_OS_LINUX
568 int res = static_cast<int>( QDir( "/proc/self/fd" ).entryList().size() );
569 if ( res == 0 )
570 res = -1;
571 return res;
572#else
573 return -1;
574#endif
575}
576
578{
579 const int nFileLimit = QgsFileUtils::openedFileLimit();
580 const int nFileCount = QgsFileUtils::openedFileCount();
581 // We need some margin as Qt will crash if it cannot create some file descriptors
582 constexpr int SOME_MARGIN = 20;
583 return nFileCount > 0 && nFileLimit > 0 && nFileCount + filesToBeOpened > nFileLimit - SOME_MARGIN;
584}
585
586QStringList QgsFileUtils::splitPathToComponents( const QString &input )
587{
588 QStringList result;
589 QString path = QDir::cleanPath( input );
590 if ( path.isEmpty() )
591 return result;
592
593 const QString fileName = QFileInfo( path ).fileName();
594 if ( !fileName.isEmpty() )
595 result << fileName;
596 else if ( QFileInfo( path ).path() == path )
597 result << path;
598
599 QString prevPath = path;
600 while ( ( path = QFileInfo( path ).path() ).length() < prevPath.length() )
601 {
602 const QString dirName = QDir( path ).dirName();
603 if ( dirName == QLatin1String( "." ) )
604 break;
605
606 result << ( !dirName.isEmpty() ? dirName : path );
607 prevPath = path;
608 }
609
610 std::reverse( result.begin(), result.end() );
611 return result;
612}
613
614QString QgsFileUtils::uniquePath( const QString &path )
615{
616 if ( ! QFileInfo::exists( path ) )
617 {
618 return path;
619 }
620
621 QFileInfo info { path };
622 const QString suffix { info.completeSuffix() };
623 const QString pathPattern { QString( suffix.isEmpty() ? path : path.chopped( suffix.length() + 1 ) ).append( suffix.isEmpty() ? QStringLiteral( "_%1" ) : QStringLiteral( "_%1." ) ).append( suffix ) };
624 int i { 2 };
625 QString uniquePath { pathPattern.arg( i ) };
626 while ( QFileInfo::exists( uniquePath ) )
627 {
628 ++i;
629 uniquePath = pathPattern.arg( i );
630 }
631 return uniquePath;
632}
DriveType
Drive types.
Definition: qgis.h:726
@ Fixed
Fixed drive.
@ Invalid
Invalid path.
@ Unknown
Unknown type.
@ RamDisk
RAM disk.
@ Cloud
Cloud storage – files may be remote or locally stored, depending on user configuration.
@ Removable
Removable drive.
@ Remote
Remote drive.
@ IncludeMetadataFile
Indicates that any associated .qmd metadata file should be included with the operation.
@ IncludeStyleFile
Indicates that any associated .qml styling file should be included with the operation.
static QString uniquePath(const QString &path)
Creates a unique file path name from a desired path by appending "_<n>" (where "<n>" is an integer nu...
static QString stringToSafeFilename(const QString &string)
Converts a string to a safe filename, replacing characters which are not safe for filenames with an '...
static QStringList findFile(const QString &file, const QString &basepath=QString(), int maxClimbs=4, int searchCeiling=4, const QString &currentDir=QString())
Will check basepath in an outward spiral up to maxClimbs levels to check if file exists.
static int openedFileCount()
Returns the number of currently opened files by the process.
static QString wildcardsFromFilter(const QString &filter)
Given a filter string like "GeoTIFF Files (*.tiff *.tif)", extracts the wildcard portion of this filt...
static bool renameDataset(const QString &oldPath, const QString &newPath, QString &error, Qgis::FileOperationFlags flags=Qgis::FileOperationFlag::IncludeMetadataFile|Qgis::FileOperationFlag::IncludeStyleFile)
Renames the dataset at oldPath to newPath, renaming both the file at oldPath and all associated sidec...
static QSet< QString > sidecarFilesForPath(const QString &path)
Returns a list of the sidecar files which exist for the dataset a the specified path.
static bool pathIsSlowDevice(const QString &path)
Returns true if the specified path is assumed to reside on a slow device, e.g.
static bool isCloseToLimitOfOpenedFiles(int filesToBeOpened=1)
Returns whether when opening new file(s) will reach, or nearly reach, the limit of simultaneously ope...
static bool fileMatchesFilter(const QString &fileName, const QString &filter)
Returns true if the given fileName matches a file filter string.
static Qgis::DriveType driveType(const QString &path) SIP_THROW(QgsNotSupportedException)
Returns the drive type for the given path.
static QString ensureFileNameHasExtension(const QString &fileName, const QStringList &extensions)
Ensures that a fileName ends with an extension from the provided list of extensions.
static QString representFileSize(qint64 bytes)
Returns the human size from bytes.
static QString addExtensionFromFilter(const QString &fileName, const QString &filter)
Ensures that a fileName ends with an extension from the specified filter string.
static int openedFileLimit()
Returns the limit of simultaneously opened files by the process.
static QStringList splitPathToComponents(const QString &path)
Given a file path, returns a list of all the components leading to that path.
static QString findClosestExistingPath(const QString &path)
Returns the top-most existing folder from path.
static QStringList extensionsFromFilter(const QString &filter)
Returns a list of the extensions contained within a file filter string.
Custom exception class which is raised when an operation is not supported.
Definition: qgsexception.h:119
Holds data provider key, description, and associated shared library file or function pointer informat...
virtual QgsProviderMetadata::ProviderCapabilities providerCapabilities() const
Returns the provider's capabilities.
@ FileBasedUris
Indicates that the provider can utilize URIs which are based on paths to files (as opposed to databas...
virtual QStringList sidecarFilesForUri(const QString &uri) const
Given a uri, returns any sidecar files which are associated with the URI and this provider.
static QgsProviderRegistry * instance(const QString &pluginPath=QString())
Means of accessing canonical single instance.
QStringList providerList() const
Returns list of available providers by their keys.
QgsProviderMetadata * providerMetadata(const QString &providerKey) const
Returns metadata of the provider or nullptr if not found.