aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEven Rouault <even.rouault@spatialys.com>2020-01-09 23:27:43 +0100
committerEven Rouault <even.rouault@spatialys.com>2020-01-09 23:46:20 +0100
commit90b6685a990b8c4931aafb508853401a89163e78 (patch)
tree26d5016def90e3e9f71a51da694ff3028e1006ed
parent9263e1d36eec53ee3c4e4d04da93a032c0596eec (diff)
downloadPROJ-90b6685a990b8c4931aafb508853401a89163e78.tar.gz
PROJ-90b6685a990b8c4931aafb508853401a89163e78.zip
Add proj_is_download_needed() and proj_download_file()
-rw-r--r--scripts/reference_exported_symbols.txt2
-rw-r--r--src/filemanager.cpp420
-rw-r--r--src/proj.h9
-rw-r--r--test/unit/gie_self_tests.cpp3
-rw-r--r--test/unit/test_network.cpp131
5 files changed, 560 insertions, 5 deletions
diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt
index a1c03e99..d10f3bc1 100644
--- a/scripts/reference_exported_symbols.txt
+++ b/scripts/reference_exported_symbols.txt
@@ -931,6 +931,7 @@ proj_cs_get_axis_info
proj_cs_get_type
proj_destroy
proj_dmstor
+proj_download_file
proj_ellipsoid_get_parameters
proj_errno
proj_errno_reset
@@ -970,6 +971,7 @@ proj_int_list_destroy
proj_is_crs
proj_is_deprecated
proj_is_derived_crs
+proj_is_download_needed
proj_is_equivalent_to
proj_list_angular_units
proj_list_destroy
diff --git a/src/filemanager.cpp b/src/filemanager.cpp
index 1c49a16b..9acea83e 100644
--- a/src/filemanager.cpp
+++ b/src/filemanager.cpp
@@ -468,6 +468,13 @@ static const char *cache_db_structure_sql =
" lastModified TEXT,"
" etag TEXT"
");"
+ "CREATE TABLE downloaded_file_properties("
+ " url TEXT PRIMARY KEY NOT NULL,"
+ " lastChecked TIMESTAMP NOT NULL,"
+ " fileSize INTEGER NOT NULL,"
+ " lastModified TEXT,"
+ " etag TEXT"
+ ");"
"CREATE TABLE chunk_data("
" id INTEGER PRIMARY KEY AUTOINCREMENT CHECK (id > 0),"
" data BLOB NOT NULL"
@@ -1350,10 +1357,6 @@ class NetworkFile : public File {
NetworkFile(const NetworkFile &) = delete;
NetworkFile &operator=(const NetworkFile &) = delete;
- static bool get_props_from_headers(PJ_CONTEXT *ctx,
- PROJ_NETWORK_HANDLE *handle,
- FileProperties &props);
-
protected:
NetworkFile(PJ_CONTEXT *ctx, const std::string &url,
PROJ_NETWORK_HANDLE *handle,
@@ -1373,6 +1376,10 @@ class NetworkFile : public File {
bool hasChanged() const override { return m_hasChanged; }
static std::unique_ptr<File> open(PJ_CONTEXT *ctx, const char *filename);
+
+ static bool get_props_from_headers(PJ_CONTEXT *ctx,
+ PROJ_NETWORK_HANDLE *handle,
+ FileProperties &props);
};
// ---------------------------------------------------------------------------
@@ -2297,6 +2304,15 @@ static void CreateDirectory(const std::string &path) {
std::string pj_context_get_user_writable_directory(PJ_CONTEXT *ctx,
bool create) {
if (ctx->user_writable_directory.empty()) {
+ // For testing purposes only
+ const char *env_var_PROJ_USER_WRITABLE_DIRECTORY =
+ getenv("PROJ_USER_WRITABLE_DIRECTORY");
+ if (env_var_PROJ_USER_WRITABLE_DIRECTORY &&
+ env_var_PROJ_USER_WRITABLE_DIRECTORY[0] != '\0') {
+ ctx->user_writable_directory = env_var_PROJ_USER_WRITABLE_DIRECTORY;
+ }
+ }
+ if (ctx->user_writable_directory.empty()) {
std::string path;
#ifdef _WIN32
std::wstring wPath;
@@ -2353,4 +2369,400 @@ std::string pj_context_get_grid_cache_filename(PJ_CONTEXT *ctx) {
return ctx->gridChunkCache.filename;
}
+// ---------------------------------------------------------------------------
+
+#ifdef WIN32
+static const char dir_chars[] = "/\\";
+#else
+static const char dir_chars[] = "/";
+#endif
+
+static bool is_tilde_slash(const char *name) {
+ return *name == '~' && strchr(dir_chars, name[1]);
+}
+
+static bool is_rel_or_absolute_filename(const char *name) {
+ return strchr(dir_chars, *name) ||
+ (*name == '.' && strchr(dir_chars, name[1])) ||
+ (!strncmp(name, "..", 2) && strchr(dir_chars, name[2])) ||
+ (name[0] != '\0' && name[1] == ':' && strchr(dir_chars, name[2]));
+}
+
+static std::string build_url(PJ_CONTEXT *ctx, const char *name) {
+ if (!is_tilde_slash(name) && !is_rel_or_absolute_filename(name) &&
+ !starts_with(name, "http://") && !starts_with(name, "https://")) {
+ std::string remote_file(pj_context_get_url_endpoint(ctx));
+ if (!remote_file.empty()) {
+ if (remote_file.back() != '/') {
+ remote_file += '/';
+ }
+ remote_file += name;
+ auto pos = remote_file.rfind('.');
+ if (pos + 4 == remote_file.size()) {
+ remote_file = remote_file.substr(0, pos) + ".tif";
+ } else {
+ // For example for resource files like 'alaska'
+ remote_file += ".tif";
+ }
+ }
+ return remote_file;
+ }
+ return name;
+}
+
//! @endcond
+
+// ---------------------------------------------------------------------------
+
+/** Return if a file must be downloaded or is already available in the
+ * PROJ user-writable directory.
+ *
+ * The file will be determinted to have to be downloaded if it does not exist
+ * yet in the user-writable directory, or if it is determined that a more recent
+ * version exists. To determine if a more recent version exists, PROJ will
+ * use the "downloaded_file_properties" table of its grid cache database.
+ * Consequently files manually placed in the user-writable
+ * directory without using this function would be considered as
+ * non-existing/obsolete and would be unconditionnaly downloaded again.
+ *
+ * This function can only be used if networking is enabled, and either
+ * the default curl network API or a custom one have been installed.
+ *
+ * @param ctx PROJ context, or NULL
+ * @param url_or_filename URL or filename (without directory component)
+ * @param ignore_ttl_setting If set to FALSE, PROJ will only check the
+ * recentness of an already downloaded file, if
+ * the delay between the last time it has been
+ * verified and the current time exceeds the TTL
+ * setting. This can save network accesses.
+ * If set to TRUE, PROJ will unconditionnally
+ * check from the server the recentness of the file.
+ * @return TRUE if the file must be downloaded with proj_download_file()
+ * @since 7.0
+ */
+
+int proj_is_download_needed(PJ_CONTEXT *ctx, const char *url_or_filename,
+ int ignore_ttl_setting) {
+ if (ctx == nullptr) {
+ ctx = pj_get_default_ctx();
+ }
+ if (!pj_context_is_network_enabled(ctx)) {
+ pj_log(ctx, PJ_LOG_ERROR, "Networking capabilities are not enabled");
+ return false;
+ }
+
+ const auto url(build_url(ctx, url_or_filename));
+ const char *filename = strrchr(url.c_str(), '/');
+ if (filename == nullptr)
+ return false;
+ const auto localFilename(
+ pj_context_get_user_writable_directory(ctx, false) + filename);
+
+ auto f = NS_PROJ::FileManager::open(ctx, localFilename.c_str());
+ if (!f) {
+ return true;
+ }
+ f.reset();
+
+ auto diskCache = NS_PROJ::DiskChunkCache::open(ctx);
+ if (!diskCache)
+ return false;
+ auto stmt =
+ diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag "
+ "FROM downloaded_file_properties WHERE url = ?");
+ if (!stmt)
+ return true;
+ stmt->bindText(url.c_str());
+ if (stmt->execute() != SQLITE_ROW) {
+ return true;
+ }
+
+ NS_PROJ::FileProperties cachedProps;
+ cachedProps.lastChecked = stmt->getInt64();
+ cachedProps.size = stmt->getInt64();
+ const char *lastModified = stmt->getText();
+ cachedProps.lastModified = lastModified ? lastModified : std::string();
+ const char *etag = stmt->getText();
+ cachedProps.etag = etag ? etag : std::string();
+
+ if (!ignore_ttl_setting) {
+ const auto ttl = NS_PROJ::pj_context_get_grid_cache_ttl(ctx);
+ if (ttl > 0) {
+ time_t curTime;
+ time(&curTime);
+ if (curTime > cachedProps.lastChecked + ttl) {
+
+ unsigned char dummy;
+ size_t size_read = 0;
+ std::string errorBuffer;
+ errorBuffer.resize(1024);
+ auto handle = ctx->networking.open(
+ ctx, url.c_str(), 0, 1, &dummy, &size_read,
+ errorBuffer.size(), &errorBuffer[0],
+ ctx->networking.user_data);
+ if (!handle) {
+ errorBuffer.resize(strlen(errorBuffer.data()));
+ pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", url.c_str(),
+ errorBuffer.c_str());
+ return false;
+ }
+ NS_PROJ::FileProperties props;
+ if (!NS_PROJ::NetworkFile::get_props_from_headers(ctx, handle,
+ props)) {
+ ctx->networking.close(ctx, handle,
+ ctx->networking.user_data);
+ return false;
+ }
+ ctx->networking.close(ctx, handle, ctx->networking.user_data);
+
+ if (props.size != cachedProps.size ||
+ props.lastModified != cachedProps.lastModified ||
+ props.etag != cachedProps.etag) {
+ return true;
+ }
+
+ stmt = diskCache->prepare(
+ "UPDATE downloaded_file_properties SET lastChecked = ? "
+ "WHERE url = ?");
+ if (!stmt)
+ return false;
+ stmt->bindInt64(curTime);
+ stmt->bindText(url.c_str());
+ if (stmt->execute() != SQLITE_DONE) {
+ auto hDB = diskCache->handle();
+ pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
+ return false;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+// ---------------------------------------------------------------------------
+
+/** Download a file in the PROJ user-writable directory.
+ *
+ * The file will only be downloaded if it does not exist yet in the
+ * user-writable directory, or if it is determined that a more recent
+ * version exists. To determine if a more recent version exists, PROJ will
+ * use the "downloaded_file_properties" table of its grid cache database.
+ * Consequently files manually placed in the user-writable
+ * directory without using this function would be considered as
+ * non-existing/obsolete and would be unconditionnaly downloaded again.
+ *
+ * This function can only be used if networking is enabled, and either
+ * the default curl network API or a custom one have been installed.
+ *
+ * @param ctx PROJ context, or NULL
+ * @param url_or_filename URL or filename (without directory component)
+ * @param ignore_ttl_setting If set to FALSE, PROJ will only check the
+ * recentness of an already downloaded file, if
+ * the delay between the last time it has been
+ * verified and the current time exceeds the TTL
+ * setting. This can save network accesses.
+ * If set to TRUE, PROJ will unconditionnally
+ * check from the server the recentness of the file.
+ * @param progress_cbk Progress callback, or NULL.
+ * The passed percentage is in the [0, 1] range.
+ * The progress callback must return TRUE
+ * if download must be continued.
+ * @param user_data User data to provide to the progress callback, or NULL
+ * @return TRUE if the download was successful (or not needed)
+ * @since 7.0
+ */
+
+int proj_download_file(PJ_CONTEXT *ctx, const char *url_or_filename,
+ int ignore_ttl_setting,
+ int (*progress_cbk)(PJ_CONTEXT *, double pct,
+ void *user_data),
+ void *user_data) {
+ if (ctx == nullptr) {
+ ctx = pj_get_default_ctx();
+ }
+ if (!pj_context_is_network_enabled(ctx)) {
+ pj_log(ctx, PJ_LOG_ERROR, "Networking capabilities are not enabled");
+ return false;
+ }
+ if (!proj_is_download_needed(ctx, url_or_filename, ignore_ttl_setting)) {
+ return true;
+ }
+
+ const auto url(build_url(ctx, url_or_filename));
+ const char *filename = strrchr(url.c_str(), '/');
+ if (filename == nullptr)
+ return false;
+ const auto localFilename(pj_context_get_user_writable_directory(ctx, true) +
+ filename);
+
+#ifdef _WIN32
+ const int nPID = GetCurrentProcessId();
+#else
+ const int nPID = getpid();
+#endif
+ char szUniqueSuffix[128];
+ snprintf(szUniqueSuffix, sizeof(szUniqueSuffix), "%d_%p", nPID, &url);
+ const auto localFilenameTmp(localFilename + szUniqueSuffix);
+ FILE *f = fopen(localFilenameTmp.c_str(), "wb");
+ if (!f) {
+ pj_log(ctx, PJ_LOG_ERROR, "Cannot create %s", localFilenameTmp.c_str());
+ return false;
+ }
+
+ constexpr size_t FULL_FILE_CHUNK_SIZE = 1024 * 1024;
+ std::vector<unsigned char> buffer(FULL_FILE_CHUNK_SIZE);
+ // For testing purposes only
+ const char *env_var_PROJ_FULL_FILE_CHUNK_SIZE =
+ getenv("PROJ_FULL_FILE_CHUNK_SIZE");
+ if (env_var_PROJ_FULL_FILE_CHUNK_SIZE &&
+ env_var_PROJ_FULL_FILE_CHUNK_SIZE[0] != '\0') {
+ buffer.resize(atoi(env_var_PROJ_FULL_FILE_CHUNK_SIZE));
+ }
+ size_t size_read = 0;
+ std::string errorBuffer;
+ errorBuffer.resize(1024);
+ auto handle = ctx->networking.open(
+ ctx, url.c_str(), 0, buffer.size(), &buffer[0], &size_read,
+ errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data);
+ if (!handle) {
+ errorBuffer.resize(strlen(errorBuffer.data()));
+ pj_log(ctx, PJ_LOG_ERROR, "Cannot open %s: %s", url.c_str(),
+ errorBuffer.c_str());
+ fclose(f);
+ unlink(localFilenameTmp.c_str());
+ return false;
+ }
+
+ time_t curTime;
+ time(&curTime);
+ NS_PROJ::FileProperties props;
+ if (!NS_PROJ::NetworkFile::get_props_from_headers(ctx, handle, props)) {
+ ctx->networking.close(ctx, handle, ctx->networking.user_data);
+ fclose(f);
+ unlink(localFilenameTmp.c_str());
+ return false;
+ }
+
+ if (size_read <
+ std::min(static_cast<unsigned long long>(buffer.size()), props.size)) {
+ pj_log(ctx, PJ_LOG_ERROR, "Did not get as many bytes as expected");
+ ctx->networking.close(ctx, handle, ctx->networking.user_data);
+ fclose(f);
+ unlink(localFilenameTmp.c_str());
+ return false;
+ }
+ if (fwrite(buffer.data(), size_read, 1, f) != 1) {
+ pj_log(ctx, PJ_LOG_ERROR, "Write error");
+ ctx->networking.close(ctx, handle, ctx->networking.user_data);
+ fclose(f);
+ unlink(localFilenameTmp.c_str());
+ return false;
+ }
+
+ unsigned long long totalDownloaded = size_read;
+ while (totalDownloaded < props.size) {
+ if (totalDownloaded + buffer.size() > props.size) {
+ buffer.resize(static_cast<size_t>(props.size - totalDownloaded));
+ }
+ errorBuffer.resize(1024);
+ size_read = ctx->networking.read_range(
+ ctx, handle, totalDownloaded, buffer.size(), &buffer[0],
+ errorBuffer.size(), &errorBuffer[0], ctx->networking.user_data);
+
+ if (size_read < buffer.size()) {
+ pj_log(ctx, PJ_LOG_ERROR, "Did not get as many bytes as expected");
+ ctx->networking.close(ctx, handle, ctx->networking.user_data);
+ fclose(f);
+ unlink(localFilenameTmp.c_str());
+ return false;
+ }
+ if (fwrite(buffer.data(), size_read, 1, f) != 1) {
+ pj_log(ctx, PJ_LOG_ERROR, "Write error");
+ ctx->networking.close(ctx, handle, ctx->networking.user_data);
+ fclose(f);
+ unlink(localFilenameTmp.c_str());
+ return false;
+ }
+
+ totalDownloaded += size_read;
+ if (progress_cbk &&
+ !progress_cbk(ctx, double(totalDownloaded) / props.size,
+ user_data)) {
+ ctx->networking.close(ctx, handle, ctx->networking.user_data);
+ fclose(f);
+ unlink(localFilenameTmp.c_str());
+ return false;
+ }
+ }
+
+ ctx->networking.close(ctx, handle, ctx->networking.user_data);
+ fclose(f);
+
+ unlink(localFilename.c_str());
+ if (rename(localFilenameTmp.c_str(), localFilename.c_str()) != 0) {
+ pj_log(ctx, PJ_LOG_ERROR, "Cannot rename %s to %s",
+ localFilenameTmp.c_str(), localFilename.c_str());
+ return false;
+ }
+
+ auto diskCache = NS_PROJ::DiskChunkCache::open(ctx);
+ if (!diskCache)
+ return false;
+ auto stmt =
+ diskCache->prepare("SELECT lastChecked, fileSize, lastModified, etag "
+ "FROM downloaded_file_properties WHERE url = ?");
+ if (!stmt)
+ return false;
+ stmt->bindText(url.c_str());
+
+ props.lastChecked = curTime;
+ auto hDB = diskCache->handle();
+
+ if (stmt->execute() == SQLITE_ROW) {
+ stmt = diskCache->prepare(
+ "UPDATE downloaded_file_properties SET lastChecked = ?, "
+ "fileSize = ?, lastModified = ?, etag = ? "
+ "WHERE url = ?");
+ if (!stmt)
+ return false;
+ stmt->bindInt64(props.lastChecked);
+ stmt->bindInt64(props.size);
+ if (props.lastModified.empty())
+ stmt->bindNull();
+ else
+ stmt->bindText(props.lastModified.c_str());
+ if (props.etag.empty())
+ stmt->bindNull();
+ else
+ stmt->bindText(props.etag.c_str());
+ stmt->bindText(url.c_str());
+ if (stmt->execute() != SQLITE_DONE) {
+ pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
+ return false;
+ }
+ } else {
+ stmt = diskCache->prepare(
+ "INSERT INTO downloaded_file_properties (url, lastChecked, "
+ "fileSize, lastModified, etag) VALUES "
+ "(?,?,?,?,?)");
+ if (!stmt)
+ return false;
+ stmt->bindText(url.c_str());
+ stmt->bindInt64(props.lastChecked);
+ stmt->bindInt64(props.size);
+ if (props.lastModified.empty())
+ stmt->bindNull();
+ else
+ stmt->bindText(props.lastModified.c_str());
+ if (props.etag.empty())
+ stmt->bindNull();
+ else
+ stmt->bindText(props.etag.c_str());
+ if (stmt->execute() != SQLITE_DONE) {
+ pj_log(ctx, PJ_LOG_ERROR, "%s", sqlite3_errmsg(hDB));
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/src/proj.h b/src/proj.h
index 5c9e81bb..8add29ac 100644
--- a/src/proj.h
+++ b/src/proj.h
@@ -443,6 +443,15 @@ void PROJ_DLL proj_grid_cache_set_ttl(PJ_CONTEXT* ctx, int ttl_seconds);
void PROJ_DLL proj_grid_cache_clear(PJ_CONTEXT* ctx);
+int PROJ_DLL proj_is_download_needed(PJ_CONTEXT* ctx,
+ const char* url_or_filename,
+ int ignore_ttl_setting);
+int PROJ_DLL proj_download_file(PJ_CONTEXT *ctx, const char *url_or_filename,
+ int ignore_ttl_setting,
+ int (*progress_cbk)(PJ_CONTEXT *, double pct,
+ void *user_data),
+ void *user_data);
+
/*! @cond Doxygen_Suppress */
/* Manage the transformation definition object PJ */
diff --git a/test/unit/gie_self_tests.cpp b/test/unit/gie_self_tests.cpp
index a3b41fb0..fc0f0748 100644
--- a/test/unit/gie_self_tests.cpp
+++ b/test/unit/gie_self_tests.cpp
@@ -362,7 +362,8 @@ TEST(gie, info_functions) {
ASSERT_NE(std::string(info.searchpath), std::string());
}
- ASSERT_TRUE(std::string(info.searchpath).find("/proj") != std::string::npos);
+ ASSERT_TRUE(std::string(info.searchpath).find("/proj") !=
+ std::string::npos);
/* proj_pj_info() */
{
diff --git a/test/unit/test_network.cpp b/test/unit/test_network.cpp
index e6e7bb7b..4e66d8c5 100644
--- a/test/unit/test_network.cpp
+++ b/test/unit/test_network.cpp
@@ -38,6 +38,12 @@
#include <sqlite3.h>
#include <time.h>
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <unistd.h>
+#endif
+
#ifdef CURL_ENABLED
#include <curl/curl.h>
#endif
@@ -1558,6 +1564,131 @@ TEST(networking, cache_lock) {
proj_context_destroy(ctx);
}
+
+// ---------------------------------------------------------------------------
+
+TEST(networking, download_whole_files) {
+ if (!networkAccessOK) {
+ return;
+ }
+
+ proj_cleanup();
+ unlink("proj_test_tmp/cache.db");
+ unlink("proj_test_tmp/ntf_r93.tif");
+ rmdir("proj_test_tmp");
+
+ putenv(const_cast<char *>("PROJ_IGNORE_USER_WRITABLE_DIRECTORY="));
+ putenv(const_cast<char *>("PROJ_USER_WRITABLE_DIRECTORY=./proj_test_tmp"));
+ putenv(const_cast<char *>("PROJ_FULL_FILE_CHUNK_SIZE=30000"));
+ auto ctx = proj_context_create();
+ proj_context_set_enable_network(ctx, true);
+
+ ASSERT_TRUE(proj_is_download_needed(ctx, "ntf_r93.gsb", false));
+
+ ASSERT_TRUE(
+ proj_download_file(ctx, "ntf_r93.gsb", false, nullptr, nullptr));
+
+ FILE *f = fopen("proj_test_tmp/ntf_r93.tif", "rb");
+ ASSERT_NE(f, nullptr);
+ fseek(f, 0, SEEK_END);
+ ASSERT_EQ(ftell(f), 93581);
+ fclose(f);
+
+ ASSERT_FALSE(proj_is_download_needed(ctx, "ntf_r93.gsb", false));
+
+ {
+ sqlite3 *hDB = nullptr;
+ sqlite3_open_v2("proj_test_tmp/cache.db", &hDB, SQLITE_OPEN_READWRITE,
+ nullptr);
+ ASSERT_NE(hDB, nullptr);
+ // Force lastChecked to the Epoch so that data is expired.
+ sqlite3_stmt *hStmt = nullptr;
+ sqlite3_prepare_v2(
+ hDB, "UPDATE downloaded_file_properties SET lastChecked = 0", -1,
+ &hStmt, nullptr);
+ ASSERT_NE(hStmt, nullptr);
+ ASSERT_EQ(sqlite3_step(hStmt), SQLITE_DONE);
+ sqlite3_finalize(hStmt);
+ sqlite3_close(hDB);
+ }
+
+ // If we ignore TTL settings, then no network access will be done
+ ASSERT_FALSE(proj_is_download_needed(ctx, "ntf_r93.gsb", true));
+
+ {
+ sqlite3 *hDB = nullptr;
+ sqlite3_open_v2("proj_test_tmp/cache.db", &hDB, SQLITE_OPEN_READWRITE,
+ nullptr);
+ ASSERT_NE(hDB, nullptr);
+ // Check that the lastChecked timestamp is still 0
+ sqlite3_stmt *hStmt = nullptr;
+ sqlite3_prepare_v2(hDB,
+ "SELECT lastChecked FROM downloaded_file_properties",
+ -1, &hStmt, nullptr);
+ ASSERT_NE(hStmt, nullptr);
+ ASSERT_EQ(sqlite3_step(hStmt), SQLITE_ROW);
+ ASSERT_EQ(sqlite3_column_int64(hStmt, 0), 0);
+ sqlite3_finalize(hStmt);
+ sqlite3_close(hDB);
+ }
+
+ // Should recheck from the CDN, update last_checked and do nothing
+ ASSERT_FALSE(proj_is_download_needed(ctx, "ntf_r93.gsb", false));
+
+ {
+ sqlite3 *hDB = nullptr;
+ sqlite3_open_v2("proj_test_tmp/cache.db", &hDB, SQLITE_OPEN_READWRITE,
+ nullptr);
+ ASSERT_NE(hDB, nullptr);
+ sqlite3_stmt *hStmt = nullptr;
+ // Check that the lastChecked timestamp has been updated
+ sqlite3_prepare_v2(hDB,
+ "SELECT lastChecked FROM downloaded_file_properties",
+ -1, &hStmt, nullptr);
+ ASSERT_NE(hStmt, nullptr);
+ ASSERT_EQ(sqlite3_step(hStmt), SQLITE_ROW);
+ ASSERT_NE(sqlite3_column_int64(hStmt, 0), 0);
+ sqlite3_finalize(hStmt);
+ hStmt = nullptr;
+
+ // Now invalid lastModified. This should trigger a new download
+ sqlite3_prepare_v2(
+ hDB, "UPDATE downloaded_file_properties SET lastChecked = 0, "
+ "lastModified = 'foo'",
+ -1, &hStmt, nullptr);
+ ASSERT_NE(hStmt, nullptr);
+ ASSERT_EQ(sqlite3_step(hStmt), SQLITE_DONE);
+ sqlite3_finalize(hStmt);
+ sqlite3_close(hDB);
+ }
+
+ ASSERT_TRUE(proj_is_download_needed(ctx, "ntf_r93.gsb", false));
+
+ // Redo download with a progress callback this time.
+ unlink("proj_test_tmp/ntf_r93.tif");
+
+ const auto cbk = [](PJ_CONTEXT *l_ctx, double pct, void *user_data) -> int {
+ auto vect = static_cast<std::vector<std::pair<PJ_CONTEXT *, double>> *>(
+ user_data);
+ vect->push_back(std::pair<PJ_CONTEXT *, double>(l_ctx, pct));
+ return true;
+ };
+
+ std::vector<std::pair<PJ_CONTEXT *, double>> vectPct;
+ ASSERT_TRUE(proj_download_file(ctx, "ntf_r93.gsb", false, cbk, &vectPct));
+ ASSERT_EQ(vectPct.size(), 3U);
+ ASSERT_EQ(vectPct.back().first, ctx);
+ ASSERT_EQ(vectPct.back().second, 1.0);
+
+ proj_context_destroy(ctx);
+ putenv(const_cast<char *>("PROJ_IGNORE_USER_WRITABLE_DIRECTORY=YES"));
+ putenv(const_cast<char *>("PROJ_USER_WRITABLE_DIRECTORY="));
+ putenv(const_cast<char *>("PROJ_FULL_FILE_CHUNK_SIZE="));
+ unlink("proj_test_tmp/cache.db");
+ unlink("proj_test_tmp/ntf_r93.tif");
+ rmdir("proj_test_tmp");
+}
+
#endif
} // namespace