diff --git a/src/lay/lay/MainConfigPage7.ui b/src/lay/lay/MainConfigPage7.ui
index f81943cac..6f2b73662 100644
--- a/src/lay/lay/MainConfigPage7.ui
+++ b/src/lay/lay/MainConfigPage7.ui
@@ -6,8 +6,8 @@
0
0
- 606
- 130
+ 611
+ 271
@@ -67,6 +67,55 @@
+ -
+
+
+ Keep Backup Files
+
+
+
-
+
+
+ When saving files, the specified number of backups are kept. Backup files are formed by adding ".1", ".2" etc. to the original file name. ".1" is the most recent one. On saving, the files will be shuffled and ".1" becomes ".2", ".2 becomes ".3" up to the specified number of backup files.
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 252
+ 20
+
+
+
+
+ -
+
+
+ Number of backup files
+
+
+
+ -
+
+
+ -
+
+
+ (0 for "no backups)
+
+
+
+
+
+
diff --git a/src/lay/lay/layConfig.h b/src/lay/lay/layConfig.h
index 9c5e9d58a..e075d14fd 100644
--- a/src/lay/lay/layConfig.h
+++ b/src/lay/lay/layConfig.h
@@ -40,6 +40,10 @@ static const std::string cfg_synchronized_views ("synchronized-views");
static const std::string cfg_edit_mode ("edit-mode");
static const std::string cfg_custom_macro_paths ("custom-macro-paths");
static const std::string cfg_mru ("mru");
+static const std::string cfg_mru_layer_properties ("mru-layer-properties");
+static const std::string cfg_mru_sessions ("mru-sessions");
+static const std::string cfg_mru_bookmarks ("mru-bookmarks");
+static const std::string cfg_keep_backups ("keep-backups");
static const std::string cfg_technologies ("technology-data");
static const std::string cfg_key_bindings ("key-bindings");
static const std::string cfg_menu_items_hidden ("menu-items-hidden");
diff --git a/src/lay/lay/layMainConfigPages.cc b/src/lay/lay/layMainConfigPages.cc
index 1c3b37ffd..9517e2276 100644
--- a/src/lay/lay/layMainConfigPages.cc
+++ b/src/lay/lay/layMainConfigPages.cc
@@ -190,6 +190,10 @@ MainConfigPage7::setup (lay::PluginRoot *root)
bool en = true;
root->config_get (cfg_layout_file_watcher_enabled, en);
mp_ui->check_for_updates->setChecked (en);
+
+ int kb = 0;
+ root->config_get (cfg_keep_backups, kb);
+ mp_ui->keep_backups->setValue (kb);
}
void
@@ -197,6 +201,7 @@ MainConfigPage7::commit (lay::PluginRoot *root)
{
try {
root->config_set (cfg_layout_file_watcher_enabled, mp_ui->check_for_updates->isChecked ());
+ root->config_set (cfg_keep_backups, mp_ui->keep_backups->value ());
} catch (...) { }
}
diff --git a/src/lay/lay/layMainWindow.cc b/src/lay/lay/layMainWindow.cc
index aa229c1ad..bed0d4d09 100644
--- a/src/lay/lay/layMainWindow.cc
+++ b/src/lay/lay/layMainWindow.cc
@@ -426,6 +426,7 @@ MainWindow::MainWindow (QApplication *app, lay::Plugin *plugin_parent, const cha
m_mode (std::numeric_limits::max ()),
mp_setup_form (0),
m_open_mode (0),
+ m_keep_backups (0),
m_disable_tab_selected (false),
m_exited (false),
dm_do_update_menu (this, &MainWindow::do_update_menu),
@@ -1606,6 +1607,14 @@ MainWindow::configure (const std::string &name, const std::string &value)
return true;
+ } else if (name == cfg_keep_backups) {
+
+ int kb = 0;
+ tl::from_string (value, kb);
+ m_keep_backups = kb;
+
+ return false; // not taken - let others set it too.
+
} else if (name == cfg_micron_digits) {
unsigned int d = 5;
@@ -3160,7 +3169,7 @@ MainWindow::cm_save_current_cell_as ()
}
}
- cv->save_as (fn, om, options, false /*don't update*/);
+ cv->save_as (fn, om, options, false /*don't update*/, m_keep_backups);
add_mru (fn, cv->tech_name ());
@@ -3239,7 +3248,7 @@ MainWindow::do_save (bool as)
break;
}
- current_view ()->save_as ((unsigned int) cv_index, fn, om, options, true);
+ current_view ()->save_as ((unsigned int) cv_index, fn, om, options, true, m_keep_backups);
add_mru (fn, current_view ()->cellview (cv_index)->tech_name ());
}
@@ -3284,7 +3293,7 @@ MainWindow::cm_save_all ()
}
}
- view (view_index)->save_as (cv_index, fn, om, options, true);
+ view (view_index)->save_as (cv_index, fn, om, options, true, m_keep_backups);
add_mru (fn, current_view ()->cellview (cv_index)->tech_name ());
}
diff --git a/src/lay/lay/layMainWindow.h b/src/lay/lay/layMainWindow.h
index 108fd4284..136a2013b 100644
--- a/src/lay/lay/layMainWindow.h
+++ b/src/lay/lay/layMainWindow.h
@@ -894,6 +894,7 @@ private:
SettingsForm *mp_setup_form;
std::vector mp_views;
int m_open_mode;
+ int m_keep_backups;
std::vector > m_mru;
QStatusBar *mp_status_bar;
QStackedWidget *mp_main_stack_widget;
diff --git a/src/laybasic/laybasic/gsiDeclLayLayoutView.cc b/src/laybasic/laybasic/gsiDeclLayLayoutView.cc
index 5ac536d89..8e12b737f 100644
--- a/src/laybasic/laybasic/gsiDeclLayLayoutView.cc
+++ b/src/laybasic/laybasic/gsiDeclLayLayoutView.cc
@@ -316,14 +316,14 @@ static void delete_layers2 (lay::LayoutView *view, unsigned int index, const std
static void save_as1 (lay::LayoutView *view, unsigned int index, const std::string &filename, const db::SaveLayoutOptions &options)
{
- view->save_as (index, filename, tl::OutputStream::OM_Auto, options, true);
+ view->save_as (index, filename, tl::OutputStream::OM_Auto, options, true, 0);
}
static void save_as2 (lay::LayoutView *view, unsigned int index, const std::string &filename, bool /*gzip*/, const db::SaveLayoutOptions &options)
{
// because set_format_from_name always returns true now, we ignore the gzip option -
// it's probably used only in that context.
- view->save_as (index, filename, tl::OutputStream::OM_Auto, options, true);
+ view->save_as (index, filename, tl::OutputStream::OM_Auto, options, true, 0);
}
#if defined(HAVE_QTBINDINGS)
diff --git a/src/laybasic/laybasic/layCellView.cc b/src/laybasic/laybasic/layCellView.cc
index 0b28e12d0..bb956246a 100644
--- a/src/laybasic/laybasic/layCellView.cc
+++ b/src/laybasic/laybasic/layCellView.cc
@@ -290,7 +290,7 @@ LayoutHandle::update_save_options (db::SaveLayoutOptions &options)
}
void
-LayoutHandle::save_as (const std::string &fn, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update)
+LayoutHandle::save_as (const std::string &fn, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update, int keep_backups)
{
if (update) {
@@ -314,8 +314,13 @@ LayoutHandle::save_as (const std::string &fn, tl::OutputStream::OutputStreamMode
{
// The write needs to be finished before the file watcher gets the new modification time
db::Writer writer (options);
- tl::OutputStream stream (fn, om);
- writer.write (*mp_layout, stream);
+ tl::OutputStream stream (fn, om, false, keep_backups);
+ try {
+ writer.write (*mp_layout, stream);
+ } catch (...) {
+ stream.reject ();
+ throw;
+ }
}
if (update) {
diff --git a/src/laybasic/laybasic/layCellView.h b/src/laybasic/laybasic/layCellView.h
index a1040dc6c..abd340ae4 100644
--- a/src/laybasic/laybasic/layCellView.h
+++ b/src/laybasic/laybasic/layCellView.h
@@ -215,7 +215,7 @@ public:
* Save the layout under the given file name and with the given options.
* If update is true, this method updates the cell view's filename, title, save options and dirty flag.
*/
- void save_as (const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update = true);
+ void save_as (const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update = true, int keep_backups = 0);
/**
* @brief Sets the save options and a flag indicating whether they are valid
diff --git a/src/laybasic/laybasic/layLayoutView.cc b/src/laybasic/laybasic/layLayoutView.cc
index 7819b8c04..ef15ab32f 100644
--- a/src/laybasic/laybasic/layLayoutView.cc
+++ b/src/laybasic/laybasic/layLayoutView.cc
@@ -2078,12 +2078,12 @@ LayoutView::signal_selection_changed ()
}
void
-LayoutView::save_as (unsigned int index, const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update)
+LayoutView::save_as (unsigned int index, const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update, int keep_backups)
{
tl_assert (index < cellviews ());
tl::SelfTimer timer (tl::verbosity () >= 11, tl::to_string (QObject::tr ("Saving")));
- cellview (index)->save_as (filename, om, options, update);
+ cellview (index)->save_as (filename, om, options, update, keep_backups);
cellview_changed (index);
}
diff --git a/src/laybasic/laybasic/layLayoutView.h b/src/laybasic/laybasic/layLayoutView.h
index 42747f34e..ba7a1f8bc 100644
--- a/src/laybasic/laybasic/layLayoutView.h
+++ b/src/laybasic/laybasic/layLayoutView.h
@@ -775,7 +775,7 @@ public:
* @brief Save the given cellview into the given file (with options)
* If "update" is true, the cell view's properties will be updated (options, filename etc.).
*/
- void save_as (unsigned int index, const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update);
+ void save_as (unsigned int index, const std::string &filename, tl::OutputStream::OutputStreamMode om, const db::SaveLayoutOptions &options, bool update, int keep_backups);
/**
* @brief Implementation of the undo operations
diff --git a/src/tl/tl/tlFileUtils.cc b/src/tl/tl/tlFileUtils.cc
index b608139c4..e63b8cd47 100644
--- a/src/tl/tl/tlFileUtils.cc
+++ b/src/tl/tl/tlFileUtils.cc
@@ -435,6 +435,21 @@ bool mkpath (const std::string &p)
return true;
}
+bool rename_file (const std::string &path, const std::string &new_name)
+{
+ // resolve relative names in new_name
+ std::string new_path = new_name;
+ if (! tl::is_absolute (new_path)) {
+ new_path = tl::combine_path (tl::dirname (path), new_name);
+ }
+
+#if defined(_WIN32)
+ return _wrename (tl::to_wstring (path).c_str (), tl::to_wstring (new_path).c_str ()) == 0;
+#else
+ return rename (tl::to_local (path).c_str (), tl::to_local (new_path).c_str ()) == 0;
+#endif
+}
+
bool rm_file (const std::string &path)
{
#if defined(_WIN32)
diff --git a/src/tl/tl/tlFileUtils.h b/src/tl/tl/tlFileUtils.h
index 9eb2277ab..5b215f569 100644
--- a/src/tl/tl/tlFileUtils.h
+++ b/src/tl/tl/tlFileUtils.h
@@ -125,6 +125,11 @@ bool TL_PUBLIC is_dir (const std::string &s);
*/
std::vector TL_PUBLIC dir_entries (const std::string &s, bool with_files = true, bool with_dirs = true, bool without_dotfiles = false);
+/**
+ * @brief Rename the given file
+ */
+bool TL_PUBLIC rename_file (const std::string &path, const std::string &new_name);
+
/**
* @brief Removes the given file and returns true on success
*/
diff --git a/src/tl/tl/tlStream.cc b/src/tl/tl/tlStream.cc
index 83271e5d7..fbdcd276a 100644
--- a/src/tl/tl/tlStream.cc
+++ b/src/tl/tl/tlStream.cc
@@ -39,6 +39,7 @@
#include "tlDeflate.h"
#include "tlAssert.h"
#include "tlFileUtils.h"
+#include "tlLog.h"
#include "tlException.h"
#include "tlString.h"
@@ -692,7 +693,15 @@ OutputStream::OutputStream (OutputStreamBase &delegate, bool as_text)
mp_buffer = new char[m_buffer_capacity];
}
-OutputStream::OutputStreamMode
+OutputStream::OutputStream (OutputStreamBase *delegate, bool as_text)
+ : m_pos (0), mp_delegate (delegate), m_owns_delegate (true), m_as_text (as_text)
+{
+ m_buffer_capacity = 16384;
+ m_buffer_pos = 0;
+ mp_buffer = new char[m_buffer_capacity];
+}
+
+OutputStream::OutputStreamMode
OutputStream::output_mode_from_filename (const std::string &abstract_path, OutputStream::OutputStreamMode om)
{
if (om == OM_Auto) {
@@ -707,16 +716,16 @@ OutputStream::output_mode_from_filename (const std::string &abstract_path, Outpu
}
static
-OutputStreamBase *create_file_stream (const std::string &path, OutputStream::OutputStreamMode om)
+OutputStreamBase *create_file_stream (const std::string &path, OutputStream::OutputStreamMode om, int keep_backups)
{
if (om == OutputStream::OM_Zlib) {
- return new OutputZLibFile (path);
+ return new OutputZLibFile (path, keep_backups);
} else {
- return new OutputFile (path);
+ return new OutputFile (path, keep_backups);
}
}
-OutputStream::OutputStream (const std::string &abstract_path, OutputStreamMode om, bool as_text)
+OutputStream::OutputStream (const std::string &abstract_path, OutputStreamMode om, bool as_text, int keep_backups)
: m_pos (0), mp_delegate (0), m_owns_delegate (false), m_as_text (as_text), m_path (abstract_path)
{
// Determine output mode
@@ -728,9 +737,9 @@ OutputStream::OutputStream (const std::string &abstract_path, OutputStreamMode o
} else if (ex.test ("pipe:")) {
mp_delegate = new OutputPipe (ex.get ());
} else if (ex.test ("file:")) {
- mp_delegate = create_file_stream (ex.get (), om);
+ mp_delegate = create_file_stream (ex.get (), om, keep_backups);
} else {
- mp_delegate = create_file_stream (abstract_path, om);
+ mp_delegate = create_file_stream (abstract_path, om, keep_backups);
}
m_owns_delegate = true;
@@ -742,21 +751,42 @@ OutputStream::OutputStream (const std::string &abstract_path, OutputStreamMode o
OutputStream::~OutputStream ()
{
- close ();
+ try {
+ close ();
+ } catch (...) {
+ // no recursive exceptions
+ }
}
void
OutputStream::close ()
{
- flush ();
+ try {
+
+ flush ();
+
+ if (mp_delegate && m_owns_delegate) {
+ delete mp_delegate;
+ mp_delegate = 0;
+ }
+ if (mp_buffer) {
+ delete[] mp_buffer;
+ mp_buffer = 0;
+ }
+
+ } catch (...) {
+
+ if (mp_delegate && m_owns_delegate) {
+ delete mp_delegate;
+ mp_delegate = 0;
+ }
+ if (mp_buffer) {
+ delete[] mp_buffer;
+ mp_buffer = 0;
+ }
+
+ throw;
- if (mp_delegate && m_owns_delegate) {
- delete mp_delegate;
- mp_delegate = 0;
- }
- if (mp_buffer) {
- delete[] mp_buffer;
- mp_buffer = 0;
}
}
@@ -867,11 +897,110 @@ OutputStream::seek (size_t pos)
m_pos = pos;
}
+// ---------------------------------------------------------------
+// OutputFileBase implementation
+
+OutputFileBase::OutputFileBase (const std::string &path, int keep_backups)
+ : m_keep_backups (keep_backups), m_path (path), m_has_error (false)
+{
+ if (tl::file_exists (path)) {
+ m_backup_path = path + ".~backup";
+ if (tl::file_exists (m_backup_path)) {
+ if (! tl::rm_file (m_backup_path)) {
+ tl::warn << tl::sprintf (tl::to_string (tr ("Could not create backup file: unable to remove existing file '%s'")), m_backup_path);
+ m_backup_path = std::string ();
+ }
+ }
+ if (! m_backup_path.empty ()) {
+ if (! tl::rename_file (path, m_backup_path)) {
+ tl::warn << tl::sprintf (tl::to_string (tr ("Could not create backup file: unable to rename original file '%s' to backup file")), path, m_backup_path);
+ m_backup_path = std::string ();
+ }
+ }
+ }
+}
+
+OutputFileBase::~OutputFileBase ()
+{
+ if (! m_backup_path.empty ()) {
+
+ if (m_has_error) {
+
+ if (! tl::rm_file (m_path)) {
+ tl::warn << tl::sprintf (tl::to_string (tr ("Could not restore backup file: unable to remove file '%s'")), m_path);
+ } else if (! tl::rename_file (m_backup_path, m_path)) {
+ tl::warn << tl::sprintf (tl::to_string (tr ("Could not restore backup file: unable to rename file '%s' back to '%s'")), m_backup_path, m_path);
+ }
+
+ } else {
+
+ if (m_keep_backups == 0) {
+
+ if (! tl::rm_file (m_backup_path)) {
+ tl::warn << tl::sprintf (tl::to_string (tr ("Could not remove backup file '%s'")), m_backup_path);
+ }
+
+ } else {
+
+ // shuffle backup files
+ int n = 1;
+ for ( ; m_keep_backups < 0 || n < m_keep_backups; ++n) {
+ std::string p = m_path + "." + tl::to_string (n);
+ if (! tl::file_exists (p)) {
+ break;
+ }
+ }
+
+ while (n > 0) {
+ std::string p = m_path + "." + tl::to_string (n);
+ std::string pprev = n > 1 ? (m_path + "." + tl::to_string (n - 1)) : m_backup_path;
+ if (tl::file_exists (p)) {
+ if (! tl::rm_file (p)) {
+ tl::warn << tl::sprintf (tl::to_string (tr ("Error shuffling backup files: unable to remove file '%s'")), p);
+ }
+ }
+ if (! tl::rename_file (pprev, p)) {
+ tl::warn << tl::sprintf (tl::to_string (tr ("Error shuffling backup files: unable to rename file '%s' to '%s'")), pprev, p);
+ }
+ --n;
+ }
+
+ }
+
+ }
+ }
+}
+
+void OutputFileBase::seek (size_t s)
+{
+ try {
+ seek_file (s);
+ } catch (...) {
+ reject ();
+ throw;
+ }
+}
+
+void OutputFileBase::write (const char *b, size_t n)
+{
+ try {
+ write_file (b, n);
+ } catch (...) {
+ reject ();
+ throw;
+ }
+}
+
+void OutputFileBase::reject ()
+{
+ m_has_error = true;
+}
+
// ---------------------------------------------------------------
// OutputFile implementation
-OutputFile::OutputFile (const std::string &path)
- : m_fd (-1)
+OutputFile::OutputFile (const std::string &path, int keep_backups)
+ : OutputFileBase (path, keep_backups), m_fd (-1)
{
m_source = path;
#if defined(_WIN32)
@@ -898,11 +1027,11 @@ OutputFile::~OutputFile ()
close (m_fd);
#endif
m_fd = -1;
- }
+ }
}
-void
-OutputFile::seek (size_t s)
+void
+OutputFile::seek_file (size_t s)
{
tl_assert (m_fd >= 0);
#if defined(_WIN64)
@@ -914,8 +1043,8 @@ OutputFile::seek (size_t s)
#endif
}
-void
-OutputFile::write (const char *b, size_t n)
+void
+OutputFile::write_file (const char *b, size_t n)
{
tl_assert (m_fd >= 0);
#if defined(_WIN32)
@@ -931,8 +1060,8 @@ OutputFile::write (const char *b, size_t n)
// ---------------------------------------------------------------
// OutputZLibFile implementation
-OutputZLibFile::OutputZLibFile (const std::string &path)
- : mp_d (new ZLibFilePrivate ())
+OutputZLibFile::OutputZLibFile (const std::string &path, int keep_backups)
+ : OutputFileBase (path, keep_backups), mp_d (new ZLibFilePrivate ())
{
m_source = path;
#if defined(_WIN32)
@@ -960,7 +1089,7 @@ OutputZLibFile::~OutputZLibFile ()
}
void
-OutputZLibFile::write (const char *b, size_t n)
+OutputZLibFile::write_file (const char *b, size_t n)
{
tl_assert (mp_d->zs != NULL);
int ret = gzwrite (mp_d->zs, (char *) b, (unsigned int) n);
diff --git a/src/tl/tl/tlStream.h b/src/tl/tl/tlStream.h
index 73c3304d7..d1366a22d 100644
--- a/src/tl/tl/tlStream.h
+++ b/src/tl/tl/tlStream.h
@@ -703,6 +703,14 @@ public:
return false;
}
+ /**
+ * @brief Rejects the output - for delegates supporting unrolling, this means the original file is restored
+ */
+ virtual void reject ()
+ {
+ // ... the default implementation does not support this feature ..
+ }
+
private:
// No copying
OutputStreamBase (const OutputStreamBase &);
@@ -831,13 +839,81 @@ private:
std::ostringstream m_stream;
};
+/**
+ * @brief A base for file writer delegates
+ *
+ * This class mainly provides safety services for the file writer.
+ * When writing a file, it will keep a backup until the file actually
+ * has been written. This way, a network or disk full error will not
+ * comprimise the file's content. The backup file name is created
+ * by appending ".~backup" to the original file path.
+ *
+ * In addition, a specified number or persistent backup files can
+ * be kept. The first backup will be called like the original file
+ * with ".1" appended. The second will be called ".2" etc.
+ * The backups will be shuffled, so ".1" is always the most recent
+ * one while older ones get bigger numbers.
+ *
+ * The number of backups can be specified with "keep_backups". If this
+ * count is zero, no backups will be kept. If the count is larger than
+ * zero, the specified maximum number of backups is kept. If less than
+ * zero, an infinite number of backups is made. But beware: shuffling
+ * will become increasingly expensive.
+ */
+class TL_PUBLIC OutputFileBase
+ : public OutputStreamBase
+{
+public:
+ /**
+ * @brief Constructor
+ *
+ * @param path The (relative) path of the file to write
+ * @param keep_backups The number of backups to keep (0: none, -1: infinite)
+ */
+ OutputFileBase (const std::string &path, int keep_backups);
+
+ /**
+ * @brief Destructor
+ */
+ virtual ~OutputFileBase ();
+
+ /**
+ * @brief Seek to the specified position
+ *
+ * Writing continues at that position after a seek.
+ */
+ virtual void seek (size_t s);
+
+ /**
+ * @brief Write to a file
+ *
+ * Implements the basic write method.
+ * Will throw a FileWriteErrorException if an error occurs.
+ */
+ virtual void write (const char *b, size_t n);
+
+ /**
+ * @brief Unrolls the output
+ */
+ virtual void reject ();
+
+protected:
+ virtual void seek_file (size_t s) = 0;
+ virtual void write_file (const char *b, size_t n) = 0;
+
+private:
+ int m_keep_backups;
+ std::string m_backup_path, m_path;
+ bool m_has_error;
+};
+
/**
* @brief A zlib output file delegate
*
* Implements the writer for a zlib stream
*/
class TL_PUBLIC OutputZLibFile
- : public OutputStreamBase
+ : public OutputFileBase
{
public:
/**
@@ -847,9 +923,10 @@ public:
* object. open() will throw a FileOpenErrorException if
* an error occurs.
*
- * @param path The (relative) path of the file to open
+ * @param path The (relative) path of the file to write
+ * @param keep_backups The number of backups to keep (0: none, -1: infinite)
*/
- OutputZLibFile (const std::string &path);
+ OutputZLibFile (const std::string &path, int keep_backups);
/**
* @brief Close the file
@@ -858,13 +935,19 @@ public:
*/
virtual ~OutputZLibFile ();
+protected:
/**
* @brief Write to a file
*
* Implements the basic write method.
* Will throw a ZLibWriteErrorException if an error occurs.
*/
- virtual void write (const char *b, size_t n);
+ virtual void write_file (const char *b, size_t n);
+
+ /**
+ * @brief The seek operation isn't implemented for zlib files
+ */
+ virtual void seek_file (size_t /*s*/) { }
private:
// No copying
@@ -881,7 +964,7 @@ private:
* Implements the writer for ordinary files.
*/
class TL_PUBLIC OutputFile
- : public OutputStreamBase
+ : public OutputFileBase
{
public:
/**
@@ -891,10 +974,10 @@ public:
* object. open() will throw a FileOpenErrorException if
* an error occurs.
*
- * @param path The (relative) path of the file to open
- * @param read True, if the file should be read, false on write.
+ * @param path The (relative) path of the file to write
+ * @param keep_backups The number of backups to keep (0: none, -1: infinite)
*/
- OutputFile (const std::string &path);
+ OutputFile (const std::string &path, int keep_backups = 0);
/**
* @brief Close the file
@@ -903,20 +986,21 @@ public:
*/
virtual ~OutputFile ();
+ /**
+ * @brief Returns a value indicating whether that stream supports seek
+ */
+ virtual bool supports_seek ()
+ {
+ return true;
+ }
+
+protected:
/**
* @brief Seek to the specified position
*
* Writing continues at that position after a seek.
*/
- virtual void seek (size_t s);
-
- /**
- * @brief Returns a value indicating whether that stream supports seek
- */
- bool supports_seek ()
- {
- return true;
- }
+ virtual void seek_file (size_t s);
/**
* @brief Write to a file
@@ -924,7 +1008,7 @@ public:
* Implements the basic write method.
* Will throw a FileWriteErrorException if an error occurs.
*/
- virtual void write (const char *b, size_t n);
+ virtual void write_file (const char *b, size_t n);
private:
// No copying
@@ -1030,13 +1114,20 @@ public:
*/
OutputStream (OutputStreamBase &delegate, bool as_text = false);
+ /**
+ * @brief Default constructor
+ *
+ * This constructor takes a delegate object. The stream will own the delegate.
+ */
+ OutputStream (OutputStreamBase *delegate, bool as_text = false);
+
/**
* @brief Open an output stream with the given path and stream mode
*
* This will automatically create a delegate object and delete it later.
* If "as_text" is true, the output will be formatted with the system's line separator.
*/
- OutputStream (const std::string &abstract_path, OutputStreamMode om = OM_Auto, bool as_text = false);
+ OutputStream (const std::string &abstract_path, OutputStreamMode om = OM_Auto, bool as_text = false, int keep_backups = 0);
/**
* @brief Destructor
@@ -1120,6 +1211,16 @@ public:
return *this;
}
+ /**
+ * @brief Rejects the output - for delegates which support backup, this means the original file is restored
+ */
+ void reject () const
+ {
+ if (mp_delegate) {
+ mp_delegate->reject ();
+ }
+ }
+
/**
* @brief Returns a value indicating whether that stream supports seek
*/
diff --git a/src/tl/unit_tests/tlFileUtils.cc b/src/tl/unit_tests/tlFileUtils.cc
index f944a1b25..ce032b462 100644
--- a/src/tl/unit_tests/tlFileUtils.cc
+++ b/src/tl/unit_tests/tlFileUtils.cc
@@ -676,3 +676,82 @@ TEST (17)
EXPECT_EQ (tl::is_same_file (yfile, tl::combine_path (dpath, "../d/y")), true);
}
+// rename_file
+TEST (18)
+{
+ std::string tp = tl::absolute_file_path (tmp_file ());
+ std::string xfile = tl::combine_path (tp, "x");
+ std::string yfile = tl::combine_path (tp, "y");
+ std::string zfile = tl::combine_path (tl::combine_path (tp, "dir"), "z");
+
+ tl::mkpath (tl::combine_path (tp, "dir"));
+
+ {
+ tl::OutputStream os (xfile);
+ os << "hello, world!\n";
+ }
+
+ EXPECT_EQ (tl::file_exists (xfile), true);
+ EXPECT_EQ (tl::file_exists (yfile), false);
+ EXPECT_EQ (tl::file_exists (zfile), false);
+
+ {
+ tl::InputStream is (xfile);
+ EXPECT_EQ (is.read_all (), "hello, world!\n");
+ }
+
+ tl::rename_file (xfile, yfile);
+
+ EXPECT_EQ (tl::file_exists (xfile), false);
+ EXPECT_EQ (tl::file_exists (yfile), true);
+ EXPECT_EQ (tl::file_exists (zfile), false);
+
+ {
+ tl::InputStream is (yfile);
+ EXPECT_EQ (is.read_all (), "hello, world!\n");
+ }
+
+ tl::rename_file (yfile, "x");
+
+ EXPECT_EQ (tl::file_exists (xfile), true);
+ EXPECT_EQ (tl::file_exists (yfile), false);
+ EXPECT_EQ (tl::file_exists (zfile), false);
+
+ {
+ tl::InputStream is (xfile);
+ EXPECT_EQ (is.read_all (), "hello, world!\n");
+ }
+
+ tl::rename_file (xfile, zfile);
+
+ EXPECT_EQ (tl::file_exists (xfile), false);
+ EXPECT_EQ (tl::file_exists (yfile), false);
+ EXPECT_EQ (tl::file_exists (zfile), true);
+
+ {
+ tl::InputStream is (zfile);
+ EXPECT_EQ (is.read_all (), "hello, world!\n");
+ }
+
+ tl::rename_file (zfile, xfile);
+
+ EXPECT_EQ (tl::file_exists (xfile), true);
+ EXPECT_EQ (tl::file_exists (yfile), false);
+ EXPECT_EQ (tl::file_exists (zfile), false);
+
+ {
+ tl::InputStream is (xfile);
+ EXPECT_EQ (is.read_all (), "hello, world!\n");
+ }
+
+ tl::rename_file (xfile, tl::combine_path ("dir", "z"));
+
+ EXPECT_EQ (tl::file_exists (xfile), false);
+ EXPECT_EQ (tl::file_exists (yfile), false);
+ EXPECT_EQ (tl::file_exists (zfile), true);
+
+ {
+ tl::InputStream is (zfile);
+ EXPECT_EQ (is.read_all (), "hello, world!\n");
+ }
+}
diff --git a/src/tl/unit_tests/tlStreamTests.cc b/src/tl/unit_tests/tlStreamTests.cc
index f2fb84218..00a49988d 100644
--- a/src/tl/unit_tests/tlStreamTests.cc
+++ b/src/tl/unit_tests/tlStreamTests.cc
@@ -162,3 +162,235 @@ TEST(TextInputStream)
EXPECT_EQ (tis.read_all (), "Hello, world!\nWith another line\n\nseparated by a LFCR and CRLF.");
}
}
+
+namespace
+{
+
+class BrokenOutputStream
+ : public tl::OutputFile
+{
+public:
+ BrokenOutputStream (const std::string &path, int keep_backups)
+ : tl::OutputFile (path, keep_backups)
+ { }
+
+ void write_file(const char *b, size_t n)
+ {
+ for (const char *p = b; p < b + n; ++p) {
+ if (*p == '!') {
+ throw tl::Exception ("Bang!");
+ }
+ }
+ tl::OutputFile::write (b, n);
+ }
+};
+
+}
+
+TEST(SafeOutput)
+{
+ std::string tp = tmp_file ("x");
+
+ {
+ tl::OutputStream os (tp);
+ os << "blabla\n";
+ }
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::OutputStream os (tp);
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
+ EXPECT_EQ (tl::file_exists (tp), true);
+ os << "Hello, world!\n";
+ }
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::InputStream is (tp);
+ EXPECT_EQ (is.read_all (), "Hello, world!\n");
+ }
+
+ try {
+ BrokenOutputStream broken (tp, 0);
+ tl::OutputStream os (broken);
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
+ EXPECT_EQ (tl::file_exists (tp), true);
+ os << "Hi!\n";
+ os.flush (); // raises the exception
+ EXPECT_EQ (true, false);
+ } catch (...) {
+ // '!' raises an exception
+ }
+
+ // The original content is restored now
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::InputStream is (tp);
+ EXPECT_EQ (is.read_all (), "Hello, world!\n");
+ }
+
+
+ try {
+ BrokenOutputStream *broken = new BrokenOutputStream (tp, 0);
+ tl::OutputStream os (broken);
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
+ EXPECT_EQ (tl::file_exists (tp), true);
+ os << "Hi!\n";
+ os.flush (); // raises the exception
+ EXPECT_EQ (true, false);
+ } catch (...) {
+ // '!' raises an exception
+ }
+
+ // The original content is restored now
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::InputStream is (tp);
+ EXPECT_EQ (is.read_all (), "Hello, world!\n");
+ }
+}
+
+
+TEST(Backups)
+{
+ std::string tp = tmp_file ("x");
+
+ {
+ tl::OutputStream os (tp, tl::OutputStream::OM_Auto, false, 2);
+ os << "1\n";
+ }
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp + ".1"), false);
+ EXPECT_EQ (tl::file_exists (tp + ".2"), false);
+ EXPECT_EQ (tl::file_exists (tp + ".3"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::InputStream is (tp);
+ EXPECT_EQ (is.read_all (), "1\n");
+ }
+
+ {
+ tl::OutputStream os (tp, tl::OutputStream::OM_Auto, false, 2);
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
+ EXPECT_EQ (tl::file_exists (tp), true);
+ os << "2\n";
+ }
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp + ".1"), true);
+ EXPECT_EQ (tl::file_exists (tp + ".2"), false);
+ EXPECT_EQ (tl::file_exists (tp + ".3"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::InputStream is (tp);
+ EXPECT_EQ (is.read_all (), "2\n");
+ }
+
+ {
+ tl::InputStream is (tp + ".1");
+ EXPECT_EQ (is.read_all (), "1\n");
+ }
+
+ {
+ tl::OutputStream os (tp, tl::OutputStream::OM_Auto, false, 2);
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
+ EXPECT_EQ (tl::file_exists (tp), true);
+ os << "3\n";
+ }
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp + ".1"), true);
+ EXPECT_EQ (tl::file_exists (tp + ".2"), true);
+ EXPECT_EQ (tl::file_exists (tp + ".3"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::InputStream is (tp);
+ EXPECT_EQ (is.read_all (), "3\n");
+ }
+
+ {
+ tl::InputStream is (tp + ".1");
+ EXPECT_EQ (is.read_all (), "2\n");
+ }
+
+ {
+ tl::InputStream is (tp + ".2");
+ EXPECT_EQ (is.read_all (), "1\n");
+ }
+
+ {
+ tl::OutputStream os (tp, tl::OutputStream::OM_Auto, false, 2);
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
+ EXPECT_EQ (tl::file_exists (tp), true);
+ os << "4\n";
+ }
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp + ".1"), true);
+ EXPECT_EQ (tl::file_exists (tp + ".2"), true);
+ EXPECT_EQ (tl::file_exists (tp + ".3"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::InputStream is (tp);
+ EXPECT_EQ (is.read_all (), "4\n");
+ }
+
+ {
+ tl::InputStream is (tp + ".1");
+ EXPECT_EQ (is.read_all (), "3\n");
+ }
+
+ {
+ tl::InputStream is (tp + ".2");
+ EXPECT_EQ (is.read_all (), "2\n");
+ }
+
+ try {
+ BrokenOutputStream broken (tp, 2);
+ tl::OutputStream os (broken);
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), true);
+ EXPECT_EQ (tl::file_exists (tp), true);
+ os << "5!\n";
+ os.flush (); // raises the exception
+ EXPECT_EQ (true, false);
+ } catch (...) {
+ // '!' raises an exception
+ }
+
+ EXPECT_EQ (tl::file_exists (tp + ".~backup"), false);
+ EXPECT_EQ (tl::file_exists (tp + ".1"), true);
+ EXPECT_EQ (tl::file_exists (tp + ".2"), true);
+ EXPECT_EQ (tl::file_exists (tp + ".3"), false);
+ EXPECT_EQ (tl::file_exists (tp), true);
+
+ {
+ tl::InputStream is (tp);
+ EXPECT_EQ (is.read_all (), "4\n");
+ }
+
+ {
+ tl::InputStream is (tp + ".1");
+ EXPECT_EQ (is.read_all (), "3\n");
+ }
+
+ {
+ tl::InputStream is (tp + ".2");
+ EXPECT_EQ (is.read_all (), "2\n");
+ }
+}
+