1 /*
2 * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 #include "filesystembase.h"
20 #include "utility.h"
21 #include "common/asserts.h"
22
23 #include <QDateTime>
24 #include <QDir>
25 #include <QUrl>
26 #include <QFile>
27 #include <QCoreApplication>
28
29 #include <sys/stat.h>
30 #include <sys/types.h>
31
32 #ifdef Q_OS_WIN
33 #include <windows.h>
34 #include <windef.h>
35 #include <winbase.h>
36 #include <fcntl.h>
37 #include <io.h>
38 #endif
39
40 namespace OCC {
41
42 Q_LOGGING_CATEGORY(lcFileSystem, "nextcloud.sync.filesystem", QtInfoMsg)
43
longWinPath(const QString & inpath)44 QString FileSystem::longWinPath(const QString &inpath)
45 {
46 #ifdef Q_OS_WIN
47 return pathtoUNC(inpath);
48 #else
49 return inpath;
50 #endif
51 }
52
setFileHidden(const QString & filename,bool hidden)53 void FileSystem::setFileHidden(const QString &filename, bool hidden)
54 {
55 #ifdef _WIN32
56 QString fName = longWinPath(filename);
57 DWORD dwAttrs;
58
59 dwAttrs = GetFileAttributesW((wchar_t *)fName.utf16());
60
61 if (dwAttrs != INVALID_FILE_ATTRIBUTES) {
62 if (hidden && !(dwAttrs & FILE_ATTRIBUTE_HIDDEN)) {
63 SetFileAttributesW((wchar_t *)fName.utf16(), dwAttrs | FILE_ATTRIBUTE_HIDDEN);
64 } else if (!hidden && (dwAttrs & FILE_ATTRIBUTE_HIDDEN)) {
65 SetFileAttributesW((wchar_t *)fName.utf16(), dwAttrs & ~FILE_ATTRIBUTE_HIDDEN);
66 }
67 }
68 #else
69 Q_UNUSED(filename);
70 Q_UNUSED(hidden);
71 #endif
72 }
73
getDefaultWritePermissions()74 static QFile::Permissions getDefaultWritePermissions()
75 {
76 QFile::Permissions result = QFile::WriteUser;
77 #ifndef Q_OS_WIN
78 mode_t mask = umask(0);
79 umask(mask);
80 if (!(mask & S_IWGRP)) {
81 result |= QFile::WriteGroup;
82 }
83 if (!(mask & S_IWOTH)) {
84 result |= QFile::WriteOther;
85 }
86 #endif
87 return result;
88 }
89
setFileReadOnly(const QString & filename,bool readonly)90 void FileSystem::setFileReadOnly(const QString &filename, bool readonly)
91 {
92 QFile file(filename);
93 QFile::Permissions permissions = file.permissions();
94
95 QFile::Permissions allWritePermissions =
96 QFile::WriteUser | QFile::WriteGroup | QFile::WriteOther | QFile::WriteOwner;
97 static QFile::Permissions defaultWritePermissions = getDefaultWritePermissions();
98
99 permissions &= ~allWritePermissions;
100 if (!readonly) {
101 permissions |= defaultWritePermissions;
102 }
103 file.setPermissions(permissions);
104 }
105
setFolderMinimumPermissions(const QString & filename)106 void FileSystem::setFolderMinimumPermissions(const QString &filename)
107 {
108 #ifdef Q_OS_MAC
109 QFile::Permissions perm = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
110 QFile file(filename);
111 file.setPermissions(perm);
112 #else
113 Q_UNUSED(filename);
114 #endif
115 }
116
117
setFileReadOnlyWeak(const QString & filename,bool readonly)118 void FileSystem::setFileReadOnlyWeak(const QString &filename, bool readonly)
119 {
120 QFile file(filename);
121 QFile::Permissions permissions = file.permissions();
122
123 if (!readonly && (permissions & QFile::WriteOwner)) {
124 return; // already writable enough
125 }
126
127 setFileReadOnly(filename, readonly);
128 }
129
rename(const QString & originFileName,const QString & destinationFileName,QString * errorString)130 bool FileSystem::rename(const QString &originFileName,
131 const QString &destinationFileName,
132 QString *errorString)
133 {
134 bool success = false;
135 QString error;
136 #ifdef Q_OS_WIN
137 QString orig = longWinPath(originFileName);
138 QString dest = longWinPath(destinationFileName);
139
140 if (isLnkFile(originFileName) || isLnkFile(destinationFileName)) {
141 success = MoveFileEx((wchar_t *)orig.utf16(),
142 (wchar_t *)dest.utf16(),
143 MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH);
144 if (!success) {
145 error = Utility::formatWinError(GetLastError());
146 }
147 } else
148 #endif
149 {
150 QFile orig(originFileName);
151 success = orig.rename(destinationFileName);
152 if (!success) {
153 error = orig.errorString();
154 }
155 }
156
157 if (!success) {
158 qCWarning(lcFileSystem) << "Error renaming file" << originFileName
159 << "to" << destinationFileName
160 << "failed: " << error;
161 if (errorString) {
162 *errorString = error;
163 }
164 }
165 return success;
166 }
167
uncheckedRenameReplace(const QString & originFileName,const QString & destinationFileName,QString * errorString)168 bool FileSystem::uncheckedRenameReplace(const QString &originFileName,
169 const QString &destinationFileName,
170 QString *errorString)
171 {
172 #ifndef Q_OS_WIN
173 bool success = false;
174 QFile orig(originFileName);
175 // We want a rename that also overwites. QFile::rename does not overwite.
176 // Qt 5.1 has QSaveFile::renameOverwrite we could use.
177 // ### FIXME
178 success = true;
179 bool destExists = fileExists(destinationFileName);
180 if (destExists && !QFile::remove(destinationFileName)) {
181 *errorString = orig.errorString();
182 qCWarning(lcFileSystem) << "Target file could not be removed.";
183 success = false;
184 }
185 if (success) {
186 success = orig.rename(destinationFileName);
187 }
188 if (!success) {
189 *errorString = orig.errorString();
190 qCWarning(lcFileSystem) << "Renaming temp file to final failed: " << *errorString;
191 return false;
192 }
193
194 #else //Q_OS_WIN
195 // You can not overwrite a read-only file on windows.
196 if (!QFileInfo(destinationFileName).isWritable()) {
197 setFileReadOnly(destinationFileName, false);
198 }
199
200 BOOL ok;
201 QString orig = longWinPath(originFileName);
202 QString dest = longWinPath(destinationFileName);
203
204 ok = MoveFileEx((wchar_t *)orig.utf16(),
205 (wchar_t *)dest.utf16(),
206 MOVEFILE_REPLACE_EXISTING + MOVEFILE_COPY_ALLOWED + MOVEFILE_WRITE_THROUGH);
207 if (!ok) {
208 *errorString = Utility::formatWinError(GetLastError());
209 qCWarning(lcFileSystem) << "Renaming temp file to final failed: " << *errorString;
210 return false;
211 }
212 #endif
213 return true;
214 }
215
openAndSeekFileSharedRead(QFile * file,QString * errorOrNull,qint64 seek)216 bool FileSystem::openAndSeekFileSharedRead(QFile *file, QString *errorOrNull, qint64 seek)
217 {
218 QString errorDummy;
219 // avoid many if (errorOrNull) later.
220 QString &error = errorOrNull ? *errorOrNull : errorDummy;
221 error.clear();
222
223 #ifdef Q_OS_WIN
224 //
225 // The following code is adapted from Qt's QFSFileEnginePrivate::nativeOpen()
226 // by including the FILE_SHARE_DELETE share mode.
227 //
228
229 // Enable full sharing.
230 DWORD shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
231
232 int accessRights = GENERIC_READ;
233 DWORD creationDisp = OPEN_EXISTING;
234
235 // Create the file handle.
236 SECURITY_ATTRIBUTES securityAtts = { sizeof(SECURITY_ATTRIBUTES), nullptr, FALSE };
237 QString fName = longWinPath(file->fileName());
238
239 HANDLE fileHandle = CreateFileW(
240 (const wchar_t *)fName.utf16(),
241 accessRights,
242 shareMode,
243 &securityAtts,
244 creationDisp,
245 FILE_ATTRIBUTE_NORMAL,
246 nullptr);
247
248 // Bail out on error.
249 if (fileHandle == INVALID_HANDLE_VALUE) {
250 error = qt_error_string();
251 return false;
252 }
253
254 // Convert the HANDLE to an fd and pass it to QFile's foreign-open
255 // function. The fd owns the handle, so when QFile later closes
256 // the fd the handle will be closed too.
257 int fd = _open_osfhandle((intptr_t)fileHandle, _O_RDONLY);
258 if (fd == -1) {
259 error = QStringLiteral("could not make fd from handle");
260 CloseHandle(fileHandle);
261 return false;
262 }
263 if (!file->open(fd, QIODevice::ReadOnly, QFile::AutoCloseHandle)) {
264 error = file->errorString();
265 _close(fd); // implicitly closes fileHandle
266 return false;
267 }
268
269 // Seek to the right spot
270 LARGE_INTEGER *li = reinterpret_cast<LARGE_INTEGER *>(&seek);
271 DWORD newFilePointer = SetFilePointer(fileHandle, li->LowPart, &li->HighPart, FILE_BEGIN);
272 if (newFilePointer == 0xFFFFFFFF && GetLastError() != NO_ERROR) {
273 error = qt_error_string();
274 return false;
275 }
276
277 return true;
278 #else
279 if (!file->open(QFile::ReadOnly)) {
280 error = file->errorString();
281 return false;
282 }
283 if (!file->seek(seek)) {
284 error = file->errorString();
285 return false;
286 }
287 return true;
288 #endif
289 }
290
291 #ifdef Q_OS_WIN
fileExistsWin(const QString & filename)292 static bool fileExistsWin(const QString &filename)
293 {
294 WIN32_FIND_DATA FindFileData;
295 HANDLE hFind;
296 QString fName = FileSystem::longWinPath(filename);
297
298 hFind = FindFirstFileW((wchar_t *)fName.utf16(), &FindFileData);
299 if (hFind == INVALID_HANDLE_VALUE) {
300 return false;
301 }
302 FindClose(hFind);
303 return true;
304 }
305 #endif
306
fileExists(const QString & filename,const QFileInfo & fileInfo)307 bool FileSystem::fileExists(const QString &filename, const QFileInfo &fileInfo)
308 {
309 #ifdef Q_OS_WIN
310 if (isLnkFile(filename)) {
311 // Use a native check.
312 return fileExistsWin(filename);
313 }
314 #endif
315 bool re = fileInfo.exists();
316 // if the filename is different from the filename in fileInfo, the fileInfo is
317 // not valid. There needs to be one initialised here. Otherwise the incoming
318 // fileInfo is re-used.
319 if (fileInfo.filePath() != filename) {
320 QFileInfo myFI(filename);
321 re = myFI.exists();
322 }
323 return re;
324 }
325
326 #ifdef Q_OS_WIN
fileSystemForPath(const QString & path)327 QString FileSystem::fileSystemForPath(const QString &path)
328 {
329 // See also QStorageInfo (Qt >=5.4) and GetVolumeInformationByHandleW (>= Vista)
330 QString drive = path.left(2);
331 if (!drive.endsWith(QLatin1Char(':')))
332 return QString();
333 drive.append(QLatin1Char('\\'));
334
335 const size_t fileSystemBufferSize = 4096;
336 TCHAR fileSystemBuffer[fileSystemBufferSize];
337
338 if (!GetVolumeInformationW(
339 reinterpret_cast<LPCWSTR>(drive.utf16()),
340 nullptr, 0,
341 nullptr, nullptr, nullptr,
342 fileSystemBuffer, fileSystemBufferSize)) {
343 return QString();
344 }
345 return QString::fromUtf16(reinterpret_cast<const ushort *>(fileSystemBuffer));
346 }
347 #endif
348
remove(const QString & fileName,QString * errorString)349 bool FileSystem::remove(const QString &fileName, QString *errorString)
350 {
351 #ifdef Q_OS_WIN
352 // You cannot delete a read-only file on windows, but we want to
353 // allow that.
354 if (!QFileInfo(fileName).isWritable()) {
355 setFileReadOnly(fileName, false);
356 }
357 #endif
358 QFile f(fileName);
359 if (!f.remove()) {
360 if (errorString) {
361 *errorString = f.errorString();
362 }
363 return false;
364 }
365 return true;
366 }
367
moveToTrash(const QString & fileName,QString * errorString)368 bool FileSystem::moveToTrash(const QString &fileName, QString *errorString)
369 {
370 // TODO: Qt 5.15 bool QFile::moveToTrash()
371 #if defined Q_OS_UNIX && !defined Q_OS_MAC
372 QString trashPath, trashFilePath, trashInfoPath;
373 QString xdgDataHome = QFile::decodeName(qgetenv("XDG_DATA_HOME"));
374 if (xdgDataHome.isEmpty()) {
375 trashPath = QDir::homePath() + QStringLiteral("/.local/share/Trash/"); // trash path that should exist
376 } else {
377 trashPath = xdgDataHome + QStringLiteral("/Trash/");
378 }
379
380 trashFilePath = trashPath + QStringLiteral("files/"); // trash file path contain delete files
381 trashInfoPath = trashPath + QStringLiteral("info/"); // trash info path contain delete files information
382
383 if (!(QDir().mkpath(trashFilePath) && QDir().mkpath(trashInfoPath))) {
384 *errorString = QCoreApplication::translate("FileSystem", "Could not make directories in trash");
385 return false; //mkpath will return true if path exists
386 }
387
388 QFileInfo f(fileName);
389
390 QDir file;
391 int suffix_number = 1;
392 if (file.exists(trashFilePath + f.fileName())) { //file in trash already exists, move to "filename.1"
393 QString path = trashFilePath + f.fileName() + QLatin1Char('.');
394 while (file.exists(path + QString::number(suffix_number))) { //or to "filename.2" if "filename.1" exists, etc
395 suffix_number++;
396 }
397 if (!file.rename(f.absoluteFilePath(), path + QString::number(suffix_number))) { // rename(file old path, file trash path)
398 *errorString = QCoreApplication::translate("FileSystem", R"(Could not move "%1" to "%2")")
399 .arg(f.absoluteFilePath(), path + QString::number(suffix_number));
400 return false;
401 }
402 } else {
403 if (!file.rename(f.absoluteFilePath(), trashFilePath + f.fileName())) { // rename(file old path, file trash path)
404 *errorString = QCoreApplication::translate("FileSystem", R"(Could not move "%1" to "%2")")
405 .arg(f.absoluteFilePath(), trashFilePath + f.fileName());
406 return false;
407 }
408 }
409
410 // create file format for trash info file----- START
411 QFile infoFile;
412 if (file.exists(trashInfoPath + f.fileName() + QStringLiteral(".trashinfo"))) { //TrashInfo file already exists, create "filename.1.trashinfo"
413 QString filename = trashInfoPath + f.fileName() + QLatin1Char('.') + QString::number(suffix_number) + QStringLiteral(".trashinfo");
414 infoFile.setFileName(filename); //filename+.trashinfo // create file information file in /.local/share/Trash/info/ folder
415 } else {
416 QString filename = trashInfoPath + f.fileName() + QStringLiteral(".trashinfo");
417 infoFile.setFileName(filename); //filename+.trashinfo // create file information file in /.local/share/Trash/info/ folder
418 }
419
420 infoFile.open(QIODevice::ReadWrite);
421
422 QTextStream stream(&infoFile); // for write data on open file
423
424 stream << "[Trash Info]\n"
425 << "Path="
426 << QUrl::toPercentEncoding(f.absoluteFilePath(), "~_-./")
427 << "\n"
428 << "DeletionDate="
429 << QDateTime::currentDateTime().toString(Qt::ISODate)
430 << '\n';
431 infoFile.close();
432
433 // create info file format of trash file----- END
434
435 return true;
436 #else
437 Q_UNUSED(fileName)
438 *errorString = QCoreApplication::translate("FileSystem", "Moving to the trash is not implemented on this platform");
439 return false;
440 #endif
441 }
442
isFileLocked(const QString & fileName)443 bool FileSystem::isFileLocked(const QString &fileName)
444 {
445 #ifdef Q_OS_WIN
446 // Check if file exists
447 const QString fName = longWinPath(fileName);
448 DWORD attr = GetFileAttributesW(reinterpret_cast<const wchar_t *>(fName.utf16()));
449 if (attr != INVALID_FILE_ATTRIBUTES) {
450 // Try to open the file with as much access as possible..
451 HANDLE win_h = CreateFileW(
452 reinterpret_cast<const wchar_t *>(fName.utf16()),
453 GENERIC_READ | GENERIC_WRITE,
454 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
455 nullptr, OPEN_EXISTING,
456 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
457 nullptr);
458
459 if (win_h == INVALID_HANDLE_VALUE) {
460 /* could not be opened, so locked? */
461 /* 32 == ERROR_SHARING_VIOLATION */
462 return true;
463 } else {
464 CloseHandle(win_h);
465 }
466 }
467 #else
468 Q_UNUSED(fileName);
469 #endif
470 return false;
471 }
472
isLnkFile(const QString & filename)473 bool FileSystem::isLnkFile(const QString &filename)
474 {
475 return filename.endsWith(QLatin1String(".lnk"));
476 }
477
isExcludeFile(const QString & filename)478 bool FileSystem::isExcludeFile(const QString &filename)
479 {
480 return filename.compare(QStringLiteral(".sync-exclude.lst"), Qt::CaseInsensitive) == 0
481 || filename.compare(QStringLiteral("exclude.lst"), Qt::CaseInsensitive) == 0
482 || filename.endsWith(QStringLiteral("/.sync-exclude.lst"), Qt::CaseInsensitive)
483 || filename.endsWith(QStringLiteral("/exclude.lst"), Qt::CaseInsensitive);
484 }
485
isJunction(const QString & filename)486 bool FileSystem::isJunction(const QString &filename)
487 {
488 #ifdef Q_OS_WIN
489 WIN32_FIND_DATA findData;
490 HANDLE hFind = FindFirstFileEx(reinterpret_cast<const wchar_t *>(longWinPath(filename).utf16()), FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, 0);
491 if (hFind != INVALID_HANDLE_VALUE) {
492 FindClose(hFind);
493 return false;
494 }
495 return findData.dwFileAttributes != INVALID_FILE_ATTRIBUTES
496 && findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT
497 && findData.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT;
498 #else
499 Q_UNUSED(filename);
500 return false;
501 #endif
502 }
503
504 #ifdef Q_OS_WIN
pathtoUNC(const QString & _str)505 QString FileSystem::pathtoUNC(const QString &_str)
506 {
507 Q_ASSERT(QFileInfo(_str).isAbsolute());
508 if (_str.isEmpty()) {
509 return _str;
510 }
511 const QString str = QDir::toNativeSeparators(_str);
512 const QLatin1Char sep('\\');
513
514 // we already have a unc path
515 if (str.startsWith(sep + sep)) {
516 return str;
517 }
518 // prepend \\?\ and to support long names
519
520 if (str.at(0) == sep) {
521 // should not happen as we require the path to be absolute
522 return QStringLiteral(R"(\\?)") + str;
523 }
524 return QStringLiteral(R"(\\?\)") + str;
525 }
526 #endif
527
528 } // namespace OCC
529