Repository: GothenburgBitFactory/timewarrior (cpp)

Issue:
tags.data modification date updated when file is not modified
Hello, I've noticed every time `timew` is run, tags.data is not only accessed but written, which updates the modification timestamp on the file even though the content was not modified. IMO the file should only be written to if the content is actually changing. Running the bare `timew` should be idempotent and cause no changes to the filesystem.

This can be particularly problematic if `timew` is being run frequently, such as by a system status bar, and if the file is under version control or being synced to cloud storage. tags.data is then constantly being reuploaded, and it makes the version history feature of Google Drive useless, for example, as the last 100 (the maximum) versions are all identical. This is especially insidious if, God forbid, you happen to have multiple systems running `timew` attempting to stay in sync. Granted this is not likely an advisable scenario, but it's interesting to know the tiny time discrepancy across my systems apparently confused my Google Drive sync so badly that it created dozens of copies of my data folder that all somehow ended up with 0 byte files. I have since disabled outputting my current time tracking status to my status bar, and started over, and haven't had a problem. I would, however, like to actually use my status bar to help me remember my status.

Here's a demonstration of the issue. I check the modification date of the tags.data file, run `timew` and check the date again. I would not expect the modification date to change, but it does.

```
$ stat /home/cgorichanaz/GDrive/Main/Tracking/time/data/tags.data
  File: /home/cgorichanaz/GDrive/Main/Tracking/time/data/tags.data
  Size: 1321            Blocks: 8          IO Block: 4096   regular file
Device: fe02h/65026d    Inode: 36702578    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/cgorichanaz)   Gid: ( 1002/cgorichanaz)
Access: 2020-02-05 21:58:31.011955127 -0800
Modify: 2020-02-05 21:58:30.991954904 -0800
Change: 2020-02-05 21:58:30.991954904 -0800
 Birth: 2020-02-05 16:32:40.445637145 -0800
#  21:58:33  0.013  ~
$ timew
There is no active time tracking.
#  21:58:39  0.014  ~  ↵ 1
$ stat /home/cgorichanaz/GDrive/Main/Tracking/time/data/tags.data
  File: /home/cgorichanaz/GDrive/Main/Tracking/time/data/tags.data
  Size: 1321            Blocks: 8          IO Block: 4096   regular file
Device: fe02h/65026d    Inode: 36702578    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/cgorichanaz)   Gid: ( 1002/cgorichanaz)
Access: 2020-02-05 21:58:41.062067311 -0800
Modify: 2020-02-05 21:58:41.015400124 -0800
Change: 2020-02-05 21:58:41.015400124 -0800
 Birth: 2020-02-05 16:32:40.445637145 -0800
```

When I run `strace timew`, it seems the tags.data file is read as read only at one point, and then after the program prints its final output, the tags.data file is opened again, this time in write only mode!

```
...
write(1, "There is no active time tracking"..., 34There is no active time tracking.
) = 34
openat(AT_FDCWD, "/home/cgorichanaz/GDrive/Main/Tracking/time/data/tags.data", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
writev(3, [{iov_base=NULL, iov_len=0}, {iov_base="{\"1:1\":{\"count\":1},\"CM-IM correc"..., iov_len=1321}], 2) = 1321
close(3)                                = 0
exit_group(1)                           = ?
+++ exited with 1 +++
```

Here are my versions:
```
$ uname -r
5.4.15-arch1-1
#  22:01:17  0.008  ~
$ python --version
Python 3.8.1
#  22:01:23  0.010  ~
$ timew --version
1.2.0
```


Discussion:
@sruffell do your changes in #283 solve this?
Yes, #283 does contain a fix for this.

The last commit in that series, sruffell/timewarrior@75de07375626, specifically addresses this issue.
I went ahead and tagged this issue in that specific commit since it was still waiting to be merged.
Hey cool, how timely! Thanks a lot!

Patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,6 +8,8 @@ include (CXXSniffer)
 
 set (PROJECT_VERSION "1.2.0")
 
+string(TOUPPER "${CMAKE_BUILD_TYPE}" uppercase_CMAKE_BUILD_TYPE)
+
 message ("-- Looking for SHA1 references")
 if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
   set (HAVE_COMMIT true)
