summaryrefslogtreecommitdiff
path: root/src/Tombstone.vala
blob: 23cd9845f4a4bc9e2c1eaeb8ffc846e4dc889e08 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution.
 */

public class TombstoneSourceCollection : DatabaseSourceCollection {
    private Gee.HashMap<File, Tombstone> file_map = new Gee.HashMap<File, Tombstone>(file_hash,
        file_equal);
    
    public TombstoneSourceCollection() {
        base ("Tombstones", get_tombstone_id);
    }
    
    public override bool holds_type_of_source(DataSource source) {
        return source is Tombstone;
    }
    
    private static int64 get_tombstone_id(DataSource source) {
        return ((Tombstone) source).get_tombstone_id().id;
    }
    
    protected override void notify_contents_altered(Gee.Iterable<DataObject>? added,
        Gee.Iterable<DataObject>? removed) {
        if (added != null) {
            foreach (DataObject object in added) {
                Tombstone tombstone = (Tombstone) object;
                
                file_map.set(tombstone.get_file(), tombstone);
            }
        }
        
        if (removed != null) {
            foreach (DataObject object in removed) {
                Tombstone tombstone = (Tombstone) object;
                
                // do we actually have this file?
                if (file_map.has_key(tombstone.get_file())) {
                    // yes, try to remove it.
                    bool is_removed = file_map.unset(tombstone.get_file());
                    assert(is_removed);
                }
                // if the hashmap didn't have the file to begin with,
                // we're already in the state we wanted to be in, so our
                // work is done; no need to assert.
            }
        }
        
        base.notify_contents_altered(added, removed);
    }
    
    protected override void notify_items_altered(Gee.Map<DataObject, Alteration> items) {
        foreach (DataObject object in items.keys) {
            Alteration alteration = items.get(object);
            if (!alteration.has_subject("file"))
                continue;
            
            Tombstone tombstone = (Tombstone) object;
            
            foreach (string detail in alteration.get_details("file")) {
                File old_file = File.new_for_path(detail);
                
                bool removed = file_map.unset(old_file);
                assert(removed);
                
                file_map.set(tombstone.get_file(), tombstone);
                
                break;
            }
        }
    }
    
    public Tombstone? locate(File file) {
        return file_map.get(file);
    }
    
    public bool matches(File file) {
        return file_map.has_key(file);
    }
    
    public void resurrect(Tombstone tombstone) {
        destroy_marked(mark(tombstone), false);
    }
    
    public void resurrect_many(Gee.Collection<Tombstone> tombstones) {
        Marker marker = mark_many(tombstones);
        
        freeze_notifications();
        DatabaseTable.begin_transaction();
        
        destroy_marked(marker, false);
        
        try {
            DatabaseTable.commit_transaction();
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
        }
        
        thaw_notifications();
    }
    
    // This initiates a scan of the tombstoned files, resurrecting them if the file is no longer
    // present on disk.  If a DirectoryMonitor is supplied, the scan will use that object's FileInfo
    // if available.  If not available or not supplied, the scan will query for the file's
    // existence.
    //
    // Note that this call is non-blocking.
    public void launch_scan(DirectoryMonitor? monitor, Cancellable? cancellable) {
        async_scan.begin(monitor, cancellable);
    }
    
    private async void async_scan(DirectoryMonitor? monitor, Cancellable? cancellable) {
        // search through all tombstones for missing files, which indicate the tombstone can go away
        Marker marker = start_marking();
        foreach (DataObject object in get_all()) {
            Tombstone tombstone = (Tombstone) object;
            File file = tombstone.get_file();
            
            FileInfo? info = null;
            if (monitor != null)
                info = monitor.get_file_info(file);
            
            // Want to be conservative here; only resurrect a tombstone if file is actually detected
            // as not present, and not some other problem (which may be intermittent)
            if (info == null) {
                try {
                    info = yield file.query_info_async(FileAttribute.STANDARD_NAME,
                        FileQueryInfoFlags.NOFOLLOW_SYMLINKS, Priority.LOW, cancellable);
                } catch (Error err) {
                    // watch for cancellation, which signals it's time to go
                    if (err is IOError.CANCELLED)
                        break;
                    
                    if (!(err is IOError.NOT_FOUND)) {
                        warning("Unable to check for existence of tombstoned file %s: %s",
                            file.get_path(), err.message);
                    }
                }
            }
            
            // if not found, resurrect
            if (info == null)
                marker.mark(tombstone);
            
            Idle.add(async_scan.callback);
            yield;
        }
        
        if (marker.get_count() > 0) {
            debug("Resurrecting %d tombstones with no backing file", marker.get_count());
            DatabaseTable.begin_transaction();
            destroy_marked(marker, false);
            try {
                DatabaseTable.commit_transaction();
            } catch (DatabaseError err2) {
                AppWindow.database_error(err2);
            }
        }
    }
}

