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