diff --git a/src/AtomicFile.cpp b/src/AtomicFile.cpp
new file mode 100644
--- /dev/null
+++ b/src/AtomicFile.cpp
@@ -0,0 +1,441 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2020, Shaun Ruffell, Thomas Lauf.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// https://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <stdio.h>
+#include <cassert>
+#include <iostream>
+#include <vector>
+#include <unistd.h>
+
+#include <format.h>
+#include <AtomicFile.h>
+#include <FS.h>
+
+struct AtomicFile::impl
+{
+  using value_type = std::shared_ptr <AtomicFile::impl>;
+  // Since it should be relatively small, keep the atomic files in a vector
+  using atomic_files_t = std::vector <value_type>;
+  using iterator = atomic_files_t::iterator;
+
+  File temp_file;
+  File real_file;
+
+  // After the file is modified in any way, all operations should deal only with
+  // the temp file until finalization.
+  bool is_temp_active {false};
+
+  impl (const Path& path);
+  ~impl ();
+
+  std::string name () const;
+  const std::string& path () const;
+  bool exists () const;
+
+  bool open ();
+  void close ();
+  void truncate ();
+  void read (std::string& content);
+  void read (std::vector <std::string>& lines);
+  void append (const std::string& content);
+  void write_raw (const std::string& content);
+
+  void finalize ();
+
+  static atomic_files_t::iterator find (const std::string& path) = delete;
+  static atomic_files_t::iterator find (const Path& path);
+
+  // Static members
+
+  // If there is a problem writing to any of the temporary files, we do not want
+  // any of them to be copied over the "real" file.
+  static bool allow_atomics;
+  static atomic_files_t atomic_files;
+};
+
+using atomic_files_t = AtomicFile::impl::atomic_files_t;
+using atomics_iterator = atomic_files_t::iterator;
+
+atomic_files_t AtomicFile::impl::atomic_files {};
+bool AtomicFile::impl::allow_atomics {true};
+
+////////////////////////////////////////////////////////////////////////////////
+AtomicFile::impl::impl (const Path& path)
+{
+  static pid_t s_pid = ::getpid ();
+  static int s_count = 0;
+  std::stringstream str; 
+
+  str << path._data << '.' << s_pid << '-' << ++s_count << ".tmp";
+  temp_file = File (str.str());
+  real_file = File (path);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+AtomicFile::impl::~impl ()
+{
+  // Make sure we remove any temporary files if AtomicFile::finalize_all was
+  // never called. Typically, this will happen when there are exceptions.
+  try
+  {
+    std::remove (temp_file._data.c_str ());
+  }
+  catch (...)
+  {
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string AtomicFile::impl::name () const
+{
+  return real_file.name ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string& AtomicFile::impl::path () const
+{
+  return real_file._data;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool AtomicFile::impl::exists () const
+{
+  return real_file.exists ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool AtomicFile::impl::open ()
+{
+  assert (!temp_file._data.empty () && !real_file._data.empty ());
+  return real_file.open ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::impl::close ()
+{
+  try
+  {
+    temp_file.close ();
+    real_file.close ();
+  }
+  catch (...)
+  {
+    allow_atomics = false;
+    throw;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::impl::truncate ()
+{
+  try 
+  {
+    temp_file.truncate ();
+    is_temp_active = true;
+  }
+  catch (...)
+  {
+    allow_atomics = false;
+    throw;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::impl::read (std::string& content)
+{
+  if (is_temp_active)
+  {
+    // Close the file before reading it in order to flush any buffers.
+    temp_file.close ();
+  }
+  return (is_temp_active) ? temp_file.read (content) :
+                            real_file.read (content);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::impl::read (std::vector <std::string>& lines)
+{
+  if (is_temp_active)
+  {
+    // Close the file before reading it in order to flush any buffers.
+    temp_file.close ();
+  }
+  return (is_temp_active) ? temp_file.read (lines) :
+                            real_file.read (lines);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::impl::append (const std::string& content)
+{
+  try
+  {
+    if (!is_temp_active)
+    {
+      is_temp_active = true;
+
+      if (real_file.exists () && ! File::copy (real_file, temp_file))
+      {
+        throw format ("Failed to copy '{1}' to '{2}'",
+                      real_file.name (), temp_file.name ());
+      }
+    }
+    return temp_file.append (content);
+  }
+  catch (...)
+  {
+    allow_atomics = false;
+    throw;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::impl::write_raw (const std::string& content)
+{
+  try
+  {
+    temp_file.write_raw (content);
+    is_temp_active = true;
+  }
+  catch (...)
+  {
+    allow_atomics = false;
+    throw;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::impl::finalize ()
+{
+  if (is_temp_active && impl::allow_atomics)
+  {
+    if (std::rename (temp_file._data.c_str (), real_file._data.c_str ()))
+    {
+      throw format("Failed copying '{1}' to '{2}'. Database corruption possible.",
+                   temp_file._data, real_file._data);
+    }
+    is_temp_active = false;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+atomics_iterator AtomicFile::impl::find (const Path& path)
+{
+  auto end = impl::atomic_files.end ();
+  auto cmp = [&path](const atomic_files_t::value_type& p)
+             {
+               return p->real_file == path;
+             };
+  auto it = std::find_if(impl::atomic_files.begin (), end, cmp);
+  return (it == end) ? end : it;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+AtomicFile::AtomicFile (const Path& path)
+{
+  auto it = impl::find (path);
+
+  if (it == impl::atomic_files.end ())
+  {
+    pimpl = std::make_shared <impl> (path._data);
+    impl::atomic_files.push_back (pimpl);
+  }
+  else
+  {
+    pimpl = *it;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+AtomicFile::AtomicFile (std::string path) : AtomicFile (Path (path))
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+AtomicFile::~AtomicFile()
+{
+  try
+  {
+    pimpl->close ();
+  }
+  catch (...)
+  {
+  }
+}
+
+AtomicFile::AtomicFile (AtomicFile&&) = default;
+AtomicFile& AtomicFile::operator= (AtomicFile&&) = default;
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string& AtomicFile::path () const
+{
+  return pimpl->path ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string AtomicFile::name () const
+{
+  return pimpl->name ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool AtomicFile::exists () const
+{
+  return pimpl->exists ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool AtomicFile::open ()
+{
+  return pimpl->open ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::close ()
+{
+  pimpl->close ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::truncate ()
+{
+  pimpl->truncate ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::read (std::string& content)
+{
+  pimpl->read (content);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::read (std::vector <std::string>& lines)
+{
+  pimpl->read (lines);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::append (const std::string& content)
+{
+  pimpl->append (content);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::write_raw (const std::string& content)
+{
+  pimpl->write_raw (content);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::append (const std::string& path, const std::string& data)
+{
+  return AtomicFile(path).append (data);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::write (const std::string& path, const std::string& data)
+{
+  AtomicFile::write (Path (path), data);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::write (const Path& path, const std::string& data)
+{
+  AtomicFile file (path);
+  file.truncate ();
+  file.write_raw (data);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::write (const Path& path, const std::vector <std::string>& lines)
+{
+  AtomicFile file (path);
+  file.truncate ();
+  for (const auto& line : lines)
+  {
+    file.append (line + '\n');
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::read (const Path& path, std::string& content)
+{
+  AtomicFile file (path);
+  file.read (content);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void AtomicFile::read (const std::string& path, std::string& content)
+{
+  AtomicFile::read (Path (path), content);
+}
+
+void AtomicFile::read (const Path& path, std::vector <std::string>& lines)
+{
+  AtomicFile (path).read (lines);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// finalize_all - Close / Flush all temporary files and rename to final.
+void AtomicFile::finalize_all ()
+{
+  if (!impl::allow_atomics)
+  {
+    throw std::string {"Unable to update database."};
+  }
+
+  // Step 1: Close / Flush all the atomic files that may still be open. If any
+  // of the files fail this step (close () will throw) then we do not want to
+  // move on to step 2
+  for (auto& file : impl::atomic_files)
+  {
+    file->close ();
+  }
+
+  atomic_files_t new_atomic_files;
+
+  // Step 2: Rename the temp files to the *real* files
+  for (auto& file : impl::atomic_files)
+  {
+    file->finalize ();
+
+    // Delete entry if we are holding the last reference
+    if (file.use_count () > 1)
+    {
+      new_atomic_files.push_back(file);
+    }
+  }
+
+  new_atomic_files.swap(impl::atomic_files);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// reset - Removes all current atomic files from finalization
+void AtomicFile::reset ()
+{
+  impl::atomic_files.clear ();
+  impl::allow_atomics = true;
+}
diff --git a/src/AtomicFile.h b/src/AtomicFile.h
new file mode 100644
--- /dev/null
+++ b/src/AtomicFile.h
@@ -0,0 +1,78 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2020, Shaun Ruffell, Thomas Lauf.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// https://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+#ifndef INCLUDED_ATOMICFILE
+#define INCLUDED_ATOMICFILE
+
+#include <memory>
+#include <vector>
+
+class Path;
+
+class AtomicFile
+{
+public:
+  AtomicFile (const Path& path);
+  AtomicFile (std::string path);
+  AtomicFile (const AtomicFile&) = delete;
+  AtomicFile (AtomicFile&&);
+  AtomicFile& operator= (const AtomicFile&) = delete;
+  AtomicFile& operator= (AtomicFile&&);
+  ~AtomicFile ();
+
+  std::string name () const;
+  const std::string& path () const;
+  bool exists () const;
+
+  bool open ();
+  void close ();
+  void truncate ();
+  void read (std::string& content);
+  void read (std::vector <std::string>& lines);
+  void append (const std::string& content);
+  void write_raw (const std::string& content);
+
+  static void append (const std::string& path, const std::string& data);
+  static void append (const Path& path, const std::string& data);
+
+  static void write (const std::string& path, const std::string& data);
+  static void write (const Path& path, const std::string& data);
+  static void write (const Path& path, const std::vector <std::string>& lines);
+
+  static void read (const std::string& path, std::string& content);
+  static void read (const Path& path, std::string& content);
+  static void read (const Path& path, std::vector <std::string>& lines);
+
+  static void finalize_all ();
+  static void reset ();
+
+public:
+  struct impl;
+
+private:
+  std::shared_ptr <impl> pimpl;
+};
+
+#endif // INCLUDED_ATOMICFILE
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -5,7 +5,8 @@ include_directories (${CMAKE_SOURCE_DIR}
                      ${CMAKE_SOURCE_DIR}/src/libshared/src
                      ${TIMEW_INCLUDE_DIRS})
 
-set (timew_SRCS CLI.cpp        CLI.h
+set (timew_SRCS AtomicFile.cpp AtomicFile.h
+                CLI.cpp        CLI.h
                 Chart.cpp      Chart.h
                                ChartConfig.h
                 Database.cpp   Database.h
diff --git a/src/Database.cpp b/src/Database.cpp
--- a/src/Database.cpp
+++ b/src/Database.cpp
@@ -33,6 +33,7 @@
 #include <iomanip>
 #include <shared.h>
 #include <timew.h>
+#include <AtomicFile.h>
 
 ////////////////////////////////////////////////////////////////////////////////
 Database::iterator::iterator (files_iterator fbegin, files_iterator fend) :
@@ -257,7 +258,10 @@ void Database::commit ()
     file.commit ();
   }
 
-  File::write (_location + "/tags.data", _tagInfoDatabase.toJson ());
+  if (_tagInfoDatabase.is_modified ())
+  {
+    AtomicFile::write (_location + "/tags.data", _tagInfoDatabase.toJson ());
+  }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -373,7 +377,7 @@ unsigned int Database::getDatafile (int year, int month)
        << std::setw (2) << std::setfill ('0') << month
        << ".data";
   auto name = file.str ();
-  auto basename = File (name).name ();
+  auto basename = Path (name).name ();
 
   // If the datafile is already initialized, return.
   for (unsigned int i = 0; i < _files.size (); ++i)
@@ -455,46 +459,75 @@ bool Database::empty ()
 ////////////////////////////////////////////////////////////////////////////////
 void Database::initializeTagDatabase ()
 {
+  _tagInfoDatabase = TagInfoDatabase ();
+  Path tags_path (_location + "/tags.data");
   std::string content;
+  const bool exists = tags_path.exists ();
 
-  if (!File::read (_location + "/tags.data", content))
+  if (exists && File::read (tags_path, content))
   {
-    auto it = Database::begin ();
-    auto end = Database::end ();
-    
-    if (it == end)
+    try
     {
-      return;
-    }
+      json::object *json = dynamic_cast <json::object *>(json::parse (content));
 
-    std::cout << "Tag info database does not exist. Recreating from interval data..." << std::endl  ;
+      if (content.empty () || (json == nullptr))
+      {
+          throw std::string ("Contents invalid.");
+      }
 
-    for (; it != end; ++it)
-    {
-      Interval interval = IntervalFactory::fromSerialization (*it);
-      for (auto& tag : interval.tags ())
+      for (auto &pair : json->_data)
       {
-        _tagInfoDatabase.incrementTag (tag);
+        auto key = str_replace (pair.first, "\\\"", "\"");
+        auto *value = (json::object *) pair.second;
+        auto iter = value->_data.find ("count");
+
+        if (iter == value->_data.end ())
+        {
+          throw format ("Failed to find \"count\" member for tag \"{1}\" in tags database.", key);
+        }
+
+        auto number = dynamic_cast<json::number *> (iter->second);
+        _tagInfoDatabase.add (key, TagInfo{(unsigned int) number->_dvalue});
       }
+
+      // Since we just loaded the database from the file, there we can clear the
+      // modified state so that we will not write it back out unless there is a
+      // new change.
+      _tagInfoDatabase.clear_modified ();
+
+      return;
+    }
+    catch (const std::string& error)
+    {
+      std::cerr << "Error parsing tags database: " << error << '\n';
     }
   }
-  else
-  {
-    auto *json = (json::object *) json::parse (content);
 
-    for (auto &pair : json->_data)
-    {
-      auto key = str_replace (pair.first, "\\\"", "\"");
-      auto *value = (json::object *) pair.second;
-      auto iter = value->_data.find ("count");
+  // We always want the tag database file to exists.
+  _tagInfoDatabase = TagInfoDatabase();
+  AtomicFile::write (_location + "/tags.data", _tagInfoDatabase.toJson ());
 
-      if (iter == value->_data.end ())
-      {
-        throw format ("Failed to find \"count\" member for tag \"{1}\" in tags database. Database corrupted?", key);
-      }
+  auto it = Database::begin ();
+  auto end = Database::end ();
+  
+  if (it == end)
+  {
+    return;
+  }
+
+  if (!exists)
+  {
+    std::cout << "Tags database does not exist. ";
+  }
+  
+  std::cout << "Recreating from interval data..." << std::endl;
 
-      auto number = dynamic_cast<json::number *> (iter->second);
-      _tagInfoDatabase.add (key, TagInfo{(unsigned int) number->_dvalue});
+  for (; it != end; ++it)
+  {
+    Interval interval = IntervalFactory::fromSerialization (*it);
+    for (auto& tag : interval.tags ())
+    {
+      _tagInfoDatabase.incrementTag (tag);
     }
   }
 }
@@ -514,7 +547,7 @@ void Database::initializeDatafiles ()
     if (file[file.length () - 8] == '-' &&
         file.find (".data") == file.length () - 5)
     {
-      auto basename = File (file).name ();
+      auto basename = Path (file).name ();
       auto year  = strtol (basename.substr (0, 4).c_str (), NULL, 10);
       auto month = strtol (basename.substr (5, 2).c_str (), NULL, 10);
       getDatafile (year, month);
diff --git a/src/Datafile.cpp b/src/Datafile.cpp
--- a/src/Datafile.cpp
+++ b/src/Datafile.cpp
@@ -32,11 +32,12 @@
 #include <sstream>
 #include <cassert>
 #include <stdlib.h>
+#include <AtomicFile.h>
 
 ////////////////////////////////////////////////////////////////////////////////
 void Datafile::initialize (const std::string& name)
 {
-  _file = File (name);
+  _file = Path (name);
 
   // From the name, which is of the form YYYY-MM.data, extract the YYYY and MM.
   auto basename = _file.name ();
@@ -129,24 +130,25 @@ void Datafile::commit ()
   // The _dirty flag indicates that the file needs to be written.
   if (_dirty)
   {
-    if (_file.open ())
+    AtomicFile file (_file);
+    if (file.open ())
     {
-      _file.lock ();
-      _file.truncate ();
-      _file.append (std::string (""));   // Seek to EOF.
-
       // Sort the intervals by ascending start time.
       std::sort (_lines.begin (), _lines.end ());
 
       // Write out all the lines.
+      file.truncate ();
       for (auto& line : _lines)
-        _file.write_raw (line + '\n');
+      {
+        file.write_raw (line + '\n');
+      }
 
-      _file.close ();
       _dirty = false;
     }
     else
+    {
       throw format ("Could not write to data file {1}", _file._data);
+    }
   }
 }
 
@@ -168,21 +170,20 @@ std::string Datafile::dump () const
 ////////////////////////////////////////////////////////////////////////////////
 void Datafile::load_lines ()
 {
-  if (_file.open ())
+  AtomicFile file (_file);
+  if (file.open ())
   {
-    _file.lock ();
-
     // Load the data.
     std::vector <std::string> read_lines;
-    _file.read (read_lines);
-    _file.close ();
+    file.read (read_lines);
+    file.close ();
 
     // Append the lines that were read.
     for (auto& line : read_lines)
       _lines.push_back (line);
 
     _lines_loaded = true;
-    debug (format ("{1}: {2} intervals", _file.name (), read_lines.size ()));
+    debug (format ("{1}: {2} intervals", file.name (), read_lines.size ()));
   }
 }
 
diff --git a/src/Datafile.h b/src/Datafile.h
--- a/src/Datafile.h
+++ b/src/Datafile.h
@@ -53,7 +53,7 @@ class Datafile
   void load_lines ();
 
 private:
-  File                      _file         {};
+  Path                      _file         {};
   bool                      _dirty        {false};
   std::vector <std::string> _lines        {};
   bool                      _lines_loaded {false};
diff --git a/src/Journal.cpp b/src/Journal.cpp
--- a/src/Journal.cpp
+++ b/src/Journal.cpp
@@ -24,7 +24,7 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-#include <FS.h>
+#include <AtomicFile.h>
 #include <format.h>
 #include <Journal.h>
 #include <TransactionsFactory.h>
@@ -54,19 +54,9 @@ void Journal::endTransaction ()
     throw "Call to end non-existent transaction";
   }
 
-  File undo (_location);
+  AtomicFile::append (_location, _currentTransaction->toString ());
 
-  if (undo.open ())
-  {
-    undo.append (_currentTransaction->toString());
-
-    undo.close ();
-    _currentTransaction.reset ();
-  }
-  else
-  {
-    throw format ("Unable to write the undo transaction to {1}", undo._data);
-  }
+  _currentTransaction.reset ();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -102,7 +92,7 @@ void Journal::recordUndoAction (
 ////////////////////////////////////////////////////////////////////////////////
 Transaction Journal::popLastTransaction ()
 {
-  File undo (_location);
+  AtomicFile undo (_location);
 
   std::vector <std::string> read_lines;
   undo.read (read_lines);
@@ -125,8 +115,8 @@ Transaction Journal::popLastTransaction ()
   Transaction last = transactions.back ();
   transactions.pop_back ();
 
-  File::remove (undo._data);
   undo.open ();
+  undo.truncate ();
 
   for (auto& transaction : transactions)
   {
diff --git a/src/Journal.h b/src/Journal.h
--- a/src/Journal.h
+++ b/src/Journal.h
@@ -37,6 +37,8 @@ class Journal
 {
 public:
   Journal() = default;
+  Journal(const Journal&) = delete;
+  Journal& operator= (const Journal&) = delete;
 
   void initialize(const std::string&);
 
diff --git a/src/Rules.cpp b/src/Rules.cpp
--- a/src/Rules.cpp
+++ b/src/Rules.cpp
@@ -34,6 +34,7 @@
 #include <cassert>
 #include <cerrno>
 #include <inttypes.h>
+#include <AtomicFile.h>
 #include <JSON.h>
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -111,7 +112,7 @@ void Rules::load (const std::string& file, int nest /* = 1 */)
 
   if (nest == 1)
   {
-    File originalFile (file);
+    Path originalFile (file);
     _original_file = originalFile._data;
 
     if (! originalFile.exists ())
@@ -123,8 +124,17 @@ void Rules::load (const std::string& file, int nest /* = 1 */)
 
   // Read the file, then parse the contents.
   std::string contents;
-  if (File::read (file, contents) && contents.length ())
-    parse (contents, nest);
+  try
+  {
+    AtomicFile::read (file, contents);
+    if (contents.length ())
+    {
+      parse (contents, nest);
+    }
+  }
+  catch (...)
+  {
+  }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -624,7 +634,9 @@ bool Rules::setConfigVariable (
   }
 
   if (change)
-    File::write (rules.file (), lines);
+  {
+    AtomicFile::write (rules.file (), lines);
+  }
 
   return change;
 }
@@ -648,7 +660,7 @@ int Rules::unsetConfigVariable (
 
   // Read config file as lines of text.
   std::vector <std::string> lines;
-  File::read (rules.file (), lines);
+  AtomicFile::read (rules.file (), lines);
 
   // If there is a non-comment line containing the entry in flattened form:
   //   a.b.c = value
@@ -703,7 +715,9 @@ int Rules::unsetConfigVariable (
   }
 
   if (change)
-    File::write (rules.file (), lines);
+  {
+    AtomicFile::write (rules.file (), lines);
+  }
 
   if (change && found)
     return 0;
diff --git a/src/TagInfoDatabase.cpp b/src/TagInfoDatabase.cpp
--- a/src/TagInfoDatabase.cpp
+++ b/src/TagInfoDatabase.cpp
@@ -46,6 +46,7 @@ int TagInfoDatabase::incrementTag (const std::string& tag)
     return -1;
   }
 
+  _is_modified = true;
   return search->second.increment ();
 }
 
@@ -63,6 +64,7 @@ int TagInfoDatabase::decrementTag (const std::string& tag)
     throw format ("Trying to decrement non-existent tag '{1}'", tag);
   }
 
+  _is_modified = true;
   return search->second.decrement ();
 }
 
@@ -71,6 +73,7 @@ int TagInfoDatabase::decrementTag (const std::string& tag)
 //
 void TagInfoDatabase::add (const std::string& tag, const TagInfo& tagInfo)
 {
+  _is_modified = true;
   _tagInformation.emplace (tag, tagInfo);
 }
 
@@ -89,6 +92,16 @@ std::set <std::string> TagInfoDatabase::tags () const
   return tags;
 }
 
+bool TagInfoDatabase::is_modified () const
+{
+  return _is_modified;
+}
+
+void TagInfoDatabase::clear_modified ()
+{
+  _is_modified = false;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 std::string TagInfoDatabase::toJson ()
 {
diff --git a/src/TagInfoDatabase.h b/src/TagInfoDatabase.h
--- a/src/TagInfoDatabase.h
+++ b/src/TagInfoDatabase.h
@@ -44,8 +44,12 @@ class TagInfoDatabase
 
   std::string toJson ();
 
+  bool is_modified () const;
+  void clear_modified ();
+
 private:
   std::map <std::string, TagInfo> _tagInformation {};
+  bool _is_modified {false};
 };
 
 #endif
diff --git a/src/commands/CmdHelp.cpp b/src/commands/CmdHelp.cpp
--- a/src/commands/CmdHelp.cpp
+++ b/src/commands/CmdHelp.cpp
@@ -28,6 +28,7 @@
 #include <commands.h>
 #include <iostream>
 #include <stdio.h>
+#include <FS.h>
 
 ////////////////////////////////////////////////////////////////////////////////
 int CmdHelpUsage (const Extensions& extensions)
@@ -71,7 +72,9 @@ int CmdHelpUsage (const Extensions& extensions)
     std::cout << "Extensions (extensions do not provide help):\n";
 
     for (auto& ext : extensions.all ())
-      std::cout << "       " << File (ext).name () << '\n';
+    {
+      std::cout << "       " << Path (ext).name () << '\n';
+    }
 
     std::cout << '\n';
   }
diff --git a/src/commands/CmdReport.cpp b/src/commands/CmdReport.cpp
--- a/src/commands/CmdReport.cpp
+++ b/src/commands/CmdReport.cpp
@@ -31,6 +31,7 @@
 #include <timew.h>
 #include <iostream>
 #include <sstream>
+#include <FS.h>
 
 ////////////////////////////////////////////////////////////////////////////////
 // Given a partial match for an extension script name, find the full patch of
@@ -42,23 +43,33 @@ static std::string findExtension (
   auto scripts = extensions.all ();
   std::vector <std::string> options;
   for (auto& script : scripts)
-    options.push_back (File (script).name ());
+  {
+    options.push_back (Path (script).name ());
+  }
 
   std::vector <std::string> matches;
   autoComplete (partial, options, matches);
 
   if (matches.empty ())
+  {
     throw format ("The report '{1}' is not recognized.", partial);
+  }
 
   if (matches.size () > 1)
+  {
     throw format ("The report '{1}' is ambiguous, and could mean one of: {2}",
                   partial,
                   join (", ", matches));
+  }
 
   // Now map matches[0] back to it's corresponding option.
   for (auto& script : scripts)
-    if (File (script).name () == matches[0])
+  {
+    if (Path (script).name () == matches[0])
+    {
       return script;
+    }
+  }
 
   return "";
 }
diff --git a/src/init.cpp b/src/init.cpp
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -185,9 +185,12 @@ void initializeDataJournalAndRules (
     data.create (0700);
 
   // Load the configuration data.
-  File configFile (dbLocation);
+  Path configFile (dbLocation);
   configFile += "timewarrior.cfg";
-  configFile.create (0600);
+  if (! configFile.exists ())
+  {
+    File (configFile).create (0600);
+  }
   rules.load (configFile._data);
 
   // This value is not written out to disk, as there would be no point. Having
diff --git a/src/timew.cpp b/src/timew.cpp
--- a/src/timew.cpp
+++ b/src/timew.cpp
@@ -25,6 +25,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 #include <cmake.h>
+#include <AtomicFile.h>
 #include <CLI.h>
 #include <Database.h>
 #include <Rules.h>
@@ -96,6 +97,8 @@ int main (int argc, const char** argv)
 
     // Save any outstanding changes.
     database.commit ();
+
+    AtomicFile::finalize_all ();
   }
 
   catch (const std::string& error)


Test patch:
diff --git a/test/.gitignore b/test/.gitignore
--- a/test/.gitignore
+++ b/test/.gitignore
@@ -1,4 +1,5 @@
 all.log
+atomic
 data.t
 exclusion.t
 helper.t
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -8,6 +8,24 @@ if(POLICY CMP0037 AND ${CMAKE_VERSION} VERSION_LESS "3.11.0")
   cmake_policy(SET CMP0037 OLD)
 endif()
 
+# If this is a debug build, check if we have libfiu installed and available on
+# the system. If so, we will be able to use it to add additional tests of the
+# failure conditions
+
+if (uppercase_CMAKE_BUILD_TYPE MATCHES "DEBUG")
+  find_library(FIU_ENABLE fiu) 
+  if (FIU_ENABLE)
+    message (STATUS "libfiu found")
+    add_definitions (-DFIU_ENABLE)
+    set (test_LIBS fiu ${TIMEW_LIBRARIES})
+  else (FIU_ENABLE)
+    message (STATUS "NOTE: install libfiu to run additional tests")
+    set (test_LIBS ${TIMEW_LIBRARIES})
+  endif (FIU_ENABLE)
+else (uppercase_CMAKE_BUILD_TYPE MATCHES "DEBUG")
+  set (test_LIBS ${TIMEW_LIBRARIES})
+endif (uppercase_CMAKE_BUILD_TYPE MATCHES "DEBUG")
+
 include_directories (${CMAKE_SOURCE_DIR}
                      ${CMAKE_SOURCE_DIR}/src
                      ${CMAKE_SOURCE_DIR}/src/libshared/src
@@ -16,7 +34,7 @@ include_directories (${CMAKE_SOURCE_DIR}
 include_directories (${CMAKE_INSTALL_PREFIX}/include)
 link_directories(${CMAKE_INSTALL_PREFIX}/lib)
 
-set (test_SRCS data.t exclusion.t helper.t interval.t range.t rules.t util.t TagInfoDatabase.t)
+set (test_SRCS atomic data.t exclusion.t helper.t interval.t range.t rules.t util.t TagInfoDatabase.t)
 
 add_custom_target (test ./run_all --verbose
                         DEPENDS ${test_SRCS}
@@ -24,7 +42,7 @@ add_custom_target (test ./run_all --verbose
 
 foreach (src_FILE ${test_SRCS})
   add_executable (${src_FILE} "${src_FILE}.cpp" test.cpp)
-  target_link_libraries (${src_FILE} timew libshared ${TIMEW_LIBRARIES})
+  target_link_libraries (${src_FILE} timew libshared ${test_LIBS})
 endforeach (src_FILE)
 
 configure_file(run_all run_all COPYONLY)
diff --git a/test/atomic.cpp b/test/atomic.cpp
new file mode 100644
--- /dev/null
+++ b/test/atomic.cpp
@@ -0,0 +1,483 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2020, Shaun Ruffell, Thomas Lauf.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// https://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <sstream>
+#include <memory>
+#include <string.h>
+#include <unistd.h>
+#include <iostream>
+#include <cstdlib>
+#include <fstream>
+#include <cmake.h>
+#include <timew.h>
+#include <test.h>
+#include <AtomicFile.h>
+#include <FS.h>
+
+#ifdef FIU_ENABLE
+
+#include <fiu.h>
+#include <fiu-control.h>
+
+#else
+
+#define fiu_init(flags) 0
+#define fiu_fail(name) 0
+#define fiu_failinfo() NULL
+#define fiu_do_on(name, action)
+#define fiu_exit_on(name)
+#define fiu_return_on(name, retval)
+
+#endif /* FIU_ENABLE */
+
+////////////////////////////////////////////////////////////////////////////////
+class TempDir
+{
+public:
+  TempDir ();
+  ~TempDir ();
+
+  bool is_empty () const;
+  void clear ();
+  std::vector <std::string> file_names () const;
+
+private:
+  std::string tmpName {};
+  std::string oldDir {};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+TempDir::TempDir ()
+{
+  oldDir = Directory::cwd ();
+
+  char template_name[] = "atomic_XXXXXX";
+  const char *cwd = ::mkdtemp (template_name);
+  if (cwd == nullptr)
+  {
+    throw std::string ("Failed to create temp directory");
+  }
+
+  tmpName = cwd;
+
+  if (::chdir (tmpName.c_str ()))
+  {
+    throw std::string ("Failed to change to temporary directory");
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+TempDir::~TempDir ()
+{
+  try
+  {
+    clear ();
+    ::chdir (oldDir.c_str ());
+
+    if (!Directory(tmpName).remove ())
+    {
+      std::cerr << "Failed to remove temp dir " << tmpName << '\n';
+    }
+  }
+  catch (...)
+  {
+    std::cerr << "Unhandled exception in " << __func__ << '\n';
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool TempDir::is_empty () const
+{
+  return file_names().empty ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::vector <std::string> TempDir::file_names () const
+{
+  return Path (tmpName).glob ("*");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void TempDir::clear ()
+{
+  for (const auto& file : file_names ())
+  {
+    File::remove (file);
+  }
+}
+
+#ifdef FIU_ENABLE
+
+// This class is a helper to make sure we turn off libfiu after the test so that
+// we can properly write to stdout / stderr.
+class FIU
+{
+public:
+  FIU()
+  {
+    enable();
+  }
+
+  FIU(const FIU&) = delete;
+  FIU& operator= (const FIU&) = delete;
+
+  ~FIU()
+  {
+    disable ();
+    std::cout << cbuffer.str();
+    std::stringstream().swap(cbuffer);
+  }
+
+  void enable ()
+  {
+    for (auto test_point : test_points)
+    {
+      fiu_enable_external(test_point, 1, NULL, 0, fiu_cb);
+    }
+  }
+
+  void disable ()
+  {
+    for (auto test_point : test_points)
+    {
+      fiu_disable(test_point);
+    }
+  }
+
+private:
+  static const std::vector <const char *> test_points;
+  static std::stringstream cbuffer;
+
+  static int external_cb_was_called;
+
+  static int fiu_cb(const char *name, int *failnum,
+                    void **failinfo, unsigned int *flags)
+  {
+    (void)name;
+    (void)flags;
+    external_cb_was_called++;
+
+    // For debugging the tests themselves...
+    // cbuffer << "fiu_cb called for " << name << '\n';
+
+    *failinfo = (void *) EIO;
+
+    return *failnum;
+  }
+};
+
+const std::vector <const char *> FIU::test_points {
+  "posix/stdio/gp/fputs",
+  "posix/io/rw/write",
+};
+
+std::stringstream FIU::cbuffer;
+int FIU::external_cb_was_called = 0;
+
+//////////////////////////////////////////////////////////////////////////////
+// Since AtomicFile is primarily for keeping the database consistent in the
+// presence of filesystem errors, these tests use libfiu to ensure that
+// AtomicFile behaves as intended when the underlying system calls fail.
+int fiu_test (UnitTest& t)
+{
+  TempDir tempDir;
+  fiu_init (0);
+
+  try { FIU fiu; AtomicFile ("test.txt").write_raw("This is  test"); t.fail ("AtomicFile::write_raw throws on error"); }
+  catch (...) {                                                      t.pass ("AtomicFile::write_raw throws on error"); }  
+
+  try { FIU fiu; AtomicFile::finalize_all ();                        t.fail ("AtomicFile::finalize_all() throws on error"); }
+  catch (...) {                                                      t.pass ("AtomicFile::finalize_all() throws on error"); }
+
+  try { FIU fiu; AtomicFile::reset (); AtomicFile::finalize_all ();  t.pass ("AtomicFile::reset clears failure state"); }
+  catch (...) {                                                      t.fail ("AtomicFile::reset clears failure state"); }
+
+  File::write ("test.txt", "line1\n");
+  {
+    AtomicFile file ("test.txt");
+    try { FIU fiu; file.append ("append1\n");                        t.fail ("AtomicFile::append throws on error"); }
+    catch (...) {                                                    t.pass ("AtomicFile::append throws on error"); }
+
+    std::string contents {"should-not-see-this"};
+    file.read (contents);
+    t.is (contents, "", "AtomicFile::append did not partially fill the file.");
+  }
+
+  try { FIU fiu; AtomicFile::finalize_all ();                       t.fail ("AtomicFile::append failures prevent finalization"); }
+  catch (...) {                                                     t.pass ("AtomicFile::append failures prevent finalization"); }
+
+  return 0;
+}
+#else
+int fiu_test (UnitTest& t)
+{
+  t.skip ("AtomicFile::write_raw throws on error");
+  t.skip ("AtomicFile::finalize_all() throws on error");
+  t.skip ("AtomicFile::reset clears failure state");
+  t.skip ("AtomicFile::append throws on error");
+  t.skip ("AtomicFile::append did not partially fill the file.");
+  t.skip ("AtomicFile::append failures prevent finalization");
+  return 0;
+}
+#endif // FIU_ENABLE
+
+//////////////////////////////////////////////////////////////////////////////
+int test (UnitTest& t)
+{
+  std::string test_name;
+
+  // This will create and change to a temporary directory that will be cleaned
+  // up when the destructor is run
+  TempDir tempDir;
+
+  std::string goldenText = "1\n";
+  std::string contents;
+  std::string expected;
+
+  Path firstFilename ("test-1.txt");
+  Path secondFilename ("test-2.txt");
+
+  {
+    AtomicFile file (firstFilename);
+    file.write_raw (goldenText);
+    file.close ();
+  }
+
+  t.is (firstFilename.exists (), false, "Shall not exists before finalize");
+  AtomicFile::finalize_all ();
+  t.is (firstFilename.exists (), true, "Shall exists after finalize");
+  File::read(firstFilename, contents);
+  t.is (contents == goldenText, true, "Shall have the correct data");
+
+  tempDir.clear ();
+
+  {
+    AtomicFile first (firstFilename);
+    AtomicFile second (secondFilename);
+
+    first.write_raw ("first\n");
+    second.write_raw ("second\n");
+    first.close ();
+    second.close ();
+  }
+
+  t.is (firstFilename.exists () || secondFilename.exists (), false, "Neither shall exist before finalize");
+
+  AtomicFile::finalize_all ();
+
+  t.is (firstFilename.exists () && secondFilename.exists (), true, "Both shall exists after finalize");
+
+  File::read(firstFilename, contents);
+  test_name = "First file shall contain the correct data";
+  expected = "first\n";
+  if (contents == expected)
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
+  }
+
+  File::read(secondFilename, contents);
+  test_name = "Second file shall contain the correct data";
+  if (contents == std::string("second\n"))
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected 'second\n' read '" + contents + "'"));
+  }
+
+  // Make sure appending works
+
+  test_name = "Appending does not update original before finalize";
+  expected = "first\n";
+
+  {
+    AtomicFile first (firstFilename);
+
+    first.append("append 1\n");
+    first.append("append 2\n");
+    first.close ();
+  }
+
+  File::read (firstFilename, contents);
+  if (contents == expected)
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
+  }
+
+  test_name = "Finalizing updates the appended data";
+  expected = "first\nappend 1\nappend 2\n";
+  AtomicFile::finalize_all ();
+  File::read (firstFilename, contents);
+  if (contents == expected)
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
+  }
+
+  test_name = "Read from Atomicfile";
+  // We do not want to update the expected
+  {
+    AtomicFile::read (firstFilename, contents);
+  }
+
+  if (contents == expected)
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
+  }
+
+  // If we read from an atomic file before finalizing, we should get the data
+  // that was written to the temporary file and not the 'real' file.
+
+  test_name = "Read from Atomicfile should read unfinalized data";
+  expected += "expected\n";
+  AtomicFile::write (firstFilename, expected);
+  AtomicFile::read (firstFilename, contents);
+  if (contents == expected)
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
+  }
+
+  AtomicFile::finalize_all ();
+  {
+    AtomicFile first (firstFilename);
+    AtomicFile second (firstFilename);
+
+    first.write_raw("first\n");
+    second.append ("second\n");
+
+    AtomicFile::finalize_all ();
+
+    first.append ("third\n");
+    second.append ("fourth\n");
+    first.append ("fifth\n");
+
+  }
+
+  test_name = "Two AtomicFiles should access same temp file (part 1)";
+
+  // The atomic files, which were closed above should return all the strings
+  // written since it was not yet finalized.
+
+  expected = "first\nsecond\nthird\nfourth\nfifth\n";
+  AtomicFile::read (firstFilename, contents);
+  if (contents == expected)
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
+  }
+
+  // But since the file was not yet finalized, the "real" file should only
+  // contain the data present since before the finalize_all () call.
+
+  test_name = "Two AtomicFiles should access same temp file (part 2)";
+  expected = "first\nsecond\n";
+  File::read (firstFilename, contents);
+  if (contents == expected)
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
+  }
+
+  // After we finalize the data, the AtomicFile and the File should now both
+  // return the same information
+
+  test_name = "Two AtomicFiles should access same temp file (part 3)";
+  AtomicFile::finalize_all ();
+  File::read (firstFilename, expected);
+  AtomicFile::read (firstFilename, contents);
+  if (contents == expected)
+  {
+    t.pass (test_name);
+  }
+  else
+  {
+    t.fail (test_name);
+    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
+  }
+
+  {
+    AtomicFile test ("test");
+  }
+
+  AtomicFile::reset ();
+  return 0;
+}
+
+int main (int, char**)
+{
+  UnitTest t (20);
+  try
+  {
+    int ret = test (t);
+    int fiu_ret = fiu_test (t);
+    return (ret == 0) ? fiu_ret : ret;
+  }
+  catch (const std::string& error)
+  {
+    std::cerr << "Test threw exception: " << error << '\n';
+  }
+  catch (...)
+  {
+    std::cerr << "Uncaught exception.\n";
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
diff --git a/test/atomic.t b/test/atomic.t
new file mode 100755
--- /dev/null
+++ b/test/atomic.t
@@ -0,0 +1,7 @@
+#!/bin/sh
+BASEDIR=$(dirname "$0")
+if ldd ${BASEDIR}/atomic | grep -q 'libfiu' ; then
+    exec fiu-run -x ${BASEDIR}/atomic
+else
+    exec ${BASEDIR}/atomic
+fi
diff --git a/test/docker/archlinux b/test/docker/archlinux
--- a/test/docker/archlinux
+++ b/test/docker/archlinux
@@ -29,4 +29,4 @@ RUN make install
 # Setup tests
 RUN make -j2 test || true
 WORKDIR /root/code/test/
-CMD ["bash", "-c", "./problems --summary ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./problems --summary ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version; exit $FAILED"]
diff --git a/test/docker/centos7 b/test/docker/centos7
--- a/test/docker/centos7
+++ b/test/docker/centos7
@@ -37,4 +37,4 @@ RUN make install
 RUN make test -j2 || true
 WORKDIR /root/code/test/
 
-CMD ["bash", "-c", "cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version ; exit $FAILED"]
diff --git a/test/docker/debianstable b/test/docker/debianstable
--- a/test/docker/debianstable
+++ b/test/docker/debianstable
@@ -37,4 +37,4 @@ RUN make install
 WORKDIR /root/code/test/
 RUN make
 
-CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version ; exit $FAILED"]
diff --git a/test/docker/debiantesting b/test/docker/debiantesting
--- a/test/docker/debiantesting
+++ b/test/docker/debiantesting
@@ -2,16 +2,18 @@ FROM debian:testing
 
 ENV DEBIAN_FRONTEND noninteractive
 
-RUN apt-get update
-RUN apt-get install -y \
+RUN apt-get update && apt-get install -y \
             cmake \
+            fiu-utils=1.00-* \
             g++ \
             git \
+            libfiu-dev=1.00-* \
             locales \
             man \
             python3 \
             python3-dateutil \
-            tzdata
+            tzdata \
+    && rm -fr /var/lib/apt/lists/*
 
 # Setup environment
 RUN update-alternatives --install /usr/bin/python python /usr/bin/python3 10
@@ -37,4 +39,4 @@ RUN make install
 WORKDIR /root/code/test/
 RUN make
 
-CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version ; exit $FAILED"]
diff --git a/test/docker/fedora28 b/test/docker/fedora28
--- a/test/docker/fedora28
+++ b/test/docker/fedora28
@@ -31,4 +31,4 @@ RUN make install
 # Setup tests
 RUN make -j2 test || true
 WORKDIR /root/code/test/
-CMD ["bash", "-c", "./problems --summary ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./problems --summary ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version; exit $FAILED"]
diff --git a/test/docker/fedora29 b/test/docker/fedora29
--- a/test/docker/fedora29
+++ b/test/docker/fedora29
@@ -31,4 +31,4 @@ RUN make install
 # Setup tests
 RUN make -j2 test || true
 WORKDIR /root/code/test/
-CMD ["bash", "-c", "./problems --summary ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./problems --summary ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version; exit $FAILED"]
diff --git a/test/docker/gentoo b/test/docker/gentoo
--- a/test/docker/gentoo
+++ b/test/docker/gentoo
@@ -34,4 +34,4 @@ RUN make install
 # Setup tests
 RUN make -j2 test || true
 WORKDIR /root/code/test
-CMD ["bash", "-c", "./problems --summary ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./problems --summary ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version; exit $FAILED"]
diff --git a/test/docker/opensuse1500 b/test/docker/opensuse1500
--- a/test/docker/opensuse1500
+++ b/test/docker/opensuse1500
@@ -32,4 +32,4 @@ RUN make install
 WORKDIR /root/code/test/
 RUN make
 
-CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version; exit $FAILED"]
diff --git a/test/docker/ubuntu1604 b/test/docker/ubuntu1604
--- a/test/docker/ubuntu1604
+++ b/test/docker/ubuntu1604
@@ -37,4 +37,4 @@ RUN make install
 WORKDIR /root/code/test/
 RUN make
 
-CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version ; exit $FAILED"]
diff --git a/test/docker/ubuntu1804 b/test/docker/ubuntu1804
--- a/test/docker/ubuntu1804
+++ b/test/docker/ubuntu1804
@@ -37,4 +37,4 @@ RUN make install
 WORKDIR /root/code/test/
 RUN make
 
-CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version"]
+CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems ; FAILED=$? ; echo timew $( timew --version ) ; python --version ; cmake --version ; gcc --version ; exit $FAILED"]
diff --git a/test/write-failure.t b/test/write-failure.t
new file mode 100755
--- /dev/null
+++ b/test/write-failure.t
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+# write-failure-test.py - Make sure the test will fail t
+
+import sys
+import os
+
+import atexit
+import filecmp
+import shutil
+import tempfile
+import subprocess
+import unittest
+
+from basetest import Timew, TestCase
+from basetest.utils import run_cmd_wait_nofail
+
+# Ensure python finds the local simpletap module
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+
+class WrappedTimew(Timew):
+    def __init__(self):
+
+        try:
+            self._fiu_path = subprocess.check_output("command -v fiu-run", shell=True).strip()
+        except:
+            raise unittest.SkipTest("fiu-run is not installed")
+
+        super().__init__()
+
+    def __call__(self, *args, **kwargs):
+        """aka t = Timew() ; t() which is now an alias to t.runSuccess()"""
+        return self.run(*args, **kwargs)
+
+    def run(self, args="", input=None, merge_streams=False, timeout=5):
+        """Invoke timew with given arguments
+
+        If you wish to pass instructions to timew such as confirmations or other
+        input via stdin, you can do so by providing a input string.
+        Such as input="y\ny\n".
+
+        If merge_streams=True stdout and stderr will be merged into stdout.
+
+        timeout = number of seconds the test will wait for every timew call.
+        Defaults to 1 second if not specified. Unit is seconds.
+
+        Returns (exit_code, stdout, stderr) if merge_streams=False
+                (exit_code, output) if merge_streams=True
+        """
+
+        # This will run timewarrior with a random chance that fputs will return
+        # error 28, "No space left on device", in order to simulate running out
+        # of space on the drive the database is on. It is random in order to
+        # catch different places in the code that are writing as part of the
+        # update (tags, undo, datafiles, configs, etc..)
+        command  = [
+                    self._fiu_path, "-x",
+                    "-c", "enable_random name=posix/stdio/gp/fputs,probability=0.05,failinfo=28",
+                    "-c", "enable_random name=posix/io/rw/write,probability=0.05,failinfo=28",
+                    "-c", "enable_random name=posix/io/rw/pwrite,probability=0.05,failinfo=28",
+                    "-c", "enable_random name=posix/io/rw/writev,probability=0.05,failinfo=28",
+                    "-c", "enable_random name=posix/io/rw/pwritev,probability=0.05,failinfo=28",
+                   ]
+
+        command.extend(self._command[:])
+
+        args = self._split_string_args_if_string(args)
+        command.extend(args)
+
+        return run_cmd_wait_nofail(command, input,
+                                   merge_streams=merge_streams,
+                                   env=self.env,
+                                   timeout=timeout)
+
+
+class TestWriteFailure(TestCase):
+
+    def setUp(self):
+        self._goldendir = tempfile.mkdtemp(prefix="timew_golden_")
+        self.t = WrappedTimew()
+
+    def tearDown(self):
+        shutil.rmtree(self._goldendir)
+
+    def save_database(self):
+        shutil.rmtree(self._goldendir)
+        shutil.copytree(self.t.datadir, self._goldendir)
+
+    def compare_dirs(self, dir1, dir2):
+        compared = filecmp.dircmp(dir1, dir2)
+        if (compared.left_only or compared.right_only or
+            compared.diff_files or compared.funny_files):
+            return False
+
+        for subdir in compared.common_dirs:
+            if not self.compare_dirs(os.path.join(dir1, subdir),
+                                     os.path.join(dir2, subdir)):
+                return False
+
+        return True
+
+    def test_write_failures_do_not_corrupt_database(self):
+        """write failures shall not corrupt the database"""
+
+        # First prepopulate the database
+        start = 10
+        for x in range(start, 0, -1):
+            self.t.runSuccess("track tag{0} {1}min ago".format((start + 1)-x, x))
+
+        self.save_database()
+
+        # Now start in with the failures and make sure the database compares
+        # equally
+        TAG_COUNT = 30
+        error_count = 0
+        success_count = 0
+        MAX_ERROR_COUNT=250
+        for x in range(TAG_COUNT, 0, -1):
+            while True: 
+                exitcode, stdout, stderr = self.t("track tag-{0} {1}s ago".format((TAG_COUNT+1)-x, x))
+                if exitcode != 0:
+                    error_count += 1
+                    if error_count >= MAX_ERROR_COUNT:
+                        break
+                    
+                    if not self.compare_dirs(self.t.datadir, self._goldendir):
+                        filecmp.dircmp(self.t.datadir, self._goldendir).report_full_closure()
+                        self.fail("{0} not equal to {1}".format(self.t.datadir, self._goldendir))
+                else:
+                    success_count += 1
+                    break
+
+            if error_count >= MAX_ERROR_COUNT:
+                break
+
+            self.save_database()
+
+        self.t.runSuccess("export")
+        print("Test pass. Timew returned an error on {0} runs and none on {1} runs.".format(error_count, success_count))
+
+if __name__ == "__main__":
+    from simpletap import TAPTestRunner
+
+    unittest.main(testRunner=TAPTestRunner())
+    #unittest.main()