Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "filesystem.h"
23 :
24 : #include <QDir>
25 : #include <QDirIterator>
26 : #include <QFileInfo>
27 : #include <QStorageInfo>
28 : #include <QDirListing>
29 :
30 : #ifdef Q_OS_WIN
31 : #include <windows.h>
32 : #endif
33 :
34 : #include "../ioretcodes.h"
35 : #include "log.h"
36 :
37 : using namespace muse;
38 : using namespace muse::io;
39 :
40 92 : FileSystem::~FileSystem()
41 : {
42 46 : std::lock_guard<std::mutex> lock(m_openStreamsMutex);
43 46 : for (auto& [id, file] : m_openStreams) {
44 0 : file->close();
45 : }
46 46 : m_openStreams.clear();
47 92 : }
48 :
49 15589 : Ret FileSystem::exists(const io::path_t& path) const
50 : {
51 15589 : QFileInfo fileInfo(path.toQString());
52 15589 : if (!fileInfo.exists()) {
53 2594 : return make_ret(Err::FSNotExist);
54 : }
55 :
56 12995 : return make_ret(Err::NoError);
57 15589 : }
58 :
59 1191 : Ret FileSystem::remove(const io::path_t& path_, bool onlyIfEmpty)
60 : {
61 1191 : QString path = path_.toQString();
62 1191 : QFileInfo fileInfo(path);
63 1191 : if (fileInfo.exists()) {
64 65 : return fileInfo.isDir() ? removeDir(path, onlyIfEmpty) : removeFile(path);
65 : }
66 :
67 1126 : return make_ret(Err::NoError);
68 1191 : }
69 :
70 4 : Ret FileSystem::clear(const io::path_t& path)
71 : {
72 4 : const QString qPath = path.toQString();
73 4 : if (!QFileInfo::exists(qPath)) {
74 0 : return true;
75 : }
76 :
77 : using ItFlag = QDirListing::IteratorFlag;
78 8 : for (const auto& dirEntry : QDirListing{ qPath, ItFlag::IncludeHidden }) {
79 2 : if (dirEntry.isDir() && !dirEntry.isSymLink()) {
80 0 : const Ret didRemove = removeDir(dirEntry.filePath());
81 0 : if (!didRemove) {
82 0 : return didRemove;
83 : }
84 :
85 0 : continue;
86 0 : }
87 :
88 2 : const Ret didRemove = removeFile(dirEntry.filePath());
89 2 : if (!didRemove) {
90 0 : return didRemove;
91 : }
92 6 : }
93 :
94 4 : return make_ok();
95 4 : }
96 :
97 0 : Ret FileSystem::copy(const io::path_t& src, const io::path_t& dst, bool replace)
98 : {
99 0 : QFileInfo srcFileInfo(src.toQString());
100 0 : if (!srcFileInfo.exists()) {
101 0 : return make_ret(Err::FSNotExist);
102 : }
103 :
104 0 : QFileInfo dstFileInfo(dst.toQString());
105 0 : if (dstFileInfo.exists()) {
106 0 : if (!replace) {
107 0 : return make_ret(Err::FSAlreadyExists);
108 : }
109 :
110 0 : Ret ret = remove(dst);
111 0 : if (!ret) {
112 0 : return ret;
113 : }
114 0 : }
115 :
116 0 : Ret ret = copyRecursively(src, dst);
117 0 : return ret;
118 0 : }
119 :
120 0 : Ret FileSystem::move(const io::path_t& src, const io::path_t& dst, bool replace)
121 : {
122 0 : QFileInfo srcFileInfo(src.toQString());
123 0 : if (!srcFileInfo.exists()) {
124 0 : return make_ret(Err::FSNotExist);
125 : }
126 :
127 0 : QFileInfo dstFileInfo(dst.toQString());
128 0 : if (dstFileInfo.exists()) {
129 0 : if (!replace) {
130 0 : return make_ret(Err::FSAlreadyExists);
131 : }
132 :
133 0 : Ret ret = remove(dst);
134 0 : if (!ret) {
135 0 : return ret;
136 : }
137 0 : }
138 :
139 0 : if (srcFileInfo.isDir()) {
140 0 : if (!QDir().rename(src.toQString(), dst.toQString())) {
141 0 : return make_ret(Err::FSMoveErrors);
142 : }
143 : } else {
144 0 : if (!QFile::rename(src.toQString(), dst.toQString())) {
145 0 : return make_ret(Err::FSMoveErrors);
146 : }
147 : }
148 :
149 0 : return make_ret(Ret::Code::Ok);
150 0 : }
151 :
152 3094 : RetVal<ByteArray> FileSystem::readFile(const io::path_t& filePath) const
153 : {
154 3094 : RetVal<ByteArray> result;
155 3094 : Ret ret = exists(filePath);
156 3094 : if (!ret) {
157 1 : result.ret = ret;
158 1 : return result;
159 : }
160 :
161 3093 : QFile file(filePath.toQString());
162 3093 : if (!file.open(QIODevice::ReadOnly)) {
163 0 : result.ret = make_ret(Err::FSReadError);
164 0 : return result;
165 : }
166 :
167 3093 : qint64 size = file.size();
168 3093 : result.val.resize(static_cast<size_t>(size));
169 :
170 3093 : file.read(reinterpret_cast<char*>(result.val.data()), size);
171 3093 : file.close();
172 :
173 3093 : result.ret = make_ret(Err::NoError);
174 3093 : return result;
175 3094 : }
176 :
177 4799 : Ret FileSystem::readFile(const io::path_t& filePath, ByteArray& data) const
178 : {
179 4799 : Ret ret = muse::make_ok();
180 :
181 4799 : QFile file(filePath.toQString());
182 4799 : if (!file.open(QIODevice::ReadOnly)) {
183 1 : ret = make_ret(Err::FSReadError);
184 1 : ret.setText(file.errorString().toStdString());
185 1 : return ret;
186 : }
187 :
188 4798 : qint64 size = file.size();
189 4798 : data.resize(static_cast<size_t>(size));
190 :
191 4798 : if (file.read(reinterpret_cast<char*>(data.data()), size) == -1) {
192 0 : ret = make_ret(Err::FSReadError);
193 0 : ret.setText(file.errorString().toStdString());
194 : }
195 :
196 4798 : file.close();
197 :
198 4798 : return ret;
199 4799 : }
200 :
201 3508 : Ret FileSystem::writeFile(const io::path_t& filePath, const ByteArray& data)
202 : {
203 3508 : Ret ret = muse::make_ok();
204 :
205 3508 : QFile file(filePath.toQString());
206 3508 : if (!file.open(QIODevice::WriteOnly)) {
207 0 : ret = make_ret(Err::FSWriteError);
208 0 : ret.setText(file.errorString().toStdString());
209 0 : return ret;
210 : }
211 :
212 3508 : const auto numBytesToWrite = static_cast<qint64>(data.size());
213 3508 : const qint64 numBytesWritten = file.write(data.constChar(), numBytesToWrite);
214 3508 : if (numBytesWritten == -1) {
215 0 : ret = make_ret(Err::FSWriteError);
216 0 : ret.setText(file.errorString().toStdString());
217 0 : return ret;
218 : }
219 :
220 3508 : if (numBytesWritten != numBytesToWrite) {
221 0 : ret = make_ret(Err::FSWriteError);
222 0 : ret.setText("Failed to write entire file");
223 0 : return ret;
224 : }
225 :
226 3508 : file.close();
227 :
228 3508 : return ret;
229 3508 : }
230 :
231 67 : Ret FileSystem::makePath(const io::path_t& path) const
232 : {
233 134 : if (!QDir().mkpath(path.toQString())) {
234 0 : return make_ret(Err::FSMakingError);
235 : }
236 :
237 67 : return make_ret(Err::NoError);
238 : }
239 :
240 0 : Ret FileSystem::makeLink(const io::path_t& targetPath, const path_t& linkPath) const
241 : {
242 : // On Windows, if targetPath points to a directory rather than a file, the
243 : // path must be normalized otherwise the shortcut file won't work. On other
244 : // platforms, normalization makes symlink resolution slightly faster.
245 0 : QString target = QDir::cleanPath(targetPath.toQString());
246 0 : QString link = linkPath.toQString();
247 :
248 : #if defined(Q_OS_WIN)
249 : // Also required to make the shortcut file valid...
250 : target = QDir::toNativeSeparators(target);
251 : link += ".lnk";
252 : #endif
253 :
254 0 : return QFile().link(target, link) ? make_ret(Err::NoError) : make_ret(Err::FSMakingError);
255 0 : }
256 :
257 5 : EntryType FileSystem::entryType(const io::path_t& path) const
258 : {
259 5 : QFileInfo fi(path.toQString());
260 5 : if (fi.isFile()) {
261 0 : return EntryType::File;
262 5 : } else if (fi.isDir()) {
263 5 : return EntryType::Dir;
264 : }
265 :
266 0 : return EntryType::Undefined;
267 5 : }
268 :
269 3 : RetVal<uint64_t> FileSystem::fileSize(const io::path_t& path) const
270 : {
271 3 : RetVal<uint64_t> rv;
272 3 : rv.ret = exists(path);
273 3 : if (!rv.ret) {
274 1 : return rv;
275 : }
276 :
277 2 : QFileInfo fi(path.toQString());
278 2 : rv.val = static_cast<uint64_t>(fi.size());
279 2 : return rv;
280 2 : }
281 :
282 0 : RetVal<uint64_t> FileSystem::availableSpace(const io::path_t& path) const
283 : {
284 0 : QStorageInfo storage(path.toQString());
285 0 : if (!storage.isValid() || !storage.isReady()) {
286 0 : return RetVal<uint64_t>(make_ret(Err::FSNotExist));
287 : }
288 :
289 0 : return RetVal<uint64_t>::make_ok(static_cast<uint64_t>(storage.bytesAvailable()));
290 0 : }
291 :
292 158 : RetVal<io::paths_t> FileSystem::scanFiles(const io::path_t& rootDir, const std::vector<std::string>& nameFilters, ScanMode mode) const
293 : {
294 158 : RetVal<io::paths_t> result;
295 158 : Ret ret = exists(rootDir);
296 158 : if (!ret) {
297 0 : result.ret = ret;
298 0 : return result;
299 : }
300 :
301 158 : QDirIterator::IteratorFlags flags = QDirIterator::NoIteratorFlags;
302 158 : QDir::Filters filters = QDir::NoDotAndDotDot | QDir::Readable;
303 :
304 158 : switch (mode) {
305 48 : case ScanMode::FilesInCurrentDir:
306 48 : filters |= QDir::Files;
307 48 : break;
308 0 : case ScanMode::FoldersInCurrentDir:
309 0 : filters |= QDir::Dirs;
310 0 : break;
311 0 : case ScanMode::FilesAndFoldersInCurrentDir:
312 0 : filters |= QDir::Files | QDir::Dirs;
313 0 : break;
314 110 : case ScanMode::FilesInCurrentDirAndSubdirs:
315 110 : flags |= QDirIterator::Subdirectories;
316 110 : filters |= QDir::Files;
317 110 : break;
318 : }
319 :
320 158 : QStringList qnameFilters;
321 216 : for (const std::string& f : nameFilters) {
322 58 : qnameFilters << QString::fromStdString(f);
323 : }
324 :
325 158 : QDirIterator it(rootDir.toQString(), qnameFilters, filters, flags);
326 :
327 736 : while (it.hasNext()) {
328 578 : result.val.push_back(it.next());
329 : }
330 :
331 158 : result.ret = make_ret(Err::NoError);
332 158 : return result;
333 158 : }
334 :
335 59 : Ret FileSystem::removeFile(const io::path_t& path)
336 : {
337 59 : QFile file(path.toQString());
338 59 : if (!file.remove()) {
339 : // Read-only files prevent directory deletion on Windows, retry with Write permission.
340 0 : LOGW() << "Failed to delete file. Setting write permission and trying again...";
341 0 : if (!file.setPermissions(file.permissions() | QFile::WriteUser)) {
342 0 : LOGE() << "Failed to set write permission for file";
343 0 : return make_ret(Err::FSRemoveError);
344 : }
345 0 : if (!file.remove()) {
346 0 : LOGE() << "Second delete attempt failed";
347 0 : return make_ret(Err::FSRemoveError);
348 : }
349 :
350 0 : return make_ok();
351 : }
352 :
353 59 : return make_ok();
354 59 : }
355 :
356 8 : Ret FileSystem::removeDir(const io::path_t& path, const bool onlyIfEmpty)
357 : {
358 8 : const QString qPath = path.toQString();
359 8 : QDir dir(qPath);
360 :
361 8 : if (!onlyIfEmpty) {
362 : // clear directory for deletion
363 4 : const Ret didClear = clear(path);
364 4 : if (!didClear) {
365 0 : LOGE() << "Failed to clear directory for deletion";
366 :
367 0 : return didClear;
368 : }
369 4 : }
370 :
371 8 : if (!dir.isEmpty()) {
372 1 : return make_ret(Err::FSDirNotEmptyError);
373 : }
374 :
375 7 : const QString dirName = dir.dirName();
376 7 : if (!dir.cdUp()) {
377 0 : return make_ret(Err::FSNotExist);
378 : }
379 :
380 7 : if (!dir.rmdir(dirName)) {
381 : // OneDrive removes delete permission on Windows. try again with write permission
382 0 : LOGW() << "Failed to delete directory. Setting write permission and trying again...";
383 0 : if (!QFile::setPermissions(qPath, QFile::permissions(qPath) | QFile::WriteUser)) {
384 0 : LOGE() << "Failed to set write permission for directory";
385 0 : return make_ret(Err::FSRemoveError);
386 : }
387 0 : if (!dir.rmdir(dirName)) {
388 0 : LOGE() << "Second delete attempt failed";
389 0 : return make_ret(Err::FSRemoveError);
390 : }
391 :
392 0 : return make_ok();
393 : }
394 :
395 7 : return make_ok();
396 8 : }
397 :
398 0 : Ret FileSystem::copyRecursively(const io::path_t& src, const io::path_t& dst) const
399 : {
400 0 : QString srcPath = src.toQString();
401 0 : QString dstPath = dst.toQString();
402 :
403 0 : QFileInfo srcFileInfo(srcPath);
404 0 : if (srcFileInfo.isDir()) {
405 0 : QDir dstDir(dstPath);
406 0 : dstDir.cdUp();
407 0 : if (!dstDir.mkdir(QFileInfo(dstPath).fileName())) {
408 0 : return make_ret(Err::FSMakingError);
409 : }
410 0 : QDir srcDir(srcPath);
411 0 : const QStringList fileNames = srcDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
412 0 : for (const QString& fileName : fileNames) {
413 0 : const QString newSrcPath = srcPath + QLatin1Char('/') + fileName;
414 0 : const QString newDstPath = dstPath + QLatin1Char('/') + fileName;
415 0 : Ret ret = copyRecursively(newSrcPath, newDstPath);
416 0 : if (!ret) {
417 0 : return ret;
418 : }
419 0 : }
420 0 : } else {
421 0 : if (!QFile::copy(srcPath, dstPath)) {
422 0 : return make_ret(Err::FSCopyError);
423 : }
424 : }
425 :
426 0 : return make_ret(Err::NoError);
427 0 : }
428 :
429 0 : void FileSystem::setAttribute(const io::path_t& path, Attribute attribute) const
430 : {
431 0 : switch (attribute) {
432 0 : case Attribute::Hidden: {
433 : #ifdef Q_OS_WIN
434 : const QString nativePath = QDir::toNativeSeparators(path.toQString());
435 : SetFileAttributes((LPCTSTR)nativePath.unicode(), FILE_ATTRIBUTE_HIDDEN);
436 : #endif
437 0 : } break;
438 : }
439 : UNUSED(path);
440 0 : }
441 :
442 0 : bool FileSystem::setPermissionsAllowedForAll(const io::path_t& path) const
443 : {
444 0 : return QFile::setPermissions(path.toQString(),
445 0 : QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner
446 0 : | QFile::ReadUser | QFile::WriteUser | QFile::ExeUser
447 0 : | QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup
448 0 : | QFile::ReadOther | QFile::WriteOther
449 0 : | QFile::ExeOther);
450 : }
451 :
452 0 : io::path_t FileSystem::temporaryDirectoryPath() const
453 : {
454 0 : return QDir::tempPath();
455 : }
456 :
457 15 : io::path_t FileSystem::canonicalFilePath(const io::path_t& filePath) const
458 : {
459 15 : return QFileInfo(filePath.toQString()).canonicalFilePath();
460 : }
461 :
462 0 : io::path_t FileSystem::absolutePath(const io::path_t& filePath) const
463 : {
464 0 : return QFileInfo(filePath.toQString()).absolutePath();
465 : }
466 :
467 1042 : path_t FileSystem::absoluteFilePath(const path_t& filePath) const
468 : {
469 1042 : return QFileInfo(filePath.toQString()).absoluteFilePath();
470 : }
471 :
472 0 : io::path_t FileSystem::cleanPath(const io::path_t& filePath) const
473 : {
474 0 : return QDir::cleanPath(filePath.toQString());
475 : }
476 :
477 0 : bool FileSystem::isSymLink(const io::path_t& filePath) const
478 : {
479 0 : return QFileInfo(filePath.toQString()).isSymLink();
480 : }
481 :
482 0 : io::path_t FileSystem::symLinkTarget(const io::path_t& filePath) const
483 : {
484 0 : return QFileInfo(filePath.toQString()).symLinkTarget();
485 : }
486 :
487 0 : DateTime FileSystem::birthTime(const io::path_t& filePath) const
488 : {
489 0 : return DateTime::fromQDateTime(QFileInfo(filePath.toQString()).birthTime());
490 : }
491 :
492 33 : DateTime FileSystem::lastModified(const io::path_t& filePath) const
493 : {
494 33 : return DateTime::fromQDateTime(QFileInfo(filePath.toQString()).lastModified());
495 : }
496 :
497 26 : RetVal<StreamId> FileSystem::openStream(const io::path_t& filePath, OpenMode mode)
498 : {
499 26 : RetVal<StreamId> result;
500 :
501 26 : auto file = std::make_unique<QFile>(filePath.toQString());
502 :
503 26 : QIODevice::OpenMode qmode = (mode == OpenMode::Append) ? QIODevice::Append : QIODevice::WriteOnly;
504 :
505 26 : if (!file->open(qmode)) {
506 1 : result.ret = make_ret(Err::FSWriteError);
507 1 : result.ret.setText(file->errorString().toStdString());
508 1 : return result;
509 : }
510 :
511 25 : std::lock_guard<std::mutex> lock(m_openStreamsMutex);
512 25 : StreamId id = m_nextStreamId++;
513 25 : m_openStreams.emplace(id, std::move(file));
514 :
515 25 : result.ret = make_ret(Err::NoError);
516 25 : result.val = id;
517 25 : return result;
518 26 : }
519 :
520 23 : Ret FileSystem::writeToStream(StreamId fileId, const ByteArray& data, uint64_t offset)
521 : {
522 23 : std::lock_guard<std::mutex> lock(m_openStreamsMutex);
523 23 : auto it = m_openStreams.find(fileId);
524 23 : if (it == m_openStreams.end()) {
525 0 : return make_ret(Err::FSWriteError);
526 : }
527 :
528 23 : QFile* file = it->second.get();
529 23 : IF_ASSERT_FAILED(file) {
530 0 : return make_ret(Err::FSWriteError);
531 : }
532 :
533 23 : if (offset != STREAM_POS_CURRENT) {
534 0 : if (!file->seek(static_cast<qint64>(offset))) {
535 0 : Ret ret = make_ret(Err::FSWriteError);
536 0 : ret.setText(file->errorString().toStdString());
537 0 : return ret;
538 0 : }
539 : }
540 :
541 23 : const auto numBytesToWrite = static_cast<qint64>(data.size());
542 23 : const qint64 written = file->write(data.constChar(), numBytesToWrite);
543 23 : if (written != numBytesToWrite) {
544 0 : Ret ret = make_ret(Err::FSWriteError);
545 0 : ret.setText(file->errorString().toStdString());
546 0 : return ret;
547 0 : }
548 :
549 23 : return make_ret(Err::NoError);
550 23 : }
551 :
552 25 : Ret FileSystem::closeStream(StreamId fileId)
553 : {
554 25 : std::lock_guard<std::mutex> lock(m_openStreamsMutex);
555 :
556 25 : auto it = m_openStreams.find(fileId);
557 25 : if (it == m_openStreams.end()) {
558 0 : return make_ret(Err::FSWriteError);
559 : }
560 :
561 25 : std::unique_ptr<QFile> file = std::move(it->second);
562 25 : m_openStreams.erase(it);
563 :
564 25 : IF_ASSERT_FAILED(file) {
565 0 : return make_ret(Err::FSWriteError);
566 : }
567 :
568 25 : file->close();
569 :
570 25 : if (file->error() != QFileDevice::NoError) {
571 0 : Ret ret = make_ret(Err::FSWriteError);
572 0 : ret.setText(file->errorString().toStdString());
573 0 : return ret;
574 0 : }
575 :
576 25 : return make_ret(Err::NoError);
577 25 : }
578 :
579 6 : Ret FileSystem::isWritable(const io::path_t& filePath) const
580 : {
581 6 : Ret ret = muse::make_ok();
582 :
583 6 : QFileInfo fileInfo(filePath.toQString());
584 :
585 6 : if (!fileInfo.exists()) {
586 5 : QFile file(filePath.toQString());
587 :
588 5 : if (!file.open(QFile::WriteOnly)) {
589 0 : ret = make_ret(Err::FSWriteError);
590 0 : ret.setText(file.errorString().toStdString());
591 : }
592 :
593 5 : file.close();
594 5 : file.remove();
595 6 : } else if (!fileInfo.isWritable()) {
596 0 : QFile file(filePath.toQString());
597 0 : if (!file.open(QFile::WriteOnly)) {
598 0 : ret = make_ret(Err::FSWriteError);
599 0 : ret.setText(file.errorString().toStdString());
600 : }
601 :
602 0 : file.close();
603 0 : }
604 :
605 6 : return ret;
606 6 : }
|