summaryrefslogtreecommitdiff
path: root/src/autosave-manager.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/autosave-manager.vala')
-rw-r--r--src/autosave-manager.vala553
1 files changed, 553 insertions, 0 deletions
diff --git a/src/autosave-manager.vala b/src/autosave-manager.vala
new file mode 100644
index 0000000..eb8f1c5
--- /dev/null
+++ b/src/autosave-manager.vala
@@ -0,0 +1,553 @@
+/*
+ * Copyright (C) 2011 Timo Kluck
+ * Author: Timo Kluck <tkluck@infty.nl>
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+/*
+ * We store autosaves in a database named
+ * ~/.cache/simple-scan/autosaves/autosaves.db
+ * It contains a single table of pages, each containing the process id (pid) of
+ * the simple-scan instance that saved it, and a hash of the Book and Page
+ * objects corresponding to it. The pixels are saved as a BLOB.
+ * Additionally, the autosaves directory contains a number of tiff files that
+ * the user can use for manual recovery.
+ *
+ * At startup, we check whether autosaves.db contains any records
+ * with a pid that does not match a current pid for simple-scan. If so, we take
+ * ownership by an UPDATE statement changing to our own pid. Then, we
+ * recover the book. We're trying our best to avoid the possible race
+ * condition if several instances of simple-scan are started simultaneously.
+ *
+ * At application exit, we delete the records corresponding to our own pid.
+ *
+ * Important notes:
+ * - We enforce that there is only one AutosaveManager instance in a given
+ * process by using a create function.
+ * - It should be possible to change the book object at runtime, although this
+ * is not used in the current implementation so it has not been tested.
+ */
+
+public class AutosaveManager
+{
+ private static string AUTOSAVE_DIR = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "autosaves");
+ private static string AUTOSAVE_NAME = "autosaves";
+ private static string AUTOSAVE_EXT = ".db";
+ private static string AUTOSAVE_FILENAME = Path.build_filename (AUTOSAVE_DIR, AUTOSAVE_NAME + AUTOSAVE_EXT);
+
+ private static string PID = ((int)(Posix.getpid ())).to_string ();
+ private static int number_of_instances = 0;
+
+ private Sqlite.Database database_connection;
+ private Book _book = null;
+
+ private uint update_timeout = 0;
+ private HashTable<Page, bool> dirty_pages;
+
+ public Book book
+ {
+ get
+ {
+ return _book;
+ }
+ set
+ {
+ if (_book != null)
+ {
+ for (var i = 0; i < _book.get_n_pages (); i++)
+ {
+ var page = _book.get_page (i);
+ on_page_removed (page);
+ }
+ _book.page_added.disconnect (on_page_added);
+ _book.page_removed.disconnect (on_page_removed);
+ _book.reordered.disconnect (on_reordered);
+ _book.cleared.disconnect (on_cleared);
+ }
+ _book = value;
+ _book.page_added.connect (on_page_added);
+ _book.page_removed.connect (on_page_removed);
+ _book.reordered.connect (on_reordered);
+ _book.cleared.connect (on_cleared);
+ for (var i = 0; i < _book.get_n_pages (); i++)
+ {
+ var page = book.get_page (i);
+ on_page_added (page);
+ }
+ }
+ }
+
+ public static AutosaveManager? create (ref Book book)
+ {
+ /* compare autosave directories with pids of current instances of simple-scan
+ * take ownership of one of the ones that are unowned by renaming to the
+ * own pid. Then open the database and fill the book with the pages it
+ * contains.
+ */
+ if (number_of_instances > 0)
+ assert_not_reached ();
+
+ var man = new AutosaveManager ();
+ number_of_instances++;
+
+ try
+ {
+ man.database_connection = open_database_connection ();
+ }
+ catch
+ {
+ warning ("Could not connect to the autosave database; no autosaves will be kept.");
+ return null;
+ }
+
+ bool any_pages_recovered = false;
+ try
+ {
+ // FIXME: this only works on linux. We can maybe use Gtk.Application and some session bus id instead?
+ string current_pids;
+ Process.spawn_command_line_sync ("pidof simple-scan | sed \"s/ /,/g\"", out current_pids);
+ current_pids = current_pids.strip ();
+ Sqlite.Statement stmt;
+ string query = @"
+ SELECT process_id, book_hash, book_revision FROM pages
+ WHERE NOT process_id IN ($current_pids)
+ LIMIT 1
+ ";
+
+ var result = man.database_connection.prepare_v2 (query, -1, out stmt);
+ if (result == Sqlite.OK)
+ {
+ while (stmt.step () == Sqlite.ROW)
+ {
+ debug ("Found at least one autosave page, taking ownership");
+ var unowned_pid = stmt.column_int (0);
+ var book_hash = stmt.column_int (1);
+ var book_revision = stmt.column_int (2);
+ /* there's a possible race condition here when several instances
+ * try to take ownership of the same rows. What would happen is
+ * that this operations would affect no rows if another process
+ * has taken ownership in the mean time. In that case, recover_book
+ * does nothing, so there should be no problem.
+ */
+ query = @"
+ UPDATE pages
+ SET process_id = $PID
+ WHERE process_id = ?2
+ AND book_hash = ?3
+ AND book_revision = ?4";
+ Sqlite.Statement stmt2;
+ result = man.database_connection.prepare_v2(query, -1, out stmt2);
+ if (result != Sqlite.OK)
+ warning (@"Error preparing statement: $query");
+
+ stmt2.bind_int64 (2, unowned_pid);
+ stmt2.bind_int64 (3, book_hash);
+ stmt2.bind_int64 (4, book_revision);
+ result = stmt2.step();
+ if (result == Sqlite.DONE)
+ {
+ any_pages_recovered = true;
+ man.recover_book (ref book);
+ }
+ else
+ warning ("Error %d while executing query", result);
+ }
+ }
+ else
+ warning ("Error %d while preparing statement", result);
+ }
+ catch (SpawnError e)
+ {
+ warning ("Could not obtain current process ids; not restoring any autosaves");
+ }
+
+ man.book = book;
+ if (!any_pages_recovered)
+ {
+ for (var i = 0; i < book.get_n_pages (); i++)
+ {
+ var page = book.get_page (i);
+ man.on_page_added (page);
+ }
+ }
+
+ return man;
+ }
+
+ private AutosaveManager ()
+ {
+ dirty_pages = new HashTable<Page, bool> (direct_hash, direct_equal);
+ }
+
+ public void cleanup ()
+ {
+ debug ("Clean exit; deleting autosave records");
+
+ if (update_timeout > 0)
+ Source.remove (update_timeout);
+ update_timeout = 0;
+
+ warn_if_fail (database_connection.exec (@"
+ DELETE FROM pages
+ WHERE process_id = $PID
+ ") == Sqlite.OK);
+ }
+
+ static Sqlite.Database open_database_connection () throws Error
+ {
+ var autosaves_dir = File.new_for_path (AUTOSAVE_DIR);
+ try
+ {
+ autosaves_dir.make_directory_with_parents ();
+ }
+ catch
+ { // the directory already exists
+ // pass
+ }
+ Sqlite.Database connection;
+ if (Sqlite.Database.open (AUTOSAVE_FILENAME, out connection) != Sqlite.OK)
+ throw new IOError.FAILED ("Could not connect to autosave database");
+ string query = @"
+ CREATE TABLE IF NOT EXISTS pages (
+ id integer PRIMARY KEY,
+ process_id integer,
+ page_hash integer,
+ book_hash integer,
+ book_revision integer,
+ page_number integer,
+ dpi integer,
+ width integer,
+ height integer,
+ depth integer,
+ n_channels integer,
+ rowstride integer,
+ color_profile string,
+ crop_x integer,
+ crop_y integer,
+ crop_width integer,
+ crop_height integer,
+ scan_direction integer,
+ pixels binary
+ )";
+ var result = connection.exec(query);
+ if (result != Sqlite.OK)
+ warning ("Error %d while executing query", result);
+ return connection;
+ }
+
+ void on_page_added (Page page)
+ {
+ insert_page (page);
+ // TODO: save a tiff file
+ page.size_changed.connect (on_page_changed);
+ page.scan_direction_changed.connect (on_page_changed);
+ page.crop_changed.connect (on_page_changed);
+ page.scan_finished.connect (on_page_changed);
+ }
+
+ public void on_page_removed (Page page)
+ {
+ page.pixels_changed.disconnect (on_page_changed);
+ page.size_changed.disconnect (on_page_changed);
+ page.scan_direction_changed.disconnect (on_page_changed);
+ page.crop_changed.disconnect (on_page_changed);
+ page.scan_finished.connect (on_page_changed);
+
+ string query = @"
+ DELETE FROM pages
+ WHERE process_id = $PID
+ AND page_hash = ?2
+ AND book_hash = ?3
+ AND book_revision = ?4
+ ";
+ Sqlite.Statement stmt;
+ var result = database_connection.prepare_v2 (query, -1, out stmt);
+ if (result != Sqlite.OK)
+ warning (@"Error $result while preparing query");
+ stmt.bind_int64 (2, direct_hash (page));
+ stmt.bind_int64 (3, direct_hash (book));
+ stmt.bind_int64 (4, cur_book_revision);
+
+ result = stmt.step();
+ if (result != Sqlite.DONE)
+ warning ("Error %d while executing query", result);
+ }
+
+ public void on_reordered ()
+ {
+ for (var i=0; i < book.get_n_pages (); i++)
+ {
+ var page = book.get_page (i);
+ string query = @"
+ UPDATE pages SET page_number = ?5
+ WHERE process_id = $PID
+ AND page_hash = ?2
+ AND book_hash = ?3
+ AND book_revision = ?4
+ ";
+ Sqlite.Statement stmt;
+ var result = database_connection.prepare_v2 (query, -1, out stmt);
+ if (result != Sqlite.OK)
+ warning (@"Error $result while preparing query");
+
+ stmt.bind_int64 (5, i);
+ stmt.bind_int64 (2, direct_hash (page));
+ stmt.bind_int64 (3, direct_hash (book));
+ stmt.bind_int64 (4, cur_book_revision);
+
+ result = stmt.step();
+ if (result != Sqlite.DONE)
+ warning ("Error %d while executing query", result);
+ }
+ }
+
+ public void on_page_changed (Page page)
+ {
+ update_page (page);
+ }
+
+ public void on_needs_saving_changed (Book book)
+ {
+ for (var n = 0; n < book.get_n_pages (); n++)
+ {
+ var page = book.get_page (n);
+ update_page (page);
+ }
+ }
+
+ private int cur_book_revision = 0;
+
+ public void on_cleared ()
+ {
+ cur_book_revision++;
+ }
+
+ private void insert_page (Page page)
+ {
+ debug ("Adding an autosave for a new page");
+ string query = @"
+ INSERT INTO pages
+ (process_id,
+ page_hash,
+ book_hash,
+ book_revision)
+ VALUES
+ ($PID,
+ ?2,
+ ?3,
+ ?4)
+ ";
+ Sqlite.Statement stmt;
+ var result = database_connection.prepare_v2 (query, -1, out stmt);
+ if (result != Sqlite.OK)
+ warning (@"Error $result while preparing query");
+
+ stmt.bind_int64 (2, direct_hash (page));
+ stmt.bind_int64 (3, direct_hash (book));
+ stmt.bind_int64 (4, cur_book_revision);
+
+ result = stmt.step();
+ if (result != Sqlite.DONE)
+ warning ("Error %d while executing query", result);
+
+ update_page (page);
+ }
+
+ private void update_page (Page page)
+ {
+ dirty_pages.insert (page, true);
+ if (update_timeout > 0)
+ Source.remove (update_timeout);
+ update_timeout = Timeout.add (100, () =>
+ {
+ var iter = HashTableIter<Page, bool> (dirty_pages);
+ Page p;
+ bool is_dirty;
+ while (iter.next (out p, out is_dirty))
+ real_update_page (p);
+
+ dirty_pages.remove_all ();
+ update_timeout = 0;
+
+ return false;
+ });
+ }
+
+ private void real_update_page (Page page)
+ {
+ debug ("Updating the autosave for a page");
+
+ int crop_x;
+ int crop_y;
+ int crop_width;
+ int crop_height;
+ page.get_crop (out crop_x, out crop_y, out crop_width, out crop_height);
+
+ Sqlite.Statement stmt;
+ string query = @"
+ UPDATE pages
+ SET
+ page_number=$(book.get_page_index (page)),
+ dpi=$(page.get_dpi ()),
+ width=$(page.get_width ()),
+ height=$(page.get_height ()),
+ depth=$(page.get_depth ()),
+ n_channels=$(page.get_n_channels ()),
+ rowstride=$(page.get_rowstride ()),
+ crop_x=$crop_x,
+ crop_y=$crop_y,
+ crop_width=$crop_width,
+ crop_height=$crop_height,
+ scan_direction=$((int)page.get_scan_direction ()),
+ color_profile=?1,
+ pixels=?2
+ WHERE process_id = $PID
+ AND page_hash = ?4
+ AND book_hash = ?5
+ AND book_revision = ?6
+ ";
+
+ var result = database_connection.prepare_v2 (query, -1, out stmt);
+ if (result != Sqlite.OK)
+ {
+ warning ("Error %d while preparing statement", result);
+ return;
+ }
+
+ stmt.bind_int64 (4, direct_hash (page));
+ stmt.bind_int64 (5, direct_hash (book));
+ stmt.bind_int64 (6, cur_book_revision);
+ result = stmt.bind_text (1, page.get_color_profile () ?? "");
+
+ if (result != Sqlite.OK)
+ warning ("Error %d while binding text", result);
+
+ if (page.get_pixels () != null)
+ {
+ // (-1) is the special value SQLITE_TRANSIENT
+ result = stmt.bind_blob (2, page.get_pixels (), page.get_pixels ().length, (DestroyNotify)(-1));
+ if (result != Sqlite.OK)
+ warning ("Error %d while binding blob", result);
+ }
+ else
+ warn_if_fail (stmt.bind_null (2) == Sqlite.OK);
+
+ warn_if_fail (stmt.step () == Sqlite.DONE);
+ }
+
+ private void recover_book (ref Book book)
+ {
+ Sqlite.Statement stmt;
+ string query = @"
+ SELECT process_id,
+ page_hash,
+ book_hash,
+ book_revision,
+ page_number,
+ dpi,
+ width,
+ height,
+ depth,
+ n_channels,
+ rowstride,
+ color_profile,
+ crop_x,
+ crop_y,
+ crop_width,
+ crop_height,
+ scan_direction,
+ pixels,
+ id
+ FROM pages
+ WHERE process_id = $PID
+ AND book_revision = (
+ SELECT MAX(book_revision) FROM pages WHERE process_id = $PID
+ )
+ ORDER BY page_number
+ ";
+
+ var result = database_connection.prepare_v2 (query, -1, out stmt);
+ if (result != Sqlite.OK)
+ warning ("Error %d while preparing statement", result);
+
+ var first = true;
+ while (Sqlite.ROW == stmt.step ())
+ {
+ debug ("Found a page that needs to be recovered");
+ if (first)
+ {
+ book.clear ();
+ first = false;
+ }
+ var dpi = stmt.column_int (5);
+ var width = stmt.column_int (6);
+ var height = stmt.column_int (7);
+ var depth = stmt.column_int (8);
+ var n_channels = stmt.column_int (9);
+ var scan_direction = (ScanDirection)stmt.column_int (16);
+
+ if (width <= 0 || height <= 0)
+ continue;
+
+ debug (@"Restoring a page of size $(width) x $(height)");
+ var new_page = book.append_page (width, height, dpi, scan_direction);
+
+ if (depth > 0 && n_channels > 0)
+ {
+ var info = new ScanPageInfo ();
+ info.width = width;
+ info.height = height;
+ info.depth = depth;
+ info.n_channels = n_channels;
+ info.dpi = dpi;
+ info.device = "";
+ new_page.set_page_info (info);
+ }
+
+ new_page.set_color_profile (stmt.column_text (11));
+ var crop_x = stmt.column_int (12);
+ var crop_y = stmt.column_int (13);
+ var crop_width = stmt.column_int (14);
+ var crop_height = stmt.column_int (15);
+ if (crop_width > 0 && crop_height > 0)
+ {
+ new_page.set_custom_crop (crop_width, crop_height);
+ new_page.move_crop (crop_x, crop_y);
+ }
+
+ uchar[] new_pixels = new uchar[stmt.column_bytes (17)];
+ Memory.copy (new_pixels, stmt.column_blob (17), stmt.column_bytes (17));
+ new_page.set_pixels (new_pixels);
+
+ var id = stmt.column_int (18);
+ debug ("Updating autosave to point to our new copy of the page");
+ query = @"
+ UPDATE pages
+ SET page_hash=?1,
+ book_hash=?2,
+ book_revision=?3
+ WHERE id = $id
+ ";
+
+ Sqlite.Statement stmt2;
+ var result2 = database_connection.prepare_v2 (query, -1, out stmt2);
+ if (result2 != Sqlite.OK)
+ warning (@"Error $result2 while preparing query");
+ stmt2.bind_int64 (1, direct_hash (new_page));
+ stmt2.bind_int64 (2, direct_hash (book));
+ stmt2.bind_int64 (3, cur_book_revision);
+
+ result2 = stmt2.step ();
+ if (result2 != Sqlite.DONE)
+ warning ("Error %d while executing query", result);
+ }
+
+ if (first)
+ debug ("No pages found to recover");
+ }
+}