1/*
2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
17 *
18 * Authored by Michal Hruby <michal.mhr@gmail.com>
19 *
20 */
21
22namespace Synapse
23{
24  public class LocatePlugin : Object, Activatable, ActionProvider
25  {
26    public bool enabled { get; set; default = true; }
27
28    public void activate ()
29    {
30
31    }
32
33    public void deactivate ()
34    {
35
36    }
37
38    private class LocateItem : SearchMatch
39    {
40      public int default_relevancy { get; set; default = MatchScore.INCREMENT_SMALL; }
41
42      // for SearchMatch interface
43      public override async Gee.List<Match> search (string query,
44                                           QueryFlags flags,
45                                           ResultSet? dest_result_set,
46                                           Cancellable? cancellable = null) throws SearchError
47      {
48        var q = Query (0, query, flags);
49        q.cancellable = cancellable;
50        ResultSet? results = yield plugin.locate (q);
51        dest_result_set.add_all (results);
52
53        return dest_result_set.get_sorted_list ();
54      }
55
56      private unowned LocatePlugin plugin;
57
58      public LocateItem (LocatePlugin plugin)
59      {
60        Object (has_thumbnail: false,
61                icon_name: "search",
62                title: _("Locate"),
63                description: _("Locate files with this name on the filesystem"));
64        this.plugin = plugin;
65      }
66    }
67
68    static void register_plugin ()
69    {
70      PluginRegistry.get_default ().register_plugin (
71        typeof (LocatePlugin),
72        _("Locate"),
73        _("Runs locate command to find files on the filesystem."),
74        "search",
75        register_plugin,
76        Environment.find_program_in_path ("locate") != null,
77        _("Unable to find \"locate\" binary")
78      );
79    }
80
81    static construct
82    {
83      register_plugin ();
84    }
85
86    LocateItem action;
87
88    construct
89    {
90      action = new LocateItem (this);
91    }
92
93    public bool handles_unknown ()
94    {
95      return true;
96    }
97
98    public async ResultSet? locate (Query q) throws SearchError
99    {
100      var our_results = QueryFlags.AUDIO | QueryFlags.DOCUMENTS
101        | QueryFlags.IMAGES | QueryFlags.UNCATEGORIZED | QueryFlags.VIDEO;
102
103      var common_flags = q.query_type & our_results;
104      // strip query
105      q.query_string = q.query_string.strip ();
106      // ignore short searches
107      if (common_flags == 0 || q.query_string.char_count () <= 1) return null;
108
109      q.check_cancellable ();
110
111      q.max_results = 256;
112      string regex = Regex.escape_string (q.query_string);
113      // FIXME: split pattern into words and search using --regexp?
114      string[] argv = {"locate", "-i", "-l", "%u".printf (q.max_results),
115                       "*%s*".printf (regex.replace (" ", "*"))};
116
117      Gee.Set<string> uris = new Gee.HashSet<string> ();
118
119      try
120      {
121        Pid pid;
122        int read_fd;
123
124        // FIXME: fork on every letter... yey!
125        Process.spawn_async_with_pipes (null, argv, null,
126                                        SpawnFlags.SEARCH_PATH,
127                                        null, out pid, null, out read_fd);
128
129        UnixInputStream read_stream = new UnixInputStream (read_fd, true);
130        DataInputStream locate_output = new DataInputStream (read_stream);
131        string? line = null;
132
133        Regex filter_re = new Regex ("/\\."); // hidden file/directory
134        do
135        {
136          line = yield locate_output.read_line_async (Priority.DEFAULT_IDLE, q.cancellable);
137          if (line != null)
138          {
139            if (filter_re.match (line)) continue;
140            var file = File.new_for_path (line);
141            uris.add (file.get_uri ());
142          }
143        } while (line != null);
144      }
145      catch (Error err)
146      {
147        if (!q.is_cancelled ()) warning ("%s", err.message);
148      }
149
150      q.check_cancellable ();
151
152      var result = new ResultSet ();
153
154      foreach (string s in uris)
155      {
156        var fi = new Utils.FileInfo (s, typeof (UriMatch));
157        yield fi.initialize ();
158        if (fi.match_obj != null && fi.file_type in q.query_type)
159        {
160          int relevancy = MatchScore.INCREMENT_SMALL; // FIXME: relevancy
161          if (fi.uri.has_prefix ("file:///home/")) relevancy += MatchScore.INCREMENT_MINOR;
162          result.add (fi.match_obj, relevancy);
163        }
164        q.check_cancellable ();
165      }
166
167      return result;
168    }
169
170    public ResultSet? find_for_match (ref Query q, Match match)
171    {
172      var our_results = QueryFlags.AUDIO | QueryFlags.DOCUMENTS
173        | QueryFlags.IMAGES | QueryFlags.UNCATEGORIZED | QueryFlags.VIDEO;
174
175      var common_flags = q.query_type & our_results;
176      // ignore short searches
177      if (common_flags == 0 || !(match is UnknownMatch)) return null;
178
179      // strip query
180      q.query_string = q.query_string.strip ();
181      bool query_empty = q.query_string == "";
182      var results = new ResultSet ();
183
184      if (query_empty)
185      {
186        results.add (action, action.default_relevancy);
187      }
188      else
189      {
190        var matchers = Query.get_matchers_for_query (q.query_string, 0,
191          RegexCompileFlags.CASELESS);
192        foreach (var matcher in matchers)
193        {
194          if (matcher.key.match (action.title))
195          {
196            results.add (action, matcher.value);
197            break;
198          }
199        }
200      }
201
202      return results;
203    }
204  }
205}
206