public class TombstonedFile {
    public File file;
    public int64 filesize;
    public string? md5;
    
    public TombstonedFile(File file, int64 filesize, string? md5) {
        this.file = file;
        this.filesize = filesize;
        this.md5 = md5;
    }
}

public class Tombstone : DataSource {
    // These values are persisted.  Do not change.
    public enum Reason {
        REMOVED_BY_USER = 0,
        AUTO_DETECTED_DUPLICATE = 1;
        
        public int serialize() {
            return (int) this;
        }
        
        public static Reason unserialize(int value) {
            switch ((Reason) value) {
                case AUTO_DETECTED_DUPLICATE:
                    return AUTO_DETECTED_DUPLICATE;
                
                // 0 is the default in the database, so it should remain so here
                case REMOVED_BY_USER:
                default:
                    return REMOVED_BY_USER;
            }
        }
    }
    
    public static TombstoneSourceCollection global = null;
    
    private TombstoneRow row;
    private File? file = null;
    
    private Tombstone(TombstoneRow row) {
        this.row = row;
    }
    
    public static void init() {
        global = new TombstoneSourceCollection();
        
        TombstoneRow[]? rows = null;
        try {
            rows = TombstoneTable.get_instance().fetch_all();
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
        }
        
        if (rows != null) {
            Gee.ArrayList<Tombstone> tombstones = new Gee.ArrayList<Tombstone>();
            foreach (TombstoneRow row in rows)
                tombstones.add(new Tombstone(row));
            
            global.add_many(tombstones);
        }
    }
    
    public static void terminate() {
    }
    
    public static void entomb_many_sources(Gee.Collection<MediaSource> sources, Reason reason)
        throws DatabaseError {
        Gee.Collection<TombstonedFile> files = new Gee.ArrayList<TombstonedFile>();
        foreach (MediaSource source in sources) {
            foreach (BackingFileState state in source.get_backing_files_state())
                files.add(new TombstonedFile(state.get_file(), state.filesize, state.md5));
        }
        
        entomb_many_files(files, reason);
    }
    
    public static void entomb_many_files(Gee.Collection<TombstonedFile> files, Reason reason)
        throws DatabaseError {
        // destroy any out-of-date tombstones so they may be updated
        Marker to_destroy = global.start_marking();
        foreach (TombstonedFile file in files) {
            Tombstone? tombstone = global.locate(file.file);
            if (tombstone != null)
                to_destroy.mark(tombstone);
        }
        
        global.destroy_marked(to_destroy, false);
        
        Gee.ArrayList<Tombstone> tombstones = new Gee.ArrayList<Tombstone>();
        foreach (TombstonedFile file in files) {
            tombstones.add(new Tombstone(TombstoneTable.get_instance().add(file.file.get_path(),
                file.filesize, file.md5, reason)));
        }
        
        global.add_many(tombstones);
    }
    
    public override string get_typename() {
        return "tombstone";
    }
    
    public override int64 get_instance_id() {
        return get_tombstone_id().id;
    }
    
    public override string get_name() {
        return row.filepath;
    }
    
    public override string to_string() {
        return "Tombstone %s".printf(get_name());
    }
    
    public TombstoneID get_tombstone_id() {
        return row.id;
    }
    
    public File get_file() {
        if (file == null)
            file = File.new_for_path(row.filepath);
        
        return file;
    }
    
    public string? get_md5() {
        return is_string_empty(row.md5) ? null : row.md5;
    }
    
    public Reason get_reason() {
        return row.reason;
    }
    
    public void move(File file) {
        try {
            TombstoneTable.get_instance().update_file(row.id, file.get_path());
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
        }
        
        string old_filepath = row.filepath;
        row.filepath = file.get_path();
        this.file = file;
        
        notify_altered(new Alteration("file", old_filepath));
    }
    
    public bool matches(File file, int64 filesize, string? md5) {
        if (row.filesize != filesize)
            return false;
        
        // normalize to deal with empty strings
        string? this_md5 = is_string_empty(row.md5) ? null : row.md5;
        string? other_md5 = is_string_empty(md5) ? null : md5;
        
        if (this_md5 != other_md5)
            return false;
        
        if (!get_file().equal(file))
            return false;
        
        return true;
    }
    
    public override void destroy() {
        try {
            TombstoneTable.get_instance().remove(row.id);
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
        }
        
        base.destroy();
    }
}