summaryrefslogtreecommitdiff
path: root/src/db/PhotoTable.vala
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff-webhosting.net>2014-07-23 09:06:59 +0200
committerJörg Frings-Fürst <debian@jff-webhosting.net>2014-07-23 09:06:59 +0200
commit4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch)
treed2e54377d14d604356c86862a326f64ae64dadd6 /src/db/PhotoTable.vala
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/db/PhotoTable.vala')
-rw-r--r--src/db/PhotoTable.vala1245
1 files changed, 1245 insertions, 0 deletions
diff --git a/src/db/PhotoTable.vala b/src/db/PhotoTable.vala
new file mode 100644
index 0000000..9891fe6
--- /dev/null
+++ b/src/db/PhotoTable.vala
@@ -0,0 +1,1245 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public struct PhotoID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public PhotoID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+
+ public uint hash() {
+ return int64_hash(id);
+ }
+
+ public static bool equal(void *a, void *b) {
+ return ((PhotoID *) a)->id == ((PhotoID *) b)->id;
+ }
+
+ public static string upgrade_photo_id_to_source_id(PhotoID photo_id) {
+ return ("%s%016" + int64.FORMAT_MODIFIER + "x").printf(Photo.TYPENAME, photo_id.id);
+ }
+}
+
+public struct ImportID {
+ public const int64 INVALID = 0;
+
+ public int64 id;
+
+ public ImportID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public static ImportID generate() {
+ TimeVal timestamp = TimeVal();
+ timestamp.get_current_time();
+ int64 id = timestamp.tv_sec;
+
+ return ImportID(id);
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+
+ public static int compare_func(ImportID? a, ImportID? b) {
+ assert (a != null && b != null);
+ return (int) (a.id - b.id);
+ }
+
+ public static int64 comparator(void *a, void *b) {
+ return ((ImportID *) a)->id - ((ImportID *) b)->id;
+ }
+}
+
+public class PhotoRow {
+ public PhotoID photo_id;
+ public BackingPhotoRow master;
+ public time_t exposure_time;
+ public ImportID import_id;
+ public EventID event_id;
+ public Orientation orientation;
+ public Gee.HashMap<string, KeyValueMap>? transformations;
+ public string md5;
+ public string thumbnail_md5;
+ public string exif_md5;
+ public time_t time_created;
+ public uint64 flags;
+ public Rating rating;
+ public string title;
+ public string comment;
+ public string? backlinks;
+ public time_t time_reimported;
+ public BackingPhotoID editable_id;
+ public bool metadata_dirty;
+
+ // Currently selected developer (RAW only)
+ public RawDeveloper developer;
+
+ // Currently selected developer (RAW only)
+ public BackingPhotoID[] development_ids;
+
+
+ public PhotoRow() {
+ master = new BackingPhotoRow();
+ editable_id = BackingPhotoID();
+ development_ids = new BackingPhotoID[RawDeveloper.as_array().length];
+ foreach (RawDeveloper d in RawDeveloper.as_array())
+ development_ids[d] = BackingPhotoID();
+ }
+}
+
+public class PhotoTable : DatabaseTable {
+ private static PhotoTable instance = null;
+
+ private PhotoTable() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS PhotoTable ("
+ + "id INTEGER PRIMARY KEY, "
+ + "filename TEXT UNIQUE NOT NULL, "
+ + "width INTEGER, "
+ + "height INTEGER, "
+ + "filesize INTEGER, "
+ + "timestamp INTEGER, "
+ + "exposure_time INTEGER, "
+ + "orientation INTEGER, "
+ + "original_orientation INTEGER, "
+ + "import_id INTEGER, "
+ + "event_id INTEGER, "
+ + "transformations TEXT, "
+ + "md5 TEXT, "
+ + "thumbnail_md5 TEXT, "
+ + "exif_md5 TEXT, "
+ + "time_created INTEGER, "
+ + "flags INTEGER DEFAULT 0, "
+ + "rating INTEGER DEFAULT 0, "
+ + "file_format INTEGER DEFAULT 0, "
+ + "title TEXT, "
+ + "backlinks TEXT, "
+ + "time_reimported INTEGER, "
+ + "editable_id INTEGER DEFAULT -1, "
+ + "metadata_dirty INTEGER DEFAULT 0, "
+ + "developer TEXT, "
+ + "develop_shotwell_id INTEGER DEFAULT -1, "
+ + "develop_camera_id INTEGER DEFAULT -1, "
+ + "develop_embedded_id INTEGER DEFAULT -1, "
+ + "comment TEXT"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create photo table", res);
+
+ // index on event_id
+ Sqlite.Statement stmt2;
+ int res2 = db.prepare_v2("CREATE INDEX IF NOT EXISTS PhotoEventIDIndex ON PhotoTable (event_id)",
+ -1, out stmt2);
+ assert(res2 == Sqlite.OK);
+
+ res2 = stmt2.step();
+ if (res2 != Sqlite.DONE)
+ fatal("create photo table", res2);
+
+ set_table_name("PhotoTable");
+ }
+
+ public static PhotoTable get_instance() {
+ if (instance == null)
+ instance = new PhotoTable();
+
+ return instance;
+ }
+
+ // PhotoRow.photo_id, event_id, master.orientation, flags, and time_created are ignored on input.
+ // All fields are set on exit with values stored in the database. editable_id field is ignored.
+ public PhotoID add(PhotoRow photo_row) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "INSERT INTO PhotoTable (filename, width, height, filesize, timestamp, exposure_time, "
+ + "orientation, original_orientation, import_id, event_id, md5, thumbnail_md5, "
+ + "exif_md5, time_created, file_format, title, rating, editable_id, developer, comment) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ ulong time_created = now_sec();
+
+ res = stmt.bind_text(1, photo_row.master.filepath);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(2, photo_row.master.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(3, photo_row.master.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(4, photo_row.master.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(5, photo_row.master.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(6, photo_row.exposure_time);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(7, photo_row.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(8, photo_row.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(9, photo_row.import_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(10, EventID.INVALID);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(11, photo_row.md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(12, photo_row.thumbnail_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(13, photo_row.exif_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(14, time_created);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(15, photo_row.master.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(16, photo_row.title);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(17, photo_row.rating.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(18, BackingPhotoID.INVALID);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(19, photo_row.developer.to_string());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(20, photo_row.comment);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ if (res != Sqlite.CONSTRAINT)
+ fatal("add_photo", res);
+
+ return PhotoID();
+ }
+
+ // fill in ignored fields with database values
+ photo_row.photo_id = PhotoID(db.last_insert_rowid());
+ photo_row.orientation = photo_row.master.original_orientation;
+ photo_row.event_id = EventID();
+ photo_row.time_created = (time_t) time_created;
+ photo_row.flags = 0;
+
+ return photo_row.photo_id;
+ }
+
+ // The only fields recognized in the PhotoRow are photo_id, dimensions,
+ // filesize, timestamp, exposure_time, original_orientation, file_format,
+ // and the md5 fields. When the method returns, time_reimported and master.orientation has been
+ // updated. editable_id is ignored. transformations are untouched; use
+ // remove_all_transformations() if necessary.
+ public void reimport(PhotoRow row) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "UPDATE PhotoTable SET width = ?, height = ?, filesize = ?, timestamp = ?, "
+ + "exposure_time = ?, orientation = ?, original_orientation = ?, md5 = ?, "
+ + "exif_md5 = ?, thumbnail_md5 = ?, file_format = ?, title = ?, time_reimported = ? "
+ + "WHERE id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ time_t time_reimported = (time_t) now_sec();
+
+ res = stmt.bind_int(1, row.master.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(2, row.master.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, row.master.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(4, row.master.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(5, row.exposure_time);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(6, row.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(7, row.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(8, row.md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(9, row.exif_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(10, row.thumbnail_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(11, row.master.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(12, row.title);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(13, time_reimported);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(14, row.photo_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("PhotoTable.reimport_master", res);
+
+ row.time_reimported = time_reimported;
+ row.orientation = row.master.original_orientation;
+ }
+
+ public bool master_exif_updated(PhotoID photoID, int64 filesize, long timestamp,
+ string md5, string? exif_md5, string? thumbnail_md5, PhotoRow row) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "UPDATE PhotoTable SET filesize = ?, timestamp = ?, md5 = ?, exif_md5 = ?,"
+ + "thumbnail_md5 =? WHERE id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(3, md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(4, exif_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(5, thumbnail_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(6, photoID.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ if (res != Sqlite.CONSTRAINT)
+ fatal("write_update_photo", res);
+
+ return false;
+ }
+
+ row.master.filesize = filesize;
+ row.master.timestamp = timestamp;
+ row.md5 = md5;
+ row.exif_md5 = exif_md5;
+ row.thumbnail_md5 = thumbnail_md5;
+
+ return true;
+ }
+
+ // Force corrupted orientations to a safe value.
+ //
+ // In previous versions of Shotwell, this field could be written to
+ // the DB as a zero due to Vala 0.14 breaking the way it handled
+ // objects passed as 'ref' arguments to methods.
+ //
+ // For further details, please see http://redmine.yorba.org/issues/4354 and
+ // https://bugzilla.gnome.org/show_bug.cgi?id=663818 .
+ private void validate_orientation(PhotoRow row) {
+ if ((row.orientation < Orientation.MIN) ||
+ (row.orientation > Orientation.MAX)) {
+ // orientation was corrupted; set it to top left.
+ set_orientation(row.photo_id, Orientation.MIN);
+ row.orientation = Orientation.MIN;
+ }
+ }
+
+ public PhotoRow? get_row(PhotoID photo_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "SELECT filename, width, height, filesize, timestamp, exposure_time, orientation, "
+ + "original_orientation, import_id, event_id, transformations, md5, thumbnail_md5, "
+ + "exif_md5, time_created, flags, rating, file_format, title, backlinks, "
+ + "time_reimported, editable_id, metadata_dirty, developer, develop_shotwell_id, "
+ + "develop_camera_id, develop_embedded_id, comment "
+ + "FROM PhotoTable WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, photo_id.id);
+ assert(res == Sqlite.OK);
+
+ if (stmt.step() != Sqlite.ROW)
+ return null;
+
+ PhotoRow row = new PhotoRow();
+ row.photo_id = photo_id;
+ row.master.filepath = stmt.column_text(0);
+ row.master.dim = Dimensions(stmt.column_int(1), stmt.column_int(2));
+ row.master.filesize = stmt.column_int64(3);
+ row.master.timestamp = (time_t) stmt.column_int64(4);
+ row.exposure_time = (time_t) stmt.column_int64(5);
+ row.orientation = (Orientation) stmt.column_int(6);
+ row.master.original_orientation = (Orientation) stmt.column_int(7);
+ row.import_id.id = stmt.column_int64(8);
+ row.event_id.id = stmt.column_int64(9);
+ row.transformations = marshall_all_transformations(stmt.column_text(10));
+ row.md5 = stmt.column_text(11);
+ row.thumbnail_md5 = stmt.column_text(12);
+ row.exif_md5 = stmt.column_text(13);
+ row.time_created = (time_t) stmt.column_int64(14);
+ row.flags = stmt.column_int64(15);
+ row.rating = Rating.unserialize(stmt.column_int(16));
+ row.master.file_format = PhotoFileFormat.unserialize(stmt.column_int(17));
+ row.title = stmt.column_text(18);
+ row.backlinks = stmt.column_text(19);
+ row.time_reimported = (time_t) stmt.column_int64(20);
+ row.editable_id = BackingPhotoID(stmt.column_int64(21));
+ row.metadata_dirty = stmt.column_int(22) != 0;
+ row.developer = stmt.column_text(23) != null ? RawDeveloper.from_string(stmt.column_text(23)) :
+ RawDeveloper.CAMERA;
+ row.development_ids[RawDeveloper.SHOTWELL] = BackingPhotoID(stmt.column_int64(24));
+ row.development_ids[RawDeveloper.CAMERA] = BackingPhotoID(stmt.column_int64(25));
+ row.development_ids[RawDeveloper.EMBEDDED] = BackingPhotoID(stmt.column_int64(26));
+ row.comment = stmt.column_text(27);
+
+ return row;
+ }
+
+ public Gee.ArrayList<PhotoRow?> get_all() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "SELECT id, filename, width, height, filesize, timestamp, exposure_time, orientation, "
+ + "original_orientation, import_id, event_id, transformations, md5, thumbnail_md5, "
+ + "exif_md5, time_created, flags, rating, file_format, title, backlinks, time_reimported, "
+ + "editable_id, metadata_dirty, developer, develop_shotwell_id, develop_camera_id, "
+ + "develop_embedded_id, comment FROM PhotoTable",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<PhotoRow?> all = new Gee.ArrayList<PhotoRow?>();
+
+ while ((res = stmt.step()) == Sqlite.ROW) {
+ PhotoRow row = new PhotoRow();
+ row.photo_id.id = stmt.column_int64(0);
+ row.master.filepath = stmt.column_text(1);
+ row.master.dim = Dimensions(stmt.column_int(2), stmt.column_int(3));
+ row.master.filesize = stmt.column_int64(4);
+ row.master.timestamp = (time_t) stmt.column_int64(5);
+ row.exposure_time = (time_t) stmt.column_int64(6);
+ row.orientation = (Orientation) stmt.column_int(7);
+ row.master.original_orientation = (Orientation) stmt.column_int(8);
+ row.import_id.id = stmt.column_int64(9);
+ row.event_id.id = stmt.column_int64(10);
+ row.transformations = marshall_all_transformations(stmt.column_text(11));
+ row.md5 = stmt.column_text(12);
+ row.thumbnail_md5 = stmt.column_text(13);
+ row.exif_md5 = stmt.column_text(14);
+ row.time_created = (time_t) stmt.column_int64(15);
+ row.flags = stmt.column_int64(16);
+ row.rating = Rating.unserialize(stmt.column_int(17));
+ row.master.file_format = PhotoFileFormat.unserialize(stmt.column_int(18));
+ row.title = stmt.column_text(19);
+ row.backlinks = stmt.column_text(20);
+ row.time_reimported = (time_t) stmt.column_int64(21);
+ row.editable_id = BackingPhotoID(stmt.column_int64(22));
+ row.metadata_dirty = stmt.column_int(23) != 0;
+ row.developer = stmt.column_text(24) != null ? RawDeveloper.from_string(stmt.column_text(24)) :
+ RawDeveloper.CAMERA;
+ row.development_ids[RawDeveloper.SHOTWELL] = BackingPhotoID(stmt.column_int64(25));
+ row.development_ids[RawDeveloper.CAMERA] = BackingPhotoID(stmt.column_int64(26));
+ row.development_ids[RawDeveloper.EMBEDDED] = BackingPhotoID(stmt.column_int64(27));
+ row.comment = stmt.column_text(28);
+
+ validate_orientation(row);
+
+ all.add(row);
+ }
+
+ return all;
+ }
+
+ // Create a duplicate of the specified row. A new byte-for-byte duplicate (including filesystem
+ // metadata) of PhotoID's file needs to back this duplicate and its editable (if exists).
+ public PhotoID duplicate(PhotoID photo_id, string new_filename, BackingPhotoID editable_id,
+ BackingPhotoID develop_shotwell, BackingPhotoID develop_camera_id,
+ BackingPhotoID develop_embedded_id) {
+ // get a copy of the original row, duplicating most (but not all) of it
+ PhotoRow original = get_row(photo_id);
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO PhotoTable (filename, width, height, filesize, "
+ + "timestamp, exposure_time, orientation, original_orientation, import_id, event_id, "
+ + "transformations, md5, thumbnail_md5, exif_md5, time_created, flags, rating, "
+ + "file_format, title, editable_id, developer, develop_shotwell_id, develop_camera_id, "
+ + "develop_embedded_id, comment) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, new_filename);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(2, original.master.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(3, original.master.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(4, original.master.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(5, original.master.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(6, original.exposure_time);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(7, original.orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(8, original.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(9, original.import_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(10, original.event_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(11, unmarshall_all_transformations(original.transformations));
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(12, original.md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(13, original.thumbnail_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(14, original.exif_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(15, now_sec());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(16, (int64) original.flags);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(17, original.rating.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(18, original.master.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(19, original.title);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(20, editable_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(21, original.developer.to_string());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(22, develop_shotwell.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(23, develop_camera_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(24, develop_embedded_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(25, original.comment);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ if (res != Sqlite.CONSTRAINT)
+ fatal("duplicate", res);
+
+ return PhotoID();
+ }
+
+ return PhotoID(db.last_insert_rowid());
+ }
+
+ public bool set_title(PhotoID photo_id, string? new_title) {
+ return update_text_by_id(photo_id.id, "title", new_title != null ? new_title : "");
+ }
+
+ public bool set_comment(PhotoID photo_id, string? new_comment) {
+ return update_text_by_id(photo_id.id, "comment", new_comment != null ? new_comment : "");
+ }
+
+ public void set_filepath(PhotoID photo_id, string filepath) throws DatabaseError {
+ update_text_by_id_2(photo_id.id, "filename", filepath);
+ }
+
+ public void update_timestamp(PhotoID photo_id, time_t timestamp) throws DatabaseError {
+ update_int64_by_id_2(photo_id.id, "timestamp", timestamp);
+ }
+
+ public bool set_exposure_time(PhotoID photo_id, time_t time) {
+ return update_int64_by_id(photo_id.id, "exposure_time", (int64) time);
+ }
+
+ public void set_import_id(PhotoID photo_id, ImportID import_id) throws DatabaseError {
+ update_int64_by_id_2(photo_id.id, "import_id", import_id.id);
+ }
+
+ public bool remove_by_file(File file) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("DELETE FROM PhotoTable WHERE filename=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, file.get_path());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ warning("remove", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public void remove(PhotoID photo_id) throws DatabaseError {
+ delete_by_id(photo_id.id);
+ }
+
+ public Gee.ArrayList<PhotoID?> get_photos() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM PhotoTable", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<PhotoID?> photo_ids = new Gee.ArrayList<PhotoID?>();
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("get_photos", res);
+
+ break;
+ }
+
+ photo_ids.add(PhotoID(stmt.column_int64(0)));
+ }
+
+ return photo_ids;
+ }
+
+ public bool set_orientation(PhotoID photo_id, Orientation orientation) {
+ return update_int_by_id(photo_id.id, "orientation", (int) orientation);
+ }
+
+ public bool replace_flags(PhotoID photo_id, uint64 flags) {
+ return update_int64_by_id(photo_id.id, "flags", (int64) flags);
+ }
+
+ public bool set_rating(PhotoID photo_id, Rating rating) {
+ return update_int_by_id(photo_id.id, "rating", rating.serialize());
+ }
+
+ public int get_event_photo_count(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM PhotoTable WHERE event_id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, event_id.id);
+ assert(res == Sqlite.OK);
+
+ int count = 0;
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("get_event_photo_count", res);
+
+ break;
+ }
+
+ count++;
+ }
+
+ return count;
+ }
+
+ public Gee.ArrayList<string> get_event_source_ids(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM PhotoTable WHERE event_id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, event_id.id);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<string> result = new Gee.ArrayList<string>();
+ for(;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("get_event_source_ids", res);
+
+ break;
+ }
+
+ result.add(PhotoID.upgrade_photo_id_to_source_id(PhotoID(stmt.column_int64(0))));
+ }
+
+ return result;
+ }
+
+ public bool event_has_photos(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM PhotoTable WHERE event_id = ? LIMIT 1", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, event_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ return false;
+ } else if (res != Sqlite.ROW) {
+ fatal("event_has_photos", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool drop_event(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE PhotoTable SET event_id = ? WHERE event_id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, EventID.INVALID);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, event_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ fatal("drop_event", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool set_event(PhotoID photo_id, EventID event_id) {
+ return update_int64_by_id(photo_id.id, "event_id", event_id.id);
+ }
+
+ private string? get_raw_transformations(PhotoID photo_id) {
+ Sqlite.Statement stmt;
+ if (!select_by_id(photo_id.id, "transformations", out stmt))
+ return null;
+
+ string trans = stmt.column_text(0);
+ if (trans == null || trans.length == 0)
+ return null;
+
+ return trans;
+ }
+
+ private bool set_raw_transformations(PhotoID photo_id, string trans) {
+ return update_text_by_id(photo_id.id, "transformations", trans);
+ }
+
+ public bool set_transformation_state(PhotoID photo_id, Orientation orientation,
+ Gee.HashMap<string, KeyValueMap>? transformations) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE PhotoTable SET orientation = ?, transformations = ? WHERE id = ?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int(1, orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(2, unmarshall_all_transformations(transformations));
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, photo_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ fatal("set_transformation_state", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public static Gee.HashMap<string, KeyValueMap>? marshall_all_transformations(string? trans) {
+ if (trans == null || trans.length == 0)
+ return null;
+
+ try {
+ KeyFile keyfile = new KeyFile();
+ if (!keyfile.load_from_data(trans, trans.length, KeyFileFlags.NONE))
+ return null;
+
+ Gee.HashMap<string, KeyValueMap> map = new Gee.HashMap<string, KeyValueMap>();
+
+ string[] objects = keyfile.get_groups();
+ foreach (string object in objects) {
+ string[] keys = keyfile.get_keys(object);
+ if (keys == null || keys.length == 0)
+ continue;
+
+ KeyValueMap key_map = new KeyValueMap(object);
+ for (int ctr = 0; ctr < keys.length; ctr++)
+ key_map.set_string(keys[ctr], keyfile.get_string(object, keys[ctr]));
+
+ map.set(object, key_map);
+ }
+
+ return map;
+ } catch (Error err) {
+ error("%s", err.message);
+ }
+ }
+
+ public static string? unmarshall_all_transformations(Gee.HashMap<string, KeyValueMap>? transformations) {
+ if (transformations == null || transformations.keys.size == 0)
+ return null;
+
+ KeyFile keyfile = new KeyFile();
+
+ foreach (string object in transformations.keys) {
+ KeyValueMap map = transformations.get(object);
+
+ foreach (string key in map.get_keys()) {
+ string? value = map.get_string(key, null);
+ assert(value != null);
+
+ keyfile.set_string(object, key, value);
+ }
+ }
+
+ size_t length;
+ string unmarshalled = keyfile.to_data(out length);
+ assert(unmarshalled != null);
+ assert(unmarshalled.length > 0);
+
+ return unmarshalled;
+ }
+
+ public bool set_transformation(PhotoID photo_id, KeyValueMap map) {
+ string trans = get_raw_transformations(photo_id);
+
+ try {
+ KeyFile keyfile = new KeyFile();
+ if (trans != null) {
+ if (!keyfile.load_from_data(trans, trans.length, KeyFileFlags.NONE))
+ return false;
+ }
+
+ Gee.Set<string> keys = map.get_keys();
+ foreach (string key in keys) {
+ string value = map.get_string(key, null);
+ assert(value != null);
+
+ keyfile.set_string(map.get_group(), key, value);
+ }
+
+ size_t length;
+ trans = keyfile.to_data(out length);
+ assert(trans != null);
+ assert(trans.length > 0);
+ } catch (Error err) {
+ error("%s", err.message);
+ }
+
+ return set_raw_transformations(photo_id, trans);
+ }
+
+ public bool remove_transformation(PhotoID photo_id, string object) {
+ string trans = get_raw_transformations(photo_id);
+ if (trans == null)
+ return true;
+
+ try {
+ KeyFile keyfile = new KeyFile();
+ if (!keyfile.load_from_data(trans, trans.length, KeyFileFlags.NONE))
+ return false;
+
+ if (!keyfile.has_group(object))
+ return true;
+
+ keyfile.remove_group(object);
+
+ size_t length;
+ trans = keyfile.to_data(out length);
+ assert(trans != null);
+ } catch (Error err) {
+ error("%s", err.message);
+ }
+
+ return set_raw_transformations(photo_id, trans);
+ }
+
+ public bool remove_all_transformations(PhotoID photo_id) {
+ if (get_raw_transformations(photo_id) == null)
+ return false;
+
+ return update_text_by_id(photo_id.id, "transformations", "");
+ }
+
+ // Use PhotoFileFormat.UNKNOWN if not to search for matching file format; it's only used if
+ // searching for MD5 duplicates.
+ private Sqlite.Statement get_duplicate_stmt(File? file, string? thumbnail_md5, string? md5,
+ PhotoFileFormat file_format) {
+ assert(file != null || thumbnail_md5 != null || md5 != null);
+
+ string sql = "SELECT id FROM PhotoTable WHERE";
+ bool first = true;
+
+ if (file != null) {
+ sql += " filename=?";
+ first = false;
+ }
+
+ if (thumbnail_md5 != null || md5 != null) {
+ if (first)
+ sql += " ((";
+ else
+ sql += " OR ((";
+ first = false;
+
+ if (thumbnail_md5 != null)
+ sql += " thumbnail_md5=?";
+
+ if (md5 != null) {
+ if (thumbnail_md5 == null)
+ sql += " md5=?";
+ else
+ sql += " OR md5=?";
+ }
+
+ sql += ")";
+
+ if (file_format != PhotoFileFormat.UNKNOWN)
+ sql += " AND file_format=?";
+
+ sql += ")";
+ }
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(sql, -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ int col = 1;
+
+ if (file != null) {
+ res = stmt.bind_text(col++, file.get_path());
+ assert(res == Sqlite.OK);
+ }
+
+ if (thumbnail_md5 != null) {
+ res = stmt.bind_text(col++, thumbnail_md5);
+ assert(res == Sqlite.OK);
+ }
+
+ if (md5 != null) {
+ res = stmt.bind_text(col++, md5);
+ assert(res == Sqlite.OK);
+ }
+
+ if ((thumbnail_md5 != null || md5 != null) && file_format != PhotoFileFormat.UNKNOWN) {
+ res = stmt.bind_int(col++, file_format.serialize());
+ assert(res == Sqlite.OK);
+ }
+
+ return stmt;
+ }
+
+ public bool has_duplicate(File? file, string? thumbnail_md5, string? md5, PhotoFileFormat file_format) {
+ Sqlite.Statement stmt = get_duplicate_stmt(file, thumbnail_md5, md5, file_format);
+ int res = stmt.step();
+
+ if (res == Sqlite.DONE) {
+ // not found
+ return false;
+ } else if (res == Sqlite.ROW) {
+ // at least one found
+ return true;
+ } else {
+ fatal("has_duplicate", res);
+
+ return false;
+ }
+ }
+
+ public PhotoID[] get_duplicate_ids(File? file, string? thumbnail_md5, string? md5,
+ PhotoFileFormat file_format) {
+ Sqlite.Statement stmt = get_duplicate_stmt(file, thumbnail_md5, md5, file_format);
+
+ PhotoID[] ids = new PhotoID[0];
+
+ int res = stmt.step();
+ while (res == Sqlite.ROW) {
+ ids += PhotoID(stmt.column_int64(0));
+ res = stmt.step();
+ }
+
+ return ids;
+ }
+
+ public void update_backlinks(PhotoID photo_id, string? backlinks) throws DatabaseError {
+ update_text_by_id_2(photo_id.id, "backlinks", backlinks != null ? backlinks : "");
+ }
+
+ public void attach_editable(PhotoRow row, BackingPhotoID editable_id) throws DatabaseError {
+ update_int64_by_id_2(row.photo_id.id, "editable_id", editable_id.id);
+
+ row.editable_id = editable_id;
+ }
+
+ public void detach_editable(PhotoRow row) throws DatabaseError {
+ update_int64_by_id_2(row.photo_id.id, "editable_id", BackingPhotoID.INVALID);
+
+ row.editable_id = BackingPhotoID();
+ }
+
+ public void set_metadata_dirty(PhotoID photo_id, bool dirty) throws DatabaseError {
+ update_int_by_id_2(photo_id.id, "metadata_dirty", dirty ? 1 : 0);
+ }
+
+ public void update_raw_development(PhotoRow row, RawDeveloper rd, BackingPhotoID backing_photo_id)
+ throws DatabaseError {
+
+ string col;
+ switch (rd) {
+ case RawDeveloper.SHOTWELL:
+ col = "develop_shotwell_id";
+ break;
+
+ case RawDeveloper.CAMERA:
+ col = "develop_camera_id";
+ break;
+
+ case RawDeveloper.EMBEDDED:
+ col = "develop_embedded_id";
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ row.development_ids[rd] = backing_photo_id;
+ update_int64_by_id_2(row.photo_id.id, col, backing_photo_id.id);
+
+ if (backing_photo_id.id != BackingPhotoID.INVALID)
+ update_text_by_id_2(row.photo_id.id, "developer", rd.to_string());
+ }
+
+ public void remove_development(PhotoRow row, RawDeveloper rd) throws DatabaseError {
+ update_raw_development(row, rd, BackingPhotoID());
+ }
+
+}
+
+//
+// BackingPhotoTable
+//
+// BackingPhotoTable is designed to hold any number of alternative backing photos
+// for a Photo. In the first implementation it was designed for editable photos (Edit with
+// External Editor), but if other such alternates are needed, this is where to store them.
+//
+// Note that no transformations are held here.
+//
+
+public struct BackingPhotoID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public BackingPhotoID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+}
+
+public class BackingPhotoRow {
+ public BackingPhotoID id;
+ public time_t time_created;
+ public string? filepath = null;
+ public int64 filesize;
+ public time_t timestamp;
+ public PhotoFileFormat file_format;
+ public Dimensions dim;
+ public Orientation original_orientation;
+
+ public bool matches_file_info(FileInfo info) {
+ if (filesize != info.get_size())
+ return false;
+
+ return timestamp == info.get_modification_time().tv_sec;
+ }
+
+ public bool is_touched(FileInfo info) {
+ if (filesize != info.get_size())
+ return false;
+
+ return timestamp != info.get_modification_time().tv_sec;
+ }
+
+ // Copies another backing photo row into this one.
+ public void copy_from(BackingPhotoRow from) {
+ id = from.id;
+ time_created = from.time_created;
+ filepath = from.filepath;
+ filesize = from.filesize;
+ timestamp = from.timestamp;
+ file_format = from.file_format;
+ dim = from.dim;
+ original_orientation = from.original_orientation;
+ }
+}
+
+public class BackingPhotoTable : DatabaseTable {
+ private static BackingPhotoTable instance = null;
+
+ private BackingPhotoTable() {
+ set_table_name("BackingPhotoTable");
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "BackingPhotoTable "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "filepath TEXT UNIQUE NOT NULL, "
+ + "timestamp INTEGER, "
+ + "filesize INTEGER, "
+ + "width INTEGER, "
+ + "height INTEGER, "
+ + "original_orientation INTEGER, "
+ + "file_format INTEGER, "
+ + "time_created INTEGER "
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create PhotoBackingTable", res);
+ }
+
+ public static BackingPhotoTable get_instance() {
+ if (instance == null)
+ instance = new BackingPhotoTable();
+
+ return instance;
+ }
+
+ public void add(BackingPhotoRow state) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO BackingPhotoTable "
+ + "(filepath, timestamp, filesize, width, height, original_orientation, "
+ + "file_format, time_created) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ time_t time_created = (time_t) now_sec();
+
+ res = stmt.bind_text(1, state.filepath);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, state.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, state.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(4, state.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(5, state.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(6, state.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(7, state.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(8, (int64) time_created);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("PhotoBackingTable.add", res);
+
+ state.id = BackingPhotoID(db.last_insert_rowid());
+ state.time_created = time_created;
+ }
+
+ public BackingPhotoRow? fetch(BackingPhotoID id) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT filepath, timestamp, filesize, width, height, "
+ + "original_orientation, file_format, time_created FROM BackingPhotoTable WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ return null;
+ else if (res != Sqlite.ROW)
+ throw_error("BackingPhotoTable.fetch_for_photo", res);
+
+ BackingPhotoRow row = new BackingPhotoRow();
+ row.id = id;
+ row.filepath = stmt.column_text(0);
+ row.timestamp = (time_t) stmt.column_int64(1);
+ row.filesize = stmt.column_int64(2);
+ row.dim = Dimensions(stmt.column_int(3), stmt.column_int(4));
+ row.original_orientation = (Orientation) stmt.column_int(5);
+ row.file_format = PhotoFileFormat.unserialize(stmt.column_int(6));
+ row.time_created = (time_t) stmt.column_int64(7);
+
+ return row;
+ }
+
+ // Everything but filepath is updated.
+ public void update(BackingPhotoRow row) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE BackingPhotoTable SET timestamp=?, filesize=?, "
+ + "width=?, height=?, original_orientation=?, file_format=? "
+ + "WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, row.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, row.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(3, row.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(4, row.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(5, row.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(6, row.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(7, row.id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("BackingPhotoTable.update", res);
+ }
+
+ public void update_attributes(BackingPhotoID id, time_t timestamp, int64 filesize) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE BackingPhotoTable SET timestamp=?, filesize=? WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("BackingPhotoTable.update_attributes", res);
+ }
+
+ public void remove(BackingPhotoID backing_id) throws DatabaseError {
+ delete_by_id(backing_id.id);
+ }
+
+ public void set_filepath(BackingPhotoID id, string filepath) throws DatabaseError {
+ update_text_by_id_2(id.id, "filepath", filepath);
+ }
+
+ public void update_timestamp(BackingPhotoID id, time_t timestamp) throws DatabaseError {
+ update_int64_by_id_2(id.id, "timestamp", timestamp);
+ }
+}
+