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