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
  66             && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0;
  67 }
  68 } // namespace
  69 
  70 bool isDirectory(const tstring &filePath) {
  71     return isDirectoryAttrs(GetFileAttributes(filePath.c_str()));
  72 }
  73 
  74 bool isDirectoryNotEmpty(const tstring &dirPath) {
  75     if (!isDirectory(dirPath)) {
  76         return false;
  77     }
  78     return FALSE == PathIsDirectoryEmpty(dirPath.c_str());
  79 }
  80 
  81 tstring dirname(const tstring &path) {
  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,
 153             FILE_ATTRIBUTE_NORMAL, NULL);
 154     // if the file exists => h == INVALID_HANDLE_VALUE & GetLastError
 155     // returns ERROR_FILE_EXISTS
 156     if (h != INVALID_HANDLE_VALUE) {
 157         CloseHandle(h);
 158         LOG_TRACE(tstrings::any() << "Created [" << path << "] file");
 159         return true;
 160     }
 161     return false;
 162 }
 163 
 164 } // namespace
 165 
 166 tstring createTempFile(const tstring &prefix, const tstring &suffix,
 167         const tstring &path) {
 168     const tstring invalidChars = reservedFilenameChars();
 169 
 170     if (prefix.find_first_of(invalidChars) != tstring::npos) {
 171         JP_THROW(tstrings::any() << "Illegal characters in prefix=" << prefix);
 172     }
 173 
 174     if (suffix.find_first_of(invalidChars) != tstring::npos) {
 175         JP_THROW(tstrings::any() << "Illegal characters in suffix=" << suffix);
 176     }
 177 
 178     int rnd = (int)GetTickCount();
 179 
 180     // do no more than 100 attempts
 181     for (int i=0; i<100; i++) {
 182         const tstring filePath = mkpath() << path << (prefix
 183                 + (tstrings::any() << (rnd + i)).tstr() + suffix);
 184         if (createNewFile(filePath)) {
 185             return filePath;
 186         }
 187     }
 188 
 189     // 100 attempts failed
 190     JP_THROW(tstrings::any() << "createTempFile("  << prefix << ", "
 191                                                     << suffix << ", "
 192                                                     << path << ") failed");
 193 }
 194 
 195 tstring createTempDirectory(const tstring &prefix, const tstring &suffix,
 196         const tstring &basedir) {
 197     const tstring filePath = createTempFile(prefix, suffix, basedir);
 198     // delete the file and create directory with the same name
 199     deleteFile(filePath);
 200     createDirectory(filePath);
 201     return filePath;
 202 }
 203 
 204 tstring createUniqueFile(const tstring &prototype) {
 205     if (createNewFile(prototype)) {
 206         return prototype;
 207     }
 208 
 209     return createTempFile(replaceSuffix(basename(prototype)),
 210             suffix(prototype), dirname(prototype));
 211 }
 212 
 213 namespace {
 214 
 215 void createDir(const tstring path, LPSECURITY_ATTRIBUTES saAttr,
 216         tstring_array* createdDirs=0) {
 217     if (CreateDirectory(path.c_str(), saAttr)) {
 218         LOG_TRACE(tstrings::any() << "Created [" << path << "] directory");
 219         if (createdDirs) {
 220             createdDirs->push_back(removeTrailingSlash(path));
 221         }
 222     } else {
 223         const DWORD createDirectoryErr = GetLastError();
 224         // if saAttr is specified, fail even if the directory exists
 225         if (saAttr != NULL || !isDirectory(path)) {
 226             JP_THROW(SysError(tstrings::any() << "CreateDirectory("
 227                 << path << ") failed", CreateDirectory, createDirectoryErr));
 228         }
 229     }
 230 }
 231 
 232 }
 233 
 234 void createDirectory(const tstring &path, tstring_array* createdDirs) {
 235     const tstring dirPath = removeTrailingSlash(path) + _T("\\");
 236 
 237     tstring::size_type pos = dirPath.find_first_of(_T("\\/"));
 238     while (pos != tstring::npos) {
 239         const tstring subdirPath = dirPath.substr(0, pos + 1);
 240         createDir(subdirPath, NULL, createdDirs);
 241         pos = dirPath.find_first_of(_T("\\/"), pos + 1);
 242     }
 243 }
 244 
 245 
 246 void copyFile(const tstring& fromPath, const tstring& toPath,
 247         bool failIfExists) {
 248     createDirectory(dirname(toPath));
 249     if (!CopyFile(fromPath.c_str(), toPath.c_str(),
 250             (failIfExists ? TRUE : FALSE))) {
 251         JP_THROW(SysError(tstrings::any()
 252                 << "CopyFile(" << fromPath << ", " << toPath << ", "
 253                 << failIfExists << ") failed", CopyFile));
 254     }
 255     LOG_TRACE(tstrings::any() << "Copied [" << fromPath << "] file to ["
 256             << toPath << "]");
 257 }
 258 
 259 
 260 namespace {
 261 
 262 void moveFileImpl(const tstring& fromPath, const tstring& toPath,
 263         DWORD flags) {
 264     const bool isDir = isDirectory(fromPath);
 265     if (!MoveFileEx(fromPath.c_str(), toPath.empty() ? NULL : toPath.c_str(),
 266             flags)) {
 267         JP_THROW(SysError(tstrings::any() << "MoveFileEx(" << fromPath
 268                 << ", " << toPath << ", " << flags << ") failed", MoveFileEx));
 269     }
 270 
 271     const bool onReboot = 0 != (flags & MOVEFILE_DELAY_UNTIL_REBOOT);
 272 
 273     const LPCTSTR label = isDir ? _T("folder") : _T("file");
 274 
 275     tstrings::any msg;
 276     if (!toPath.empty()) {
 277         if (onReboot) {
 278             msg << "Move";
 279         } else {
 280             msg << "Moved";
 281         }
 282         msg << " '" << fromPath << "' " << label << " to '" << toPath << "'";
 283     } else {
 284         if (onReboot) {
 285             msg << "Delete";
 286         } else {
 287             msg << "Deleted";
 288         }
 289         msg << " '" << fromPath << "' " << label;
 290     }
 291     if (onReboot) {
 292         msg << " on reboot";
 293     }
 294     LOG_TRACE(msg);
 295 }
 296 
 297 } // namespace
 298 
 299 
 300 void moveFile(const tstring& fromPath, const tstring& toPath,
 301         bool failIfExists) {
 302     createDirectory(dirname(toPath));
 303 
 304     DWORD flags = MOVEFILE_COPY_ALLOWED;
 305     if (!failIfExists) {
 306         flags |= MOVEFILE_REPLACE_EXISTING;
 307     }
 308 
 309     moveFileImpl(fromPath, toPath, flags);
 310 }
 311 
 312 void deleteFile(const tstring &path)
 313 {
 314     if (!deleteFile(path, std::nothrow)) {
 315         JP_THROW(SysError(tstrings::any()
 316                 << "DeleteFile(" << path << ") failed", DeleteFile));
 317     }
 318 }
 319 
 320 namespace {
 321 
 322 bool notFound(const DWORD status=GetLastError()) {
 323     return status == ERROR_FILE_NOT_FOUND || status == ERROR_PATH_NOT_FOUND;
 324 }
 325 
 326 bool deleteFileImpl(const std::nothrow_t &, const tstring &path) {
 327     const bool deleted = (DeleteFile(path.c_str()) != 0);
 328     if (deleted) {
 329         LOG_TRACE(tstrings::any() << "Deleted [" << path << "] file");
 330         return true;
 331     }
 332     return notFound();
 333 }
 334 
 335 } // namespace
 336 
 337 bool deleteFile(const tstring &path, const std::nothrow_t &) throw()
 338 {
 339     bool deleted = deleteFileImpl(std::nothrow, path);
 340     const DWORD status = GetLastError();
 341     if (!deleted && status == ERROR_ACCESS_DENIED) {
 342         DWORD attrs = GetFileAttributes(path.c_str());
 343         SetLastError(status);
 344         if (attrs == INVALID_FILE_ATTRIBUTES) {
 345             return false;
 346         }
 347         if (attrs & FILE_ATTRIBUTE_READONLY) {
 348             // DeleteFile() failed because file is R/O.
 349             // Remove R/O attribute and retry DeleteFile().
 350             attrs &= ~FILE_ATTRIBUTE_READONLY;
 351             if (SetFileAttributes(path.c_str(), attrs)) {
 352                 LOG_TRACE(tstrings::any() << "Discarded R/O attribute from ["
 353                                                         << path << "] file");
 354                 deleted = deleteFileImpl(std::nothrow, path);
 355             } else {
 356                 LOG_WARNING(SysError(tstrings::any()
 357                             << "Failed to discard R/O attribute from ["
 358                             << path << "] file. File will not be deleted",
 359                             SetFileAttributes).what());
 360                 SetLastError(status);
 361             }
 362         }
 363     }
 364 
 365     return deleted || notFound();
 366 }
 367 
 368 void deleteDirectory(const tstring &path)
 369 {
 370     if (!deleteDirectory(path, std::nothrow)) {
 371         JP_THROW(SysError(tstrings::any()
 372                 << "RemoveDirectory(" << path << ") failed", RemoveDirectory));
 373     }
 374 }
 375 
 376 bool deleteDirectory(const tstring &path, const std::nothrow_t &) throw()
 377 {
 378     const bool deleted = (RemoveDirectory(path.c_str()) != 0);
 379     if (deleted) {
 380         LOG_TRACE(tstrings::any() << "Deleted [" << path << "] directory");
 381     }
 382     return deleted || notFound();
 383 }
 384 
 385 namespace {
 386 
 387 class DeleteFilesCallback: public DirectoryCallback {
 388 public:
 389     explicit DeleteFilesCallback(bool ff): failfast(ff), failed(false) {
 390     }
 391 
 392     virtual bool onFile(const tstring& path) {
 393         if (failfast) {
 394             deleteFile(path);
 395         } else {
 396             updateStatus(deleteFile(path, std::nothrow));
 397         }
 398         return true;
 399     }
 400 
 401     bool good() const {
 402         return !failed;
 403     }
 404 
 405 protected:
 406     void updateStatus(bool success) {
 407         if (!success) {
 408             failed = true;
 409         }
 410     }
 411 
 412     const bool failfast;
 413 private:
 414     bool failed;
 415 };
 416 
 417 class DeleteAllCallback: public DeleteFilesCallback {
 418 public:
 419     explicit DeleteAllCallback(bool failfast): DeleteFilesCallback(failfast) {
 420     }
 421 
 422     virtual bool onDirectory(const tstring& path) {
 423         if (failfast) {
 424             deleteDirectoryRecursive(path);
 425         } else {
 426             updateStatus(deleteDirectoryRecursive(path, std::nothrow));
 427         }
 428         return true;
 429     }
 430 };
 431 
 432 
 433 class BatchDeleter {
 434     const tstring dirPath;
 435     bool recursive;
 436 public:
 437     explicit BatchDeleter(const tstring& path): dirPath(path) {
 438         deleteSubdirs(false);
 439     }
 440 
 441     BatchDeleter& deleteSubdirs(bool v) {
 442         recursive = v;
 443         return *this;
 444     }
 445 
 446     void execute() const {
 447         if (!isFileExists(dirPath)) {
 448             return;
 449         }
 450         iterateDirectory(true /* fail fast */);
 451         if (recursive) {
 452             deleteDirectory(dirPath);
 453         }
 454     }
 455 
 456     bool execute(const std::nothrow_t&) const {
 457         if (!isFileExists(dirPath)) {
 458             return true;
 459         }
 460 
 461         if (!isDirectory(dirPath)) {
 462             return false;
 463         }
 464 
 465         JP_TRY;
 466         if (!iterateDirectory(false /* ignore errors */)) {
 467             return false;
 468         }
 469         if (recursive) {
 470             return deleteDirectory(dirPath, std::nothrow);
 471         }
 472         return true;
 473         JP_CATCH_ALL;
 474 
 475         return false;
 476     }
 477 
 478 private:
 479     bool iterateDirectory(bool failfast) const {
 480         std::unique_ptr<DeleteFilesCallback> callback;
 481         if (recursive) {
 482             callback = std::unique_ptr<DeleteFilesCallback>(
 483                                             new DeleteAllCallback(failfast));
 484         } else {
 485             callback = std::unique_ptr<DeleteFilesCallback>(
 486                                             new DeleteFilesCallback(failfast));
 487         }
 488 
 489         FileUtils::iterateDirectory(dirPath, *callback);
 490         return callback->good();
 491     }
 492 };
 493 
 494 } // namespace
 495 
 496 void deleteFilesInDirectory(const tstring &dirPath) {
 497     BatchDeleter(dirPath).execute();
 498 }
 499 
 500 bool deleteFilesInDirectory(const tstring &dirPath,
 501                                             const std::nothrow_t &) throw() {
 502     return BatchDeleter(dirPath).execute(std::nothrow);
 503 }
 504 
 505 void deleteDirectoryRecursive(const tstring &dirPath) {
 506     BatchDeleter(dirPath).deleteSubdirs(true).execute();
 507 }
 508 
 509 bool deleteDirectoryRecursive(const tstring &dirPath,
 510                                             const std::nothrow_t &) throw() {
 511     return BatchDeleter(dirPath).deleteSubdirs(true).execute(std::nothrow);
 512 }
 513 
 514 namespace {
 515 
 516 struct FindFileDeleter {
 517     typedef HANDLE pointer;
 518 
 519     void operator()(HANDLE h) {
 520         if (h && h != INVALID_HANDLE_VALUE) {
 521             FindClose(h);
 522         }
 523     }
 524 };
 525 
 526 typedef std::unique_ptr<HANDLE, FindFileDeleter> UniqueFindFileHandle;
 527 
 528 }; // namesace
 529 void iterateDirectory(const tstring &dirPath, DirectoryCallback& callback)
 530 {
 531     const tstring searchString = combinePath(dirPath, _T("*"));
 532     WIN32_FIND_DATA findData;
 533     UniqueFindFileHandle h(FindFirstFile(searchString.c_str(), &findData));
 534     if (h.get() == INVALID_HANDLE_VALUE) {
 535         // GetLastError() == ERROR_FILE_NOT_FOUND is OK
 536         // - no files in the directory
 537         // ERROR_PATH_NOT_FOUND is returned
 538         // if the parent directory does not exist
 539         if (GetLastError() != ERROR_FILE_NOT_FOUND) {
 540             JP_THROW(SysError(tstrings::any() << "FindFirstFile("
 541                     << dirPath << ") failed", FindFirstFile));
 542         }
 543         return;
 544     }
 545 
 546     do {
 547         const tstring fname(findData.cFileName);
 548         const tstring filePath = combinePath(dirPath, fname);
 549         if (!isDirectoryAttrs(findData.dwFileAttributes)) {
 550             if (!callback.onFile(filePath)) {
 551                 return;
 552             }
 553         } else if (fname != _T(".") && fname != _T("..")) {
 554             if (!callback.onDirectory(filePath)) {
 555                 return;
 556             }
 557         }
 558     } while (FindNextFile(h.get(), &findData));
 559 
 560     // expect GetLastError() == ERROR_NO_MORE_FILES
 561     if (GetLastError() != ERROR_NO_MORE_FILES) {
 562         JP_THROW(SysError(tstrings::any() << "FindNextFile("
 563                 << dirPath << ") failed", FindNextFile));
 564     }
 565 }
 566 
 567 
 568 tstring replaceSuffix(const tstring& path, const tstring& newSuffix) {
 569     return (path.substr(0, path.size() - suffix(path).size()) + newSuffix);
 570 }
 571 
 572 
 573 DirectoryIterator& DirectoryIterator::findItems(tstring_array& v) {
 574     if (!isDirectory(root)) {
 575         return *this;
 576     }
 577 
 578     iterateDirectory(root, *this);
 579     v.insert(v.end(), items.begin(), items.end());
 580     items = tstring_array();
 581     return *this;
 582 }
 583 
 584 bool DirectoryIterator::onFile(const tstring& path) {
 585     if (theWithFiles) {
 586         items.push_back(path);
 587     }
 588     return true;
 589 }
 590 
 591 bool DirectoryIterator::onDirectory(const tstring& path) {
 592     if (theWithFolders) {
 593         items.push_back(path);
 594     }
 595     if (theRecurse) {
 596         DirectoryIterator(path).recurse(theRecurse)
 597                 .withFiles(theWithFiles)
 598                 .withFolders(theWithFolders)
 599                 .findItems(items);
 600     }
 601     return true;
 602 }
 603 
 604 
 605 namespace {
 606 
 607 struct DeleterFunctor {
 608     // Order of items in the following enum is important!
 609     // It controls order in which items of particular type will be deleted.
 610     // See Deleter::execute().
 611     enum {
 612         File,
 613         FilesInDirectory,
 614         RecursiveDirectory,
 615         EmptyDirectory
 616     };
 617 
 618     void operator () (const Deleter::Path& path) const {
 619         switch (path.second) {
 620 #define DELETE_SOME(o, f)\
 621         case o:\
 622             f(path.first, std::nothrow);\
 623             break
 624 
 625         DELETE_SOME(File, deleteFile);
 626         DELETE_SOME(EmptyDirectory, deleteDirectory);
 627         DELETE_SOME(FilesInDirectory, deleteFilesInDirectory);
 628         DELETE_SOME(RecursiveDirectory, deleteDirectoryRecursive);
 629 
 630 #undef DELETE_SOME
 631         default:
 632             break;
 633         }
 634     }
 635 };
 636 
 637 } // namespace
 638 
 639 void Deleter::execute() {
 640     Paths tmp;
 641     tmp.swap(paths);
 642 
 643     // Reorder items to delete.
 644     std::stable_sort(tmp.begin(), tmp.end(), [] (const Paths::value_type& a,
 645                                                 const Paths::value_type& b) {
 646         return a.second < b.second;
 647     });
 648 
 649     std::for_each(tmp.begin(), tmp.end(), DeleterFunctor());
 650 }
 651 
 652 Deleter& Deleter::appendFile(const tstring& path) {
 653     paths.push_back(std::make_pair(path, DeleterFunctor::File));
 654     return *this;
 655 }
 656 
 657 Deleter& Deleter::appendEmptyDirectory(const Directory& dir) {
 658      tstring path =  normalizePath(removeTrailingSlash(dir));
 659      const tstring parent = normalizePath(removeTrailingSlash(dir.parent));
 660      while(parent != path) {
 661          appendEmptyDirectory(path);
 662          path = dirname(path);
 663      }
 664 
 665     return *this;
 666 }
 667 
 668 Deleter& Deleter::appendEmptyDirectory(const tstring& path) {
 669     paths.push_back(std::make_pair(path, DeleterFunctor::EmptyDirectory));
 670     return *this;
 671 }
 672 
 673 Deleter& Deleter::appendAllFilesInDirectory(const tstring& path) {
 674     paths.push_back(std::make_pair(path, DeleterFunctor::FilesInDirectory));
 675     return *this;
 676 }
 677 
 678 Deleter& Deleter::appendRecursiveDirectory(const tstring& path) {
 679     paths.push_back(std::make_pair(path, DeleterFunctor::RecursiveDirectory));
 680     return *this;
 681 }
 682 
 683 
 684 FileWriter::FileWriter(const tstring& path): dstPath(path) {
 685     tmpFile = FileUtils::createTempFile(_T("jds"), _T(".tmp"),
 686             FileUtils::dirname(path));
 687 
 688     cleaner.appendFile(tmpFile);
 689 
 690     // we want to get exception on error
 691     tmp.exceptions(std::ifstream::failbit | std::ifstream::badbit);
 692     tmp.open(tmpFile, std::ios::binary | std::ios::trunc);
 693 }
 694 
 695 FileWriter& FileWriter::write(const void* buf, size_t bytes) {
 696     tmp.write(static_cast<const char*>(buf), bytes);
 697     return *this;
 698 }
 699 
 700 void FileWriter::finalize() {
 701     tmp.close();
 702 
 703     FileUtils::moveFile(tmpFile, dstPath, false);
 704 
 705     // cancel file deletion
 706     cleaner.cancel();
 707 }
 708 
 709 } //  namespace FileUtils