1 /*
   2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 #include <memory>
  27 #include <algorithm>
  28 #include <shlwapi.h>
  29 
  30 #include "FileUtils.h"
  31 #include "WinErrorHandling.h"
  32 #include "Log.h"
  33 
  34 
  35 // Needed by FileUtils::isDirectoryNotEmpty
  36 #pragma comment(lib, "shlwapi")
  37 
  38 
  39 namespace FileUtils {
  40 
  41 namespace {
  42 
  43 
  44 tstring reservedFilenameChars() {
  45     tstring buf;
  46     for (char charCode = 0; charCode < 32; ++charCode) {
  47         buf.append(1, charCode);
  48     }
  49     buf += _T("<>:\"|?*/\\");
  50     return buf;
  51 }
  52 
  53 } // namespace
  54 
  55 bool isDirSeparator(const tstring::value_type c) {
  56     return (c == '/' || c == '\\');
  57 }
  58 
  59 bool isFileExists(const tstring &filePath) {
  60     return GetFileAttributes(filePath.c_str()) != INVALID_FILE_ATTRIBUTES;
  61 }
  62 
  63 namespace {
  64 bool isDirectoryAttrs(const DWORD attrs) {
  65     return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0;
  66 }
  67 } // namespace
  68 
  69 bool isDirectory(const tstring &filePath) {
  70     return isDirectoryAttrs(GetFileAttributes(filePath.c_str()));
  71 }
  72 
  73 bool isDirectoryNotEmpty(const tstring &dirPath) {
  74     if (!isDirectory(dirPath)) {
  75         return false;
  76     }
  77     return FALSE == PathIsDirectoryEmpty(dirPath.c_str());
  78 }
  79 
  80 tstring dirname(const tstring &path)
  81 {
  82     tstring::size_type pos = path.find_last_of(_T("\\/"));
  83     if (pos != tstring::npos) {
  84         pos = path.find_last_not_of(_T("\\/"), pos); // skip trailing slashes
  85     }
  86     return pos == tstring::npos ? tstring() : path.substr(0, pos + 1);
  87 }
  88 
  89 tstring basename(const tstring &path) {
  90     const tstring::size_type pos = path.find_last_of(_T("\\/"));
  91     if (pos == tstring::npos) {
  92         return path;
  93     }
  94     return path.substr(pos + 1);
  95 }
  96 
  97 tstring suffix(const tstring &path) {
  98     const tstring::size_type pos = path.rfind('.');
  99     if (pos == tstring::npos) {
 100         return tstring();
 101     }
 102     const tstring::size_type dirSepPos = path.find_first_of(_T("\\/"),
 103                                                             pos + 1);
 104     if (dirSepPos != tstring::npos) {
 105         return tstring();
 106     }
 107     // test for '/..' and '..' cases
 108     if (pos != 0 && path[pos - 1] == '.'
 109                             && (pos == 1 || isDirSeparator(path[pos - 2]))) {
 110         return tstring();
 111     }
 112     return path.substr(pos);
 113 }
 114 
 115 tstring combinePath(const tstring& parent, const tstring& child) {
 116     if (parent.empty()) {
 117         return child;
 118     }
 119     if (child.empty()) {
 120         return parent;
 121     }
 122 
 123     tstring parentWOSlash = removeTrailingSlash(parent);
 124     // also handle the case when child contains starting slash
 125     bool childHasSlash = isDirSeparator(child.front());
 126     tstring childWOSlash = childHasSlash ? child.substr(1) : child;
 127 
 128     return parentWOSlash + _T("\\") + childWOSlash;
 129 }
 130 
 131 tstring removeTrailingSlash(const tstring& path) {
 132     if (path.empty()) {
 133         return path;
 134     }
 135     tstring::const_reverse_iterator it = path.rbegin();
 136     tstring::const_reverse_iterator end = path.rend();
 137 
 138     while (it != end && isDirSeparator(*it)) {
 139         ++it;
 140     }
 141     return path.substr(0, end - it);
 142 }
 143 
 144 tstring normalizePath(tstring v) {
 145     std::replace(v.begin(), v.end(), '/', '\\');
 146     return tstrings::toLower(v);
 147 }
 148 
 149 namespace {
 150 
 151 bool createNewFile(const tstring& path) {
 152     HANDLE h = CreateFile(path.c_str(), GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
 153     // if the file exists => h == INVALID_HANDLE_VALUE & GetLastError returns ERROR_FILE_EXISTS
 154     if (h != INVALID_HANDLE_VALUE) {
 155         CloseHandle(h);
 156         LOG_TRACE(tstrings::any() << "Created [" << path << "] file");
 157         return true;
 158     }
 159     return false;
 160 }
 161 
 162 } // namespace
 163 
 164 tstring createTempFile(const tstring &prefix, const tstring &suffix, const tstring &path)
 165 {
 166     const tstring invalidChars = reservedFilenameChars();
 167 
 168     if (prefix.find_first_of(invalidChars) != tstring::npos) {
 169         JP_THROW(tstrings::any() << "Illegal characters in prefix=" << prefix);
 170     }
 171 
 172     if (suffix.find_first_of(invalidChars) != tstring::npos) {
 173         JP_THROW(tstrings::any() << "Illegal characters in suffix=" << suffix);
 174     }
 175 
 176     int rnd = (int)GetTickCount();
 177 
 178     // do no more than 100 attempts
 179     for (int i=0; i<100; i++) {
 180         const tstring filePath = mkpath() << path << (prefix + (tstrings::any() << (rnd + i)).tstr() + suffix);
 181         if (createNewFile(filePath)) {
 182             return filePath;
 183         }
 184     }
 185 
 186     // 100 attempts failed
 187     JP_THROW(tstrings::any() << "createTempFile("  << prefix << ", "
 188                                                     << suffix << ", "
 189                                                     << path << ") failed");
 190 }
 191 
 192 tstring createTempDirectory(const tstring &prefix, const tstring &suffix, const tstring &basedir) {
 193     const tstring filePath = createTempFile(prefix, suffix, basedir);
 194     // delete the file and create directory with the same name
 195     deleteFile(filePath);
 196     createDirectory(filePath);
 197     return filePath;
 198 }
 199 
 200 tstring createUniqueFile(const tstring &prototype) {
 201     if (createNewFile(prototype)) {
 202         return prototype;
 203     }
 204 
 205     return createTempFile(replaceSuffix(basename(prototype)),
 206                                         suffix(prototype), dirname(prototype));
 207 }
 208 
 209 namespace {
 210 
 211 void createDir(const tstring path, LPSECURITY_ATTRIBUTES saAttr, tstring_array* createdDirs=0) {
 212     if (CreateDirectory(path.c_str(), saAttr)) {
 213         LOG_TRACE(tstrings::any() << "Created [" << path << "] directory");
 214         if (createdDirs) {
 215             createdDirs->push_back(removeTrailingSlash(path));
 216         }
 217     } else {
 218         const DWORD createDirectoryErr = GetLastError();
 219         // if saAttr is specified, fail even if the directory exists
 220         if (saAttr != NULL || !isDirectory(path)) {
 221             JP_THROW(SysError(tstrings::any() << "CreateDirectory(" << path << ") failed",
 222                                     CreateDirectory, createDirectoryErr));
 223         }
 224     }
 225 }
 226 
 227 }
 228 
 229 void createDirectory(const tstring &path, tstring_array* createdDirs) {
 230     const tstring dirPath = removeTrailingSlash(path) + _T("\\");
 231 
 232     tstring::size_type pos = dirPath.find_first_of(_T("\\/"));
 233     while (pos != tstring::npos) {
 234         const tstring subdirPath = dirPath.substr(0, pos + 1);
 235         createDir(subdirPath, NULL, createdDirs);
 236         pos = dirPath.find_first_of(_T("\\/"), pos + 1);
 237     }
 238 }
 239 
 240 
 241 void copyFile(const tstring& fromPath, const tstring& toPath, bool failIfExists) {
 242     createDirectory(dirname(toPath));
 243     if (!CopyFile(fromPath.c_str(), toPath.c_str(), (failIfExists ? TRUE : FALSE))) {
 244         JP_THROW(SysError(tstrings::any()
 245                     << "CopyFile(" << fromPath << ", " << toPath << ", "
 246                                     << failIfExists << ") failed", CopyFile));
 247     }
 248     LOG_TRACE(tstrings::any() << "Copied [" << fromPath << "] file to ["
 249                                                             << toPath << "]");
 250 }
 251 
 252 
 253 namespace {
 254 
 255 void moveFileImpl(const tstring& fromPath, const tstring& toPath,
 256                                                                 DWORD flags) {
 257     const bool isDir = isDirectory(fromPath);
 258     if (!MoveFileEx(fromPath.c_str(), toPath.empty() ? NULL : toPath.c_str(),
 259                                                                     flags)) {
 260         JP_THROW(SysError(tstrings::any() << "MoveFileEx(" << fromPath
 261                         << ", " << toPath << ", " << flags << ") failed",
 262                                                                 MoveFileEx));
 263     }
 264 
 265     const bool onReboot = 0 != (flags & MOVEFILE_DELAY_UNTIL_REBOOT);
 266 
 267     const LPCTSTR label = isDir ? _T("folder") : _T("file");
 268 
 269     tstrings::any msg;
 270     if (!toPath.empty()) {
 271         if (onReboot) {
 272             msg << "Move";
 273         } else {
 274             msg << "Moved";
 275         }
 276         msg << " '" << fromPath << "' " << label << " to '" << toPath << "'";
 277     } else {
 278         if (onReboot) {
 279             msg << "Delete";
 280         } else {
 281             msg << "Deleted";
 282         }
 283         msg << " '" << fromPath << "' " << label;
 284     }
 285     if (onReboot) {
 286         msg << " on reboot";
 287     }
 288     LOG_TRACE(msg);
 289 }
 290 
 291 } // namespace
 292 
 293 
 294 void moveFile(const tstring& fromPath, const tstring& toPath, bool failIfExists)
 295 {
 296     createDirectory(dirname(toPath));
 297 
 298     DWORD flags = MOVEFILE_COPY_ALLOWED;
 299     if (!failIfExists) {
 300         flags |= MOVEFILE_REPLACE_EXISTING;
 301     }
 302 
 303     moveFileImpl(fromPath, toPath, flags);
 304 }
 305 
 306 void deleteFile(const tstring &path)
 307 {
 308     if (!deleteFile(path, std::nothrow)) {
 309         JP_THROW(SysError(tstrings::any()
 310                         << "DeleteFile(" << path << ") failed", DeleteFile));
 311     }
 312 }
 313 
 314 namespace {
 315 
 316 bool notFound(const DWORD status=GetLastError()) {
 317     return status == ERROR_FILE_NOT_FOUND || status == ERROR_PATH_NOT_FOUND;
 318 }
 319 
 320 bool deleteFileImpl(const std::nothrow_t &, const tstring &path) {
 321     const bool deleted = (DeleteFile(path.c_str()) != 0);
 322     if (deleted) {
 323         LOG_TRACE(tstrings::any() << "Deleted [" << path << "] file");
 324         return true;
 325     }
 326     return notFound();
 327 }
 328 
 329 } // namespace
 330 
 331 bool deleteFile(const tstring &path, const std::nothrow_t &) throw()
 332 {
 333     bool deleted = deleteFileImpl(std::nothrow, path);
 334     const DWORD status = GetLastError();
 335     if (!deleted && status == ERROR_ACCESS_DENIED) {
 336         DWORD attrs = GetFileAttributes(path.c_str());
 337         SetLastError(status);
 338         if (attrs == INVALID_FILE_ATTRIBUTES) {
 339             return false;
 340         }
 341         if (attrs & FILE_ATTRIBUTE_READONLY) {
 342             // DeleteFile() failed because file is R/O.
 343             // Remove R/O attribute and retry DeleteFile().
 344             attrs &= ~FILE_ATTRIBUTE_READONLY;
 345             if (SetFileAttributes(path.c_str(), attrs)) {
 346                 LOG_TRACE(tstrings::any() << "Discarded R/O attribute from ["
 347                                                         << path << "] file");
 348                 deleted = deleteFileImpl(std::nothrow, path);
 349             } else {
 350                 LOG_WARNING(SysError(tstrings::any()
 351                             << "Failed to discard R/O attribute from ["
 352                             << path << "] file. File will not be deleted",
 353                             SetFileAttributes).what());
 354                 SetLastError(status);
 355             }
 356         }
 357     }
 358 
 359     return deleted || notFound();
 360 }
 361 
 362 void deleteDirectory(const tstring &path)
 363 {
 364     if (!deleteDirectory(path, std::nothrow)) {
 365         JP_THROW(SysError(tstrings::any()
 366                 << "RemoveDirectory(" << path << ") failed", RemoveDirectory));
 367     }
 368 }
 369 
 370 bool deleteDirectory(const tstring &path, const std::nothrow_t &) throw()
 371 {
 372     const bool deleted = (RemoveDirectory(path.c_str()) != 0);
 373     if (deleted) {
 374         LOG_TRACE(tstrings::any() << "Deleted [" << path << "] directory");
 375     }
 376     return deleted || notFound();
 377 }
 378 
 379 namespace {
 380 
 381 class DeleteFilesCallback: public DirectoryCallback {
 382 public:
 383     explicit DeleteFilesCallback(bool ff): failfast(ff), failed(false) {
 384     }
 385 
 386     virtual bool onFile(const tstring& path) {
 387         if (failfast) {
 388             deleteFile(path);
 389         } else {
 390             updateStatus(deleteFile(path, std::nothrow));
 391         }
 392         return true;
 393     }
 394 
 395     bool good() const {
 396         return !failed;
 397     }
 398 
 399 protected:
 400     void updateStatus(bool success) {
 401         if (!success) {
 402             failed = true;
 403         }
 404     }
 405 
 406     const bool failfast;
 407 private:
 408     bool failed;
 409 };
 410 
 411 class DeleteAllCallback: public DeleteFilesCallback {
 412 public:
 413     explicit DeleteAllCallback(bool failfast): DeleteFilesCallback(failfast) {
 414     }
 415 
 416     virtual bool onDirectory(const tstring& path) {
 417         if (failfast) {
 418             deleteDirectoryRecursive(path);
 419         } else {
 420             updateStatus(deleteDirectoryRecursive(path, std::nothrow));
 421         }
 422         return true;
 423     }
 424 };
 425 
 426 
 427 class BatchDeleter {
 428     const tstring dirPath;
 429     bool recursive;
 430 public:
 431     explicit BatchDeleter(const tstring& path): dirPath(path) {
 432         deleteSubdirs(false);
 433     }
 434 
 435     BatchDeleter& deleteSubdirs(bool v) {
 436         recursive = v;
 437         return *this;
 438     }
 439 
 440     void execute() const {
 441         if (!isFileExists(dirPath)) {
 442             return;
 443         }
 444         iterateDirectory(true /* fail fast */);
 445         if (recursive) {
 446             deleteDirectory(dirPath);
 447         }
 448     }
 449 
 450     bool execute(const std::nothrow_t&) const {
 451         if (!isFileExists(dirPath)) {
 452             return true;
 453         }
 454 
 455         if (!isDirectory(dirPath)) {
 456             return false;
 457         }
 458 
 459         JP_TRY;
 460         if (!iterateDirectory(false /* ignore errors */)) {
 461             return false;
 462         }
 463         if (recursive) {
 464             return deleteDirectory(dirPath, std::nothrow);
 465         }
 466         return true;
 467         JP_CATCH_ALL;
 468 
 469         return false;
 470     }
 471 
 472 private:
 473     bool iterateDirectory(bool failfast) const {
 474         std::unique_ptr<DeleteFilesCallback> callback;
 475         if (recursive) {
 476             callback = std::unique_ptr<DeleteFilesCallback>(
 477                                             new DeleteAllCallback(failfast));
 478         } else {
 479             callback = std::unique_ptr<DeleteFilesCallback>(
 480                                             new DeleteFilesCallback(failfast));
 481         }
 482 
 483         FileUtils::iterateDirectory(dirPath, *callback);
 484         return callback->good();
 485     }
 486 };
 487 
 488 } // namespace
 489 
 490 void deleteFilesInDirectory(const tstring &dirPath) {
 491     BatchDeleter(dirPath).execute();
 492 }
 493 
 494 bool deleteFilesInDirectory(const tstring &dirPath,
 495                                             const std::nothrow_t &) throw() {
 496     return BatchDeleter(dirPath).execute(std::nothrow);
 497 }
 498 
 499 void deleteDirectoryRecursive(const tstring &dirPath) {
 500     BatchDeleter(dirPath).deleteSubdirs(true).execute();
 501 }
 502 
 503 bool deleteDirectoryRecursive(const tstring &dirPath,
 504                                             const std::nothrow_t &) throw() {
 505     return BatchDeleter(dirPath).deleteSubdirs(true).execute(std::nothrow);
 506 }
 507 
 508 namespace {
 509 
 510 struct FindFileDeleter {
 511     typedef HANDLE pointer;
 512     
 513     void operator()(HANDLE h) {
 514         if (h && h != INVALID_HANDLE_VALUE) {
 515             FindClose(h);
 516         }
 517     }
 518 };
 519 
 520 typedef std::unique_ptr<HANDLE, FindFileDeleter> UniqueFindFileHandle;
 521 
 522 }; // namesace
 523 void iterateDirectory(const tstring &dirPath, DirectoryCallback& callback)
 524 {
 525     const tstring searchString = combinePath(dirPath, _T("*"));
 526     WIN32_FIND_DATA findData;
 527     UniqueFindFileHandle h(FindFirstFile(searchString.c_str(), &findData));
 528     if (h.get() == INVALID_HANDLE_VALUE) {
 529         // GetLastError() == ERROR_FILE_NOT_FOUND is OK - no files in the directory
 530         // ERROR_PATH_NOT_FOUND is returned if the parent directory does not exist
 531         if (GetLastError() != ERROR_FILE_NOT_FOUND) {
 532             JP_THROW(SysError(tstrings::any() << "FindFirstFile(" << dirPath << ") failed", FindFirstFile));
 533         }
 534         return;
 535     }
 536 
 537     do {
 538         const tstring fname(findData.cFileName);
 539         const tstring filePath = combinePath(dirPath, fname);
 540         if (!isDirectoryAttrs(findData.dwFileAttributes)) {
 541             if (!callback.onFile(filePath)) {
 542                 return;
 543             }
 544         } else if (fname != _T(".") && fname != _T("..")) {
 545             if (!callback.onDirectory(filePath)) {
 546                 return;
 547             }
 548         }
 549     } while (FindNextFile(h.get(), &findData));
 550 
 551     // expect GetLastError() == ERROR_NO_MORE_FILES
 552     if (GetLastError() != ERROR_NO_MORE_FILES) {
 553         JP_THROW(SysError(tstrings::any() << "FindNextFile(" << dirPath << ") failed", FindNextFile));
 554     }
 555 }
 556 
 557 
 558 tstring replaceSuffix(const tstring& path, const tstring& newSuffix) {
 559     return (path.substr(0, path.size() - suffix(path).size()) + newSuffix);
 560 }
 561 
 562 
 563 DirectoryIterator& DirectoryIterator::findItems(tstring_array& v) {
 564     if (!isDirectory(root)) {
 565         return *this;
 566     }
 567 
 568     iterateDirectory(root, *this);
 569     v.insert(v.end(), items.begin(), items.end());
 570     items = tstring_array();
 571     return *this;
 572 }
 573 
 574 bool DirectoryIterator::onFile(const tstring& path) {
 575     if (theWithFiles) {
 576         items.push_back(path);
 577     }
 578     return true;
 579 }
 580 
 581 bool DirectoryIterator::onDirectory(const tstring& path) {
 582     if (theWithFolders) {
 583         items.push_back(path);
 584     }
 585     if (theRecurse) {
 586         DirectoryIterator(path).recurse(theRecurse)
 587                                     .withFiles(theWithFiles)
 588                                     .withFolders(theWithFolders)
 589                                     .findItems(items);
 590     }
 591     return true;
 592 }
 593 
 594 
 595 namespace {
 596 
 597 struct DeleterFunctor {
 598     // Order of items in the following enum is important!
 599     // It controls order in which items of particular type will be deleted.
 600     // See Deleter::execute().
 601     enum {
 602         File,
 603         FilesInDirectory,
 604         RecursiveDirectory,
 605         EmptyDirectory
 606     };
 607 
 608     void operator () (const Deleter::Path& path) const {
 609         switch (path.second) {
 610 #define DELETE_SOME(o, f)\
 611         case o:\
 612             f(path.first, std::nothrow);\
 613             break
 614 
 615         DELETE_SOME(File, deleteFile);
 616         DELETE_SOME(EmptyDirectory, deleteDirectory);
 617         DELETE_SOME(FilesInDirectory, deleteFilesInDirectory);
 618         DELETE_SOME(RecursiveDirectory, deleteDirectoryRecursive);
 619 
 620 #undef DELETE_SOME
 621         default:
 622             break;
 623         }
 624     }
 625 };
 626 
 627 } // namespace
 628 
 629 void Deleter::execute() {
 630     Paths tmp;
 631     tmp.swap(paths);
 632 
 633     // Reorder items to delete.
 634     std::stable_sort(tmp.begin(), tmp.end(), [] (const Paths::value_type& a,
 635                                                 const Paths::value_type& b) {
 636         return a.second < b.second;
 637     });
 638 
 639     std::for_each(tmp.begin(), tmp.end(), DeleterFunctor());
 640 }
 641 
 642 Deleter& Deleter::appendFile(const tstring& path) {
 643     paths.push_back(std::make_pair(path, DeleterFunctor::File));
 644     return *this;
 645 }
 646 
 647 Deleter& Deleter::appendEmptyDirectory(const Directory& dir) {
 648      tstring path =  normalizePath(removeTrailingSlash(dir));
 649      const tstring parent = normalizePath(removeTrailingSlash(dir.parent));
 650      while(parent != path) {
 651          appendEmptyDirectory(path);
 652          path = dirname(path);
 653      }
 654 
 655     return *this;
 656 }
 657 
 658 Deleter& Deleter::appendEmptyDirectory(const tstring& path) {
 659     paths.push_back(std::make_pair(path,
 660                                 DeleterFunctor::EmptyDirectory));
 661     return *this;
 662 }
 663 
 664 Deleter& Deleter::appendAllFilesInDirectory(const tstring& path) {
 665     paths.push_back(std::make_pair(path,
 666                                 DeleterFunctor::FilesInDirectory));
 667     return *this;
 668 }
 669 
 670 Deleter& Deleter::appendRecursiveDirectory(const tstring& path) {
 671     paths.push_back(std::make_pair(path,
 672                             DeleterFunctor::RecursiveDirectory));
 673     return *this;
 674 }
 675 
 676 
 677 FileWriter::FileWriter(const tstring& path): dstPath(path) {
 678     tmpFile = FileUtils::createTempFile(_T("jds"), _T(".tmp"),
 679                                                     FileUtils::dirname(path));
 680 
 681     cleaner.appendFile(tmpFile);
 682 
 683     // we want to get exception on error
 684     tmp.exceptions(std::ifstream::failbit | std::ifstream::badbit);
 685     tmp.open(tmpFile, std::ios::binary | std::ios::trunc);
 686 }
 687 
 688 FileWriter& FileWriter::write(const void* buf, size_t bytes) {
 689     tmp.write(static_cast<const char*>(buf), bytes);
 690     return *this;
 691 }
 692 
 693 void FileWriter::finalize() {
 694     tmp.close();
 695 
 696     FileUtils::moveFile(tmpFile, dstPath, false);
 697 
 698     // cancel file deletion
 699     cleaner.cancel();
 700 }
 701 
 702 } //  namespace FileUtils