ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
download.cc
Go to the documentation of this file.
1 // Copyright (c) 2018, ETH Zurich and UNC Chapel Hill.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are met:
6 //
7 // * Redistributions of source code must retain the above copyright
8 // notice, this list of conditions and the following disclaimer.
9 //
10 // * Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 //
14 // * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15 // its contributors may be used to endorse or promote products derived
16 // from this software without specific prior written permission.
17 //
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 // POSSIBILITY OF SUCH DAMAGE.
29 
30 #include "util/download.h"
31 
32 #ifdef COLMAP_DOWNLOAD_ENABLED
33 
34 #include "util/logging.h"
35 #include "util/misc.h"
36 #include "util/string.h"
37 
38 // BoringSSL is currently compatible with OpenSSL 1.1.0
39 #define OPENSSL_API_COMPAT 10100
40 // clang-format off
41 // Must include openssl before curl to build on Windows.
42 #include <openssl/sha.h>
43 
44 // https://stackoverflow.com/a/41873190/1255535
45 #ifdef _MSC_VER
46 #pragma comment(lib, "wldap32.lib")
47 #pragma comment(lib, "crypt32.lib")
48 #pragma comment(lib, "Ws2_32.lib")
49 #define USE_SSLEAY
50 #define USE_OPENSSL
51 #endif
52 
53 // CURL_STATICLIB is only needed on Windows when using static curl library
54 #ifdef _MSC_VER
55 #define CURL_STATICLIB
56 #endif
57 
58 #include <curl/curl.h>
59 #include <curl/easy.h>
60 // clang-format on
61 
62 #include <cstdio>
63 #include <cstdlib>
64 #include <filesystem>
65 #include <fstream>
66 #include <functional>
67 #include <iomanip>
68 #include <iostream>
69 #include <mutex>
70 #include <optional>
71 #include <sstream>
72 #include <string_view>
73 #include <vector>
74 
75 #ifndef _MSC_VER
76 extern "C" {
77 extern char** environ;
78 }
79 #endif
80 
81 namespace colmap {
82 
83 namespace {
84 
85 size_t WriteCurlData(char* buf,
86  size_t size,
87  size_t nmemb,
88  std::ostringstream* data_stream) {
89  *data_stream << std::string_view(buf, size * nmemb);
90  return size * nmemb;
91 }
92 
93 // Structure to hold progress callback and cancel flag
94 struct ProgressData {
95  DownloadProgressCallback callback;
96  bool* canceled;
97 };
98 
99 int CurlProgressCallback(void* clientp,
100  curl_off_t dltotal,
101  curl_off_t dlnow,
102  curl_off_t /* ultotal */,
103  curl_off_t /* ulnow */) {
104  ProgressData* progress_data = static_cast<ProgressData*>(clientp);
105 
106  // Check if download was canceled
107  if (progress_data && progress_data->canceled && *progress_data->canceled) {
108  return 1; // Return non-zero to cancel download
109  }
110 
111  if (progress_data && progress_data->callback) {
112  // Call the user-provided callback (convert curl_off_t to int64_t)
113  progress_data->callback(static_cast<int64_t>(dlnow), static_cast<int64_t>(dltotal));
114 
115  // Check again after callback (user might have canceled)
116  if (progress_data->canceled && *progress_data->canceled) {
117  return 1; // Return non-zero to cancel download
118  }
119  } else {
120  // Default behavior: print progress to stdout (for non-GUI applications)
121  if (dltotal <= 0) {
122  return 0; // Continue download
123  }
124 
125  const double percent = 100.0 * static_cast<double>(dlnow) / static_cast<double>(dltotal);
126  const double dlnow_mb = static_cast<double>(dlnow) / (1024.0 * 1024.0);
127  const double dltotal_mb = static_cast<double>(dltotal) / (1024.0 * 1024.0);
128 
129  // Print progress bar: [====> ] 45.2% (12.3/27.2 MB)
130  const int bar_width = 40;
131  const int filled = static_cast<int>(bar_width * percent / 100.0);
132 
133  // Use \r to overwrite the same line (single-line progress bar)
134  // Use std::cout for normal log output
135  std::cout << "\r[";
136  for (int i = 0; i < bar_width; ++i) {
137  if (i < filled) {
138  std::cout << "=";
139  } else if (i == filled) {
140  std::cout << ">";
141  } else {
142  std::cout << " ";
143  }
144  }
145  std::cout << "] " << std::fixed << std::setprecision(1) << percent << "% ("
146  << dlnow_mb << "/" << dltotal_mb << " MB)" << std::flush;
147  }
148 
149  return 0; // Continue download
150 }
151 
152 struct CurlHandle {
153  CurlHandle() {
154  static std::once_flag global_curl_init;
155  std::call_once(global_curl_init, []() {
156  curl_global_init(CURL_GLOBAL_ALL);
157  });
158 
159  ptr = curl_easy_init();
160  }
161 
162  ~CurlHandle() { curl_easy_cleanup(ptr); }
163 
164  CURL* ptr;
165 };
166 
167 std::string SHA256DigestToHex(const unsigned char* digest, size_t length) {
168  std::ostringstream hex;
169  for (size_t i = 0; i < length; ++i) {
170  hex << std::hex << std::setw(2) << std::setfill('0')
171  << static_cast<unsigned int>(digest[i]);
172  }
173  return hex.str();
174 }
175 
176 std::optional<std::string> GetEnvSafe(const char* key) {
177 #ifdef _MSC_VER
178  size_t size = 0;
179  getenv_s(&size, nullptr, 0, key);
180  if (size == 0) {
181  return std::nullopt;
182  }
183  std::string value(size, ' ');
184  getenv_s(&size, value.data(), size, key);
185  // getenv_s returns a null-terminated string, so we need to remove the
186  // trailing null character in our std::string.
187  CHECK_EQ(value.back(), '\0');
188  return value.substr(0, size - 1);
189 #else
190  // Non-MSVC replacement for std::getenv_s. The safe variant
191  // std::getenv_s is not available on all platforms, unfortunately.
192  // Stores environment variables as: "key1=value1", "key2=value2", ..., null
193  char** env = environ;
194  const std::string_view key_sv(key);
195  for (; *env; ++env) {
196  const std::string_view key_value(*env);
197  if (key_sv.size() < key_value.size() &&
198  key_value.substr(0, key_sv.size()) == key_sv &&
199  key_value[key_sv.size()] == '=') {
200  return std::string(key_value.substr(
201  key_sv.size() + 1, key_value.size() - key_sv.size() - 1));
202  }
203  }
204  return std::nullopt;
205 #endif
206 }
207 
208 } // namespace
209 
210 std::optional<std::string> DownloadFile(
211  const std::string& url,
212  DownloadProgressCallback progress_callback) {
213  std::cout << "Downloading file from: " << url << std::endl;
214 
215  CurlHandle handle;
216  CHECK_NOTNULL(handle.ptr);
217 
218  curl_easy_setopt(handle.ptr, CURLOPT_URL, url.c_str());
219  curl_easy_setopt(handle.ptr, CURLOPT_FOLLOWLOCATION, 1L);
220  curl_easy_setopt(handle.ptr, CURLOPT_WRITEFUNCTION, &WriteCurlData);
221  std::ostringstream data_stream;
222  curl_easy_setopt(handle.ptr, CURLOPT_WRITEDATA, &data_stream);
223 
224  // Enable progress callback for download progress display
225  bool canceled = false;
226  ProgressData progress_data{progress_callback, &canceled};
227  curl_easy_setopt(handle.ptr, CURLOPT_XFERINFOFUNCTION, CurlProgressCallback);
228  curl_easy_setopt(handle.ptr, CURLOPT_XFERINFODATA, &progress_data);
229  curl_easy_setopt(handle.ptr, CURLOPT_NOPROGRESS, 0L);
230 
231  // Respect SSL_CERT_FILE and SSL_CERT_DIR environment variables for
232  // cross-distribution compatibility (e.g., Ubuntu vs RHEL-based systems).
233  const std::optional<std::string> ssl_cert_file = GetEnvSafe("SSL_CERT_FILE");
234  if (ssl_cert_file.has_value() && !ssl_cert_file->empty()) {
235  VLOG(2) << "Using SSL_CERT_FILE: " << *ssl_cert_file;
236  curl_easy_setopt(handle.ptr, CURLOPT_CAINFO, ssl_cert_file->c_str());
237  }
238  const std::optional<std::string> ssl_cert_dir = GetEnvSafe("SSL_CERT_DIR");
239  if (ssl_cert_dir.has_value() && !ssl_cert_dir->empty()) {
240  VLOG(2) << "Using SSL_CERT_DIR: " << *ssl_cert_dir;
241  curl_easy_setopt(handle.ptr, CURLOPT_CAPATH, ssl_cert_dir->c_str());
242  }
243 
244  const CURLcode code = curl_easy_perform(handle.ptr);
245 
246  // Print newline after progress bar (only if using default callback)
247  // Use std::cout for normal log output
248  if (!progress_callback) {
249  std::cout << std::endl; // Move to next line after progress bar
250  }
251 
252  // Check if download was canceled
253  if (canceled || code == CURLE_ABORTED_BY_CALLBACK) {
254  std::cerr << "WARNING: Download was canceled by user" << std::endl;
255  return std::nullopt;
256  }
257 
258  if (code != CURLE_OK) {
259  if (code == CURLE_SSL_CACERT_BADFILE || code == CURLE_SSL_CACERT) {
260  std::cerr << "ERROR: Curl SSL certificate error (code " << code
261  << "). Try setting SSL_CERT_FILE to point to your system's "
262  "CA certificate bundle (e.g., "
263  "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt on "
264  "Ubuntu/Debian)." << std::endl;
265  } else {
266  std::cerr << "ERROR: Curl failed to perform request with code: " << code
267  << " (" << curl_easy_strerror(code) << ")" << std::endl;
268  }
269  return std::nullopt;
270  }
271 
272  long response_code = 0;
273  curl_easy_getinfo(handle.ptr, CURLINFO_RESPONSE_CODE, &response_code);
274  if (response_code != 0 && (response_code < 200 || response_code >= 300)) {
275  std::cerr << "ERROR: Request failed with status: " << response_code << std::endl;
276  return std::nullopt;
277  }
278 
279  std::string data_str = data_stream.str();
280  std::cout << "Downloaded " << data_str.size() << " bytes ("
281  << std::fixed << std::setprecision(2)
282  << static_cast<double>(data_str.size()) / (1024.0 * 1024.0) << " MB)" << std::endl;
283 
284  return data_str;
285 }
286 
287 std::string ComputeSHA256(const std::string_view& str) {
288  unsigned char digest[SHA256_DIGEST_LENGTH];
289  SHA256(reinterpret_cast<const unsigned char*>(str.data()), str.size(), digest);
290  return SHA256DigestToHex(digest, SHA256_DIGEST_LENGTH);
291 }
292 
293 namespace {
294 
295 std::optional<std::filesystem::path> download_cache_dir_overwrite;
296 
297 std::optional<std::filesystem::path> HomeDir() {
298 #ifdef _MSC_VER
299  std::optional<std::string> userprofile = GetEnvSafe("USERPROFILE");
300  if (userprofile.has_value()) {
301  return *userprofile;
302  }
303  const std::optional<std::string> homedrive = GetEnvSafe("HOMEDRIVE");
304  const std::optional<std::string> homepath = GetEnvSafe("HOMEPATH");
305  if (!homedrive.has_value() || !homepath.has_value()) {
306  return std::nullopt;
307  }
308  return std::filesystem::path(*homedrive) / std::filesystem::path(*homepath);
309 #else
310  std::optional<std::string> home = GetEnvSafe("HOME");
311  if (!home.has_value()) {
312  return std::nullopt;
313  }
314  return *home;
315 #endif
316 }
317 
318 } // namespace
319 
320 std::string DownloadAndCacheFile(
321  const std::string& uri,
322  DownloadProgressCallback progress_callback) {
323  const std::vector<std::string> parts = StringSplit(uri, ";");
324  CHECK_EQ(parts.size(), 3)
325  << "Invalid URI format. Expected: <url>;<name>;<sha256>";
326 
327  const std::string& url = parts[0];
328  CHECK(!url.empty());
329  const std::string& name = parts[1];
330  CHECK(!name.empty());
331  const std::string& sha256 = parts[2];
332  CHECK_EQ(sha256.size(), 64);
333 
334  std::filesystem::path download_cache_dir;
335  if (download_cache_dir_overwrite.has_value()) {
336  download_cache_dir = *download_cache_dir_overwrite;
337  } else {
338  const std::optional<std::filesystem::path> home_dir = HomeDir();
339  CHECK(home_dir.has_value());
340  download_cache_dir = *home_dir / ".cache" / "cloudViewer";
341  }
342 
343  if (!std::filesystem::exists(download_cache_dir)) {
344  VLOG(2) << "Creating download cache directory: " << download_cache_dir;
345  CHECK(std::filesystem::create_directories(download_cache_dir));
346  }
347 
348  const auto path = download_cache_dir / (sha256 + "-" + name);
349 
350  if (std::filesystem::exists(path)) {
351  std::cout << "File already cached. Using cached file at: " << path << std::endl;
352  std::vector<char> blob;
353  ReadBinaryBlob(path.string(), &blob);
354  CHECK_EQ(ComputeSHA256(std::string_view(blob.data(), blob.size())), sha256)
355  << "The cached file does not match the expected SHA256";
356  } else {
357  std::cout << "File not found in cache. Downloading from: " << url << std::endl;
358  std::cout << "Cache directory: " << download_cache_dir << std::endl;
359  std::cout << "Cache file path: " << path << std::endl;
360  const std::optional<std::string> blob = DownloadFile(url, progress_callback);
361  CHECK(blob.has_value()) << "Failed to download file";
362  CHECK_EQ(ComputeSHA256(std::string_view(blob->data(), blob->size())), sha256)
363  << "The downloaded file does not match the expected SHA256";
364  std::cout << "Caching file at: " << path << std::endl;
365  std::vector<char> blob_vec(blob->begin(), blob->end());
366  WriteBinaryBlob(path.string(), blob_vec);
367  std::cout << "File successfully cached at: " << path << std::endl;
368  }
369 
370  return path.string();
371 }
372 
373 void OverwriteDownloadCacheDir(std::filesystem::path path) {
374  download_cache_dir_overwrite = std::move(path);
375 }
376 
377 bool IsURI(const std::string& uri) {
378  return StringStartsWith(uri, "http://") ||
379  StringStartsWith(uri, "https://") || StringStartsWith(uri, "file://");
380 }
381 
382 std::filesystem::path GetCachedFilePath(const std::string& uri) {
383  if (!IsURI(uri)) {
384  return std::filesystem::path(); // Not a URI, return empty
385  }
386 
387  const std::vector<std::string> parts = StringSplit(uri, ";");
388  if (parts.size() != 3) {
389  return std::filesystem::path(); // Invalid URI format
390  }
391 
392  const std::string& sha256 = parts[2];
393  if (sha256.size() != 64) {
394  return std::filesystem::path(); // Invalid SHA256
395  }
396 
397  const std::string& name = parts[1];
398  if (name.empty()) {
399  return std::filesystem::path();
400  }
401 
402  std::filesystem::path download_cache_dir;
403 #ifdef COLMAP_DOWNLOAD_ENABLED
404  if (download_cache_dir_overwrite.has_value()) {
405  download_cache_dir = *download_cache_dir_overwrite;
406  } else {
407 #endif
408  const std::optional<std::filesystem::path> home_dir = HomeDir();
409  if (!home_dir.has_value()) {
410  return std::filesystem::path();
411  }
412  download_cache_dir = *home_dir / ".cache" / "cloudViewer";
413 #ifdef COLMAP_DOWNLOAD_ENABLED
414  }
415 #endif
416 
417  const auto path = download_cache_dir / (sha256 + "-" + name);
418  if (std::filesystem::exists(path)) {
419  return path;
420  }
421 
422  return std::filesystem::path(); // File doesn't exist
423 }
424 
425 std::filesystem::path MaybeDownloadAndCacheFile(const std::string& uri) {
426  if (!IsURI(uri)) {
427  return uri;
428  }
429 #ifdef COLMAP_DOWNLOAD_ENABLED
430  return DownloadAndCacheFile(uri);
431 #else
432  throw std::runtime_error("COLMAP was compiled without download support");
433 #endif
434 }
435 
436 } // namespace colmap
437 
438 #endif // COLMAP_DOWNLOAD_ENABLED
std::function< void(std::shared_ptr< core::Tensor >)> callback
int size
std::string name
QTextStream & endl(QTextStream &stream)
Definition: QtCompat.h:718
static const std::string path
Definition: PointCloud.cpp:59
constexpr nullopt_t nullopt
Definition: Optional.h:136
void ReadBinaryBlob(const std::string &path, std::vector< T > *data)
Definition: misc.h:159
bool IsURI(const std::string &uri)
std::filesystem::path GetCachedFilePath(const std::string &uri)
bool StringStartsWith(const std::string &str, const std::string &prefix)
Definition: string.cc:173
void WriteBinaryBlob(const std::string &path, const std::vector< T > &data)
Definition: misc.h:171
std::vector< std::string > StringSplit(const std::string &str, const std::string &delim)
Definition: string.cc:166
std::filesystem::path MaybeDownloadAndCacheFile(const std::string &uri)