Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/prusa3d/PrusaSlicer.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/slic3r/Config')
-rw-r--r--src/slic3r/Config/Snapshot.cpp532
-rw-r--r--src/slic3r/Config/Snapshot.hpp129
-rw-r--r--src/slic3r/Config/Version.cpp319
-rw-r--r--src/slic3r/Config/Version.hpp88
4 files changed, 1068 insertions, 0 deletions
diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp
new file mode 100644
index 000000000..704fbcfa1
--- /dev/null
+++ b/src/slic3r/Config/Snapshot.cpp
@@ -0,0 +1,532 @@
+#include "Snapshot.hpp"
+#include "../GUI/AppConfig.hpp"
+#include "../GUI/PresetBundle.hpp"
+#include "../Utils/Time.hpp"
+
+#include <time.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/nowide/cstdio.hpp>
+#include <boost/nowide/fstream.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/property_tree/ptree.hpp>
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Config.hpp"
+#include "../../libslic3r/FileParserError.hpp"
+#include "../../libslic3r/Utils.hpp"
+
+#define SLIC3R_SNAPSHOTS_DIR "snapshots"
+#define SLIC3R_SNAPSHOT_FILE "snapshot.ini"
+
+namespace Slic3r {
+namespace GUI {
+namespace Config {
+
+void Snapshot::clear()
+{
+ this->id.clear();
+ this->time_captured = 0;
+ this->slic3r_version_captured = Semver::invalid();
+ this->comment.clear();
+ this->reason = SNAPSHOT_UNKNOWN;
+ this->print.clear();
+ this->filaments.clear();
+ this->printer.clear();
+}
+
+void Snapshot::load_ini(const std::string &path)
+{
+ this->clear();
+
+ auto throw_on_parse_error = [&path](const std::string &msg) {
+ throw file_parser_error(std::string("Failed loading the snapshot file. Reason: ") + msg, path);
+ };
+
+ // Load the snapshot.ini file.
+ boost::property_tree::ptree tree;
+ try {
+ boost::nowide::ifstream ifs(path);
+ boost::property_tree::read_ini(ifs, tree);
+ } catch (const std::ifstream::failure &err) {
+ throw file_parser_error(std::string("The snapshot file cannot be loaded. Reason: ") + err.what(), path);
+ } catch (const std::runtime_error &err) {
+ throw_on_parse_error(err.what());
+ }
+
+ // Parse snapshot.ini
+ std::string group_name_vendor = "Vendor:";
+ std::string key_filament = "filament";
+ std::string key_prefix_model = "model_";
+ for (auto &section : tree) {
+ if (section.first == "snapshot") {
+ // Parse the common section.
+ for (auto &kvp : section.second) {
+ if (kvp.first == "id")
+ this->id = kvp.second.data();
+ else if (kvp.first == "time_captured") {
+ this->time_captured = Slic3r::Utils::parse_time_ISO8601Z(kvp.second.data());
+ if (this->time_captured == (time_t)-1)
+ throw_on_parse_error("invalid timestamp");
+ } else if (kvp.first == "slic3r_version_captured") {
+ auto semver = Semver::parse(kvp.second.data());
+ if (! semver)
+ throw_on_parse_error("invalid slic3r_version_captured semver");
+ this->slic3r_version_captured = *semver;
+ } else if (kvp.first == "comment") {
+ this->comment = kvp.second.data();
+ } else if (kvp.first == "reason") {
+ std::string rsn = kvp.second.data();
+ if (rsn == "upgrade")
+ this->reason = SNAPSHOT_UPGRADE;
+ else if (rsn == "downgrade")
+ this->reason = SNAPSHOT_DOWNGRADE;
+ else if (rsn == "before_rollback")
+ this->reason = SNAPSHOT_BEFORE_ROLLBACK;
+ else if (rsn == "user")
+ this->reason = SNAPSHOT_USER;
+ else
+ this->reason = SNAPSHOT_UNKNOWN;
+ }
+ }
+ } else if (section.first == "presets") {
+ // Load the names of the active presets.
+ for (auto &kvp : section.second) {
+ if (kvp.first == "print") {
+ this->print = kvp.second.data();
+ } else if (boost::starts_with(kvp.first, "filament")) {
+ int idx = 0;
+ if (kvp.first == "filament" || sscanf(kvp.first.c_str(), "filament_%d", &idx) == 1) {
+ if (int(this->filaments.size()) <= idx)
+ this->filaments.resize(idx + 1, std::string());
+ this->filaments[idx] = kvp.second.data();
+ }
+ } else if (kvp.first == "printer") {
+ this->printer = kvp.second.data();
+ }
+ }
+ } else if (boost::starts_with(section.first, group_name_vendor) && section.first.size() > group_name_vendor.size()) {
+ // Vendor specific section.
+ VendorConfig vc;
+ vc.name = section.first.substr(group_name_vendor.size());
+ for (auto &kvp : section.second) {
+ if (kvp.first == "version" || kvp.first == "min_slic3r_version" || kvp.first == "max_slic3r_version") {
+ // Version of the vendor specific config bundle bundled with this snapshot.
+ auto semver = Semver::parse(kvp.second.data());
+ if (! semver)
+ throw_on_parse_error("invalid " + kvp.first + " format for " + section.first);
+ if (kvp.first == "version")
+ vc.version.config_version = *semver;
+ else if (kvp.first == "min_slic3r_version")
+ vc.version.min_slic3r_version = *semver;
+ else
+ vc.version.max_slic3r_version = *semver;
+ } else if (boost::starts_with(kvp.first, key_prefix_model) && kvp.first.size() > key_prefix_model.size()) {
+ // Parse the printer variants installed for the current model.
+ auto &set_variants = vc.models_variants_installed[kvp.first.substr(key_prefix_model.size())];
+ std::vector<std::string> variants;
+ if (unescape_strings_cstyle(kvp.second.data(), variants))
+ for (auto &variant : variants)
+ set_variants.insert(std::move(variant));
+ }
+ }
+ this->vendor_configs.emplace_back(std::move(vc));
+ }
+ }
+ // Sort the vendors lexicographically.
+ std::sort(this->vendor_configs.begin(), this->vendor_configs.begin(),
+ [](const VendorConfig &cfg1, const VendorConfig &cfg2) { return cfg1.name < cfg2.name; });
+}
+
+static std::string reason_string(const Snapshot::Reason reason)
+{
+ switch (reason) {
+ case Snapshot::SNAPSHOT_UPGRADE:
+ return "upgrade";
+ case Snapshot::SNAPSHOT_DOWNGRADE:
+ return "downgrade";
+ case Snapshot::SNAPSHOT_BEFORE_ROLLBACK:
+ return "before_rollback";
+ case Snapshot::SNAPSHOT_USER:
+ return "user";
+ case Snapshot::SNAPSHOT_UNKNOWN:
+ default:
+ return "unknown";
+ }
+}
+
+void Snapshot::save_ini(const std::string &path)
+{
+ boost::nowide::ofstream c;
+ c.open(path, std::ios::out | std::ios::trunc);
+ c << "# " << Slic3r::header_slic3r_generated() << std::endl;
+
+ // Export the common "snapshot".
+ c << std::endl << "[snapshot]" << std::endl;
+ c << "id = " << this->id << std::endl;
+ c << "time_captured = " << Slic3r::Utils::format_time_ISO8601Z(this->time_captured) << std::endl;
+ c << "slic3r_version_captured = " << this->slic3r_version_captured.to_string() << std::endl;
+ c << "comment = " << this->comment << std::endl;
+ c << "reason = " << reason_string(this->reason) << std::endl;
+
+ // Export the active presets at the time of the snapshot.
+ c << std::endl << "[presets]" << std::endl;
+ c << "print = " << this->print << std::endl;
+ c << "filament = " << this->filaments.front() << std::endl;
+ for (size_t i = 1; i < this->filaments.size(); ++ i)
+ c << "filament_" << std::to_string(i) << " = " << this->filaments[i] << std::endl;
+ c << "printer = " << this->printer << std::endl;
+
+ // Export the vendor configs.
+ for (const VendorConfig &vc : this->vendor_configs) {
+ c << std::endl << "[Vendor:" << vc.name << "]" << std::endl;
+ c << "version = " << vc.version.config_version.to_string() << std::endl;
+ c << "min_slic3r_version = " << vc.version.min_slic3r_version.to_string() << std::endl;
+ c << "max_slic3r_version = " << vc.version.max_slic3r_version.to_string() << std::endl;
+ // Export installed printer models and their variants.
+ for (const auto &model : vc.models_variants_installed) {
+ if (model.second.size() == 0)
+ continue;
+ const std::vector<std::string> variants(model.second.begin(), model.second.end());
+ const auto escaped = escape_strings_cstyle(variants);
+ c << "model_" << model.first << " = " << escaped << std::endl;
+ }
+ }
+ c.close();
+}
+
+void Snapshot::export_selections(AppConfig &config) const
+{
+ assert(filaments.size() >= 1);
+ config.clear_section("presets");
+ config.set("presets", "print", print);
+ config.set("presets", "filament", filaments.front());
+ for (int i = 1; i < filaments.size(); ++i) {
+ char name[64];
+ sprintf(name, "filament_%d", i);
+ config.set("presets", name, filaments[i]);
+ }
+ config.set("presets", "printer", printer);
+}
+
+void Snapshot::export_vendor_configs(AppConfig &config) const
+{
+ std::map<std::string, std::map<std::string, std::set<std::string>>> vendors;
+ for (const VendorConfig &vc : vendor_configs)
+ vendors[vc.name] = vc.models_variants_installed;
+ config.set_vendors(std::move(vendors));
+}
+
+// Perform a deep compare of the active print / filament / printer / vendor directories.
+// Return true if the content of the current print / filament / printer / vendor directories
+// matches the state stored in this snapshot.
+bool Snapshot::equal_to_active(const AppConfig &app_config) const
+{
+ // 1) Check, whether this snapshot contains the same set of active vendors, printer models and variants
+ // as app_config.
+ {
+ std::set<std::string> matched;
+ for (const VendorConfig &vc : this->vendor_configs) {
+ auto it_vendor_models_variants = app_config.vendors().find(vc.name);
+ if (it_vendor_models_variants == app_config.vendors().end() ||
+ it_vendor_models_variants->second != vc.models_variants_installed)
+ // There are more vendors enabled in the snapshot than currently installed.
+ return false;
+ matched.insert(vc.name);
+ }
+ for (const std::pair<std::string, std::map<std::string, std::set<std::string>>> &v : app_config.vendors())
+ if (matched.find(v.first) == matched.end() && ! v.second.empty())
+ // There are more vendors currently installed than enabled in the snapshot.
+ return false;
+ }
+
+ // 2) Check, whether this snapshot references the same set of ini files as the current state.
+ boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir());
+ boost::filesystem::path snapshot_dir = boost::filesystem::path(Slic3r::data_dir()) / SLIC3R_SNAPSHOTS_DIR / this->id;
+ for (const char *subdir : { "print", "filament", "printer", "vendor" }) {
+ boost::filesystem::path path1 = data_dir / subdir;
+ boost::filesystem::path path2 = snapshot_dir / subdir;
+ std::vector<std::string> files1, files2;
+ for (auto &dir_entry : boost::filesystem::directory_iterator(path1))
+ if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini"))
+ files1.emplace_back(dir_entry.path().filename().string());
+ for (auto &dir_entry : boost::filesystem::directory_iterator(path2))
+ if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini"))
+ files2.emplace_back(dir_entry.path().filename().string());
+ std::sort(files1.begin(), files1.end());
+ std::sort(files2.begin(), files2.end());
+ if (files1 != files2)
+ return false;
+ for (const std::string &filename : files1) {
+ FILE *f1 = boost::nowide::fopen((path1 / filename).string().c_str(), "rb");
+ FILE *f2 = boost::nowide::fopen((path2 / filename).string().c_str(), "rb");
+ bool same = true;
+ if (f1 && f2) {
+ char buf1[4096];
+ char buf2[4096];
+ do {
+ size_t r1 = fread(buf1, 1, 4096, f1);
+ size_t r2 = fread(buf2, 1, 4096, f2);
+ if (r1 != r2 || memcmp(buf1, buf2, r1)) {
+ same = false;
+ break;
+ }
+ } while (! feof(f1) || ! feof(f2));
+ } else
+ same = false;
+ if (f1)
+ fclose(f1);
+ if (f2)
+ fclose(f2);
+ if (! same)
+ return false;
+ }
+ }
+ return true;
+}
+
+size_t SnapshotDB::load_db()
+{
+ boost::filesystem::path snapshots_dir = SnapshotDB::create_db_dir();
+
+ m_snapshots.clear();
+
+ // Walk over the snapshot directories and load their index.
+ std::string errors_cummulative;
+ for (auto &dir_entry : boost::filesystem::directory_iterator(snapshots_dir))
+ if (boost::filesystem::is_directory(dir_entry.status())) {
+ // Try to read "snapshot.ini".
+ boost::filesystem::path path_ini = dir_entry.path() / SLIC3R_SNAPSHOT_FILE;
+ Snapshot snapshot;
+ try {
+ snapshot.load_ini(path_ini.string());
+ } catch (const std::runtime_error &err) {
+ errors_cummulative += err.what();
+ errors_cummulative += "\n";
+ continue;
+ }
+ // Check that the name of the snapshot directory matches the snapshot id stored in the snapshot.ini file.
+ if (dir_entry.path().filename().string() != snapshot.id) {
+ errors_cummulative += std::string("Snapshot ID ") + snapshot.id + " does not match the snapshot directory " + dir_entry.path().filename().string() + "\n";
+ continue;
+ }
+ m_snapshots.emplace_back(std::move(snapshot));
+ }
+ // Sort the snapshots by their date/time.
+ std::sort(m_snapshots.begin(), m_snapshots.end(), [](const Snapshot &s1, const Snapshot &s2) { return s1.time_captured < s2.time_captured; });
+ if (! errors_cummulative.empty())
+ throw std::runtime_error(errors_cummulative);
+ return m_snapshots.size();
+}
+
+void SnapshotDB::update_slic3r_versions(std::vector<Index> &index_db)
+{
+ for (Snapshot &snapshot : m_snapshots) {
+ for (Snapshot::VendorConfig &vendor_config : snapshot.vendor_configs) {
+ auto it = std::find_if(index_db.begin(), index_db.end(), [&vendor_config](const Index &idx) { return idx.vendor() == vendor_config.name; });
+ if (it != index_db.end()) {
+ Index::const_iterator it_version = it->find(vendor_config.version.config_version);
+ if (it_version != it->end()) {
+ vendor_config.version.min_slic3r_version = it_version->min_slic3r_version;
+ vendor_config.version.max_slic3r_version = it_version->max_slic3r_version;
+ }
+ }
+ }
+ }
+}
+
+static void copy_config_dir_single_level(const boost::filesystem::path &path_src, const boost::filesystem::path &path_dst)
+{
+ if (! boost::filesystem::is_directory(path_dst) &&
+ ! boost::filesystem::create_directory(path_dst))
+ throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + path_dst.string());
+
+ for (auto &dir_entry : boost::filesystem::directory_iterator(path_src))
+ if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini"))
+ boost::filesystem::copy_file(dir_entry.path(), path_dst / dir_entry.path().filename(), boost::filesystem::copy_option::overwrite_if_exists);
+}
+
+static void delete_existing_ini_files(const boost::filesystem::path &path)
+{
+ if (! boost::filesystem::is_directory(path))
+ return;
+ for (auto &dir_entry : boost::filesystem::directory_iterator(path))
+ if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini"))
+ boost::filesystem::remove(dir_entry.path());
+}
+
+const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment)
+{
+ boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir());
+ boost::filesystem::path snapshot_db_dir = SnapshotDB::create_db_dir();
+
+ // 1) Prepare the snapshot structure.
+ Snapshot snapshot;
+ // Snapshot header.
+ snapshot.time_captured = Slic3r::Utils::get_current_time_utc();
+ snapshot.id = Slic3r::Utils::format_time_ISO8601Z(snapshot.time_captured);
+ snapshot.slic3r_version_captured = *Semver::parse(SLIC3R_VERSION); // XXX: have Semver Slic3r version
+ snapshot.comment = comment;
+ snapshot.reason = reason;
+ // Active presets at the time of the snapshot.
+ snapshot.print = app_config.get("presets", "print");
+ snapshot.filaments.emplace_back(app_config.get("presets", "filament"));
+ snapshot.printer = app_config.get("presets", "printer");
+ for (unsigned int i = 1; i < 1000; ++ i) {
+ char name[64];
+ sprintf(name, "filament_%d", i);
+ if (! app_config.has("presets", name))
+ break;
+ snapshot.filaments.emplace_back(app_config.get("presets", name));
+ }
+ // Vendor specific config bundles and installed printers.
+ for (const std::pair<std::string, std::map<std::string, std::set<std::string>>> &vendor : app_config.vendors()) {
+ Snapshot::VendorConfig cfg;
+ cfg.name = vendor.first;
+ cfg.models_variants_installed = vendor.second;
+ for (auto it = cfg.models_variants_installed.begin(); it != cfg.models_variants_installed.end();)
+ if (it->second.empty())
+ cfg.models_variants_installed.erase(it ++);
+ else
+ ++ it;
+ // Read the active config bundle, parse the config version.
+ PresetBundle bundle;
+ bundle.load_configbundle((data_dir / "vendor" / (cfg.name + ".ini")).string(), PresetBundle::LOAD_CFGBUNDLE_VENDOR_ONLY);
+ for (const VendorProfile &vp : bundle.vendors)
+ if (vp.id == cfg.name)
+ cfg.version.config_version = vp.config_version;
+ // Fill-in the min/max slic3r version from the config index, if possible.
+ try {
+ // Load the config index for the vendor.
+ Index index;
+ index.load(data_dir / "vendor" / (cfg.name + ".idx"));
+ auto it = index.find(cfg.version.config_version);
+ if (it != index.end()) {
+ cfg.version.min_slic3r_version = it->min_slic3r_version;
+ cfg.version.max_slic3r_version = it->max_slic3r_version;
+ }
+ } catch (const std::runtime_error &err) {
+ }
+ snapshot.vendor_configs.emplace_back(std::move(cfg));
+ }
+
+ boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id;
+ boost::filesystem::create_directory(snapshot_dir);
+
+ // Backup the presets.
+ for (const char *subdir : { "print", "filament", "printer", "vendor" })
+ copy_config_dir_single_level(data_dir / subdir, snapshot_dir / subdir);
+ snapshot.save_ini((snapshot_dir / "snapshot.ini").string());
+ assert(m_snapshots.empty() || m_snapshots.back().time_captured <= snapshot.time_captured);
+ m_snapshots.emplace_back(std::move(snapshot));
+ return m_snapshots.back();
+}
+
+const Snapshot& SnapshotDB::restore_snapshot(const std::string &id, AppConfig &app_config)
+{
+ for (const Snapshot &snapshot : m_snapshots)
+ if (snapshot.id == id) {
+ this->restore_snapshot(snapshot, app_config);
+ return snapshot;
+ }
+ throw std::runtime_error(std::string("Snapshot with id " + id + " was not found."));
+}
+
+void SnapshotDB::restore_snapshot(const Snapshot &snapshot, AppConfig &app_config)
+{
+ boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir());
+ boost::filesystem::path snapshot_db_dir = SnapshotDB::create_db_dir();
+ boost::filesystem::path snapshot_dir = snapshot_db_dir / snapshot.id;
+ // Remove existing ini files and restore the ini files from the snapshot.
+ for (const char *subdir : { "print", "filament", "printer", "vendor" }) {
+ delete_existing_ini_files(data_dir / subdir);
+ copy_config_dir_single_level(snapshot_dir / subdir, data_dir / subdir);
+ }
+ // Update AppConfig with the selections of the print / filament / printer profiles
+ // and about the installed printer types and variants.
+ snapshot.export_selections(app_config);
+ snapshot.export_vendor_configs(app_config);
+}
+
+bool SnapshotDB::is_on_snapshot(AppConfig &app_config) const
+{
+ // Is the "on_snapshot" configuration value set?
+ std::string on_snapshot = app_config.get("on_snapshot");
+ if (on_snapshot.empty())
+ // No, we are not on a snapshot.
+ return false;
+ // Is the "on_snapshot" equal to the current configuration state?
+ auto it_snapshot = this->snapshot(on_snapshot);
+ if (it_snapshot != this->end() && it_snapshot->equal_to_active(app_config))
+ // Yes, we are on the snapshot.
+ return true;
+ // No, we are no more on a snapshot. Reset the state.
+ app_config.set("on_snapshot", "");
+ return false;
+}
+
+SnapshotDB::const_iterator SnapshotDB::snapshot_with_vendor_preset(const std::string &vendor_name, const Semver &config_version)
+{
+ auto it_found = m_snapshots.end();
+ Snapshot::VendorConfig key;
+ key.name = vendor_name;
+ for (auto it = m_snapshots.begin(); it != m_snapshots.end(); ++ it) {
+ const Snapshot &snapshot = *it;
+ auto it_vendor_config = std::lower_bound(snapshot.vendor_configs.begin(), snapshot.vendor_configs.end(),
+ key, [](const Snapshot::VendorConfig &cfg1, const Snapshot::VendorConfig &cfg2) { return cfg1.name < cfg2.name; });
+ if (it_vendor_config != snapshot.vendor_configs.end() && it_vendor_config->name == vendor_name &&
+ config_version == it_vendor_config->version.config_version) {
+ // Vendor config found with the correct version.
+ // Save it, but continue searching, as we want the newest snapshot.
+ it_found = it;
+ }
+ }
+ return it_found;
+}
+
+SnapshotDB::const_iterator SnapshotDB::snapshot(const std::string &id) const
+{
+ for (const_iterator it = m_snapshots.begin(); it != m_snapshots.end(); ++ it)
+ if (it->id == id)
+ return it;
+ return m_snapshots.end();
+}
+
+boost::filesystem::path SnapshotDB::create_db_dir()
+{
+ boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir());
+ boost::filesystem::path snapshots_dir = data_dir / SLIC3R_SNAPSHOTS_DIR;
+ for (const boost::filesystem::path &path : { data_dir, snapshots_dir }) {
+ boost::filesystem::path subdir = path;
+ subdir.make_preferred();
+ if (! boost::filesystem::is_directory(subdir) &&
+ ! boost::filesystem::create_directory(subdir))
+ throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + subdir.string());
+ }
+ return snapshots_dir;
+}
+
+SnapshotDB& SnapshotDB::singleton()
+{
+ static SnapshotDB instance;
+ static bool loaded = false;
+ if (! loaded) {
+ try {
+ loaded = true;
+ // Load the snapshot database.
+ instance.load_db();
+ // Load the vendor specific configuration indices.
+ std::vector<Index> index_db = Index::load_db();
+ // Update the min / max slic3r versions compatible with the configurations stored inside the snapshots
+ // based on the min / max slic3r versions defined by the vendor specific config indices.
+ instance.update_slic3r_versions(index_db);
+ } catch (std::exception &ex) {
+ }
+ }
+ return instance;
+}
+
+} // namespace Config
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/Config/Snapshot.hpp b/src/slic3r/Config/Snapshot.hpp
new file mode 100644
index 000000000..a916dfe92
--- /dev/null
+++ b/src/slic3r/Config/Snapshot.hpp
@@ -0,0 +1,129 @@
+#ifndef slic3r_GUI_Snapshot_
+#define slic3r_GUI_Snapshot_
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <boost/filesystem.hpp>
+
+#include "Version.hpp"
+#include "../Utils/Semver.hpp"
+
+namespace Slic3r {
+
+class AppConfig;
+
+namespace GUI {
+namespace Config {
+
+class Index;
+
+// A snapshot contains:
+// Slic3r.ini
+// vendor/
+// print/
+// filament/
+// printer/
+class Snapshot
+{
+public:
+ enum Reason {
+ SNAPSHOT_UNKNOWN,
+ SNAPSHOT_UPGRADE,
+ SNAPSHOT_DOWNGRADE,
+ SNAPSHOT_BEFORE_ROLLBACK,
+ SNAPSHOT_USER,
+ };
+
+ Snapshot() { clear(); }
+
+ void clear();
+ void load_ini(const std::string &path);
+ void save_ini(const std::string &path);
+
+ // Export the print / filament / printer selections to be activated into the AppConfig.
+ void export_selections(AppConfig &config) const;
+ void export_vendor_configs(AppConfig &config) const;
+
+ // Perform a deep compare of the active print / filament / printer / vendor directories.
+ // Return true if the content of the current print / filament / printer / vendor directories
+ // matches the state stored in this snapshot.
+ bool equal_to_active(const AppConfig &app_config) const;
+
+ // ID of a snapshot should equal to the name of the snapshot directory.
+ // The ID contains the date/time, reason and comment to be human readable.
+ std::string id;
+ std::time_t time_captured;
+ // Which Slic3r version captured this snapshot?
+ Semver slic3r_version_captured = Semver::invalid();
+ // Comment entered by the user at the start of the snapshot capture.
+ std::string comment;
+ Reason reason;
+
+ std::string format_reason() const;
+
+ // Active presets at the time of the snapshot.
+ std::string print;
+ std::vector<std::string> filaments;
+ std::string printer;
+
+ // Annotation of the vendor configuration stored in the snapshot.
+ // This information is displayed to the user and used to decide compatibility
+ // of the configuration stored in the snapshot with the running Slic3r version.
+ struct VendorConfig {
+ // Name of the vendor contained in this snapshot.
+ std::string name;
+ // Version of the vendor config contained in this snapshot, along with compatibility data.
+ Version version;
+ // Which printer models of this vendor were installed, and which variants of the models?
+ std::map<std::string, std::set<std::string>> models_variants_installed;
+ };
+ // List of vendor configs contained in this snapshot, sorted lexicographically.
+ std::vector<VendorConfig> vendor_configs;
+};
+
+class SnapshotDB
+{
+public:
+ // Initialize the SnapshotDB singleton instance. Load the database if it has not been loaded yet.
+ static SnapshotDB& singleton();
+
+ typedef std::vector<Snapshot>::const_iterator const_iterator;
+
+ // Load the snapshot database from the snapshots directory.
+ // If the snapshot directory or its parent does not exist yet, it will be created.
+ // Returns a number of snapshots loaded.
+ size_t load_db();
+ void update_slic3r_versions(std::vector<Index> &index_db);
+
+ // Create a snapshot directory, copy the vendor config bundles, user print/filament/printer profiles,
+ // create an index.
+ const Snapshot& take_snapshot(const AppConfig &app_config, Snapshot::Reason reason, const std::string &comment = "");
+ const Snapshot& restore_snapshot(const std::string &id, AppConfig &app_config);
+ void restore_snapshot(const Snapshot &snapshot, AppConfig &app_config);
+ // Test whether the AppConfig's on_snapshot variable points to an existing snapshot, and the existing snapshot
+ // matches the current state. If it does not match the current state, the AppConfig's "on_snapshot" ID is reset.
+ bool is_on_snapshot(AppConfig &app_config) const;
+ // Finds the newest snapshot, which contains a config bundle for vendor_name with config_version.
+ const_iterator snapshot_with_vendor_preset(const std::string &vendor_name, const Semver &config_version);
+
+ const_iterator begin() const { return m_snapshots.begin(); }
+ const_iterator end() const { return m_snapshots.end(); }
+ const_iterator snapshot(const std::string &id) const;
+ const std::vector<Snapshot>& snapshots() const { return m_snapshots; }
+
+private:
+ // Create the snapshots directory if it does not exist yet.
+ static boost::filesystem::path create_db_dir();
+
+ // Snapshots are sorted by their date/time, oldest first.
+ std::vector<Snapshot> m_snapshots;
+};
+
+} // namespace Config
+} // namespace GUI
+} // namespace Slic3r
+
+#endif /* slic3r_GUI_Snapshot_ */
diff --git a/src/slic3r/Config/Version.cpp b/src/slic3r/Config/Version.cpp
new file mode 100644
index 000000000..a85322eca
--- /dev/null
+++ b/src/slic3r/Config/Version.cpp
@@ -0,0 +1,319 @@
+#include "Version.hpp"
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/nowide/fstream.hpp>
+
+#include "../../libslic3r/libslic3r.h"
+#include "../../libslic3r/Config.hpp"
+#include "../../libslic3r/FileParserError.hpp"
+#include "../../libslic3r/Utils.hpp"
+
+namespace Slic3r {
+namespace GUI {
+namespace Config {
+
+static const Semver s_current_slic3r_semver(SLIC3R_VERSION);
+
+// Optimized lexicographic compare of two pre-release versions, ignoring the numeric suffix.
+static int compare_prerelease(const char *p1, const char *p2)
+{
+ for (;;) {
+ char c1 = *p1 ++;
+ char c2 = *p2 ++;
+ bool a1 = std::isalpha(c1) && c1 != 0;
+ bool a2 = std::isalpha(c2) && c2 != 0;
+ if (a1) {
+ if (a2) {
+ if (c1 != c2)
+ return (c1 < c2) ? -1 : 1;
+ } else
+ return 1;
+ } else {
+ if (a2)
+ return -1;
+ else
+ return 0;
+ }
+ }
+ // This shall never happen.
+ return 0;
+}
+
+bool Version::is_slic3r_supported(const Semver &slic3r_version) const
+{
+ if (! slic3r_version.in_range(min_slic3r_version, max_slic3r_version))
+ return false;
+ // Now verify, whether the configuration pre-release status is compatible with the Slic3r's pre-release status.
+ // Alpha Slic3r will happily load any configuration, while beta Slic3r will ignore alpha configurations etc.
+ const char *prerelease_slic3r = slic3r_version.prerelease();
+ const char *prerelease_config = this->config_version.prerelease();
+ if (prerelease_config == nullptr)
+ // Released config is always supported.
+ return true;
+ else if (prerelease_slic3r == nullptr)
+ // Released slic3r only supports released configs.
+ return false;
+ // Compare the pre-release status of Slic3r against the config.
+ // If the prerelease status of slic3r is lexicographically lower or equal
+ // to the prerelease status of the config, accept it.
+ return compare_prerelease(prerelease_slic3r, prerelease_config) != 1;
+}
+
+bool Version::is_current_slic3r_supported() const
+{
+ return this->is_slic3r_supported(s_current_slic3r_semver);
+}
+
+#if 0
+//TODO: This test should be moved to a unit test, once we have C++ unit tests in place.
+static int version_test()
+{
+ Version v;
+ v.config_version = *Semver::parse("1.1.2");
+ v.min_slic3r_version = *Semver::parse("1.38.0");
+ v.max_slic3r_version = Semver::inf();
+ assert(v.is_slic3r_supported(*Semver::parse("1.38.0")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.38.0-alpha")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.37.0-alpha")));
+ // Test the prerelease status.
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc2")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0")));
+ v.config_version = *Semver::parse("1.1.2-alpha");
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-rc2")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0")));
+ v.config_version = *Semver::parse("1.1.2-alpha1");
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-rc2")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0")));
+ v.config_version = *Semver::parse("1.1.2-beta");
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-rc")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0-rc2")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0")));
+ v.config_version = *Semver::parse("1.1.2-rc");
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc2")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0")));
+ v.config_version = *Semver::parse("1.1.2-rc2");
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-alpha1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-beta1")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc")));
+ assert(v.is_slic3r_supported(*Semver::parse("1.39.0-rc2")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.39.0")));
+ // Test the upper boundary.
+ v.config_version = *Semver::parse("1.1.2");
+ v.max_slic3r_version = *Semver::parse("1.39.3-beta1");
+ assert(v.is_slic3r_supported(*Semver::parse("1.38.0")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.38.0-alpha")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.38.0-alpha1")));
+ assert(! v.is_slic3r_supported(*Semver::parse("1.37.0-alpha")));
+ return 0;
+}
+static int version_test_run = version_test();
+#endif
+
+inline char* left_trim(char *c)
+{
+ for (; *c == ' ' || *c == '\t'; ++ c);
+ return c;
+}
+
+inline char* right_trim(char *start)
+{
+ char *end = start + strlen(start) - 1;
+ for (; end >= start && (*end == ' ' || *end == '\t'); -- end);
+ *(++ end) = 0;
+ return end;
+}
+
+inline std::string unquote_value(char *value, char *end, const std::string &path, int idx_line)
+{
+ std::string svalue;
+ if (value == end) {
+ // Empty string is a valid string.
+ } else if (*value == '"') {
+ if (++ value > -- end || *end != '"')
+ throw file_parser_error("String not enquoted correctly", path, idx_line);
+ *end = 0;
+ if (! unescape_string_cstyle(value, svalue))
+ throw file_parser_error("Invalid escape sequence inside a quoted string", path, idx_line);
+ } else
+ svalue.assign(value, end);
+ return svalue;
+}
+
+inline std::string unquote_version_comment(char *value, char *end, const std::string &path, int idx_line)
+{
+ std::string svalue;
+ if (value == end) {
+ // Empty string is a valid string.
+ } else if (*value == '"') {
+ if (++ value > -- end || *end != '"')
+ throw file_parser_error("Version comment not enquoted correctly", path, idx_line);
+ *end = 0;
+ if (! unescape_string_cstyle(value, svalue))
+ throw file_parser_error("Invalid escape sequence inside a quoted version comment", path, idx_line);
+ } else
+ svalue.assign(value, end);
+ return svalue;
+}
+
+size_t Index::load(const boost::filesystem::path &path)
+{
+ m_configs.clear();
+ m_vendor = path.stem().string();
+
+ boost::nowide::ifstream ifs(path.string());
+ std::string line;
+ size_t idx_line = 0;
+ Version ver;
+ while (std::getline(ifs, line)) {
+ ++ idx_line;
+ // Skip the initial white spaces.
+ char *key = left_trim(const_cast<char*>(line.data()));
+ if (*key == '#')
+ // Skip a comment line.
+ continue;
+ // Right trim the line.
+ char *end = right_trim(key);
+ if (key == end)
+ // Skip an empty line.
+ continue;
+ // Keyword may only contain alphanumeric characters. Semantic version may in addition contain "+.-".
+ char *key_end = key;
+ bool maybe_semver = true;
+ for (; *key_end != 0; ++ key_end) {
+ if (std::isalnum(*key_end) || strchr("+.-", *key_end) != nullptr) {
+ // It may be a semver.
+ } else if (*key_end == '_') {
+ // Cannot be a semver, but it may be a key.
+ maybe_semver = false;
+ } else
+ // End of semver or keyword.
+ break;
+ }
+ if (*key_end != 0 && *key_end != ' ' && *key_end != '\t' && *key_end != '=')
+ throw file_parser_error("Invalid keyword or semantic version", path, idx_line);
+ char *value = left_trim(key_end);
+ bool key_value_pair = *value == '=';
+ if (key_value_pair)
+ value = left_trim(value + 1);
+ *key_end = 0;
+ boost::optional<Semver> semver;
+ if (maybe_semver)
+ semver = Semver::parse(key);
+ if (key_value_pair) {
+ if (semver)
+ throw file_parser_error("Key cannot be a semantic version", path, idx_line);\
+ // Verify validity of the key / value pair.
+ std::string svalue = unquote_value(value, end, path.string(), idx_line);
+ if (strcmp(key, "min_slic3r_version") == 0 || strcmp(key, "max_slic3r_version") == 0) {
+ if (! svalue.empty())
+ semver = Semver::parse(svalue);
+ if (! semver)
+ throw file_parser_error(std::string(key) + " must referece a valid semantic version", path, idx_line);
+ if (strcmp(key, "min_slic3r_version") == 0)
+ ver.min_slic3r_version = *semver;
+ else
+ ver.max_slic3r_version = *semver;
+ } else {
+ // Ignore unknown keys, as there may come new keys in the future.
+ }
+ continue;
+ }
+ if (! semver)
+ throw file_parser_error("Invalid semantic version", path, idx_line);
+ ver.config_version = *semver;
+ ver.comment = (end <= key_end) ? "" : unquote_version_comment(value, end, path.string(), idx_line);
+ m_configs.emplace_back(ver);
+ }
+
+ // Sort the configs by their version.
+ std::sort(m_configs.begin(), m_configs.end(), [](const Version &v1, const Version &v2) { return v1.config_version < v2.config_version; });
+ return m_configs.size();
+}
+
+Semver Index::version() const
+{
+ Semver ver = Semver::zero();
+ for (const Version &cv : m_configs)
+ if (cv.config_version >= ver)
+ ver = cv.config_version;
+ return ver;
+}
+
+Index::const_iterator Index::find(const Semver &ver) const
+{
+ Version key;
+ key.config_version = ver;
+ auto it = std::lower_bound(m_configs.begin(), m_configs.end(), key,
+ [](const Version &v1, const Version &v2) { return v1.config_version < v2.config_version; });
+ return (it == m_configs.end() || it->config_version == ver) ? it : m_configs.end();
+}
+
+Index::const_iterator Index::recommended() const
+{
+ int idx = -1;
+ const_iterator highest = this->end();
+ for (const_iterator it = this->begin(); it != this->end(); ++ it)
+ if (it->is_current_slic3r_supported() &&
+ (highest == this->end() || highest->config_version < it->config_version))
+ highest = it;
+ return highest;
+}
+
+std::vector<Index> Index::load_db()
+{
+ boost::filesystem::path cache_dir = boost::filesystem::path(Slic3r::data_dir()) / "cache";
+
+ std::vector<Index> index_db;
+ std::string errors_cummulative;
+ for (auto &dir_entry : boost::filesystem::directory_iterator(cache_dir))
+ if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".idx")) {
+ Index idx;
+ try {
+ idx.load(dir_entry.path());
+ } catch (const std::runtime_error &err) {
+ errors_cummulative += err.what();
+ errors_cummulative += "\n";
+ continue;
+ }
+ index_db.emplace_back(std::move(idx));
+ }
+
+ if (! errors_cummulative.empty())
+ throw std::runtime_error(errors_cummulative);
+ return index_db;
+}
+
+} // namespace Config
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/Config/Version.hpp b/src/slic3r/Config/Version.hpp
new file mode 100644
index 000000000..acb0ae460
--- /dev/null
+++ b/src/slic3r/Config/Version.hpp
@@ -0,0 +1,88 @@
+#ifndef slic3r_GUI_ConfigIndex_
+#define slic3r_GUI_ConfigIndex_
+
+#include <string>
+#include <vector>
+
+#include <boost/filesystem.hpp>
+
+#include "../../libslic3r/FileParserError.hpp"
+#include "../Utils/Semver.hpp"
+
+namespace Slic3r {
+namespace GUI {
+namespace Config {
+
+// Configuration bundle version.
+struct Version
+{
+ // Version of this config.
+ Semver config_version = Semver::invalid();
+ // Minimum Slic3r version, for which this config is applicable.
+ Semver min_slic3r_version = Semver::zero();
+ // Maximum Slic3r version, for which this config is recommended.
+ // Slic3r should read older configuration and upgrade to a newer format,
+ // but likely there has been a better configuration published, using the new features.
+ Semver max_slic3r_version = Semver::inf();
+ // Single comment line.
+ std::string comment;
+
+ bool is_slic3r_supported(const Semver &slicer_version) const;
+ bool is_current_slic3r_supported() const;
+};
+
+// Index of vendor specific config bundle versions and Slic3r compatibilities.
+// The index is being downloaded from the internet, also an initial version of the index
+// is contained in the Slic3r installation.
+//
+// The index has a simple format:
+//
+// min_sic3r_version =
+// max_slic3r_version =
+// config_version "comment"
+// config_version "comment"
+// ...
+// min_slic3r_version =
+// max_slic3r_version =
+// config_version comment
+// config_version comment
+// ...
+//
+// The min_slic3r_version, max_slic3r_version keys are applied to the config versions below,
+// empty slic3r version means an open interval.
+class Index
+{
+public:
+ typedef std::vector<Version>::const_iterator const_iterator;
+ // Read a config index file in the simple format described in the Index class comment.
+ // Throws Slic3r::file_parser_error and the standard std file access exceptions.
+ size_t load(const boost::filesystem::path &path);
+
+ const std::string& vendor() const { return m_vendor; }
+ // Returns version of the index as the highest version of all the configs.
+ // If there is no config, Semver::zero() is returned.
+ Semver version() const;
+
+ const_iterator begin() const { return m_configs.begin(); }
+ const_iterator end() const { return m_configs.end(); }
+ const_iterator find(const Semver &ver) const;
+ const std::vector<Version>& configs() const { return m_configs; }
+ // Finds a recommended config to be installed for the current Slic3r version.
+ // Returns configs().end() if such version does not exist in the index. This shall never happen
+ // if the index is valid.
+ const_iterator recommended() const;
+
+ // Load all vendor specific indices.
+ // Throws Slic3r::file_parser_error and the standard std file access exceptions.
+ static std::vector<Index> load_db();
+
+private:
+ std::string m_vendor;
+ std::vector<Version> m_configs;
+};
+
+} // namespace Config
+} // namespace GUI
+} // namespace Slic3r
+
+#endif /* slic3r_GUI_ConfigIndex_ */