/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2023 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include #include #include #include #include namespace { struct [[codegen::Dictionary(UrlSynchronization)]] Parameters { // The URL or urls from where the files are downloaded. If multiple URLs are // provided, all files will be downloaded to the same directory and the filename // parameter must not be specified simultaneously std::variant> url; // This identifier will be part of the used folder structure and, can be used to // manually find the downloaded folder in the synchronization folder std::string identifier [[codegen::identifier()]]; // If this value is set to 'true' and it is not overwritten by the global // settings, the file(s) pointed to by this URLSynchronization will always be // downloaded, thus overwriting the local files. This is useful for files that are // updated regularly remotely and should be fetch at every startup std::optional forceOverride [[codegen::key("Override")]]; // If this value is set to 'true' (the default), the hash of the URL is appended // to the directory name to produce a unique directory under all circumstances. If // this is not desired, the URLSynchronization use the bare directory name alone // if this value is 'false'. If this value is 'false', the identifier has to be // specified std::optional useHash; // Optional to provide filename to override the one which is otherwise // automatically created from the url. If this value is specified, the url // parameter only only contain exactly one URL std::optional filename; }; #include "urlsynchronization_codegen.cpp" } // namespace namespace openspace { documentation::Documentation UrlSynchronization::Documentation() { return codegen::doc("sync_synchronization_url"); } UrlSynchronization::UrlSynchronization(const ghoul::Dictionary& dictionary, std::filesystem::path synchronizationRoot) : ResourceSynchronization(std::move(synchronizationRoot)) { const Parameters p = codegen::bake(dictionary); if (std::holds_alternative(p.url)) { _urls.push_back(std::get(p.url)); } else if (std::holds_alternative>(p.url)) { _urls = std::get>(p.url); } else { throw ghoul::MissingCaseException(); } if (p.filename.has_value() && _urls.size() > 1) { throw ghoul::RuntimeError(fmt::format( "UrlSynchronization ({}) requested overwrite filename but specified {} URLs " "to download, which is not legal", p.identifier, _urls.size() )); } _filename = p.filename.value_or(_filename); _forceOverride = p.forceOverride.value_or(_forceOverride); const bool useHash = p.useHash.value_or(true); _identifier = p.identifier; if (useHash) { // We just merge all of the URLs together to generate a hash that works for this std::vector urls = _urls; std::sort(urls.begin(), urls.end()); size_t hash = std::hash{}( std::accumulate(urls.begin(), urls.end(), std::string()) ); _identifier += fmt::format("({})", hash); } } UrlSynchronization::~UrlSynchronization() { if (_syncThread.joinable()) { cancel(); _syncThread.join(); } } std::filesystem::path UrlSynchronization::directory() const { return _synchronizationRoot / "url" / _identifier / "files"; } void UrlSynchronization::start() { if (isSyncing()) { return; } _state = State::Syncing; if (hasSyncFile() && !_forceOverride) { _state = State::Resolved; return; } _syncThread = std::thread([this]() { std::unordered_map fileSizes; std::mutex fileSizeMutex; size_t nDownloads = 0; std::atomic_bool startedAllDownloads = false; std::vector> downloads; for (const std::string& url : _urls) { if (_filename.empty() || _urls.size() > 1) { std::filesystem::path fn = std::filesystem::path(url).filename(); if (fn.empty() && url.back() == '/') { // If the user provided a path that ends in / the `filename` will // result in an empty path with causes the downloading to fail fn = std::filesystem::path(url).parent_path().filename(); } std::string name = fn.string(); // We can not create filenames with question marks name.erase(std::remove(name.begin(), name.end(), '?'), name.end()); _filename = name; } std::filesystem::path destination = directory() / (_filename + ".tmp"); auto download = std::make_unique( url, destination, HttpFileDownload::Overwrite::Yes ); HttpFileDownload* dl = download.get(); downloads.push_back(std::move(download)); ++nDownloads; dl->onProgress( [this, url, &fileSizes, &fileSizeMutex, &startedAllDownloads, &nDownloads](int64_t, std::optional totalBytes) { if (!totalBytes.has_value()) { return !_shouldCancel; } std::lock_guard guard(fileSizeMutex); fileSizes[url] = *totalBytes; if (!_nTotalBytesKnown && startedAllDownloads && fileSizes.size() == nDownloads) { _nTotalBytesKnown = true; _nTotalBytes = 0; for (const std::pair& fs : fileSizes) { _nTotalBytes += fs.second; } } return !_shouldCancel; }); dl->start(); } startedAllDownloads = true; bool failed = false; for (const std::unique_ptr& d : downloads) { d->wait(); if (!d->hasSucceeded()) { failed = true; continue; } // If we are forcing the override, we download to a temporary file first, so // when we are done here, we need to rename the file to the original name std::filesystem::path tempName = d->destination(); std::filesystem::path originalName = tempName; // Remove the .tmp extension originalName.replace_extension(""); if (std::filesystem::is_regular_file(originalName)) { std::filesystem::remove(originalName); } std::error_code ec; std::filesystem::rename(tempName, originalName, ec); if (ec) { LERRORC( "URLSynchronization", fmt::format("Error renaming file {} to {}", tempName, originalName) ); failed = true; } } if (!failed) { createSyncFile(); } else { for (const std::unique_ptr& d : downloads) { d->cancel(); } } _state = State::Resolved; }); } void UrlSynchronization::cancel() { _shouldCancel = true; _state = State::Unsynced; } std::string UrlSynchronization::generateUid() { return _identifier; } } // namespace openspace