diff options
| author | Carl Hetherington <cth@carlh.net> | 2023-05-20 22:51:49 +0200 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2024-05-06 20:42:50 +0200 |
| commit | a3fcbb3a76e079a5485a0552ea5d35b8d6739116 (patch) | |
| tree | 58f6476b7197c0e32b5aa3d52d0859a9b04db268 /src/lib | |
| parent | a4105c6e8dc83407abc9b12e80c958673c942888 (diff) | |
Use sqlite for cinema and DKDM recipient lists.
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/cinema.cc | 103 | ||||
| -rw-r--r-- | src/lib/cinema.h | 33 | ||||
| -rw-r--r-- | src/lib/cinema_list.cc | 466 | ||||
| -rw-r--r-- | src/lib/cinema_list.h | 126 | ||||
| -rw-r--r-- | src/lib/config.cc | 210 | ||||
| -rw-r--r-- | src/lib/config.h | 67 | ||||
| -rw-r--r-- | src/lib/dkdm_recipient.cc | 29 | ||||
| -rw-r--r-- | src/lib/dkdm_recipient.h | 6 | ||||
| -rw-r--r-- | src/lib/dkdm_recipient_list.cc | 243 | ||||
| -rw-r--r-- | src/lib/dkdm_recipient_list.h | 90 | ||||
| -rw-r--r-- | src/lib/exceptions.h | 55 | ||||
| -rw-r--r-- | src/lib/id.cc | 30 | ||||
| -rw-r--r-- | src/lib/id.h | 48 | ||||
| -rw-r--r-- | src/lib/kdm_cli.cc | 134 | ||||
| -rw-r--r-- | src/lib/kdm_with_metadata.h | 7 | ||||
| -rw-r--r-- | src/lib/screen.cc | 46 | ||||
| -rw-r--r-- | src/lib/screen.h | 10 | ||||
| -rw-r--r-- | src/lib/sqlite_statement.cc | 111 | ||||
| -rw-r--r-- | src/lib/sqlite_statement.h | 50 | ||||
| -rw-r--r-- | src/lib/sqlite_table.cc | 79 | ||||
| -rw-r--r-- | src/lib/sqlite_table.h | 54 | ||||
| -rw-r--r-- | src/lib/sqlite_transaction.cc | 50 | ||||
| -rw-r--r-- | src/lib/sqlite_transaction.h | 40 | ||||
| -rw-r--r-- | src/lib/unzipper.cc | 14 | ||||
| -rw-r--r-- | src/lib/unzipper.h | 3 | ||||
| -rw-r--r-- | src/lib/util.cc | 20 | ||||
| -rw-r--r-- | src/lib/util.h | 1 | ||||
| -rw-r--r-- | src/lib/wscript | 9 |
28 files changed, 1713 insertions, 421 deletions
diff --git a/src/lib/cinema.cc b/src/lib/cinema.cc deleted file mode 100644 index b1681fc28..000000000 --- a/src/lib/cinema.cc +++ /dev/null @@ -1,103 +0,0 @@ -/* - Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net> - - This file is part of DCP-o-matic. - - DCP-o-matic is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - DCP-o-matic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. - -*/ - - -#include "cinema.h" -#include "screen.h" -#include "dcpomatic_assert.h" -#include <libcxml/cxml.h> -#include <dcp/raw_convert.h> -#include <libxml++/libxml++.h> - - -using std::make_shared; -using std::shared_ptr; -using std::string; -using dcp::raw_convert; -using dcpomatic::Screen; - - -Cinema::Cinema (cxml::ConstNodePtr node) - : name (node->string_child ("Name")) - , notes (node->optional_string_child("Notes").get_value_or("")) -{ - for (auto i: node->node_children("Email")) { - emails.push_back (i->content ()); - } - - int hour = 0; - - if (node->optional_number_child<int>("UTCOffset")) { - hour = node->number_child<int>("UTCOffset"); - } else { - hour = node->optional_number_child<int>("UTCOffsetHour").get_value_or(0); - } - - int minute = node->optional_number_child<int>("UTCOffsetMinute").get_value_or(0); - - utc_offset= { hour, minute }; -} - -/* This is necessary so that we can use shared_from_this in add_screen (which cannot be done from - a constructor) -*/ -void -Cinema::read_screens (cxml::ConstNodePtr node) -{ - for (auto i: node->node_children("Screen")) { - add_screen (make_shared<Screen>(i)); - } -} - -void -Cinema::as_xml (xmlpp::Element* parent) const -{ - cxml::add_text_child(parent, "Name", name); - - for (auto i: emails) { - cxml::add_text_child(parent, "Email", i); - } - - cxml::add_text_child(parent, "Notes", notes); - - cxml::add_text_child(parent, "UTCOffsetHour", raw_convert<string>(utc_offset.hour())); - cxml::add_text_child(parent, "UTCOffsetMinute", raw_convert<string>(utc_offset.minute())); - - for (auto i: _screens) { - i->as_xml(cxml::add_child(parent, "Screen")); - } -} - -void -Cinema::add_screen (shared_ptr<Screen> s) -{ - s->cinema = shared_from_this (); - _screens.push_back (s); -} - -void -Cinema::remove_screen (shared_ptr<Screen> s) -{ - auto iter = std::find(_screens.begin(), _screens.end(), s); - if (iter != _screens.end()) { - _screens.erase(iter); - } -} - diff --git a/src/lib/cinema.h b/src/lib/cinema.h index 05f6fb7fc..44f232f91 100644 --- a/src/lib/cinema.h +++ b/src/lib/cinema.h @@ -18,31 +18,24 @@ */ + /** @file src/lib/cinema.h * @brief Cinema class. */ #include <dcp/utc_offset.h> -#include <libcxml/cxml.h> #include <memory> +#include <string> +#include <vector> -namespace xmlpp { - class Element; -} - -namespace dcpomatic { - class Screen; -} - /** @class Cinema * @brief A description of a Cinema for KDM generation. * - * This is a cinema name, some metadata and a list of - * Screen objects. + * This is a cinema name and some metadata. */ -class Cinema : public std::enable_shared_from_this<Cinema> +class Cinema { public: Cinema(std::string const & name_, std::vector<std::string> const & e, std::string notes_, dcp::UTCOffset utc_offset_) @@ -52,24 +45,8 @@ public: , utc_offset(std::move(utc_offset_)) {} - explicit Cinema (cxml::ConstNodePtr); - - void read_screens (cxml::ConstNodePtr); - - void as_xml (xmlpp::Element *) const; - - void add_screen (std::shared_ptr<dcpomatic::Screen>); - void remove_screen (std::shared_ptr<dcpomatic::Screen>); - std::string name; std::vector<std::string> emails; std::string notes; dcp::UTCOffset utc_offset; - - std::vector<std::shared_ptr<dcpomatic::Screen>> screens() const { - return _screens; - } - -private: - std::vector<std::shared_ptr<dcpomatic::Screen>> _screens; }; diff --git a/src/lib/cinema_list.cc b/src/lib/cinema_list.cc new file mode 100644 index 000000000..41b9dbab3 --- /dev/null +++ b/src/lib/cinema_list.cc @@ -0,0 +1,466 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "cinema.h" +#include "cinema_list.h" +#include "config.h" +#include "dcpomatic_assert.h" +#include "exceptions.h" +#include "screen.h" +#include "sqlite_statement.h" +#include "sqlite_transaction.h" +#include "util.h" +#include <dcp/certificate.h> +#include <sqlite3.h> +#include <boost/algorithm/string.hpp> +#include <iostream> +#include <numeric> + + +using std::pair; +using std::make_pair; +using std::string; +using std::vector; +using boost::optional; + + +CinemaList::CinemaList() + : _cinemas("cinemas") + , _screens("screens") + , _trusted_devices("trusted_devices") +{ + setup_tables(); + setup(Config::instance()->cinemas_file()); +} + + +CinemaList::CinemaList(boost::filesystem::path db_file) + : _cinemas("cinemas") + , _screens("screens") + , _trusted_devices("trusted_devices") +{ + setup_tables(); + setup(db_file); +} + + +void +CinemaList::setup_tables() +{ + _cinemas.add_column("name", "TEXT"); + _cinemas.add_column("emails", "TEXT"); + _cinemas.add_column("notes", "TEXT"); + _cinemas.add_column("utc_offset_hour", "INTEGER"); + _cinemas.add_column("utc_offset_minute", "INTEGER"); + + _screens.add_column("cinema", "INTEGER"); + _screens.add_column("name", "TEXT"); + _screens.add_column("notes", "TEXT"); + _screens.add_column("recipient", "TEXT"); + _screens.add_column("recipient_file", "TEXT"); + + _trusted_devices.add_column("screen", "INTEGER"); + _trusted_devices.add_column("certificate_or_thumbprint", "TEXT"); +} + + +void +CinemaList::read_legacy_file(boost::filesystem::path xml_file) +{ + cxml::Document doc("Cinemas"); + doc.read_file(xml_file); + read_legacy_document(doc); +} + + +void +CinemaList::read_legacy_string(std::string const& xml) +{ + cxml::Document doc("Cinemas"); + doc.read_string(xml); + read_legacy_document(doc); +} + + +void +CinemaList::read_legacy_document(cxml::Document const& doc) +{ + for (auto cinema_node: doc.node_children("Cinema")) { + vector<string> emails; + for (auto email_node: cinema_node->node_children("Email")) { + emails.push_back(email_node->content()); + } + + int hour = 0; + if (cinema_node->optional_number_child<int>("UTCOffset")) { + hour = cinema_node->number_child<int>("UTCOffset"); + } else { + hour = cinema_node->optional_number_child<int>("UTCOffsetHour").get_value_or(0); + } + + int minute = cinema_node->optional_number_child<int>("UTCOffsetMinute").get_value_or(0); + + Cinema cinema( + cinema_node->string_child("Name"), + emails, + cinema_node->string_child("Notes"), + dcp::UTCOffset(hour, minute) + ); + + auto cinema_id = add_cinema(cinema); + + for (auto screen_node: cinema_node->node_children("Screen")) { + optional<dcp::Certificate> recipient; + if (auto recipient_string = screen_node->optional_string_child("Recipient")) { + recipient = dcp::Certificate(*recipient_string); + } + vector<TrustedDevice> trusted_devices; + for (auto trusted_device_node: screen_node->node_children("TrustedDevice")) { + trusted_devices.push_back(TrustedDevice(trusted_device_node->content())); + } + dcpomatic::Screen screen( + screen_node->string_child("Name"), + screen_node->string_child("Notes"), + recipient, + screen_node->optional_string_child("RecipientFile"), + trusted_devices + ); + add_screen(cinema_id, screen); + } + } +} + + +void +CinemaList::clear() +{ + for (auto table: { "cinemas", "screens", "trusted_devices" }) { + SQLiteStatement sql(_db, String::compose("DELETE FROM %1", table)); + sql.execute(); + } +} + + +void +CinemaList::setup(boost::filesystem::path db_file) +{ +#ifdef DCPOMATIC_WINDOWS + auto rc = sqlite3_open16(db_file.c_str(), &_db); +#else + auto rc = sqlite3_open(db_file.c_str(), &_db); +#endif + if (rc != SQLITE_OK) { + throw FileError("Could not open SQLite database", db_file); + } + + sqlite3_busy_timeout(_db, 500); + + SQLiteStatement cinemas(_db, _cinemas.create()); + cinemas.execute(); + + SQLiteStatement screens(_db, _screens.create()); + screens.execute(); + + SQLiteStatement devices(_db, _trusted_devices.create()); + devices.execute(); +} + + +CinemaList::CinemaList(CinemaList&& other) + : _db(other._db) + , _cinemas(std::move(other._cinemas)) + , _screens(std::move(other._screens)) + , _trusted_devices(std::move(other._trusted_devices)) +{ + other._db = nullptr; +} + + +CinemaList& +CinemaList::operator=(CinemaList&& other) +{ + if (this != &other) { + _db = other._db; + other._db = nullptr; + } + return *this; +} + + +CinemaID +CinemaList::add_cinema(Cinema const& cinema) +{ + SQLiteStatement statement(_db, _cinemas.insert()); + + statement.bind_text(1, cinema.name); + statement.bind_text(2, join_strings(cinema.emails)); + statement.bind_text(3, cinema.notes); + statement.bind_int64(4, cinema.utc_offset.hour()); + statement.bind_int64(5, cinema.utc_offset.minute()); + + statement.execute(); + + return sqlite3_last_insert_rowid(_db); +} + + +void +CinemaList::update_cinema(CinemaID id, Cinema const& cinema) +{ + SQLiteStatement statement(_db, _cinemas.update("WHERE id=?")); + + statement.bind_text(1, cinema.name); + statement.bind_text(2, join_strings(cinema.emails)); + statement.bind_text(3, cinema.notes); + statement.bind_int64(4, cinema.utc_offset.hour()); + statement.bind_int64(5, cinema.utc_offset.minute()); + statement.bind_int64(6, id.get()); + + statement.execute(); +} + + +void +CinemaList::remove_cinema(CinemaID id) +{ + SQLiteStatement statement(_db, "DELETE FROM cinemas WHERE ID=?"); + statement.bind_int64(1, id.get()); + statement.execute(); +} + + +CinemaList::~CinemaList() +{ + if (_db) { + sqlite3_close(_db); + } +} + + +static +vector<pair<CinemaID, Cinema>> +cinemas_from_result(SQLiteStatement& statement) +{ + vector<pair<CinemaID, Cinema>> output; + + statement.execute([&output](SQLiteStatement& statement) { + DCPOMATIC_ASSERT(statement.data_count() == 6); + CinemaID const id = statement.column_int64(0); + auto const name = statement.column_text(1); + auto const join_strings = statement.column_text(2); + vector<string> emails; + boost::algorithm::split(emails, join_strings, boost::is_any_of(" ")); + auto const notes = statement.column_text(3); + auto const utc_offset_hour = static_cast<int>(statement.column_int64(4)); + auto const utc_offset_minute = static_cast<int>(statement.column_int64(5)); + output.push_back(make_pair(id, Cinema(name, { emails }, notes, dcp::UTCOffset{utc_offset_hour, utc_offset_minute}))); + }); + + return output; +} + + +vector<pair<CinemaID, Cinema>> +CinemaList::cinemas() const +{ + SQLiteStatement statement(_db, _cinemas.select("ORDER BY name ASC")); + return cinemas_from_result(statement); +} + + +optional<Cinema> +CinemaList::cinema(CinemaID id) const +{ + SQLiteStatement statement(_db, _cinemas.select("WHERE id=?")); + statement.bind_int64(1, id.get()); + auto result = cinemas_from_result(statement); + if (result.empty()) { + return {}; + } + return result[0].second; +} + +ScreenID +CinemaList::add_screen(CinemaID cinema_id, dcpomatic::Screen const& screen) +{ + SQLiteTransaction transaction(_db); + + SQLiteStatement add_screen(_db, _screens.insert()); + + add_screen.bind_int64(1, cinema_id.get()); + add_screen.bind_text(2, screen.name); + add_screen.bind_text(3, screen.notes); + add_screen.bind_text(4, screen.recipient->certificate(true)); + add_screen.bind_text(5, screen.recipient_file.get_value_or("")); + + add_screen.execute(); + + auto const screen_id = sqlite3_last_insert_rowid(_db); + + for (auto device: screen.trusted_devices) { + SQLiteStatement add_device(_db, _trusted_devices.insert()); + add_device.bind_int64(1, screen_id); + add_device.bind_text(2, device.as_string()); + } + + transaction.commit(); + + return screen_id; +} + + +dcpomatic::Screen +CinemaList::screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const +{ + auto certificate_string = statement.column_text(4); + optional<dcp::Certificate> certificate = certificate_string.empty() ? optional<dcp::Certificate>() : dcp::Certificate(certificate_string); + auto recipient_file_string = statement.column_text(5); + optional<string> recipient_file = recipient_file_string.empty() ? optional<string>() : recipient_file_string; + + SQLiteStatement trusted_devices_statement(_db, _trusted_devices.select("WHERE screen=?")); + trusted_devices_statement.bind_int64(1, screen_id.get()); + vector<TrustedDevice> trusted_devices; + trusted_devices_statement.execute([&trusted_devices](SQLiteStatement& statement) { + DCPOMATIC_ASSERT(statement.data_count() == 1); + auto description = statement.column_text(1); + if (boost::algorithm::starts_with(description, "-----BEGIN CERTIFICATE")) { + trusted_devices.push_back(TrustedDevice(dcp::Certificate(description))); + } else { + trusted_devices.push_back(TrustedDevice(description)); + } + }); + + return dcpomatic::Screen(statement.column_text(2), statement.column_text(3), certificate, recipient_file, trusted_devices); +} + + +optional<dcpomatic::Screen> +CinemaList::screen(ScreenID screen_id) const +{ + SQLiteStatement statement(_db, _screens.select("WHERE id=?")); + statement.bind_int64(1, screen_id.get()); + + optional<dcpomatic::Screen> output; + + statement.execute([this, &output, screen_id](SQLiteStatement& statement) { + DCPOMATIC_ASSERT(statement.data_count() == 6); + output = screen_from_result(statement, screen_id); + }); + + return output; +} + + + +vector<pair<ScreenID, dcpomatic::Screen>> +CinemaList::screens_from_result(SQLiteStatement& statement) const +{ + vector<pair<ScreenID, dcpomatic::Screen>> output; + + statement.execute([this, &output](SQLiteStatement& statement) { + DCPOMATIC_ASSERT(statement.data_count() == 6); + ScreenID const screen_id = statement.column_int64(0); + output.push_back({screen_id, screen_from_result(statement, screen_id)}); + }); + + return output; +} + + +vector<pair<ScreenID, dcpomatic::Screen>> +CinemaList::screens(CinemaID cinema_id) const +{ + SQLiteStatement statement(_db, _screens.select("WHERE cinema=?")); + statement.bind_int64(1, cinema_id.get()); + return screens_from_result(statement); +} + + +vector<pair<ScreenID, dcpomatic::Screen>> +CinemaList::screens_by_cinema_and_name(CinemaID id, std::string const& name) const +{ + SQLiteStatement statement(_db, _screens.select("WHERE cinema=? AND name=?")); + statement.bind_int64(1, id.get()); + statement.bind_text(2, name); + return screens_from_result(statement); +} + + +optional<std::pair<CinemaID, Cinema>> +CinemaList::cinema_by_name_or_email(std::string const& text) const +{ + SQLiteStatement statement(_db, _cinemas.select("WHERE name LIKE ? OR EMAILS LIKE ?")); + auto const wildcard = string("%") + text + "%"; + statement.bind_text(1, wildcard); + statement.bind_text(2, wildcard); + + auto all = cinemas_from_result(statement); + if (all.empty()) { + return {}; + } + return all[0]; +} + + +void +CinemaList::update_screen(ScreenID id, dcpomatic::Screen const& screen) +{ + SQLiteStatement statement(_db, _screens.update("WHERE id=?")); + + statement.bind_text(1, screen.name); + statement.bind_text(2, screen.notes); + statement.bind_text(3, screen.recipient->certificate(true)); + statement.bind_text(4, screen.recipient_file.get_value_or("")); + statement.bind_int64(5, id.get()); + + statement.execute(); +} + + +void +CinemaList::remove_screen(ScreenID id) +{ + SQLiteStatement statement(_db, "DELETE FROM screens WHERE ID=?"); + statement.bind_int64(1, id.get()); + statement.execute(); +} + + +optional<dcp::UTCOffset> +CinemaList::unique_utc_offset(std::set<CinemaID> const& cinemas_to_check) +{ + optional<dcp::UTCOffset> offset; + + for (auto const& cinema: cinemas()) { + if (cinemas_to_check.find(cinema.first) == cinemas_to_check.end()) { + continue; + } + + if (!offset) { + offset = cinema.second.utc_offset; + } else if (cinema.second.utc_offset != *offset) { + return dcp::UTCOffset(); + } + } + + return offset; +} + diff --git a/src/lib/cinema_list.h b/src/lib/cinema_list.h new file mode 100644 index 000000000..c91f29476 --- /dev/null +++ b/src/lib/cinema_list.h @@ -0,0 +1,126 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_CINEMA_LIST_H +#define DCPOMATIC_CINEMA_LIST_H + + +#include "id.h" +#include "sqlite_table.h" +#include <libcxml/cxml.h> +#include <dcp/utc_offset.h> +#include <boost/filesystem.hpp> +#include <boost/optional.hpp> +#include <sqlite3.h> +#include <set> + + +class Cinema; +namespace dcpomatic { + class Screen; +} +class SQLiteStatement; + + +class CinemaID : public ID +{ +public: + CinemaID(sqlite3_int64 id) + : ID(id) {} + + bool operator<(CinemaID const& other) const { + return get() < other.get(); + } +}; + + +class ScreenID : public ID +{ +public: + ScreenID(sqlite3_int64 id) + : ID(id) {} + + bool operator==(ScreenID const& other) const { + return get() == other.get(); + } + + bool operator!=(ScreenID const& other) const { + return get() != other.get(); + } + + bool operator<(ScreenID const& other) const { + return get() < other.get(); + } +}; + + +class CinemaList +{ +public: + CinemaList(); + CinemaList(boost::filesystem::path db_file); + ~CinemaList(); + + CinemaList(CinemaList const&) = delete; + CinemaList& operator=(CinemaList const&) = delete; + + CinemaList(CinemaList&& other); + CinemaList& operator=(CinemaList&& other); + + void read_legacy_file(boost::filesystem::path xml_file); + void read_legacy_string(std::string const& xml); + + void clear(); + + CinemaID add_cinema(Cinema const& cinema); + void update_cinema(CinemaID id, Cinema const& cinema); + void remove_cinema(CinemaID id); + std::vector<std::pair<CinemaID, Cinema>> cinemas() const; + boost::optional<Cinema> cinema(CinemaID id) const; + boost::optional<std::pair<CinemaID, Cinema>> cinema_by_partial_name(std::string const& text) const; + boost::optional<std::pair<CinemaID, Cinema>> cinema_by_name_or_email(std::string const& text) const; + + ScreenID add_screen(CinemaID cinema_id, dcpomatic::Screen const& screen); + void update_screen(ScreenID id, dcpomatic::Screen const& screen); + void remove_screen(ScreenID id); + boost::optional<dcpomatic::Screen> screen(ScreenID screen_id) const; + std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens(CinemaID cinema_id) const; + std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens_by_cinema_and_name(CinemaID id, std::string const& name) const; + + boost::optional<dcp::UTCOffset> unique_utc_offset(std::set<CinemaID> const& cinemas); + +private: + dcpomatic::Screen screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const; + std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens_from_result(SQLiteStatement& statement) const; + void setup_tables(); + void setup(boost::filesystem::path db_file); + void read_legacy_document(cxml::Document const& doc); + + sqlite3* _db = nullptr; + SQLiteTable _cinemas; + SQLiteTable _screens; + SQLiteTable _trusted_devices; +}; + + + +#endif + diff --git a/src/lib/config.cc b/src/lib/config.cc index 33b1a8656..c2c2cc244 100644 --- a/src/lib/config.cc +++ b/src/lib/config.cc @@ -19,14 +19,14 @@ */ -#include "cinema.h" +#include "cinema_list.h" #include "colour_conversion.h" #include "compose.hpp" #include "config.h" #include "constants.h" #include "cross.h" #include "dcp_content_type.h" -#include "dkdm_recipient.h" +#include "dkdm_recipient_list.h" #include "dkdm_wrapper.h" #include "film.h" #include "filter.h" @@ -139,8 +139,8 @@ Config::set_defaults () /* At the moment we don't write these files anywhere new after a version change, so they will be read from * ~/.config/dcpomatic2 (or equivalent) and written back there. */ - _cinemas_file = read_path ("cinemas.xml"); - _dkdm_recipients_file = read_path ("dkdm_recipients.xml"); + _cinemas_file = read_path("cinemas.sqlite3"); + _dkdm_recipients_file = read_path("dkdm_recipients.sqlite3"); _show_hints_before_make_dcp = true; _confirm_kdm_email = true; _kdm_container_name_format = dcp::NameFormat("KDM_%f_%c"); @@ -276,7 +276,7 @@ Config::backup () copy_file(path_to_copy, add_number(path_to_copy, n), ec); }; - /* Make a backup copy of any config.xml, cinemas.xml, dkdm_recipients.xml that we might be about + /* Make a backup copy of any config.xml, cinemas.sqlite3, dkdm_recipients.sqlite3 that we might be about * to write over. This is more intended for the situation where we have a corrupted config.xml, * and decide to overwrite it with a new one (possibly losing important details in the corrupted * file). But we might as well back up the other files while we're about it. @@ -296,15 +296,6 @@ Config::backup () void Config::read () -{ - read_config(); - read_cinemas(); - read_dkdm_recipients(); -} - - -void -Config::read_config() try { cxml::Document f ("Config"); @@ -406,11 +397,6 @@ try _default_kdm_directory = f.optional_string_child("DefaultKDMDirectory"); - /* Read any cinemas that are still lying around in the config file - * from an old version. - */ - read_cinemas (f); - _mail_server = f.string_child ("MailServer"); _mail_port = f.optional_number_child<int> ("MailPort").get_value_or (25); @@ -548,8 +534,8 @@ try _dkdms->add (DKDMBase::read (i)); } } - _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.xml").string()); - _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.xml").string()); + _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.sqlite3").string()); + _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.sqlite3").string()); _show_hints_before_make_dcp = f.optional_bool_child("ShowHintsBeforeMakeDCP").get_value_or (true); _confirm_kdm_email = f.optional_bool_child("ConfirmKDMEmail").get_value_or (true); _kdm_container_name_format = dcp::NameFormat (f.optional_string_child("KDMContainerNameFormat").get_value_or ("KDM %f %c")); @@ -688,40 +674,6 @@ catch (...) { } -void -Config::read_cinemas() -{ - if (dcp::filesystem::exists(_cinemas_file)) { - try { - cxml::Document f("Cinemas"); - f.read_file(dcp::filesystem::fix_long_path(_cinemas_file)); - read_cinemas(f); - } catch (...) { - backup(); - FailedToLoad(LoadFailure::CINEMAS); - write_cinemas(); - } - } -} - - -void -Config::read_dkdm_recipients() -{ - if (dcp::filesystem::exists(_dkdm_recipients_file)) { - try { - cxml::Document f("DKDMRecipients"); - f.read_file(dcp::filesystem::fix_long_path(_dkdm_recipients_file)); - read_dkdm_recipients(f); - } catch (...) { - backup(); - FailedToLoad(LoadFailure::DKDM_RECIPIENTS); - write_dkdm_recipients(); - } - } -} - - /** @return Singleton instance */ Config * Config::instance () @@ -729,6 +681,30 @@ Config::instance () if (_instance == nullptr) { _instance = new Config; _instance->read (); + + auto cinemas_file = _instance->cinemas_file(); + if (cinemas_file.extension() == ".xml") { + auto sqlite = cinemas_file; + sqlite.replace_extension(".sqlite3"); + + if (dcp::filesystem::exists(cinemas_file) && !dcp::filesystem::exists(sqlite)) { + _instance->set_cinemas_file(sqlite); + CinemaList cinemas; + cinemas.read_legacy_file(cinemas_file); + } + } + + auto dkdm_recipients_file = _instance->dkdm_recipients_file(); + if (dkdm_recipients_file.extension() == ".xml") { + auto sqlite = dkdm_recipients_file; + sqlite.replace_extension(".sqlite3"); + + if (dcp::filesystem::exists(dkdm_recipients_file) && !dcp::filesystem::exists(sqlite)) { + _instance->set_dkdm_recipients_file(sqlite); + DKDMRecipientList recipients; + recipients.read_legacy_file(dkdm_recipients_file); + } + } } return _instance; @@ -739,8 +715,6 @@ void Config::write () const { write_config (); - write_cinemas (); - write_dkdm_recipients (); } void @@ -1222,20 +1196,6 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t } -void -Config::write_cinemas () const -{ - write_file ("Cinemas", "Cinema", "1", _cinemas, _cinemas_file); -} - - -void -Config::write_dkdm_recipients () const -{ - write_file ("DKDMRecipients", "DKDMRecipient", "1", _dkdm_recipients, _dkdm_recipients_file); -} - - boost::filesystem::path Config::default_directory_or (boost::filesystem::path a) const { @@ -1397,20 +1357,6 @@ Config::have_existing (string file) void -Config::read_cinemas (cxml::Document const & f) -{ - _cinemas.clear (); - for (auto i: f.node_children("Cinema")) { - /* Slightly grotty two-part construction of Cinema here so that we can use - shared_from_this. - */ - auto cinema = make_shared<Cinema>(i); - cinema->read_screens (i); - _cinemas.push_back (cinema); - } -} - -void Config::set_cinemas_file (boost::filesystem::path file) { if (file == _cinemas_file) { @@ -1419,25 +1365,20 @@ Config::set_cinemas_file (boost::filesystem::path file) _cinemas_file = file; - if (dcp::filesystem::exists(_cinemas_file)) { - /* Existing file; read it in */ - cxml::Document f ("Cinemas"); - f.read_file(dcp::filesystem::fix_long_path(_cinemas_file)); - read_cinemas (f); - } - - changed (CINEMAS); changed (OTHER); } void -Config::read_dkdm_recipients (cxml::Document const & f) +Config::set_dkdm_recipients_file(boost::filesystem::path file) { - _dkdm_recipients.clear (); - for (auto i: f.node_children("DKDMRecipient")) { - _dkdm_recipients.push_back (make_shared<DKDMRecipient>(i)); + if (file == _dkdm_recipients_file) { + return; } + + _dkdm_recipients_file = file; + + changed(OTHER); } @@ -1670,10 +1611,10 @@ save_all_config_as_zip (boost::filesystem::path zip_file) auto config = Config::instance(); zipper.add ("config.xml", dcp::file_to_string(config->config_read_file())); if (dcp::filesystem::exists(config->cinemas_file())) { - zipper.add ("cinemas.xml", dcp::file_to_string(config->cinemas_file())); + zipper.add("cinemas.sqlite3", dcp::file_to_string(config->cinemas_file())); } if (dcp::filesystem::exists(config->dkdm_recipients_file())) { - zipper.add ("dkdm_recipients.xml", dcp::file_to_string(config->dkdm_recipients_file())); + zipper.add("dkdm_recipients.sqlite3", dcp::file_to_string(config->dkdm_recipients_file())); } zipper.close (); @@ -1681,22 +1622,58 @@ save_all_config_as_zip (boost::filesystem::path zip_file) void -Config::load_from_zip(boost::filesystem::path zip_file) +Config::load_from_zip(boost::filesystem::path zip_file, CinemasAction action) { + backup(); + + auto const current_cinemas = cinemas_file(); + /* This is (unfortunately) a full path, and the user can't change it, so + * we always want to use that same path in the future no matter what is in the + * config.xml that we are about to load. + */ + auto const current_dkdm_recipients = dkdm_recipients_file(); + Unzipper unzipper(zip_file); dcp::write_string_to_file(unzipper.get("config.xml"), config_write_file()); - try { - dcp::write_string_to_file(unzipper.get("cinemas.xml"), cinemas_file()); - dcp::write_string_to_file(unzipper.get("dkdm_recipient.xml"), dkdm_recipients_file()); - } catch (std::runtime_error&) {} + if (action == CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG) { + /* Read the zipped config, so that the cinemas file path is the new one and + * we write the cinemas to it. + */ + read(); + boost::filesystem::create_directories(cinemas_file().parent_path()); + set_dkdm_recipients_file(current_dkdm_recipients); + } + + if (unzipper.contains("cinemas.xml") && action != CinemasAction::IGNORE) { + CinemaList cinemas; + cinemas.clear(); + cinemas.read_legacy_string(unzipper.get("cinemas.xml")); + } + + if (unzipper.contains("dkdm_recipients.xml")) { + DKDMRecipientList recipients; + recipients.clear(); + recipients.read_legacy_string(unzipper.get("dkdm_recipients.xml")); + } - read(); + if (unzipper.contains("cinemas.sqlite3") && action != CinemasAction::IGNORE) { + dcp::write_string_to_file(unzipper.get("cinemas.sqlite3"), cinemas_file()); + } + + if (unzipper.contains("dkdm_recipients.sqlite3")) { + dcp::write_string_to_file(unzipper.get("dkdm_recipients.sqlite3"), dkdm_recipients_file()); + } + + if (action != CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG) { + /* Read the zipped config, then reset the cinemas file to be the old one */ + read(); + set_cinemas_file(current_cinemas); + set_dkdm_recipients_file(current_dkdm_recipients); + } changed(Property::USE_ANY_SERVERS); changed(Property::SERVERS); - changed(Property::CINEMAS); - changed(Property::DKDM_RECIPIENTS); changed(Property::SOUND); changed(Property::SOUND_OUTPUT); changed(Property::PLAYER_CONTENT_DIRECTORY); @@ -1733,6 +1710,25 @@ Config::initial_path(string id) const } +bool +Config::zip_contains_cinemas(boost::filesystem::path zip) +{ + Unzipper unzipper(zip); + return unzipper.contains("cinemas.sqlite3") || unzipper.contains("cinemas.xml"); +} + + +boost::filesystem::path +Config::cinemas_file_from_zip(boost::filesystem::path zip) +{ + Unzipper unzipper(zip); + DCPOMATIC_ASSERT(unzipper.contains("config.xml")); + cxml::Document document("Config"); + document.read_string(unzipper.get("config.xml")); + return document.string_child("CinemasFile"); +} + + #ifdef DCPOMATIC_GROK Config::Grok::Grok(cxml::ConstNodePtr node) diff --git a/src/lib/config.h b/src/lib/config.h index a7b238c04..67c784620 100644 --- a/src/lib/config.h +++ b/src/lib/config.h @@ -50,6 +50,8 @@ class DKDMRecipient; class Film; class Ratio; +#undef IGNORE + extern void save_all_config_as_zip (boost::filesystem::path zip_file); @@ -81,13 +83,28 @@ public: boost::filesystem::path default_directory_or (boost::filesystem::path a) const; boost::filesystem::path default_kdm_directory_or (boost::filesystem::path a) const; - void load_from_zip(boost::filesystem::path zip_file); + enum class CinemasAction + { + /** Copy the cinemas.{xml,sqlite3} in the ZIP file to the path + * specified in the current config, overwriting whatever is there, + * and use that path. + */ + WRITE_TO_CURRENT_PATH, + /** Copy the cinemas.{xml,sqlite3} in the ZIP file over the path + * specified in the config.xml from the ZIP, overwriting whatever + * is there and creating any required directories, and use + * that path. + */ + WRITE_TO_PATH_IN_ZIPPED_CONFIG, + /** Do nothing with the cinemas.{xml,sqlite3} in the ZIP file */ + IGNORE + }; + + void load_from_zip(boost::filesystem::path zip_file, CinemasAction action); enum Property { USE_ANY_SERVERS, SERVERS, - CINEMAS, - DKDM_RECIPIENTS, SOUND, SOUND_OUTPUT, PLAYER_CONTENT_DIRECTORY, @@ -163,14 +180,6 @@ public: return _tms_password; } - std::list<std::shared_ptr<Cinema>> cinemas () const { - return _cinemas; - } - - std::list<std::shared_ptr<DKDMRecipient>> dkdm_recipients () const { - return _dkdm_recipients; - } - std::list<int> allowed_dcp_frame_rates () const { return _allowed_dcp_frame_rates; } @@ -716,26 +725,6 @@ public: maybe_set (_tms_password, p); } - void add_cinema (std::shared_ptr<Cinema> c) { - _cinemas.push_back (c); - changed (CINEMAS); - } - - void remove_cinema (std::shared_ptr<Cinema> c) { - _cinemas.remove (c); - changed (CINEMAS); - } - - void add_dkdm_recipient (std::shared_ptr<DKDMRecipient> c) { - _dkdm_recipients.push_back (c); - changed (DKDM_RECIPIENTS); - } - - void remove_dkdm_recipient (std::shared_ptr<DKDMRecipient> c) { - _dkdm_recipients.remove (c); - changed (DKDM_RECIPIENTS); - } - void set_allowed_dcp_frame_rates (std::list<int> const & r) { maybe_set (_allowed_dcp_frame_rates, r); } @@ -965,6 +954,8 @@ public: void set_cinemas_file (boost::filesystem::path file); + void set_dkdm_recipients_file(boost::filesystem::path file); + void set_show_hints_before_make_dcp (bool s) { maybe_set (_show_hints_before_make_dcp, s); } @@ -1254,8 +1245,6 @@ public: */ enum class LoadFailure { CONFIG, - CINEMAS, - DKDM_RECIPIENTS }; static boost::signals2::signal<void (LoadFailure)> FailedToLoad; /** Emitted if read() issued a warning which the user might want to know about */ @@ -1275,8 +1264,6 @@ public: void write () const override; void write_config () const; - void write_cinemas () const; - void write_dkdm_recipients () const; void link (boost::filesystem::path new_file) const; void copy_and_link (boost::filesystem::path new_file) const; bool have_write_permission () const; @@ -1297,6 +1284,9 @@ public: static bool have_existing (std::string); static boost::filesystem::path config_read_file (); static boost::filesystem::path config_write_file (); + static bool zip_contains_cinemas(boost::filesystem::path zip); + static boost::filesystem::path cinemas_file_from_zip(boost::filesystem::path zip); + template <class T> void maybe_set (T& member, T new_value, Property prop = OTHER) { @@ -1319,15 +1309,10 @@ public: private: Config (); void read () override; - void read_config(); - void read_cinemas(); - void read_dkdm_recipients(); void set_defaults (); void set_kdm_email_to_default (); void set_notification_email_to_default (); void set_cover_sheet_to_default (); - void read_cinemas (cxml::Document const & f); - void read_dkdm_recipients (cxml::Document const & f); std::shared_ptr<dcp::CertificateChain> create_certificate_chain (); boost::filesystem::path directory_or (boost::optional<boost::filesystem::path> dir, boost::filesystem::path a) const; void add_to_history_internal (std::vector<boost::filesystem::path>& h, boost::filesystem::path p); @@ -1394,8 +1379,6 @@ private: */ boost::optional<boost::filesystem::path> _default_kdm_directory; bool _upload_after_make_dcp; - std::list<std::shared_ptr<Cinema>> _cinemas; - std::list<std::shared_ptr<DKDMRecipient>> _dkdm_recipients; std::string _mail_server; int _mail_port; EmailProtocol _mail_protocol; diff --git a/src/lib/dkdm_recipient.cc b/src/lib/dkdm_recipient.cc index 4e56a37c4..83ba96de6 100644 --- a/src/lib/dkdm_recipient.cc +++ b/src/lib/dkdm_recipient.cc @@ -19,6 +19,7 @@ */ +#include "cinema_list.h" #include "config.h" #include "dkdm_recipient.h" #include "film.h" @@ -33,36 +34,16 @@ using std::vector; using dcp::raw_convert; -DKDMRecipient::DKDMRecipient (cxml::ConstNodePtr node) - : KDMRecipient (node) -{ - for (auto i: node->node_children("Email")) { - emails.push_back (i->content()); - } -} - - -void -DKDMRecipient::as_xml (xmlpp::Element* node) const -{ - KDMRecipient::as_xml (node); - - for (auto i: emails) { - cxml::add_text_child(node, "Email", i); - } -} - - KDMWithMetadataPtr kdm_for_dkdm_recipient ( shared_ptr<const Film> film, boost::filesystem::path cpl, - shared_ptr<DKDMRecipient> recipient, + DKDMRecipient const& recipient, dcp::LocalTime valid_from, dcp::LocalTime valid_to ) { - if (!recipient->recipient) { + if (!recipient.recipient) { return {}; } @@ -72,7 +53,7 @@ kdm_for_dkdm_recipient ( } auto const decrypted_kdm = film->make_kdm(cpl, valid_from, valid_to); - auto const kdm = decrypted_kdm.encrypt(signer, recipient->recipient.get(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0); + auto const kdm = decrypted_kdm.encrypt(signer, recipient.recipient.get(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0); dcp::NameFormat::Map name_values; name_values['f'] = kdm.content_title_text(); @@ -80,6 +61,6 @@ kdm_for_dkdm_recipient ( name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false); name_values['i'] = kdm.cpl_id(); - return make_shared<KDMWithMetadata>(name_values, nullptr, recipient->emails, kdm); + return make_shared<KDMWithMetadata>(name_values, CinemaID(0), recipient.emails, kdm); } diff --git a/src/lib/dkdm_recipient.h b/src/lib/dkdm_recipient.h index 3317ae6f9..64da41cff 100644 --- a/src/lib/dkdm_recipient.h +++ b/src/lib/dkdm_recipient.h @@ -41,10 +41,6 @@ public: } - explicit DKDMRecipient (cxml::ConstNodePtr); - - void as_xml (xmlpp::Element *) const override; - std::vector<std::string> emails; }; @@ -53,7 +49,7 @@ KDMWithMetadataPtr kdm_for_dkdm_recipient ( std::shared_ptr<const Film> film, boost::filesystem::path cpl, - std::shared_ptr<DKDMRecipient> recipient, + DKDMRecipient const& recipient, dcp::LocalTime valid_from, dcp::LocalTime valid_to ); diff --git a/src/lib/dkdm_recipient_list.cc b/src/lib/dkdm_recipient_list.cc new file mode 100644 index 000000000..34179337e --- /dev/null +++ b/src/lib/dkdm_recipient_list.cc @@ -0,0 +1,243 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "config.h" +#include "dkdm_recipient.h" +#include "dkdm_recipient_list.h" +#include "sqlite_statement.h" +#include "sqlite_transaction.h" +#include "util.h" +#include <boost/algorithm/string.hpp> + + +using std::make_pair; +using std::pair; +using std::string; +using std::vector; +using boost::optional; + + +DKDMRecipientList::DKDMRecipientList() + : _dkdm_recipients("dkdm_recipients") +{ + setup(Config::instance()->dkdm_recipients_file()); +} + + +DKDMRecipientList::DKDMRecipientList(boost::filesystem::path db_file) + : _dkdm_recipients("dkdm_recipients") +{ + setup(db_file); +} + + + +DKDMRecipientList::~DKDMRecipientList() +{ + if (_db) { + sqlite3_close(_db); + } +} + + +void +DKDMRecipientList::read_legacy_file(boost::filesystem::path xml_file) +{ + cxml::Document doc("DKDMRecipients"); + doc.read_file(xml_file); + + read_legacy_document(doc); +} + + +void +DKDMRecipientList::read_legacy_string(string const& xml) +{ + cxml::Document doc("DKDMRecipients"); + doc.read_file(xml); + + read_legacy_document(doc); +} + + +void +DKDMRecipientList::read_legacy_document(cxml::Document const& doc) +{ + for (auto recipient_node: doc.node_children("DKDMRecipient")) { + vector<string> emails; + for (auto email_node: recipient_node->node_children("Email")) { + emails.push_back(email_node->content()); + } + + optional<dcp::Certificate> certificate; + if (auto certificate_string = recipient_node->optional_string_child("Recipient")) { + certificate = dcp::Certificate(*certificate_string); + } + + DKDMRecipient recipient( + recipient_node->string_child("Name"), + recipient_node->string_child("Notes"), + certificate, + emails + ); + + add_dkdm_recipient(recipient); + } +} + + +void +DKDMRecipientList::setup(boost::filesystem::path db_file) +{ + _dkdm_recipients.add_column("name", "TEXT"); + _dkdm_recipients.add_column("notes", "TEXT"); + _dkdm_recipients.add_column("recipient", "TEXT"); + _dkdm_recipients.add_column("emails", "TEXT"); + +#ifdef DCPOMATIC_WINDOWS + auto rc = sqlite3_open16(db_file.c_str(), &_db); +#else + auto rc = sqlite3_open(db_file.c_str(), &_db); +#endif + if (rc != SQLITE_OK) { + throw FileError("Could not open SQLite database", db_file); + } + + sqlite3_busy_timeout(_db, 500); + + SQLiteStatement screens(_db, _dkdm_recipients.create()); + screens.execute(); +} + + +DKDMRecipientList::DKDMRecipientList(DKDMRecipientList&& other) + : _dkdm_recipients(std::move(other._dkdm_recipients)) +{ + _db = other._db; + other._db = nullptr; +} + + +DKDMRecipientList& +DKDMRecipientList::operator=(DKDMRecipientList&& other) +{ + if (this != &other) { + _db = other._db; + other._db = nullptr; + } + return *this; +} + + +DKDMRecipientID +DKDMRecipientList::add_dkdm_recipient(DKDMRecipient const& dkdm_recipient) +{ + SQLiteStatement add_dkdm_recipient(_db, _dkdm_recipients.insert()); + + add_dkdm_recipient.bind_text(1, dkdm_recipient.name); + add_dkdm_recipient.bind_text(2, dkdm_recipient.notes); + add_dkdm_recipient.bind_text(3, dkdm_recipient.recipient ? dkdm_recipient.recipient->certificate(true) : ""); + add_dkdm_recipient.bind_text(4, join_strings(dkdm_recipient.emails)); + + add_dkdm_recipient.execute(); + + return sqlite3_last_insert_rowid(_db); +} + + +void +DKDMRecipientList::update_dkdm_recipient(DKDMRecipientID id, DKDMRecipient const& dkdm_recipient) +{ + SQLiteStatement add_dkdm_recipient(_db, _dkdm_recipients.update("WHERE id=?")); + + add_dkdm_recipient.bind_text(1, dkdm_recipient.name); + add_dkdm_recipient.bind_text(2, dkdm_recipient.notes); + add_dkdm_recipient.bind_text(3, dkdm_recipient.recipient ? dkdm_recipient.recipient->certificate(true) : ""); + add_dkdm_recipient.bind_text(4, join_strings(dkdm_recipient.emails)); + add_dkdm_recipient.bind_int64(5, id.get()); + + add_dkdm_recipient.execute(); +} + + +void +DKDMRecipientList::remove_dkdm_recipient(DKDMRecipientID id) +{ + SQLiteStatement statement(_db, "DELETE FROM dkdm_recipients WHERE ID=?"); + statement.bind_int64(1, id.get()); + statement.execute(); +} + + +static +vector<pair<DKDMRecipientID, DKDMRecipient>> +dkdm_recipients_from_result(SQLiteStatement& statement) +{ + vector<pair<DKDMRecipientID, DKDMRecipient>> output; + + statement.execute([&output](SQLiteStatement& statement) { + DCPOMATIC_ASSERT(statement.data_count() == 5); + DKDMRecipientID const id = statement.column_int64(0); + auto const name = statement.column_text(1); + auto const notes = statement.column_text(2); + auto certificate_string = statement.column_text(3); + optional<dcp::Certificate> certificate = certificate_string.empty() ? optional<dcp::Certificate>() : dcp::Certificate(certificate_string); + auto const join_with_spaces = statement.column_text(4); + vector<string> emails; + boost::algorithm::split(emails, join_with_spaces, boost::is_any_of(" ")); + output.push_back(make_pair(id, DKDMRecipient(name, notes, certificate, { emails }))); + }); + + return output; +} + + + + +vector<std::pair<DKDMRecipientID, DKDMRecipient>> +DKDMRecipientList::dkdm_recipients() const +{ + SQLiteStatement statement(_db, _dkdm_recipients.select("ORDER BY name ASC")); + return dkdm_recipients_from_result(statement); +} + + +boost::optional<DKDMRecipient> +DKDMRecipientList::dkdm_recipient(DKDMRecipientID id) const +{ + SQLiteStatement statement(_db, _dkdm_recipients.select("WHERE id=?")); + statement.bind_int64(1, id.get()); + auto result = dkdm_recipients_from_result(statement); + if (result.empty()) { + return {}; + } + return result[0].second; +} + + +void +DKDMRecipientList::clear() +{ + SQLiteStatement sql(_db, "DELETE FROM dkdm_recipients"); + sql.execute(); +} + + diff --git a/src/lib/dkdm_recipient_list.h b/src/lib/dkdm_recipient_list.h new file mode 100644 index 000000000..fc4d84b60 --- /dev/null +++ b/src/lib/dkdm_recipient_list.h @@ -0,0 +1,90 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_DKDM_RECIPIENT_LIST_H +#define DCPOMATIC_DKDM_RECIPIENT_LIST_H + + +#include "id.h" +#include "sqlite_table.h" +#include <libcxml/cxml.h> +#include <boost/filesystem.hpp> +#include <boost/optional.hpp> + + +class DKDMRecipient; + + +class DKDMRecipientID : public ID +{ +public: + DKDMRecipientID(sqlite3_int64 id) + : ID(id) {} + + bool operator==(DKDMRecipientID const& other) const { + return get() == other.get(); + } + + bool operator!=(DKDMRecipientID const& other) const { + return get() != other.get(); + } + + bool operator<(DKDMRecipientID const& other) const { + return get() < other.get(); + } +}; + + +class DKDMRecipientList +{ +public: + DKDMRecipientList(); + DKDMRecipientList(boost::filesystem::path db_file); + ~DKDMRecipientList(); + + DKDMRecipientList(DKDMRecipientList const&) = delete; + DKDMRecipientList& operator=(DKDMRecipientList const&) = delete; + + DKDMRecipientList(DKDMRecipientList&& other); + DKDMRecipientList& operator=(DKDMRecipientList&& other); + + void read_legacy_file(boost::filesystem::path xml_file); + void read_legacy_string(std::string const& xml); + + void clear(); + + DKDMRecipientID add_dkdm_recipient(DKDMRecipient const& dkdm_recipient); + void update_dkdm_recipient(DKDMRecipientID id, DKDMRecipient const& dkdm_recipient); + void remove_dkdm_recipient(DKDMRecipientID id); + std::vector<std::pair<DKDMRecipientID, DKDMRecipient>> dkdm_recipients() const; + boost::optional<DKDMRecipient> dkdm_recipient(DKDMRecipientID id) const; + +private: + void setup(boost::filesystem::path db_file); + void read_legacy_document(cxml::Document const& doc); + + sqlite3* _db = nullptr; + SQLiteTable _dkdm_recipients; +}; + + +#endif + diff --git a/src/lib/exceptions.h b/src/lib/exceptions.h index 10231f59a..e08392689 100644 --- a/src/lib/exceptions.h +++ b/src/lib/exceptions.h @@ -34,6 +34,7 @@ extern "C" { } #include <boost/filesystem.hpp> #include <boost/optional.hpp> +#include <sqlite3.h> #include <cstring> #include <stdexcept> @@ -480,4 +481,58 @@ public: }; +class SQLError : public std::runtime_error +{ +public: + SQLError(sqlite3* db, char const* s) + : std::runtime_error(get_message(db, s)) + { + _filename = get_filename(db); + } + + SQLError(sqlite3* db, int rc) + : std::runtime_error(get_message(db, rc)) + { + _filename = get_filename(db); + } + + SQLError(sqlite3* db, int rc, std::string doing) + : std::runtime_error(get_message(db, rc, doing)) + { + _filename = get_filename(db); + } + + boost::filesystem::path filename() const { + return _filename; + } + +private: + boost::filesystem::path get_filename(sqlite3* db) + { + if (auto filename = sqlite3_db_filename(db, "main")) { + return filename; + } + + return {}; + } + + std::string get_message(sqlite3* db, char const* s) + { + return String::compose("%1 (in %2)", s, get_filename(db)); + } + + std::string get_message(sqlite3* db, int rc) + { + return String::compose("%1 (in %2)", sqlite3_errstr(rc), get_filename(db)); + } + + std::string get_message(sqlite3* db, int rc, std::string doing) + { + return String::compose("%1 (while doing %2) (in %3)", sqlite3_errstr(rc), doing, get_filename(db)); + } + + boost::filesystem::path _filename; +}; + + #endif diff --git a/src/lib/id.cc b/src/lib/id.cc new file mode 100644 index 000000000..2891fb4ab --- /dev/null +++ b/src/lib/id.cc @@ -0,0 +1,30 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "id.h" + + +bool +operator==(ID const& a, ID const& b) +{ + return a.get() == b.get(); +} + diff --git a/src/lib/id.h b/src/lib/id.h new file mode 100644 index 000000000..ca4721039 --- /dev/null +++ b/src/lib/id.h @@ -0,0 +1,48 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_ID_H +#define DCPOMATIC_ID_H + + +#include <sqlite3.h> + + +class ID +{ +public: + sqlite3_int64 get() const { + return _id; + } + +protected: + ID(sqlite3_int64 id) + : _id(id) {} + +private: + sqlite3_int64 _id; +}; + + +bool operator==(ID const& a, ID const& b); + + +#endif diff --git a/src/lib/kdm_cli.cc b/src/lib/kdm_cli.cc index 21e8c75d3..9d5b54b9a 100644 --- a/src/lib/kdm_cli.cc +++ b/src/lib/kdm_cli.cc @@ -25,6 +25,7 @@ #include "cinema.h" +#include "cinema_list.h" #include "config.h" #include "dkdm_wrapper.h" #include "email.h" @@ -43,6 +44,7 @@ using std::dynamic_pointer_cast; using std::list; using std::make_shared; +using std::pair; using std::runtime_error; using std::shared_ptr; using std::string; @@ -175,32 +177,25 @@ write_files ( } -static -shared_ptr<Cinema> -find_cinema (string cinema_name) +class ScreenDetails { - auto cinemas = Config::instance()->cinemas (); - auto i = cinemas.begin(); - while ( - i != cinemas.end() && - (*i)->name != cinema_name && - find ((*i)->emails.begin(), (*i)->emails.end(), cinema_name) == (*i)->emails.end()) { - - ++i; - } - - if (i == cinemas.end ()) { - throw KDMCLIError (String::compose("could not find cinema \"%1\"", cinema_name)); - } +public: + ScreenDetails(CinemaID const& cinema_id, Cinema const& cinema, Screen const& screen) + : cinema_id(cinema_id) + , cinema(cinema) + , screen(screen) + {} - return *i; -} + CinemaID cinema_id; + Cinema cinema; + Screen screen; +}; static void from_film ( - vector<shared_ptr<Screen>> screens, + vector<ScreenDetails> const& screens, boost::filesystem::path film_dir, bool verbose, boost::filesystem::path output, @@ -241,11 +236,22 @@ from_film ( try { list<KDMWithMetadataPtr> kdms; - for (auto i: screens) { + for (auto screen_details: screens) { std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm = [film, cpl](dcp::LocalTime begin, dcp::LocalTime end) { return film->make_kdm(cpl, begin, end); }; - auto p = kdm_for_screen(make_kdm, i, valid_from, valid_to, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio, period_checks); + auto p = kdm_for_screen( + make_kdm, + screen_details.cinema_id, + screen_details.cinema, + screen_details.screen, + valid_from, + valid_to, + formulation, + disable_forensic_marking_picture, + disable_forensic_marking_audio, + period_checks + ); if (p) { kdms.push_back (p); } @@ -350,7 +356,7 @@ kdm_from_dkdm ( static void from_dkdm ( - vector<shared_ptr<Screen>> screens, + vector<ScreenDetails> const& screens, dcp::DecryptedKDM dkdm, bool verbose, boost::filesystem::path output, @@ -370,15 +376,15 @@ from_dkdm ( try { list<KDMWithMetadataPtr> kdms; - for (auto i: screens) { - if (!i->recipient) { + for (auto const& screen_details: screens) { + if (!screen_details.screen.recipient) { continue; } auto const kdm = kdm_from_dkdm( dkdm, - i->recipient.get(), - i->trusted_device_thumbprints(), + screen_details.screen.recipient.get(), + screen_details.screen.trusted_device_thumbprints(), valid_from, valid_to, formulation, @@ -387,14 +393,14 @@ from_dkdm ( ); dcp::NameFormat::Map name_values; - name_values['c'] = i->cinema ? i->cinema->name : ""; - name_values['s'] = i->name; + name_values['c'] = screen_details.cinema.name; + name_values['s'] = screen_details.screen.name; name_values['f'] = kdm.content_title_text(); name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false); name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false); name_values['i'] = kdm.cpl_id(); - kdms.push_back(make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector<string>(), kdm)); + kdms.push_back(make_shared<KDMWithMetadata>(name_values, screen_details.cinema_id, screen_details.cinema.emails, kdm)); } write_files (kdms, zip, output, container_name_format, filename_format, verbose, out); if (email) { @@ -451,12 +457,15 @@ try boost::filesystem::path output = dcp::filesystem::current_path(); auto container_name_format = Config::instance()->kdm_container_name_format(); auto filename_format = Config::instance()->kdm_filename_format(); + /* either a cinema name to search for, or the name of a cinema to associate with certificate */ optional<string> cinema_name; - shared_ptr<Cinema> cinema; + /* either a screen name to search for, or the name of a screen to associate with certificate */ + optional<string> screen_name; + /* a certificate that we will use to make up a temporary cinema and screen */ optional<boost::filesystem::path> projector_certificate; optional<boost::filesystem::path> decryption_key; - optional<string> screen; - vector<shared_ptr<Screen>> screens; + /* trusted devices that we will use to make up a temporary cinema and screen */ + vector<TrustedDevice> trusted_devices; optional<dcp::EncryptedKDM> dkdm; optional<dcp::LocalTime> valid_from; optional<dcp::LocalTime> valid_to; @@ -562,27 +571,16 @@ try verbose = true; break; case 'c': - /* This could be a cinema to search for in the configured list or the name of a cinema being - built up on-the-fly in the option. Cater for both possilibities here by storing the name - (for lookup) and by creating a Cinema which the next Screen will be added to. - */ cinema_name = optarg; - cinema = make_shared<Cinema>(optarg, vector<string>(), "", dcp::UTCOffset()); break; case 'S': - /* Similarly, this could be the name of a new (temporary) screen or the name of a screen - * to search for. - */ - screen = optarg; + screen_name = optarg; break; case 'C': projector_certificate = optarg; break; case 'T': - /* A trusted device ends up in the last screen we made */ - if (!screens.empty ()) { - screens.back()->trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg)))); - } + trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg)))); break; case 'G': decryption_key = optarg; @@ -618,20 +616,21 @@ try Config::instance()->set_cinemas_file(*cinemas_file); } + /* If we've been given a certificate we can make up a temporary cinema and screen (not written to the + * database) to then use for making KDMs. + */ + optional<Cinema> temp_cinema; + optional<Screen> temp_screen; if (projector_certificate) { - /* Make a new screen and add it to the current cinema */ + temp_cinema = Cinema(cinema_name.get_value_or(""), {}, "", dcp::UTCOffset()); dcp::CertificateChain chain(dcp::file_to_string(*projector_certificate)); - auto screen_to_add = std::make_shared<Screen>(screen.get_value_or(""), "", chain.leaf(), boost::none, vector<TrustedDevice>()); - if (cinema) { - cinema->add_screen(screen_to_add); - } - screens.push_back(screen_to_add); + temp_screen = Screen(screen_name.get_value_or(""), "", chain.leaf(), boost::none, trusted_devices); } if (command == "list-cinemas") { - auto cinemas = Config::instance()->cinemas (); - for (auto i: cinemas) { - out (String::compose("%1 (%2)", i->name, Email::address_list(i->emails))); + CinemaList cinemas; + for (auto const& cinema: cinemas.cinemas()) { + out(String::compose("%1 (%2)", cinema.second.name, Email::address_list(cinema.second.emails))); } return {}; } @@ -660,15 +659,32 @@ try throw KDMCLIError ("you must specify --valid-from"); } - if (screens.empty()) { + if (optind >= argc) { + throw KDMCLIError ("no film, CPL ID or DKDM specified"); + } + + vector<ScreenDetails> screens; + + if (!temp_cinema) { if (!cinema_name) { - throw KDMCLIError ("you must specify either a cinema or one or more screens using certificate files"); + throw KDMCLIError("you must specify either a cinema or one or more screens using certificate files"); } - screens = find_cinema (*cinema_name)->screens (); - if (screen) { - screens.erase(std::remove_if(screens.begin(), screens.end(), [&screen](shared_ptr<Screen> s) { return s->name != *screen; }), screens.end()); + CinemaList cinema_list; + if (auto cinema = cinema_list.cinema_by_name_or_email(*cinema_name)) { + if (screen_name) { + for (auto screen: cinema_list.screens_by_cinema_and_name(cinema->first, *screen_name)) { + screens.push_back({cinema->first, cinema->second, screen.second}); + } + } else { + for (auto screen: cinema_list.screens(cinema->first)) { + screens.push_back({cinema->first, cinema->second, screen.second}); + } + } } + } else { + DCPOMATIC_ASSERT(temp_screen); + screens.push_back({CinemaID(0), *temp_cinema, *temp_screen}); } if (duration_string) { diff --git a/src/lib/kdm_with_metadata.h b/src/lib/kdm_with_metadata.h index fbeeffbc1..6198564b1 100644 --- a/src/lib/kdm_with_metadata.h +++ b/src/lib/kdm_with_metadata.h @@ -23,6 +23,7 @@ #define DCPOMATIC_KDM_WITH_METADATA_H +#include "id.h" #include <dcp/encrypted_kdm.h> #include <dcp/name_format.h> @@ -33,7 +34,7 @@ class Cinema; class KDMWithMetadata { public: - KDMWithMetadata(dcp::NameFormat::Map const& name_values, void const* group, std::vector<std::string> emails, dcp::EncryptedKDM kdm) + KDMWithMetadata(dcp::NameFormat::Map const& name_values, ID group, std::vector<std::string> emails, dcp::EncryptedKDM kdm) : _name_values (name_values) , _group (group) , _emails (emails) @@ -54,7 +55,7 @@ public: boost::optional<std::string> get (char k) const; - void const* group () const { + ID group() const { return _group; } @@ -64,7 +65,7 @@ public: private: dcp::NameFormat::Map _name_values; - void const* _group; + ID _group; std::vector<std::string> _emails; dcp::EncryptedKDM _kdm; }; diff --git a/src/lib/screen.cc b/src/lib/screen.cc index 38c474850..b77eb6b52 100644 --- a/src/lib/screen.cc +++ b/src/lib/screen.cc @@ -20,6 +20,7 @@ #include "cinema.h" +#include "cinema_list.h" #include "config.h" #include "film.h" #include "kdm_util.h" @@ -39,29 +40,6 @@ using boost::optional; using namespace dcpomatic; -Screen::Screen (cxml::ConstNodePtr node) - : KDMRecipient (node) -{ - for (auto i: node->node_children ("TrustedDevice")) { - if (boost::algorithm::starts_with(i->content(), "-----BEGIN CERTIFICATE-----")) { - trusted_devices.push_back (TrustedDevice(dcp::Certificate(i->content()))); - } else { - trusted_devices.push_back (TrustedDevice(i->content())); - } - } -} - - -void -Screen::as_xml (xmlpp::Element* parent) const -{ - KDMRecipient::as_xml (parent); - for (auto i: trusted_devices) { - cxml::add_text_child(parent, "TrustedDevice", i.as_string()); - } -} - - vector<string> Screen::trusted_device_thumbprints () const { @@ -76,7 +54,9 @@ Screen::trusted_device_thumbprints () const KDMWithMetadataPtr kdm_for_screen ( std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm, - shared_ptr<const dcpomatic::Screen> screen, + CinemaID cinema_id, + Cinema const& cinema, + Screen const& screen, dcp::LocalTime valid_from, dcp::LocalTime valid_to, dcp::Formulation formulation, @@ -85,13 +65,11 @@ kdm_for_screen ( vector<KDMCertificatePeriod>& period_checks ) { - if (!screen->recipient) { + if (!screen.recipient) { return {}; } - auto cinema = screen->cinema; - - period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema ? cinema->name : "", screen->name, screen->recipient.get(), valid_from, valid_to)); + period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema.name, screen.name, screen.recipient.get(), valid_from, valid_to)); auto signer = Config::instance()->signer_chain(); if (!signer->valid()) { @@ -99,21 +77,17 @@ kdm_for_screen ( } auto kdm = make_kdm(valid_from, valid_to).encrypt( - signer, screen->recipient.get(), screen->trusted_device_thumbprints(), formulation, disable_forensic_marking_picture, disable_forensic_marking_audio + signer, screen.recipient.get(), screen.trusted_device_thumbprints(), formulation, disable_forensic_marking_picture, disable_forensic_marking_audio ); dcp::NameFormat::Map name_values; - if (cinema) { - name_values['c'] = cinema->name; - } else { - name_values['c'] = ""; - } - name_values['s'] = screen->name; + name_values['c'] = cinema.name; + name_values['s'] = screen.name; name_values['f'] = kdm.content_title_text(); name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false); name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false); name_values['i'] = kdm.cpl_id(); - return make_shared<KDMWithMetadata>(name_values, cinema.get(), cinema ? cinema->emails : vector<string>(), kdm); + return make_shared<KDMWithMetadata>(name_values, cinema_id, cinema.emails, kdm); } diff --git a/src/lib/screen.h b/src/lib/screen.h index 0a275aa34..89ebc3ab4 100644 --- a/src/lib/screen.h +++ b/src/lib/screen.h @@ -23,6 +23,7 @@ #define DCPOMATIC_SCREEN_H +#include "cinema_list.h" #include "kdm_recipient.h" #include "kdm_util.h" #include "kdm_with_metadata.h" @@ -63,12 +64,7 @@ public: , trusted_devices (trusted_devices_) {} - explicit Screen (cxml::ConstNodePtr); - - void as_xml (xmlpp::Element *) const override; std::vector<std::string> trusted_device_thumbprints () const; - - std::shared_ptr<Cinema> cinema; std::vector<TrustedDevice> trusted_devices; }; @@ -78,7 +74,9 @@ public: KDMWithMetadataPtr kdm_for_screen ( std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm, - std::shared_ptr<const dcpomatic::Screen> screen, + CinemaID cinema_id, + Cinema const& cinema, + dcpomatic::Screen const& screen, dcp::LocalTime valid_from, dcp::LocalTime valid_to, dcp::Formulation formulation, diff --git a/src/lib/sqlite_statement.cc b/src/lib/sqlite_statement.cc new file mode 100644 index 000000000..b3ec1fb81 --- /dev/null +++ b/src/lib/sqlite_statement.cc @@ -0,0 +1,111 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "exceptions.h" +#include "sqlite_statement.h" + + +using std::function; +using std::string; + + +SQLiteStatement::SQLiteStatement(sqlite3* db, string const& statement) + : _db(db) +{ +#ifdef DCPOMATIC_HAVE_SQLITE3_PREPARE_V3 + auto rc = sqlite3_prepare_v3(_db, statement.c_str(), -1, 0, &_stmt, nullptr); +#else + auto rc = sqlite3_prepare_v2(_db, statement.c_str(), -1, &_stmt, nullptr); +#endif + if (rc != SQLITE_OK) { + throw SQLError(_db, rc, statement); + } +} + + +SQLiteStatement::~SQLiteStatement() +{ + sqlite3_finalize(_stmt); +} + + +void +SQLiteStatement::bind_text(int index, string const& value) +{ + auto rc = sqlite3_bind_text(_stmt, index, value.c_str(), -1, SQLITE_TRANSIENT); + if (rc != SQLITE_OK) { + throw SQLError(_db, rc); + } +} + + +void +SQLiteStatement::bind_int64(int index, int64_t value) +{ + auto rc = sqlite3_bind_int64(_stmt, index, value); + if (rc != SQLITE_OK) { + throw SQLError(_db, rc); + } +} + + +void +SQLiteStatement::execute(function<void(SQLiteStatement&)> row, function<void()> busy) +{ + while (true) { + auto const rc = sqlite3_step(_stmt); + switch (rc) { + case SQLITE_BUSY: + busy(); + break; + case SQLITE_DONE: + return; + case SQLITE_ROW: + row(*this); + break; + case SQLITE_ERROR: + case SQLITE_MISUSE: + throw SQLError(_db, sqlite3_errmsg(_db)); + } + } +} + + +int +SQLiteStatement::data_count() +{ + return sqlite3_data_count(_stmt); +} + + +int64_t +SQLiteStatement::column_int64(int index) +{ + return sqlite3_column_int64(_stmt, index); +} + + +string +SQLiteStatement::column_text(int index) +{ + return reinterpret_cast<const char*>(sqlite3_column_text(_stmt, index)); +} + diff --git a/src/lib/sqlite_statement.h b/src/lib/sqlite_statement.h new file mode 100644 index 000000000..3c2246efb --- /dev/null +++ b/src/lib/sqlite_statement.h @@ -0,0 +1,50 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include <sqlite3.h> +#include <functional> +#include <string> + + +class SQLiteStatement +{ +public: + SQLiteStatement(sqlite3* db, std::string const& statement); + ~SQLiteStatement(); + + SQLiteStatement(SQLiteStatement const&) = delete; + SQLiteStatement& operator=(SQLiteStatement const&) = delete; + + void bind_text(int index, std::string const& value); + void bind_int64(int index, int64_t value); + + int64_t column_int64(int index); + std::string column_text(int index); + + void execute(std::function<void(SQLiteStatement&)> row = std::function<void(SQLiteStatement& statement)>(), std::function<void()> busy = std::function<void()>()); + + int data_count(); + +private: + sqlite3* _db; + sqlite3_stmt* _stmt; +}; + diff --git a/src/lib/sqlite_table.cc b/src/lib/sqlite_table.cc new file mode 100644 index 000000000..f00fa6b74 --- /dev/null +++ b/src/lib/sqlite_table.cc @@ -0,0 +1,79 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "dcpomatic_assert.h" +#include "compose.hpp" +#include "sqlite_table.h" +#include "util.h" + + +using std::string; +using std::vector; + + +void +SQLiteTable::add_column(string const& name, string const& type) +{ + _columns.push_back(name); + _types.push_back(type); +} + + +string +SQLiteTable::create() const +{ + DCPOMATIC_ASSERT(!_columns.empty()); + DCPOMATIC_ASSERT(_columns.size() == _types.size()); + vector<string> columns(_columns.size()); + for (size_t i = 0; i < _columns.size(); ++i) { + columns[i] = _columns[i] + " " + _types[i]; + } + return String::compose("CREATE TABLE IF NOT EXISTS %1 (id INTEGER PRIMARY KEY, %2)", _name, join_strings(columns, ", ")); +} + + +string +SQLiteTable::insert() const +{ + DCPOMATIC_ASSERT(!_columns.empty()); + vector<string> placeholders(_columns.size(), "?"); + return String::compose("INSERT INTO %1 (%2) VALUES (%3)", _name, join_strings(_columns, ", "), join_strings(placeholders, ", ")); +} + + +string +SQLiteTable::update(string const& condition) const +{ + DCPOMATIC_ASSERT(!_columns.empty()); + vector<string> placeholders(_columns.size()); + for (size_t i = 0; i < _columns.size(); ++i) { + placeholders[i] = _columns[i] + "=?"; + } + + return String::compose("UPDATE %1 SET %2 %3", _name, join_strings(placeholders, ", "), condition); +} + + +string +SQLiteTable::select(string const& condition) const +{ + return String::compose("SELECT id,%1 FROM %2 %3", join_strings(_columns, ","), _name, condition); +} diff --git a/src/lib/sqlite_table.h b/src/lib/sqlite_table.h new file mode 100644 index 000000000..43c9491ed --- /dev/null +++ b/src/lib/sqlite_table.h @@ -0,0 +1,54 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_SQLITE_TABLE_H +#define DCPOMATIC_SQLITE_TABLE_H + +#include <string> +#include <vector> + + +class SQLiteTable +{ +public: + SQLiteTable(std::string name) + : _name(std::move(name)) + {} + + SQLiteTable(SQLiteTable const&) = default; + SQLiteTable(SQLiteTable&&) = default; + + void add_column(std::string const& name, std::string const& type); + + std::string create() const; + std::string insert() const; + std::string update(std::string const& condition) const; + std::string select(std::string const& condition) const; + +private: + std::string _name; + std::vector<std::string> _columns; + std::vector<std::string> _types; +}; + + +#endif + diff --git a/src/lib/sqlite_transaction.cc b/src/lib/sqlite_transaction.cc new file mode 100644 index 000000000..239d85020 --- /dev/null +++ b/src/lib/sqlite_transaction.cc @@ -0,0 +1,50 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "sqlite_statement.h" +#include "sqlite_transaction.h" + + +SQLiteTransaction::SQLiteTransaction(sqlite3* db) + : _db(db) +{ + SQLiteStatement statement(_db, "BEGIN TRANSACTION"); + statement.execute(); +} + + +SQLiteTransaction::~SQLiteTransaction() +{ + if (_rollback) { + SQLiteStatement rollback(_db, "ROLLBACK"); + rollback.execute(); + } +} + + +void +SQLiteTransaction::commit() +{ + SQLiteStatement commit(_db, "COMMIT"); + commit.execute(); + _rollback = false; +} + diff --git a/src/lib/sqlite_transaction.h b/src/lib/sqlite_transaction.h new file mode 100644 index 000000000..0f6319243 --- /dev/null +++ b/src/lib/sqlite_transaction.h @@ -0,0 +1,40 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include <sqlite3.h> + + +class SQLiteTransaction +{ +public: + SQLiteTransaction(sqlite3* db); + ~SQLiteTransaction(); + + SQLiteTransaction(SQLiteTransaction const&) = delete; + SQLiteTransaction& operator=(SQLiteTransaction const&) = delete; + + void commit(); + +private: + sqlite3* _db; + bool _rollback = true; +}; + diff --git a/src/lib/unzipper.cc b/src/lib/unzipper.cc index f0170e7e0..8d468f24f 100644 --- a/src/lib/unzipper.cc +++ b/src/lib/unzipper.cc @@ -56,8 +56,20 @@ Unzipper::~Unzipper() } +bool +Unzipper::contains(string const& filename) const +{ + auto file = zip_fopen(_zip, filename.c_str(), 0); + bool exists = file != nullptr; + if (file) { + zip_fclose(file); + } + return exists; +} + + string -Unzipper::get(string const& filename) +Unzipper::get(string const& filename) const { auto file = zip_fopen(_zip, filename.c_str(), 0); if (!file) { diff --git a/src/lib/unzipper.h b/src/lib/unzipper.h index 7cab6e5f4..76b2fe45a 100644 --- a/src/lib/unzipper.h +++ b/src/lib/unzipper.h @@ -33,7 +33,8 @@ public: Unzipper(Unzipper const&) = delete; Unzipper& operator=(Unzipper const&) = delete; - std::string get(std::string const& filename); + std::string get(std::string const& filename) const; + bool contains(std::string const& filename) const; private: struct zip* _zip; diff --git a/src/lib/util.cc b/src/lib/util.cc index 2f5c1ce49..282011d65 100644 --- a/src/lib/util.cc +++ b/src/lib/util.cc @@ -89,10 +89,11 @@ LIBDCP_ENABLE_WARNINGS #include <dbghelp.h> #endif #include <signal.h> +#include <climits> #include <iomanip> #include <iostream> #include <fstream> -#include <climits> +#include <numeric> #include <stdexcept> #ifdef DCPOMATIC_POSIX #include <execinfo.h> @@ -121,9 +122,6 @@ using std::vector; using std::wstring; using boost::thread; using boost::optional; -using boost::lexical_cast; -using boost::bad_lexical_cast; -using boost::scoped_array; using dcp::Size; using dcp::raw_convert; using dcp::locale_convert; @@ -1151,6 +1149,7 @@ setup_grok_library_path() } #endif + string screen_names_to_string(vector<string> names) { @@ -1185,3 +1184,16 @@ report_problem() return String::compose(_("Please report this problem by using Help -> Report a problem or via email to %1"), variant::report_problem_email()); } + +string +join_strings(vector<string> const& in, string const& separator) +{ + if (in.empty()) { + return {}; + } + + return std::accumulate(std::next(in.begin()), in.end(), in.front(), [separator](string a, string b) { + return a + separator + b; + }); +} + diff --git a/src/lib/util.h b/src/lib/util.h index eac855bef..7c40c5ce8 100644 --- a/src/lib/util.h +++ b/src/lib/util.h @@ -101,6 +101,7 @@ extern void capture_ffmpeg_logs(); #ifdef DCPOMATIC_GROK extern void setup_grok_library_path(); #endif +extern std::string join_strings(std::vector<std::string> const& in, std::string const& separator = " "); template <class T> diff --git a/src/lib/wscript b/src/lib/wscript index a4758c737..68b988eb3 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -49,7 +49,7 @@ sources = """ text_decoder.cc case_insensitive_sorter.cc check_content_job.cc - cinema.cc + cinema_list.cc cinema_sound_processor.cc change_signaller.cc collator.cc @@ -85,6 +85,7 @@ sources = """ decoder_part.cc digester.cc dkdm_recipient.cc + dkdm_recipient_list.cc dkdm_wrapper.cc dolby_cp750.cc email.cc @@ -126,6 +127,7 @@ sources = """ frame_rate_change.cc guess_crop.cc hints.cc + id.cc internet.cc image.cc image_content.cc @@ -184,6 +186,9 @@ sources = """ state.cc spl.cc spl_entry.cc + sqlite_statement.cc + sqlite_table.cc + sqlite_transaction.cc string_log_entry.cc string_text_file.cc string_text_file_content.cc @@ -239,7 +244,7 @@ def build(bld): BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2 BOOST_REGEX SAMPLERATE POSTPROC TIFF SSH DCP CXML GLIB LZMA XML++ CURL ZIP BZ2 FONTCONFIG PANGOMM CAIROMM XMLSEC SUB ICU NETTLE PNG JPEG LEQM_NRT - LIBZ + LIBZ SQLITE3 """ if bld.env.TARGET_OSX: |
