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// boost
13#include <boost/algorithm/string/predicate.hpp>
14
15// Poco
16#include <Poco/DateTimeFormat.h>
17#include <Poco/DateTimeFormatter.h>
18#include <Poco/DirectoryIterator.h>
19#include <Poco/File.h>
20#include <Poco/Path.h>
21// Visual Studio complains with the inclusion of Poco/FileStream
22// disabling this warning.
23#if defined(_WIN32) || defined(_WIN64)
24#pragma warning(push)
25#pragma warning(disable : 4250)
26#include <Poco/FileStream.h>
27#include <Poco/NullStream.h>
28#include <Winhttp.h>
29#pragma warning(pop)
30#else
31#include <Poco/FileStream.h>
32#include <Poco/NullStream.h>
33#include <cstdlib>
34#endif
35
36// jsoncpp
37#include <json/json.h>
38
39// std
40#include <fstream>
41
42namespace Mantid::DataHandling {
43using namespace Kernel;
44using namespace Poco::Net;
45
46// Register the algorithm into the AlgorithmFactory
47DECLARE_ALGORITHM(DownloadInstrument)
48
49//----------------------------------------------------------------------------------------------
53
54//----------------------------------------------------------------------------------------------
55
57const std::string DownloadInstrument::name() const { return "DownloadInstrument"; }
58
60int DownloadInstrument::version() const { return 1; }
61
63const std::string DownloadInstrument::category() const { return "DataHandling\\Instrument"; }
64
66const std::string DownloadInstrument::summary() const {
67 return "Checks the Mantid instrument repository against the local "
68 "instrument files, and downloads updates as appropriate.";
69}
70
71//----------------------------------------------------------------------------------------------
76
77 declareProperty("ForceUpdate", false, "Ignore cache information");
78 declareProperty("FileDownloadCount", 0, "The number of files downloaded by this algorithm", Direction::Output);
79}
80
81//----------------------------------------------------------------------------------------------
85 StringToStringMap fileMap;
86 setProperty("FileDownloadCount", 0);
87
88 // to aid in general debugging, always ask github for what the rate limit
89 // status is. This doesn't count against rate limit.
90 try {
91 GitHubApiHelper inetHelper;
94 g_log.debug() << "Unable to get the rate limit from GitHub: " << ex.what() << '\n';
95 }
96
97 try {
98 fileMap = processRepository();
100 std::string errorText(ex.what());
101 if (errorText.find("rate limit") != std::string::npos) {
102 g_log.information() << "Instrument Definition Update: " << errorText << '\n';
103 } else {
104 // log the failure at Notice Level
105 g_log.notice("Internet Connection Failed - cannot update instrument "
106 "definitions. Please check your connection. If you are behind a "
107 "proxy server, consider setting proxy.host and proxy.port in "
108 "the Mantid properties file or using the config object.");
109 // log this error at information level
110 g_log.information() << errorText << '\n';
111 }
112 return;
113 }
114
115 if (fileMap.empty()) {
116 g_log.notice("All instrument definitions up to date");
117 } else {
118 std::string s = (fileMap.size() > 1) ? "s" : "";
119 g_log.notice() << "Downloading " << fileMap.size() << " file" << s << " from the instrument repository\n";
120 }
121
122 for (auto &itMap : fileMap) {
123 // download a file
124 if (boost::algorithm::ends_with(itMap.second, "Facilities.xml")) {
125 g_log.notice("A new Facilities.xml file has been downloaded, this will "
126 "take effect next time Mantid is started.");
127 } else {
128 g_log.information() << "Downloading \"" << itMap.second << "\" from \"" << itMap.first << "\"\n";
129 }
130 doDownloadFile(itMap.first, itMap.second);
131 }
132
133 setProperty("FileDownloadCount", static_cast<int>(fileMap.size()));
134}
135
136namespace {
137// Converts a json chunk to a url for the raw file contents.
138std::string getDownloadUrl(Json::Value &contents) {
139 std::string url = contents.get("download_url", "").asString();
140 if (url.empty()) { // guess it from html url
141 url = contents.get("html_url", "").asString();
142 if (url.empty())
143 throw std::runtime_error("Failed to find download link");
144 url = url + "?raw=1";
145 }
146
147 return url;
148}
149} // namespace
150
152 // get the instrument directories
153 auto instrumentDirs = Mantid::Kernel::ConfigService::Instance().getInstrumentDirectories();
154 Poco::Path installPath(instrumentDirs.back());
155 installPath.makeDirectory();
156 Poco::Path localPath(instrumentDirs[0]);
157 localPath.makeDirectory();
158
159 // get the date of the local github.json file if it exists
160 Poco::Path gitHubJson(localPath, "github.json");
161 Poco::File gitHubJsonFile(gitHubJson);
162 Poco::DateTime gitHubJsonDate(1900, 1, 1);
163 bool forceUpdate = this->getProperty("ForceUpdate");
164 if ((!forceUpdate) && gitHubJsonFile.exists() && gitHubJsonFile.isFile()) {
165 gitHubJsonDate = gitHubJsonFile.getLastModified();
166 }
167
168 // get the file list from github
169 StringToStringMap headers;
170 headers.emplace("if-modified-since",
171 Poco::DateTimeFormatter::format(gitHubJsonDate, Poco::DateTimeFormat::HTTP_FORMAT));
172 std::string gitHubInstrumentRepoUrl = ConfigService::Instance().getString("UpdateInstrumentDefinitions.URL");
173 if (gitHubInstrumentRepoUrl.empty()) {
174 throw std::runtime_error("Property UpdateInstrumentDefinitions.URL is not defined, "
175 "this should point to the location of the instrument "
176 "directory in the github API "
177 "e.g. "
178 "https://api.github.com/repos/mantidproject/mantid/contents/"
179 "instrument.");
180 }
181 StringToStringMap fileMap;
182 try {
183 doDownloadFile(gitHubInstrumentRepoUrl, gitHubJson.toString(), headers);
184 } catch (Exception::InternetError &ex) {
185 if (ex.errorCode() == static_cast<int>(InternetHelper::HTTPStatus::NOT_MODIFIED)) {
186 // No changes since last time
187 return fileMap;
188 } else {
189 throw;
190 }
191 }
192
193 // update local repo files
194 Poco::Path installRepoFile(localPath, "install.json");
195 StringToStringMap installShas = getFileShas(installPath.toString());
196 Poco::Path localRepoFile(localPath, "local.json");
197 StringToStringMap localShas = getFileShas(localPath.toString());
198
199 // verify repo info was downloaded correctly
200 if (gitHubJsonFile.getSize() == 0) {
201 std::stringstream msg;
202 msg << "Encountered empty file \"" << gitHubJson.toString() << "\" while determining what to download";
203 throw std::runtime_error(msg.str());
204 }
205
206 // Parse the server JSON response
207 ::Json::CharReaderBuilder readerBuilder;
208 Json::Value serverContents;
209 Poco::FileStream fileStream(gitHubJson.toString(), std::ios::in);
210
211 std::string errors;
212 Json::parseFromStream(readerBuilder, fileStream, &serverContents, &errors);
213 if (errors.size() != 0) {
214 throw std::runtime_error("Unable to parse server JSON file \"" + gitHubJson.toString() + "\"");
215 }
216 fileStream.close();
217
218 std::unordered_set<std::string> repoFilenames;
219
220 for (auto &serverElement : serverContents) {
221 std::string name = serverElement.get("name", "").asString();
222 repoFilenames.insert(name);
223 Poco::Path filePath(localPath, name);
224 if (filePath.getExtension() != "xml")
225 continue;
226 std::string sha = serverElement.get("sha", "").asString();
227 std::string downloadUrl = getDownloadUrl(serverElement);
228
229 // Find shas
230 std::string localSha = getValueOrDefault(localShas, name, "");
231 std::string installSha = getValueOrDefault(installShas, name, "");
232 // Different sha1 on github cf local and global
233 // this will also catch when file is only present on github (as local sha
234 // will be "")
235 if ((sha != installSha) && (sha != localSha)) {
236 fileMap.emplace(downloadUrl,
237 filePath.toString()); // ACTION - DOWNLOAD to localPath
238 } else if ((!localSha.empty()) && (sha == installSha) && (sha != localSha)) // matches install, but different local
239 {
240 fileMap.emplace(downloadUrl, filePath.toString()); // ACTION - DOWNLOAD to
241 // localPath and
242 // overwrite
243 }
244 }
245
246 // remove any .xml files from the local appdata directory that are not present
247 // in the remote instrument repo
248 removeOrphanedFiles(localPath.toString(), repoFilenames);
249
250 return fileMap;
251}
252
261 const std::string &key, const std::string &defaultValue) const {
262 auto element = mapping.find(key);
263 return (element != mapping.end()) ? element->second : defaultValue;
264}
265
271 StringToStringMap filesToSha;
272 try {
273 using Poco::DirectoryIterator;
274 DirectoryIterator end;
275 for (DirectoryIterator it(directoryPath); it != end; ++it) {
276 const auto &entryPath = Poco::Path(it->path());
277 if (entryPath.getExtension() != "xml")
278 continue;
279 std::string sha1 = ChecksumHelper::gitSha1FromFile(entryPath.toString());
280 // Track sha1
281 filesToSha.emplace(entryPath.getFileName(), sha1);
282 }
283 } catch (Poco::Exception &ex) {
284 g_log.error() << "DownloadInstrument: failed to parse the directory: " << directoryPath << " : " << ex.className()
285 << " : " << ex.displayText() << '\n';
286 // silently ignore this exception.
287 } catch (std::exception &ex) {
288 std::stringstream ss;
289 ss << "unknown exception while checking local file system. " << ex.what() << ". Input = " << directoryPath;
290 throw std::runtime_error(ss.str());
291 }
292
293 return filesToSha;
294}
295
301size_t DownloadInstrument::removeOrphanedFiles(const std::string &directoryPath,
302 const std::unordered_set<std::string> &filenamesToKeep) const {
303 // hold files to delete in a set so we don't remove files while iterating over
304 // the directory.
305 std::vector<std::string> filesToDelete;
306
307 try {
308 using Poco::DirectoryIterator;
309 DirectoryIterator end;
310 for (DirectoryIterator it(directoryPath); it != end; ++it) {
311 const auto &entryPath = Poco::Path(it->path());
312 if (entryPath.getExtension() != "xml")
313 continue;
314 if (filenamesToKeep.find(entryPath.getFileName()) == filenamesToKeep.end()) {
315 g_log.debug() << "File not found in remote instrument repository, will "
316 "be deleted: "
317 << entryPath.getFileName() << '\n';
318 filesToDelete.emplace_back(it->path());
319 }
320 }
321 } catch (Poco::Exception &ex) {
322 g_log.error() << "DownloadInstrument: failed to list the directory: " << directoryPath << " : " << ex.className()
323 << " : " << ex.displayText() << '\n';
324 // silently ignore this exception.
325 } catch (std::exception &ex) {
326 std::stringstream ss;
327 ss << "unknown exception while checking local file system. " << ex.what() << ". Input = " << directoryPath;
328 throw std::runtime_error(ss.str());
329 }
330
331 // delete any identified files
332 try {
333 for (const auto &filename : filesToDelete) {
334 Poco::File file(filename);
335 file.remove();
336 }
337 } catch (Poco::Exception &ex) {
338 g_log.error() << "DownloadInstrument: failed to delete file: " << ex.className() << " : " << ex.displayText()
339 << '\n';
340 // silently ignore this exception.
341 } catch (std::exception &ex) {
342 std::stringstream ss;
343 ss << "unknown exception while deleting file: " << ex.what();
344 throw std::runtime_error(ss.str());
345 }
346
347 g_log.debug() << filesToDelete.size() << " Files deleted.\n";
348
349 return filesToDelete.size();
350}
351
366 const std::string &localFilePath,
367 const StringToStringMap &headers) {
368 Poco::File localFile(localFilePath);
369 if (localFile.exists()) {
370 if (!localFile.canWrite()) {
371 std::stringstream msg;
372 msg << "Cannot write file \"" << localFilePath << "\"";
373 throw std::runtime_error(msg.str());
374 }
375 } else {
376 localFile = Poco::File(Poco::Path(localFilePath).parent().toString());
377 if (!localFile.canWrite()) {
378 std::stringstream msg;
379 msg << "Cannot write file \"" << localFilePath << "\"";
380 throw std::runtime_error(msg.str());
381 }
382 }
383
384 GitHubApiHelper inetHelper;
385 inetHelper.headers().insert(headers.begin(), headers.end());
386 const auto retStatus = inetHelper.downloadFile(urlFile, localFilePath);
387 return retStatus;
388}
389
390} // namespace Mantid::DataHandling
#define DECLARE_ALGORITHM(classname)
Definition: Algorithm.h:576
void declareProperty(std::unique_ptr< Kernel::Property > p, const std::string &doc="") override
Add a property to the list of managed properties.
Definition: Algorithm.cpp:1913
TypedValue getProperty(const std::string &name) const override
Get the value of a property.
Definition: Algorithm.cpp:2076
std::string toString() const override
Serialize an object to a string.
Definition: Algorithm.cpp:905
Kernel::Logger & g_log
Definition: Algorithm.h:451
DownloadInstrument : Downloads one or more instrument files to the local instrument cache from the in...
void exec() override
Execute the algorithm.
int version() const override
Algorithm's version for identification.
size_t removeOrphanedFiles(const std::string &directoryPath, const std::unordered_set< std::string > &filenamesToKeep) const
removes any .xml files in a directory that are not in filenamesToKeep
StringToStringMap getFileShas(const std::string &directoryPath)
Creates or updates the json file of a directories contents.
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
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.
Definition: Exception.cpp:311
const int & errorCode() const
Writes out the range and limits.
Definition: Exception.cpp:317
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:114
void notice(const std::string &msg)
Logs at notice level.
Definition: Logger.cpp:95
void error(const std::string &msg)
Logs at error level.
Definition: Logger.cpp:77
void information(const std::string &msg)
Logs at information level.
Definition: Logger.cpp:105
static T & Instance()
Return a reference to the Singleton instance, creating it if it does not already exist Creation is do...
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