/* Copyright 2016 Software Freedom Conservancy Inc. * * This software is licensed under the GNU LGPL (version 2.1 or later). * See the COPYING file in this distribution. */ public class BackingFileState { public string filepath; public int64 filesize; public time_t modification_time; public string? md5; public BackingFileState(string filepath, int64 filesize, time_t modification_time, string? md5) { this.filepath = filepath; this.filesize = filesize; this.modification_time = modification_time; this.md5 = md5; } public BackingFileState.from_photo_row(BackingPhotoRow photo_row, string? md5) { this.filepath = photo_row.filepath; this.filesize = photo_row.filesize; this.modification_time = photo_row.timestamp; this.md5 = md5; } public File get_file() { return File.new_for_path(filepath); } } public abstract class MediaSource : ThumbnailSource, Indexable { public virtual signal void master_replaced(File old_file, File new_file) { } private Event? event = null; private string? indexable_keywords = null; protected MediaSource(int64 object_id = INVALID_OBJECT_ID) { base (object_id); } protected static inline uint64 internal_add_flags(uint64 flags, uint64 selector) { return (flags | selector); } protected static inline uint64 internal_remove_flags(uint64 flags, uint64 selector) { return (flags & ~selector); } protected static inline bool internal_is_flag_set(uint64 flags, uint64 selector) { return ((flags & selector) != 0); } protected virtual void notify_master_replaced(File old_file, File new_file) { master_replaced(old_file, new_file); } protected override void notify_altered(Alteration alteration) { Alteration local = alteration; if (local.has_detail("metadata", "name") || local.has_detail("metadata", "comment") || local.has_detail("backing", "master")) { update_indexable_keywords(); local = local.compress(new Alteration("indexable", "keywords")); } base.notify_altered(local); } // use this method as a kind of post-constructor initializer; it means the DataSource has been // added or removed to a SourceCollection. protected override void notify_membership_changed(DataCollection? collection) { if (collection != null && indexable_keywords == null) { // don't fire the alteration here, as the MediaSource is only being added to its // SourceCollection update_indexable_keywords(); } base.notify_membership_changed(collection); } private void update_indexable_keywords() { string[] indexables = new string[3]; indexables[0] = get_title(); indexables[1] = get_basename(); indexables[2] = get_comment(); indexable_keywords = prepare_indexable_strings(indexables); } public unowned string? get_indexable_keywords() { return indexable_keywords; } protected abstract bool set_event_id(EventID id); protected bool delete_original_file() { bool ret = false; File file = get_master_file(); try { ret = file.trash(null); } catch (Error err) { // log error but don't abend, as this is not fatal to operation (also, could be // the photo is removed because it could not be found during a verify) message("Unable to move original photo %s to trash: %s", file.get_path(), err.message); } // remove empty directories corresponding to imported path, but only if file is located // inside the user's Pictures directory if (file.has_prefix(AppDirs.get_import_dir())) { File parent = file; while (!parent.equal(AppDirs.get_import_dir())) { parent = parent.get_parent(); if ((parent == null) || (parent.equal(AppDirs.get_import_dir()))) break; try { if (!query_is_directory_empty(parent)) break; } catch (Error err) { warning("Unable to query file info for %s: %s", parent.get_path(), err.message); break; } try { parent.delete(null); debug("Deleted empty directory %s", parent.get_path()); } catch (Error err) { // again, log error but don't abend message("Unable to delete empty directory %s: %s", parent.get_path(), err.message); } } } return ret; } public override string get_name() { string? title = get_title(); return is_string_empty(title) ? get_basename() : title; } public virtual string get_basename() { return get_file().get_basename(); } public abstract File get_file(); public abstract File get_master_file(); public abstract uint64 get_master_filesize(); public abstract uint64 get_filesize(); public abstract time_t get_timestamp(); // Must return at least one, for the master file. public abstract BackingFileState[] get_backing_files_state(); public abstract string? get_title(); public abstract string? get_comment(); public abstract void set_title(string? title); public abstract bool set_comment(string? comment); public static string? prep_title(string? title) { return prepare_input_text(title, PrepareInputTextOptions.DEFAULT & ~PrepareInputTextOptions.EMPTY_IS_NULL, DEFAULT_USER_TEXT_INPUT_LENGTH); } public static string? prep_comment(string? comment) { return prepare_input_text(comment, PrepareInputTextOptions.DEFAULT & ~PrepareInputTextOptions.STRIP_CRLF & ~PrepareInputTextOptions.EMPTY_IS_NULL, -1); } public abstract Rating get_rating(); public abstract void set_rating(Rating rating); public abstract void increase_rating(); public abstract void decrease_rating(); public abstract Dimensions get_dimensions(Photo.Exception disallowed_steps = Photo.Exception.NONE); // A preview pixbuf is one that can be quickly generated and scaled as a preview. For media // type that support transformations (i.e. photos) it is fully transformed. // // Note that an unscaled scaling is not considered a performance-killer for this method, // although the quality of the pixbuf may be quite poor compared to the actual unscaled // transformed pixbuf. public abstract Gdk.Pixbuf get_preview_pixbuf(Scaling scaling) throws Error; public abstract bool is_trashed(); public abstract void trash(); public abstract void untrash(); public abstract bool is_offline(); public abstract void mark_offline(); public abstract void mark_online(); public abstract string get_master_md5(); // WARNING: some child classes of MediaSource (e.g. Photo) implement this method in a // non-thread safe manner for efficiency. public abstract EventID get_event_id(); public Event? get_event() { if (event != null) return event; EventID event_id = get_event_id(); if (!event_id.is_valid()) return null; event = Event.global.fetch(event_id); return event; } public bool set_event(Event? new_event) { EventID event_id = (new_event != null) ? new_event.get_event_id() : EventID(); if (get_event_id().id == event_id.id) return true; bool committed = set_event_id(event_id); if (committed) { if (event != null) event.detach(this); if (new_event != null) new_event.attach(this); event = new_event; notify_altered(new Alteration("metadata", "event")); } return committed; } public static void set_many_to_event(Gee.Collection media_sources, Event? event, TransactionController controller) throws Error { EventID event_id = (event != null) ? event.get_event_id() : EventID(); controller.begin(); foreach (MediaSource media in media_sources) { Event? old_event = media.get_event(); if (old_event != null) old_event.detach(media); media.set_event_id(event_id); media.event = event; } if (event != null) event.attach_many(media_sources); Alteration alteration = new Alteration("metadata", "event"); foreach (MediaSource media in media_sources) media.notify_altered(alteration); controller.commit(); } public abstract time_t get_exposure_time(); public abstract ImportID get_import_id(); } public class MediaSourceHoldingTank : DatabaseSourceHoldingTank { private Gee.HashMap master_file_map = new Gee.HashMap( file_hash, file_equal); public MediaSourceHoldingTank(MediaSourceCollection sources, SourceHoldingTank.CheckToKeep check_to_keep, GetSourceDatabaseKey get_key) { base (sources, check_to_keep, get_key); } public MediaSource? fetch_by_master_file(File file) { return master_file_map.get(file); } public MediaSource? fetch_by_md5(string md5) { foreach (MediaSource source in master_file_map.values) { if (source.get_master_md5() == md5) { return source; } } return null; } protected override void notify_contents_altered(Gee.Collection? added, Gee.Collection? removed) { if (added != null) { foreach (DataSource source in added) { MediaSource media_source = (MediaSource) source; master_file_map.set(media_source.get_master_file(), media_source); media_source.master_replaced.connect(on_master_source_replaced); } } if (removed != null) { foreach (DataSource source in removed) { MediaSource media_source = (MediaSource) source; bool is_removed = master_file_map.unset(media_source.get_master_file()); assert(is_removed); media_source.master_replaced.disconnect(on_master_source_replaced); } } base.notify_contents_altered(added, removed); } private void on_master_source_replaced(MediaSource media_source, File old_file, File new_file) { bool removed = master_file_map.unset(old_file); assert(removed); master_file_map.set(new_file, media_source); } } // This class is good for any MediaSourceCollection that is backed by a DatabaseTable (which should // be all of them, but if not, they should construct their own implementation). public class MediaSourceTransactionController : TransactionController { private MediaSourceCollection sources; public MediaSourceTransactionController(MediaSourceCollection sources) { this.sources = sources; } protected override void begin_impl() throws Error { DatabaseTable.begin_transaction(); sources.freeze_notifications(); } protected override void commit_impl() throws Error { sources.thaw_notifications(); DatabaseTable.commit_transaction(); } } public abstract class MediaSourceCollection : DatabaseSourceCollection { public abstract TransactionController transaction_controller { get; } private MediaSourceHoldingTank trashcan = null; private MediaSourceHoldingTank offline_bin = null; private Gee.HashMap by_master_file = new Gee.HashMap( file_hash, file_equal); private Gee.MultiMap import_rolls = new Gee.TreeMultiMap(ImportID.compare_func); private Gee.TreeSet sorted_import_ids = new Gee.TreeSet(ImportID.compare_func); private Gee.Set flagged = new Gee.HashSet(); // This signal is fired when MediaSources are added to the collection due to a successful import. // "items-added" and "contents-altered" will follow. public virtual signal void media_import_starting(Gee.Collection media) { } // This signal is fired when MediaSources have been added to the collection due to a successful // import and import postprocessing has completed (such as adding an import Photo to its Tags). // Thus, signals that have already been fired (in this order) are "media-imported", "items-added", // "contents-altered" before this signal. public virtual signal void media_import_completed(Gee.Collection media) { } public virtual signal void master_file_replaced(MediaSource media, File old_file, File new_file) { } public virtual signal void trashcan_contents_altered(Gee.Collection? added, Gee.Collection? removed) { } public virtual signal void import_roll_altered() { } public virtual signal void offline_contents_altered(Gee.Collection? added, Gee.Collection? removed) { } public virtual signal void flagged_contents_altered() { } protected MediaSourceCollection(string name, GetSourceDatabaseKey source_key_func) { base(name, source_key_func); trashcan = create_trashcan(); offline_bin = create_offline_bin(); } public static void filter_media(Gee.Collection media, Gee.Collection? photos, Gee.Collection