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