Mantid
Loading...
Searching...
No Matches
DownloadInstrument.cpp
Go to the documentation of this file.
1// Mantid Repository : https://github.com/mantidproject/mantid
2//
3// Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
4// NScD Oak Ridge National Laboratory, European Spallation Source,
5// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
6// SPDX - License - Identifier: GPL - 3.0 +
11
12// Poco
13#include <Poco/DateTimeFormat.h>
14#include <Poco/DateTimeFormatter.h>
15// Visual Studio complains with the inclusion of Poco/FileStream
16// disabling this warning.
17#if defined(_WIN32) || defined(_WIN64)
18#pragma warning(push)
19#pragma warning(disable : 4250)
20#include <Poco/FileStream.h>
21#include <Poco/NullStream.h>
22#include <Winhttp.h>
23#pragma warning(pop)
24#else
25#include <Poco/FileStream.h>
26#include <Poco/NullStream.h>
27#include <cstdlib>
28#endif
29
30// jsoncpp
31#include <json/json.h>
32
33// std
34#include <filesystem>
35#include <fstream>
36
37namespace Mantid::DataHandling {
38using namespace Kernel;
39using namespace Poco::Net;
40
41// Register the algorithm into the AlgorithmFactory
42DECLARE_ALGORITHM(DownloadInstrument)
43
44//----------------------------------------------------------------------------------------------
48
49//----------------------------------------------------------------------------------------------
50
52const std::string DownloadInstrument::name() const { return "DownloadInstrument"; }
53
55int DownloadInstrument::version() const { return 1; }
56
58const std::string DownloadInstrument::category() const { return "DataHandling\\Instrument"; }
59
61const std::string DownloadInstrument::summary() const {
62 return "Checks the Mantid instrument repository against the local "
63 "instrument files, and downloads updates as appropriate.";
64}
65
66//----------------------------------------------------------------------------------------------
71
72 declareProperty("ForceUpdate", false, "Ignore cache information");
73 declareProperty("FileDownloadCount", 0, "The number of files downloaded by this algorithm", Direction::Output);
74}
75
76//----------------------------------------------------------------------------------------------
80 setProperty("FileDownloadCount", 0);
81
82 // to aid in general debugging, always ask github for what the rate limit
83 // status is. This doesn't count against rate limit.
84 try {
85 GitHubApiHelper inetHelper;
88 g_log.debug() << "Unable to get the rate limit from GitHub: " << ex.what() << '\n';
89 }
90
91 StringToStringMap fileMap;
92 try {
93 fileMap = processRepository();
95 std::string errorText(ex.what());
96 if (errorText.find("rate limit") != std::string::npos) {
97 g_log.information() << "Instrument Definition Update: " << errorText << '\n';
98 } else {
99 // log the failure at Notice Level
100 g_log.notice("Internet Connection Failed - cannot update instrument "
101 "definitions. Please check your connection. If you are behind a "
102 "proxy server, consider setting proxy.host and proxy.port in "
103 "the Mantid properties file or using the config object.");
104 // log this error at information level
105 g_log.information() << errorText << '\n';
106 }
107 return;
108 }
109
110 if (fileMap.empty()) {
111 g_log.notice("All instrument definitions up to date");
112 } else {
113 std::string s = (fileMap.size() > 1) ? "s" : "";
114 g_log.notice() << "Downloading " << fileMap.size() << " file" << s << " from the instrument repository\n";
115 }
116
117 for (auto &itMap : fileMap) {
118 // download a file
119 if (itMap.second.ends_with("Facilities.xml")) {
120 g_log.notice("A new Facilities.xml file has been downloaded, this will "
121 "take effect next time Mantid is started.");
122 } else {
123 g_log.information() << "Downloading \"" << itMap.second << "\" from \"" << itMap.first << "\"\n";
124 }
125 doDownloadFile(itMap.first, itMap.second);
127 }
128
129 setProperty("FileDownloadCount", static_cast<int>(fileMap.size()));
130}
131
132namespace {
133// Converts a json chunk to a url for the raw file contents.
134std::string getDownloadUrl(Json::Value &contents) {
135 std::string url = contents.get("download_url", "").asString();
136 if (url.empty()) { // guess it from html url
137 url = contents.get("html_url", "").asString();
138 if (url.empty())
139 throw std::runtime_error("Failed to find download link");
140 url = url + "?raw=1";
141 }
142
143 return url;
144}
145} // namespace
146
148 // get the instrument directories
149 auto instrumentDirs = Mantid::Kernel::ConfigService::Instance().getInstrumentDirectories();
150 std::filesystem::path installPath(instrumentDirs.back());
151 std::filesystem::create_directories(installPath);
152 std::filesystem::path localPath(instrumentDirs[0]);
153 std::filesystem::create_directories(localPath);
154
155 // get the date of the local github.json file if it exists
156 std::filesystem::path gitHubJsonFile = localPath / "github.json";
157 Poco::DateTime gitHubJsonDate(1900, 1, 1);
158 bool forceUpdate = this->getProperty("ForceUpdate");
159 if ((!forceUpdate) && std::filesystem::exists(gitHubJsonFile) && std::filesystem::is_regular_file(gitHubJsonFile)) {
160 auto ftime = std::filesystem::last_write_time(gitHubJsonFile);
161 auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
162 ftime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now());
163 std::time_t cftime = std::chrono::system_clock::to_time_t(sctp);
164 gitHubJsonDate = Poco::Timestamp::fromEpochTime(cftime);
165 }
166
167 // get the file list from github
168 StringToStringMap headers;
169 headers.emplace("if-modified-since",
170 Poco::DateTimeFormatter::format(gitHubJsonDate, Poco::DateTimeFormat::HTTP_FORMAT));
171 std::string gitHubInstrumentRepoUrl = ConfigService::Instance().getString("UpdateInstrumentDefinitions.URL");
172 if (gitHubInstrumentRepoUrl.empty()) {
173 throw std::runtime_error("Property UpdateInstrumentDefinitions.URL is not defined, "
174 "this should point to the location of the instrument "
175 "directory in the github API "
176 "e.g. "
177 "https://api.github.com/repos/mantidproject/mantid/contents/"
178 "instrument.");
179 }
180 StringToStringMap fileMap;
181 try {
182 doDownloadFile(gitHubInstrumentRepoUrl, gitHubJsonFile.string(), headers);
183 } catch (Exception::InternetError &ex) {
184 if (ex.errorCode() == static_cast<int>(InternetHelper::HTTPStatus::NOT_MODIFIED)) {
185 // No changes since last time
186 return fileMap;
187 } else {
188 throw;
189 }
190 }
191
192 // update local repo files
193 std::filesystem::path installRepoFile = localPath / "install.json";
194 StringToStringMap installShas = getFileShas(installPath);
195 std::filesystem::path localRepoFile = localPath / "local.json";
196 StringToStringMap localShas = getFileShas(localPath);
197
198 // verify repo info was downloaded correctly
199 if (std::filesystem::file_size(gitHubJsonFile) == 0) {
200 std::stringstream msg;
201 msg << "Encountered empty file \"" << gitHubJsonFile.string() << "\" while determining what to download";
202 throw std::runtime_error(msg.str());
203 }
204
205 // Parse the server JSON response
206 ::Json::CharReaderBuilder readerBuilder;
207 Json::Value serverContents;
208 Poco::FileStream fileStream(gitHubJsonFile.string(), std::ios::in);
209
210 std::string errors;
211 Json::parseFromStream(readerBuilder, fileStream, &serverContents, &errors);
212 if (errors.size() != 0) {
213 throw std::runtime_error("Unable to parse server JSON file \"" + gitHubJsonFile.string() + "\"");
214 }
215 fileStream.close();
216
217 std::unordered_set<std::string> repoFilenames;
218
219 for (auto &serverElement : serverContents) {
220 std::string elementName = serverElement.get("name", "").asString();
221 repoFilenames.insert(elementName);
222 std::filesystem::path filePath = localPath / elementName;
223 if (filePath.extension().string() != ".xml")
224 continue;
225 std::string sha = serverElement.get("sha", "").asString();
226 std::string downloadUrl = getDownloadUrl(serverElement);
227
228 // Find shas
229 std::string localSha = getValueOrDefault(localShas, elementName, "");
230 std::string installSha = getValueOrDefault(installShas, elementName, "");
231 // Different sha1 on github cf local and global
232 // this will also catch when file is only present on github (as local sha
233 // will be "")
234 if ((sha != installSha) && (sha != localSha)) {
235 fileMap.emplace(downloadUrl,
236 filePath.string()); // ACTION - DOWNLOAD to localPath
237 } else if ((!localSha.empty()) && (sha == installSha) && (sha != localSha)) // matches install, but different local
238 {
239 fileMap.emplace(downloadUrl, filePath.string()); // ACTION - DOWNLOAD to
240 // localPath and
241 // overwrite
242 }
243 }
244
245 // remove any .xml files from the local appdata directory that are not present
246 // in the remote instrument repo
247 removeOrphanedFiles(localPath.string(), repoFilenames);
248
249 return fileMap;
250}
251
260 const std::string &key, const std::string &defaultValue) const {
261 auto element = mapping.find(key);
262 return (element != mapping.end()) ? element->second : defaultValue;
263}
264
270 StringToStringMap filesToSha;
271 try {
272 for (auto const &it : std::filesystem::directory_iterator{directoryPath}) {
273 auto const &entryPath = it.path();
274 if (entryPath.extension().string() != ".xml")
275 continue;
276 std::string sha1 = ChecksumHelper::gitSha1FromFile(entryPath.string());
277 // Track sha1
278 filesToSha.emplace(entryPath.filename().string(), sha1);
279 }
280 } catch (Poco::Exception &ex) {
281 g_log.error() << "DownloadInstrument: failed to parse the directory: " << directoryPath << " : " << ex.className()
282 << " : " << ex.displayText() << '\n';
283 // silently ignore this exception.
284 } catch (std::exception &ex) {
285 std::stringstream ss;
286 ss << "unknown exception while checking local file system. " << ex.what() << ". Input = " << directoryPath;
287 throw std::runtime_error(ss.str());
288 }
289
290 return filesToSha;
291}
292
298size_t DownloadInstrument::removeOrphanedFiles(const std::filesystem::path &directoryPath,
299 const std::unordered_set<std::string> &filenamesToKeep) const {
300 // hold files to delete in a set so we don't remove files while iterating over
301 // the directory.
302 std::vector<std::filesystem::path> filesToDelete;
303
304 try {
305 for (auto const &it : std::filesystem::directory_iterator{directoryPath}) {
306 auto const entryPath = it.path();
307 if (entryPath.extension().string() != ".xml")
308 continue;
309 if (filenamesToKeep.find(entryPath.filename().string()) == filenamesToKeep.end()) {
310 g_log.debug() << "File not found in remote instrument repository, will "
311 "be deleted: "
312 << entryPath.filename().string() << '\n';
313 filesToDelete.emplace_back(entryPath);
314 }
315 }
316 } catch (Poco::Exception &ex) {
317 g_log.error() << "DownloadInstrument: failed to list the directory: " << directoryPath << " : " << ex.className()
318 << " : " << ex.displayText() << '\n';
319 // silently ignore this exception.
320 } catch (std::exception &ex) {
321 std::stringstream ss;
322 ss << "unknown exception while checking local file system. " << ex.what() << ". Input = " << directoryPath;
323 throw std::runtime_error(ss.str());
324 }
325
326 // delete any identified files
327 try {
328 for (const auto &filepath : filesToDelete) {
329 std::filesystem::remove(filepath);
330 }
331 } catch (Poco::Exception &ex) {
332 g_log.error() << "DownloadInstrument: failed to delete file: " << ex.className() << " : " << ex.displayText()
333 << '\n';
334 // silently ignore this exception.
335 } catch (std::exception &ex) {
336 std::stringstream ss;
337 ss << "unknown exception while deleting file: " << ex.what();
338 throw std::runtime_error(ss.str());
339 }
340
341 g_log.debug() << filesToDelete.size() << " Files deleted.\n";
342
343 return filesToDelete.size();
344}
345
360 const std::string &localFilePath,
361 const StringToStringMap &headers) {
362 std::filesystem::path localFile(localFilePath);
363 if (std::filesystem::exists(localFile)) {
364 auto perms = std::filesystem::status(localFile).permissions();
365 if ((perms & std::filesystem::perms::owner_write) == std::filesystem::perms::none) {
366 std::stringstream msg;
367 msg << "Cannot write file \"" << localFilePath << "\"";
368 throw std::runtime_error(msg.str());
369 }
370 } else {
371 localFile = std::filesystem::path(localFilePath).parent_path();
372 auto perms = std::filesystem::status(localFile).permissions();
373 if ((perms & std::filesystem::perms::owner_write) == std::filesystem::perms::none) {
374 std::stringstream msg;
375 msg << "Cannot write file \"" << localFilePath << "\"";
376 throw std::runtime_error(msg.str());
377 }
378 }
379
380 GitHubApiHelper inetHelper;
381 inetHelper.headers().insert(headers.begin(), headers.end());
382 const auto retStatus = inetHelper.downloadFile(urlFile, localFilePath);
383 return retStatus;
384}
385
386} // namespace Mantid::DataHandling
#define DECLARE_ALGORITHM(classname)
Definition Algorithm.h:538
void declareProperty(std::unique_ptr< Kernel::Property > p, const std::string &doc="") override
Add a property to the list of managed properties.
TypedValue getProperty(const std::string &name) const override
Get the value of a property.
Kernel::Logger & g_log
Definition Algorithm.h:422
void interruption_point()
This is called during long-running operations, and check if the algorithm has requested that it be ca...
DownloadInstrument : Downloads one or more instrument files to the local instrument cache from the in...
void exec() override
Execute the algorithm.
StringToStringMap getFileShas(const std::filesystem::path &directoryPath)
Creates or updates the json file of a directories contents.
int version() const override
Algorithm's version for identification.
virtual Kernel::InternetHelper::HTTPStatus doDownloadFile(const std::string &urlFile, const std::string &localFilePath="", const StringToStringMap &headers=StringToStringMap())
Download a url and fetch it inside the local path given.
std::map< std::string, std::string > StringToStringMap
void init() override
Initialize the algorithm's properties.
const std::string name() const override
Algorithms name for identification.
std::string getValueOrDefault(const StringToStringMap &mapping, const std::string &key, const std::string &defaultValue) const
size_t removeOrphanedFiles(const std::filesystem::path &directoryPath, const std::unordered_set< std::string > &filenamesToKeep) const
removes any .xml files in a directory that are not in filenamesToKeep
const std::string summary() const override
Algorithm's summary for use in the GUI and help.
const std::string category() const override
Algorithm's category for identification.
Exception thrown when error occurs accessing an internet resource.
Definition Exception.h:321
const char * what() const noexcept override
Overloaded reporting method.
const int & errorCode() const
Writes out the range and limits.
GitHubApiHelper : A helper class for supporting access to the github api through HTTP and HTTPS,...
std::string getRateLimitDescription()
String describing the rate limit status.
IPropertyManager * setProperty(const std::string &name, const T &value)
Templated method to set the value of a PropertyWithValue.
StringToStringMap & headers()
Returns a reference to the headers map.
virtual HTTPStatus downloadFile(const std::string &urlFile, const std::string &localFilePath="")
Download a url and fetch it inside the local path given.
void debug(const std::string &msg)
Logs at debug level.
Definition Logger.cpp:145
void notice(const std::string &msg)
Logs at notice level.
Definition Logger.cpp:126
void error(const std::string &msg)
Logs at error level.
Definition Logger.cpp:108
void information(const std::string &msg)
Logs at information level.
Definition Logger.cpp:136
MANTID_KERNEL_DLL std::string gitSha1FromFile(const std::string &filepath)
create a git checksum from a file (these match the git hash-object command)
Describes the direction (within an algorithm) of a Property.
Definition Property.h:50
@ Output
An output workspace.
Definition Property.h:54