Support for setting the content server URL

This commit is contained in:
Veloman Yunkan 2025-09-06 17:56:22 +04:00
parent c205a4703b
commit 968d1c1067
10 changed files with 73 additions and 35 deletions

View File

@ -64,6 +64,7 @@ namespace kiwix
void setBlockExternalLinks(bool blockExternalLinks) void setBlockExternalLinks(bool blockExternalLinks)
{ m_blockExternalLinks = blockExternalLinks; } { m_blockExternalLinks = blockExternalLinks; }
void setCatalogOnlyMode(bool enable) { m_catalogOnlyMode = enable; } void setCatalogOnlyMode(bool enable) { m_catalogOnlyMode = enable; }
void setContentServerUrl(std::string url) { m_contentServerUrl = url; }
void setIpMode(IpMode mode) { m_ipMode = mode; } void setIpMode(IpMode mode) { m_ipMode = mode; }
int getPort() const; int getPort() const;
IpAddress getAddress() const; IpAddress getAddress() const;
@ -85,6 +86,7 @@ namespace kiwix
IpMode m_ipMode = IpMode::AUTO; IpMode m_ipMode = IpMode::AUTO;
int m_ipConnectionLimit = 0; int m_ipConnectionLimit = 0;
bool m_catalogOnlyMode = false; bool m_catalogOnlyMode = false;
std::string m_contentServerUrl;
std::unique_ptr<InternalServer> mp_server; std::unique_ptr<InternalServer> mp_server;
}; };
} }

View File

