1 /* FileUtils.cpp */
2 
3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
4  *
5  * This file is part of sayonara player
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11 
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16 
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "Utils/FileUtils.h"
22 #include "Utils/Utils.h"
23 #include "Utils/Algorithm.h"
24 #include "Utils/Logger/Logger.h"
25 #include "Utils/Language/Language.h"
26 #include "Utils/Set.h"
27 
28 #include <QUrl>
29 #include <QDir>
30 #include <QFile>
31 #include <QFileInfo>
32 #include <QStringList>
33 #include <QCryptographicHash>
34 
35 namespace Algorithm = ::Util::Algorithm;
36 
cleanFilename(const QString & path)37 QString Util::File::cleanFilename(const QString& path)
38 {
39 	return (path.trimmed().isEmpty()) ?
40 	       QString("") :
41 	       QDir::cleanPath(path);
42 }
43 
removeFilesInDirectory(const QString & dirName)44 void Util::File::removeFilesInDirectory(const QString& dirName)
45 {
46 	removeFilesInDirectory(dirName, QStringList());
47 }
48 
removeFilesInDirectory(const QString & dirName,const QStringList & filters)49 void Util::File::removeFilesInDirectory(const QString& dirName, const QStringList& filters)
50 {
51 	if(dirName.contains(".."))
52 	{
53 		return;
54 	}
55 
56 	auto dir = QDir(dirName);
57 	dir.setNameFilters(filters);
58 
59 	const auto fileInfoList = dir.entryInfoList
60 		(
61 			QDir::Filters(QDir::System | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot)
62 		);
63 
64 	for(const auto& fileInfo : fileInfoList)
65 	{
66 		const auto path = fileInfo.absoluteFilePath();
67 
68 		if(fileInfo.isSymLink())
69 		{
70 			QFile::remove(path);
71 		}
72 
73 		else if(fileInfo.isDir())
74 		{
75 			removeFilesInDirectory(path);
76 			QDir().rmdir(path);
77 		}
78 
79 		else if(fileInfo.isFile())
80 		{
81 			QFile::remove(path);
82 		}
83 	}
84 }
85 
deleteFiles(const QStringList & paths)86 void Util::File::deleteFiles(const QStringList& paths)
87 {
88 	if(paths.isEmpty())
89 	{
90 		return;
91 	}
92 
93 	spLog(Log::Develop, "Util::File") << "I will delete " << paths;
94 	auto sortedPaths = paths;
95 	Algorithm::sort(sortedPaths, [](const QString& str1, const QString& str2) {
96 		return (str1.size() > str2.size());
97 	});
98 
99 	for(const auto& path : Algorithm::AsConst(sortedPaths))
100 	{
101 		if(path.contains(".."))
102 		{
103 			continue;
104 		}
105 
106 		const auto info = QFileInfo(path);
107 		if(!info.exists())
108 		{
109 			continue;
110 		}
111 
112 		if(info.isSymLink())
113 		{
114 			QFile::remove(info.absoluteFilePath());
115 		}
116 
117 		else if(info.isDir())
118 		{
119 			removeFilesInDirectory(path);
120 			QDir().rmdir(path);
121 		}
122 
123 		else
124 		{
125 			QFile::remove(path);
126 		}
127 	}
128 }
129 
getParentDirectory(const QString & filename)130 QString Util::File::getParentDirectory(const QString& filename)
131 {
132 	const auto cleaned = cleanFilename(filename);
133 	const auto index = cleaned.lastIndexOf(QDir::separator());
134 
135 	return (index > 0)
136 	       ? cleanFilename(cleaned.left(index))
137 	       : QDir::rootPath();
138 }
139 
getFilenameOfPath(const QString & path)140 QString Util::File::getFilenameOfPath(const QString& path)
141 {
142 	return QDir(cleanFilename(path)).dirName();
143 }
144 
splitFilename(const QString & src,QString & path,QString & filename)145 void Util::File::splitFilename(const QString& src, QString& path, QString& filename)
146 {
147 	path = Util::File::getParentDirectory(src);
148 	filename = Util::File::getFilenameOfPath(src);
149 }
150 
splitFilename(const QString & src)151 std::pair<QString, QString> Util::File::splitFilename(const QString& src)
152 {
153 	std::pair<QString, QString> ret;
154 	splitFilename(src, ret.first, ret.second);
155 
156 	return ret;
157 }
158 
getParentDirectories(const QStringList & files)159 QStringList Util::File::getParentDirectories(const QStringList& files)
160 {
161 	Util::Set<QString> folders;
162 	for(const auto& file : files)
163 	{
164 		const auto folder = getParentDirectory(file);
165 		folders.insert(folder);
166 	}
167 
168 	return QStringList(folders.toList());
169 }
170 
getAbsoluteFilename(const QString & filename)171 QString Util::File::getAbsoluteFilename(const QString& filename)
172 {
173 	return QDir(filename).absolutePath();
174 }
175 
getFilesizeString(Filesize filesize)176 QString Util::File::getFilesizeString(Filesize filesize)
177 {
178 	Filesize kb = 1 << 10;  // 1024
179 	Filesize mb = kb << 10;
180 	Filesize gb = mb << 10;
181 
182 	QString size;
183 	if(filesize > gb)
184 	{
185 		size =
186 			QString::number(filesize / gb) + "." + QString::number((filesize / mb) % gb).left(2) +
187 			" " + Lang::get(Lang::GB);
188 	}
189 
190 	else if(filesize > mb)
191 	{
192 		size =
193 			QString::number(filesize / mb) + "." + QString::number((filesize / kb) % mb).left(2) +
194 			" " + Lang::get(Lang::MB);
195 	}
196 
197 	else
198 	{
199 		size = QString::number(filesize / kb) + " " + +" " + Lang::get(Lang::KB);
200 	}
201 
202 	return size;
203 }
204 
isUrl(const QString & str)205 bool Util::File::isUrl(const QString& str)
206 {
207 	const auto url = QUrl(str);
208 	return (url.isValid() && !url.scheme().isEmpty());
209 }
210 
isWWW(const QString & str)211 bool Util::File::isWWW(const QString& str)
212 {
213 	const auto schemes = QStringList{"http", "https", "ftp", "itpc", "feed"};
214 	const auto url = QUrl(str);
215 	const auto scheme = url.scheme().toLower();
216 
217 	return (url.isValid() && schemes.contains(scheme));
218 }
219 
isDir(const QString & filename)220 bool Util::File::isDir(const QString& filename)
221 {
222 	return QFileInfo(filename).isDir();
223 }
224 
isFile(const QString & filename)225 bool Util::File::isFile(const QString& filename)
226 {
227 	return QFileInfo(filename).isFile();
228 }
229 
isSoundFile(const QString & filename)230 bool Util::File::isSoundFile(const QString& filename)
231 {
232 	const auto extensions = Util::soundfileExtensions(true);
233 	return Algorithm::contains(extensions, [&filename](const auto& extension) {
234 		return (filename.endsWith(extension.rightRef(4), Qt::CaseInsensitive));
235 	});
236 }
237 
isPlaylistFile(const QString & filename)238 bool Util::File::isPlaylistFile(const QString& filename)
239 {
240 	const auto extensions = Util::playlistExtensions(true);
241 	return Algorithm::contains(extensions, [&filename](const auto& extension) {
242 		return (filename.endsWith(extension.rightRef(4), Qt::CaseInsensitive));
243 	});
244 }
245 
isImageFile(const QString & filename)246 bool Util::File::isImageFile(const QString& filename)
247 {
248 	const auto extensions = Util::imageExtensions(true);
249 	return Algorithm::contains(extensions, [&filename](const auto& extension) {
250 		return (filename.endsWith(extension.rightRef(4), Qt::CaseInsensitive));
251 	});
252 }
253 
createDirectories(const QString & path)254 bool Util::File::createDirectories(const QString& path)
255 {
256 	return QDir().mkpath(path);
257 }
258 
isAbsolute(const QString & filename)259 bool Util::File::isAbsolute(const QString& filename)
260 {
261 	return QDir::isAbsolutePath(filename);
262 }
263 
writeFile(const QByteArray & arr,const QString & filename)264 bool Util::File::writeFile(const QByteArray& arr, const QString& filename)
265 {
266 	QFile f(filename);
267 	if(!f.open(QFile::WriteOnly))
268 	{
269 		return false;
270 	}
271 
272 	const auto bytesWritten = f.write(arr);
273 
274 	f.close();
275 
276 	return (bytesWritten >= arr.size());
277 }
278 
readFileIntoByteArray(const QString & filename,QByteArray & content)279 bool Util::File::readFileIntoByteArray(const QString& filename, QByteArray& content)
280 {
281 	QFile file(filename);
282 	content.clear();
283 
284 	if(!file.open(QIODevice::ReadOnly))
285 	{
286 		return false;
287 	}
288 
289 	while(!file.atEnd())
290 	{
291 		const auto arr = file.read(4096);
292 		content.append(arr);
293 	}
294 
295 	file.close();
296 
297 	return (content.size() > 0);
298 }
299 
readFileIntoString(const QString & filename,QString & content)300 bool Util::File::readFileIntoString(const QString& filename, QString& content)
301 {
302 	QFile file(filename);
303 	content.clear();
304 	if(!file.open(QIODevice::ReadOnly))
305 	{
306 		return false;
307 	}
308 
309 	while(!file.atEnd())
310 	{
311 		content.append(QString::fromUtf8(file.readLine()));
312 	}
313 
314 	file.close();
315 
316 	return !content.isEmpty();
317 }
318 
319 // hallo.txta
320 // . = 5
getFileExtension(const QString & filename)321 QString Util::File::getFileExtension(const QString& filename)
322 {
323 	const auto lastDot = filename.lastIndexOf(".");
324 	const auto lastSep = filename.lastIndexOf(QDir::separator());
325 
326 	if(lastDot < 0 && lastDot < lastSep)
327 	{
328 		return QString();
329 	}
330 
331 	const auto ext = filename.mid(lastDot + 1);
332 	return (ext.size() > 4) ? QString() : ext;
333 }
334 
checkFile(const QString & filepath)335 bool Util::File::checkFile(const QString& filepath)
336 {
337 	return (isWWW(filepath) || exists(filepath));
338 }
339 
createSymlink(const QString & source,const QString & target)340 bool Util::File::createSymlink(const QString& source, const QString& target)
341 {
342 	QFileInfo targetInfo(target);
343 	if(targetInfo.exists() && targetInfo.isSymLink())
344 	{
345 		Util::File::deleteFiles({target});
346 	}
347 
348 	QFile f(source);
349 	return f.link(target);
350 }
351 
checkSymLink(const QString & symlinkPath)352 bool Util::File::checkSymLink(const QString& symlinkPath)
353 {
354 	const auto info = QFileInfo(symlinkPath);
355 	if(info.isSymLink())
356 	{
357 		if(Util::File::exists(info.symLinkTarget()))
358 		{
359 			return true;
360 		}
361 
362 		Util::File::deleteFiles({symlinkPath});
363 	}
364 
365 	return false;
366 }
367 
getCommonDirectory(QString dir1,QString dir2)368 QString Util::File::getCommonDirectory(QString dir1, QString dir2)
369 {
370 	while(dir1.compare(dir2) != 0)
371 	{
372 		while(dir1.size() > dir2.size())
373 		{
374 			auto d1 = QDir(dir1);
375 			const auto canUp = d1.cdUp();
376 			if(!canUp)
377 			{
378 				return QDir::rootPath();
379 			}
380 			dir1 = d1.absolutePath();
381 		}
382 
383 		while(dir2.size() > dir1.size())
384 		{
385 			auto d2 = QDir(dir2);
386 			const auto canUp = d2.cdUp();
387 			if(!canUp)
388 			{
389 				return QDir::rootPath();
390 			}
391 
392 			dir2 = d2.absolutePath();
393 		}
394 	}
395 
396 	return dir1;
397 }
398 
getCommonDirectory(const QStringList & paths)399 QString Util::File::getCommonDirectory(const QStringList& paths)
400 {
401 	if(paths.isEmpty())
402 	{
403 		return QDir::rootPath();
404 	}
405 
406 	if(paths.size() == 1)
407 	{
408 		return QDir(paths[0]).absolutePath();
409 	}
410 
411 	QString ret;
412 	QStringList absolutePaths;
413 	for(const auto& path : paths)
414 	{
415 		const auto filename = getAbsoluteFilename(path);
416 		const auto info = QFileInfo(filename);
417 
418 		if(info.isFile() || info.isDir())
419 		{
420 			absolutePaths << QDir(filename).absolutePath();
421 		}
422 
423 		else if(info.isRoot())
424 		{
425 			return QDir::rootPath();
426 		}
427 	}
428 
429 	if(absolutePaths.isEmpty())
430 	{
431 		return QDir::rootPath();
432 	}
433 
434 	if(absolutePaths.size() == 1)
435 	{
436 		return absolutePaths[0];
437 	}
438 
439 	ret = absolutePaths[0];
440 
441 	for(const auto& absolutePath : Algorithm::AsConst(absolutePaths))
442 	{
443 		ret = getCommonDirectory(ret, absolutePath);
444 	}
445 
446 	return ret;
447 }
448 
splitDirectories(const QString & path)449 QStringList Util::File::splitDirectories(const QString& path)
450 {
451 	auto ret = path.split(QDir::separator());
452 	ret.removeAll("");
453 
454 	return ret;
455 }
456 
createDir(const QString & dirName)457 bool Util::File::createDir(const QString& dirName)
458 {
459 	return (QDir(dirName).exists() || QDir().mkdir(dirName));
460 }
461 
copyDir(const QString & sourceDirectory,const QString & targetDirectory,QString & newFilename)462 bool Util::File::copyDir(const QString& sourceDirectory, const QString& targetDirectory,
463                          QString& newFilename)
464 {
465 	newFilename.clear();
466 
467 	if(!canCopyDir(sourceDirectory, targetDirectory))
468 	{
469 		return false;
470 	}
471 
472 	spLog(Log::Debug, "File") << "Copy " << sourceDirectory << " to " << targetDirectory;
473 	spLog(Log::Debug, "File") << "Create dir: " << targetDirectory;
474 	if(!createDir(targetDirectory))
475 	{
476 		return false;
477 	}
478 
479 	const auto src = QDir(sourceDirectory);
480 	const auto copyTo = QDir(targetDirectory).absoluteFilePath(src.dirName());
481 
482 	spLog(Log::Debug, "File") << "Create dir: " << copyTo;
483 	if(!createDir(copyTo))
484 	{
485 		return false;
486 	}
487 
488 	const auto srcInfos = src.entryInfoList(QStringList(),
489 	                                        (QDir::Files | QDir::Dirs |
490 	                                         QDir::Filter::NoDotAndDotDot));
491 	for(const auto& info : srcInfos)
492 	{
493 		if(info.isDir())
494 		{
495 			QString nf;
496 			const auto success = copyDir(info.filePath(), copyTo, nf);
497 			if(!success)
498 			{
499 				return false;
500 			}
501 		}
502 
503 		else if(info.isSymLink())
504 		{
505 			newFilename = info.filePath();
506 			newFilename.remove(sourceDirectory);
507 			newFilename.prepend(copyTo);
508 
509 			const auto target = info.symLinkTarget();
510 			createSymlink(info.symLinkTarget(), newFilename);
511 		}
512 
513 		else
514 		{
515 			const auto oldFilename = info.filePath();
516 
517 			newFilename = oldFilename;
518 			newFilename.remove(sourceDirectory);
519 			newFilename.prepend(copyTo);
520 
521 			auto file = QFile(oldFilename);
522 			spLog(Log::Debug, "File") << "Copy file " << oldFilename << " to " << newFilename;
523 			file.copy(newFilename);
524 		}
525 	}
526 
527 	newFilename = QDir(targetDirectory).filePath(src.dirName());
528 
529 	return true;
530 }
531 
moveDir(const QString & sourceDirectory,const QString & targetDirectory,QString & newFilename)532 bool Util::File::moveDir(const QString& sourceDirectory, const QString& targetDirectory,
533                          QString& newFilename)
534 {
535 	newFilename = QString();
536 
537 	QDir s(sourceDirectory);
538 	QDir t(targetDirectory);
539 
540 	bool success = renameDir(sourceDirectory, t.filePath(s.dirName()));
541 
542 	if(success)
543 	{
544 		newFilename = t.filePath(s.dirName());
545 	}
546 
547 	return success;
548 }
549 
canCopyDir(const QString & srcDir,const QString & targetDirectory)550 bool Util::File::canCopyDir(const QString& srcDir, const QString& targetDirectory)
551 {
552 	if(srcDir.isEmpty())
553 	{
554 		return false;
555 	}
556 
557 	if(targetDirectory.isEmpty())
558 	{
559 		return false;
560 	}
561 
562 	if(QString(targetDirectory + "/").startsWith(srcDir + "/"))
563 	{
564 		return false;
565 	}
566 
567 	if(!exists(srcDir))
568 	{
569 		return false;
570 	}
571 
572 	return true;
573 }
574 
renameDir(const QString & srcDir,const QString & newDir)575 bool Util::File::renameDir(const QString& srcDir, const QString& newDir)
576 {
577 	return QDir().rename(srcDir, newDir);
578 }
579 
getMD5Sum(const QString & filename)580 QByteArray Util::File::getMD5Sum(const QString& filename)
581 {
582 	QFile f(filename);
583 	if(f.open(QFile::ReadOnly))
584 	{
585 		QCryptographicHash hash(QCryptographicHash::Md5);
586 		if(hash.addData(&f))
587 		{
588 			return hash.result().toHex();
589 		}
590 	}
591 
592 	return QByteArray();
593 }
594 
moveFiles(const QStringList & files,const QString & dir,QStringList & newNames)595 bool Util::File::moveFiles(const QStringList& files, const QString& dir, QStringList& newNames)
596 {
597 	bool success = true;
598 	for(const auto& file : files)
599 	{
600 		QString newName;
601 		success = moveFile(file, dir, newName);
602 		if(!success)
603 		{
604 			continue;
605 		}
606 
607 		newNames << newName;
608 	}
609 
610 	return success;
611 }
612 
renameFile(const QString & oldName,const QString & newName)613 bool Util::File::renameFile(const QString& oldName, const QString& newName)
614 {
615 	QFileInfo info(oldName);
616 	if(!info.isFile())
617 	{
618 		return false;
619 	}
620 
621 	return QFile(oldName).rename(newName);
622 }
623 
copyFiles(const QStringList & files,const QString & dir,QStringList & newNames)624 bool Util::File::copyFiles(const QStringList& files, const QString& dir, QStringList& newNames)
625 {
626 	bool success = true;
627 	for(const auto& file : files)
628 	{
629 		QString newName;
630 		success = copyFile(file, dir, newName);
631 		if(!success)
632 		{
633 			continue;
634 		}
635 
636 		newNames << newName;
637 	}
638 
639 	return success;
640 }
641 
moveFile(const QString & file,const QString & dir,QString & newName)642 bool Util::File::moveFile(const QString& file, const QString& dir, QString& newName)
643 {
644 	if(copyFile(file, dir, newName))
645 	{
646 		return QFile(file).remove();
647 	}
648 
649 	return false;
650 }
651 
copyFile(const QString & file,const QString & dir,QString & newName)652 bool Util::File::copyFile(const QString& file, const QString& dir, QString& newName)
653 {
654 	newName.clear();
655 
656 	if(!QFileInfo(dir).isDir())
657 	{
658 		return false;
659 	}
660 
661 	if(!QFileInfo(file).isFile())
662 	{
663 		return false;
664 	}
665 
666 	const auto pureFilename = getFilenameOfPath(file);
667 	newName = QDir(dir).absoluteFilePath(pureFilename);
668 
669 	return QFile(file).copy(newName);
670 }
671 
exists(const QString & filename)672 bool Util::File::exists(const QString& filename)
673 {
674 	return (!filename.isEmpty()) && QFile::exists(filename);
675 }
676 
isSamePath(const QString & filename1,const QString & filename2)677 bool Util::File::isSamePath(const QString& filename1, const QString& filename2)
678 {
679 	const auto cleaned1 = cleanFilename(filename1);
680 	const auto cleaned2 = cleanFilename(filename2);
681 
682 #ifdef Q_OS_UNIX
683 	return (cleaned1.compare(cleaned2) == 0);
684 #else
685 	return (cleaned1.compare(cleaned2, Qt::CaseInsensitive) == 0);
686 #endif
687 }
688 
isSubdir(const QString & dir,const QString & parentDir)689 bool Util::File::isSubdir(const QString& dir, const QString& parentDir)
690 {
691 	if(isSamePath(dir, parentDir))
692 	{
693 		return false;
694 	}
695 
696 	const auto cleanedDir = cleanFilename(dir);
697 	const auto cleanedParentDir = cleanFilename(parentDir);
698 
699 	if(cleanedDir.isEmpty() || cleanedParentDir.isEmpty())
700 	{
701 		return false;
702 	}
703 
704 	const QFileInfo info(cleanedDir);
705 
706 	QDir d1(cleanedDir);
707 	if(info.exists() && info.isFile())
708 	{
709 		const auto d1String = Util::File::getParentDirectory(cleanedDir);
710 		if(isSamePath(d1String, parentDir))
711 		{
712 			return true;
713 		}
714 
715 		d1 = QDir(d1String);
716 
717 	}
718 
719 	const QDir d2(cleanedParentDir);
720 
721 	while(!d1.isRoot())
722 	{
723 		d1 = QDir(Util::File::getParentDirectory(d1.absolutePath()));
724 		if(isSamePath(d1.absolutePath(), d2.absolutePath()))
725 		{
726 			return true;
727 		}
728 	}
729 
730 	return false;
731 }
732 
invalidFilenameChars()733 QList<QChar> Util::File::invalidFilenameChars()
734 {
735 	return
736 		{
737 			'*', '?', '/', '\\'
738 		};
739 }
740