@ -50,6 +50,13 @@ class LibraryDumper
*/ */
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; } void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
/**
* Set the URL of the content server.
*
* @param url the URL of the content server to use.
*/
void setContentServerUrl(const std::string& url) { this->contentServerUrl = url; }
/** /**
* Set some informations about the search results. * Set some informations about the search results.
* *
@ -81,6 +88,7 @@ class LibraryDumper
const kiwix::NameMapper* const nameMapper; const kiwix::NameMapper* const nameMapper;
std::string libraryId; std::string libraryId;
std::string rootLocation; std::string rootLocation;
std::string contentServerUrl;
std::string m_userLang; std::string m_userLang;
int m_totalResults; int m_totalResults;
int m_startIndex; int m_startIndex;

View File

@ -62,11 +62,15 @@ IllustrationInfo getBookIllustrationInfo(const Book& book)
return illustrations; return illustrations;
} }
std::string fullEntryXML(const Book& book, const std::string& rootLocation, const std::string& contentId) std::string fullEntryXML(const Book& book,
const std::string& rootLocation,
const std::string& contentServerUrl,
const std::string& contentId)
{ {
const auto bookDate = book.getDate() + "T00:00:00Z"; const auto bookDate = book.getDate() + "T00:00:00Z";
const kainjow::mustache::object data{ const kainjow::mustache::object data{
{"root", rootLocation}, {"root", rootLocation},
{"contentServerUrl", onlyAsNonEmptyMustacheValue(contentServerUrl)},
{"id", book.getId()}, {"id", book.getId()},
{"name", book.getName()}, {"name", book.getName()},
{"title", book.getTitle()}, {"title", book.getTitle()},
@ -103,7 +107,12 @@ std::string partialEntryXML(const Book& book, const std::string& rootLocation)
return render_template(xmlTemplate, data); return render_template(xmlTemplate, data);
} }
BooksData getBooksData(const Library* library, const NameMapper* nameMapper, const std::vector<std::string>& bookIds, const std::string& rootLocation, bool partial) BooksData getBooksData(const Library* library,
const NameMapper* nameMapper,
const std::vector<std::string>& bookIds,
const std::string& rootLocation,
const std::string& contentServerUrl,
bool partial)
{ {
BooksData booksData; BooksData booksData;
for ( const auto& bookId : bookIds ) { for ( const auto& bookId : bookIds ) {
@ -112,7 +121,7 @@ BooksData getBooksData(const Library* library, const NameMapper* nameMapper, con
const std::string contentId = nameMapper->getNameForId(bookId); const std::string contentId = nameMapper->getNameForId(bookId);
const auto entryXML = partial const auto entryXML = partial
? partialEntryXML(book, rootLocation) ? partialEntryXML(book, rootLocation)
: fullEntryXML(book, rootLocation, contentId); : fullEntryXML(book, rootLocation, contentServerUrl, contentId);
booksData.push_back(kainjow::mustache::object{ {"entry", entryXML} }); booksData.push_back(kainjow::mustache::object{ {"entry", entryXML} });
} catch ( const std::out_of_range& ) { } catch ( const std::out_of_range& ) {
// the book was removed from the library since its id was obtained // the book was removed from the library since its id was obtained
@ -127,7 +136,7 @@ BooksData getBooksData(const Library* library, const NameMapper* nameMapper, con
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
{ {
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, false); const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, contentServerUrl, false);
const kainjow::mustache::object template_data{ const kainjow::mustache::object template_data{
{"date", gen_date_str()}, {"date", gen_date_str()},
{"root", rootLocation}, {"root", rootLocation},
@ -145,7 +154,7 @@ string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const s
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query, bool partial) const string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query, bool partial) const
{ {
const auto endpointRoot = rootLocation + "/catalog/v2"; const auto endpointRoot = rootLocation + "/catalog/v2";
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, partial); const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, contentServerUrl, partial);
const char* const endpoint = partial ? "/partial_entries" : "/entries"; const char* const endpoint = partial ? "/partial_entries" : "/entries";
const std::string url = endpoint + (query.empty() ? "" : "?" + query); const std::string url = endpoint + (query.empty() ? "" : "?" + query);
@ -170,7 +179,7 @@ std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
const std::string contentId = nameMapper->getNameForId(bookId); const std::string contentId = nameMapper->getNameForId(bookId);
return XML_HEADER return XML_HEADER
+ "\n" + "\n"
+ fullEntryXML(book, rootLocation, contentId); + fullEntryXML(book, rootLocation, contentServerUrl, contentId);
} }
std::string OPDSDumper::categoriesOPDSFeed() const std::string OPDSDumper::categoriesOPDSFeed() const

View File

@ -54,7 +54,8 @@ bool Server::start() {
m_ipMode, m_ipMode,
m_indexTemplateString, m_indexTemplateString,
m_ipConnectionLimit, m_ipConnectionLimit,
m_catalogOnlyMode)); m_catalogOnlyMode,
m_contentServerUrl));
return mp_server->start(); return mp_server->start();
} }

View File

@ -436,7 +436,8 @@ InternalServer::InternalServer(LibraryPtr library,
IpMode ipMode, IpMode ipMode,
std::string indexTemplateString, std::string indexTemplateString,
int ipConnectionLimit, int ipConnectionLimit,
bool catalogOnlyMode) : bool catalogOnlyMode,
std::string contentServerUrl) :
m_addr(addr), m_addr(addr),
m_port(port), m_port(port),
m_root(normalizeRootUrl(root)), m_root(normalizeRootUrl(root)),
@ -456,7 +457,8 @@ InternalServer::InternalServer(LibraryPtr library,
searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)), searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))), suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
m_customizedResources(new CustomizedResources), m_customizedResources(new CustomizedResources),
m_catalogOnlyMode(catalogOnlyMode) m_catalogOnlyMode(catalogOnlyMode),
m_contentServerUrl(contentServerUrl)
{ {
m_root = urlEncode(m_root); m_root = urlEncode(m_root);
} }

View File

@ -107,7 +107,8 @@ class InternalServer {
IpMode ipMode, IpMode ipMode,
std::string indexTemplateString, std::string indexTemplateString,
int ipConnectionLimit, int ipConnectionLimit,
bool catalogOnlyMode); bool catalogOnlyMode,
std::string zimViewerURL);
virtual ~InternalServer(); virtual ~InternalServer();
MHD_Result handlerCallback(struct MHD_Connection* connection, MHD_Result handlerCallback(struct MHD_Connection* connection,
@ -161,6 +162,7 @@ class InternalServer {
std::string getLibraryId() const; std::string getLibraryId() const;
std::string getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const; std::string getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const;
OPDSDumper getOPDSDumper() const;
private: // types private: // types
class LockableSuggestionSearcher; class LockableSuggestionSearcher;
@ -195,6 +197,7 @@ class InternalServer {
std::unique_ptr<CustomizedResources> m_customizedResources; std::unique_ptr<CustomizedResources> m_customizedResources;
const bool m_catalogOnlyMode; const bool m_catalogOnlyMode;
const std::string m_contentServerUrl;
}; };
} }

View File

@ -51,6 +51,19 @@ const std::string opdsMimeType[] = {
} // unnamed namespace } // unnamed namespace
OPDSDumper InternalServer::getOPDSDumper() const
{
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
if ( !m_contentServerUrl.empty() ) {
opdsDumper.setContentServerUrl(m_contentServerUrl);
} else if ( !m_catalogOnlyMode ) {
opdsDumper.setContentServerUrl(m_root);
}
return opdsDumper;
}
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request) std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
{ {
if (m_verbose.load()) { if (m_verbose.load()) {
@ -80,9 +93,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
} }
zim::Uuid uuid; zim::Uuid uuid;
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get()); kiwix::OPDSDumper opdsDumper = getOPDSDumper();
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
std::vector<std::string> bookIdsToDump; std::vector<std::string> bookIdsToDump;
if (url == "root.xml") { if (url == "root.xml") {
uuid = zim::Uuid::generate(host); uuid = zim::Uuid::generate(host);
@ -158,9 +169,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial) std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial)
{ {
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get()); kiwix::OPDSDumper opdsDumper = getOPDSDumper();
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
const auto bookIds = search_catalog(request, opdsDumper); const auto bookIds = search_catalog(request, opdsDumper);
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial); const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
return ContentResponse::build( return ContentResponse::build(
@ -177,9 +186,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
return UrlNotFoundResponse(request); return UrlNotFoundResponse(request);
} }
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get()); kiwix::OPDSDumper opdsDumper = getOPDSDumper();
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId); const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
return ContentResponse::build( return ContentResponse::build(
opdsFeed, opdsFeed,
@ -189,9 +196,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request) std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
{ {
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get()); kiwix::OPDSDumper opdsDumper = getOPDSDumper();
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build( return ContentResponse::build(
opdsDumper.categoriesOPDSFeed(), opdsDumper.categoriesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED] opdsMimeType[OPDS_NAVIGATION_FEED]
@ -200,9 +205,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request) std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
{ {
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get()); kiwix::OPDSDumper opdsDumper = getOPDSDumper();
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build( return ContentResponse::build(
opdsDumper.languagesOPDSFeed(), opdsDumper.languagesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED] opdsMimeType[OPDS_NAVIGATION_FEED]

View File

@ -13,7 +13,8 @@
{{#icons}}<link rel="http://opds-spec.org/image/thumbnail" {{#icons}}<link rel="http://opds-spec.org/image/thumbnail"
href="{{root}}/catalog/v2/illustration/{{{id}}}/?size={{icon_size}}" href="{{root}}/catalog/v2/illustration/{{{id}}}/?size={{icon_size}}"
type="{{icon_mimetype}};width={{icon_size}};height={{icon_size}};scale=1"/> type="{{icon_mimetype}};width={{icon_size}};height={{icon_size}};scale=1"/>
{{/icons}}<link type="text/html" href="{{root}}/content/{{{content_id}}}" /> {{/icons}}{{#contentServerUrl}}<link type="text/html" href="{{contentServerUrl}}/content/{{{content_id}}}" />
{{/contentServerUrl}}
<author> <author>
<name>{{author_name}}</name> <name>{{author_name}}</name>
</author> </author>

View File

@ -18,9 +18,11 @@ protected:
const int PORT = 8002; const int PORT = 8002;
protected: protected:
void resetServer(ZimFileServer::Options options) { void resetServer(ZimFileServer::Options options, std::string contentServerUrl="") {
ZimFileServer::Cfg cfg(options);
cfg.contentServerUrl = contentServerUrl;
zfs1_.reset(); zfs1_.reset();
zfs1_.reset(new ZimFileServer(PORT, options, "./test/library.xml")); zfs1_.reset(new ZimFileServer(PORT, cfg, "./test/library.xml"));
} }
void SetUp() override { void SetUp() override {
@ -723,7 +725,12 @@ TEST_F(LibraryServerTest, catalog_v2_entries)
TEST_F(LibraryServerTest, catalog_v2_entries_catalog_only_mode) TEST_F(LibraryServerTest, catalog_v2_entries_catalog_only_mode)
{ {
resetServer(ZimFileServer::CATALOG_ONLY_MODE); const std::string contentServerUrl = "https://demo.kiwix.org";
const auto fixContentLinks = [=](std::string s) -> std::string {
s = replace(s, "/ROOT%23%3F/content", contentServerUrl + "/content");
return s;
};
resetServer(ZimFileServer::CATALOG_ONLY_MODE, contentServerUrl);
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries"); const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries");
EXPECT_EQ(r->status, 200); EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body), EXPECT_EQ(maskVariableOPDSFeedData(r->body),
@ -731,10 +738,10 @@ TEST_F(LibraryServerTest, catalog_v2_entries_catalog_only_mode)
" <title>All Entries</title>\n" " <title>All Entries</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" " <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
"\n" "\n"
CHARLES_RAY_CATALOG_ENTRY + fixContentLinks(CHARLES_RAY_CATALOG_ENTRY)
INACCESSIBLEZIMFILE_CATALOG_ENTRY + fixContentLinks(INACCESSIBLEZIMFILE_CATALOG_ENTRY)
RAY_CHARLES_CATALOG_ENTRY + fixContentLinks(RAY_CHARLES_CATALOG_ENTRY)
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY + fixContentLinks(UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY) +
"</feed>\n" "</feed>\n"
); );
} }

View File

@ -72,6 +72,7 @@ public: // types
struct Cfg struct Cfg
{ {
std::string root = "ROOT#?"; std::string root = "ROOT#?";
std::string contentServerUrl = "";
Options options = DEFAULT_OPTIONS; Options options = DEFAULT_OPTIONS;
Cfg(Options opts = DEFAULT_OPTIONS) : options(opts) {} Cfg(Options opts = DEFAULT_OPTIONS) : options(opts) {}
@ -151,6 +152,7 @@ void ZimFileServer::run(int serverPort, std::string indexTemplateString)
server->setBlockExternalLinks(cfg.options & BLOCK_EXTERNAL_LINKS); server->setBlockExternalLinks(cfg.options & BLOCK_EXTERNAL_LINKS);
server->setMultiZimSearchLimit(3); server->setMultiZimSearchLimit(3);
server->setCatalogOnlyMode(cfg.options & CATALOG_ONLY_MODE); server->setCatalogOnlyMode(cfg.options & CATALOG_ONLY_MODE);
server->setContentServerUrl(cfg.contentServerUrl);
if (!indexTemplateString.empty()) { if (!indexTemplateString.empty()) {
server->setIndexTemplateString(indexTemplateString); server->setIndexTemplateString(indexTemplateString);
} }