4 // Copyright (C) 2004 Novell, Inc.
8 // Permission is hereby granted, free of charge, to any person obtaining a
9 // copy of this software and associated documentation files (the "Software"),
10 // to deal in the Software without restriction, including without limitation
11 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 // and/or sell copies of the Software, and to permit persons to whom the
13 // Software is furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in
16 // all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 // DEALINGS IN THE SOFTWARE.
28 using System
.Collections
;
31 using System
.Threading
;
36 namespace Beagle
.Daemon
.GaimLogQueryable
{
38 // FIXME: This should be rather renamed to Gaim to be compliant with other backend names
39 [QueryableFlavor (Name
="GaimLog", Domain
=QueryDomain
.Local
, RequireInotify
=false)]
40 public class GaimLogQueryable
: LuceneFileQueryable
, IIndexableGenerator
{
42 private string config_dir
, log_dir
, icons_dir
;
44 private int polling_interval_in_seconds
= 60;
46 private bool crawling
= false;
48 private GaimBuddyListReader list
= new GaimBuddyListReader ();
50 public GaimLogQueryable () : base ("GaimLogIndex")
52 config_dir
= Path
.Combine (PathFinder
.HomeDir
, ".gaim");
53 log_dir
= Path
.Combine (config_dir
, "logs");
54 icons_dir
= Path
.Combine (config_dir
, "icons");
57 /////////////////////////////////////////////////
59 private void StartWorker()
61 if (! Directory
.Exists (log_dir
)) {
62 GLib
.Timeout
.Add (60000, new GLib
.TimeoutHandler (CheckForExistence
));
66 Log
.Info ("Starting Gaim log backend");
68 Stopwatch stopwatch
= new Stopwatch ();
71 if (Inotify
.Enabled
) {
72 Log
.Info ("Setting up inotify watches on gaim log directories");
77 task
= NewAddTask (this);
78 task
.Tag
= "Crawling Gaim logs";
81 if (!Inotify
.Enabled
) {
82 Scheduler
.TaskGroup
group = Scheduler
.NewTaskGroup ("Repeating gaim log crawler", null, AddCrawlTask
);
83 task
.AddTaskGroup (group);
86 ThisScheduler
.Add (task
);
90 Log
.Info ("Gaim log backend worker thread done in {0}", stopwatch
);
93 public override void Start ()
97 ExceptionHandlingThread
.Start (new ThreadStart (StartWorker
));
100 protected override bool IsIndexing
{
101 get { return crawling; }
104 /////////////////////////////////////////////////
106 private void AddCrawlTask ()
108 Scheduler
.Task task
= Scheduler
.TaskFromHook (new Scheduler
.TaskHook (CrawlHook
));
109 task
.Tag
= "Crawling ~/.gaim/logs to find new logfiles";
111 ThisScheduler
.Add (task
);
114 private void CrawlHook (Scheduler
.Task task
)
117 task
.Reschedule
= true;
118 task
.TriggerTime
= DateTime
.Now
.AddSeconds (polling_interval_in_seconds
);
121 private void Crawl (bool index
)
123 this.crawling
= true;
126 Inotify
.Subscribe (log_dir
, OnInotifyNewProtocol
, Inotify
.EventType
.Create
);
128 // Walk through protocol subdirs
129 foreach (string proto_dir
in DirectoryWalker
.GetDirectories (log_dir
))
130 CrawlProtocolDirectory (proto_dir
, index
);
133 private void CrawlProtocolDirectory (string proto_dir
, bool index
)
136 Inotify
.Subscribe (proto_dir
, OnInotifyNewAccount
, Inotify
.EventType
.Create
);
138 // Walk through accounts
139 foreach (string account_dir
in DirectoryWalker
.GetDirectories (proto_dir
))
140 CrawlAccountDirectory (account_dir
, index
);
143 private void CrawlAccountDirectory (string account_dir
, bool index
)
146 Inotify
.Subscribe (account_dir
, OnInotifyNewRemote
, Inotify
.EventType
.Create
);
148 // Walk through remote user conversations
149 foreach (string remote_dir
in DirectoryWalker
.GetDirectories (account_dir
)) {
150 if (remote_dir
.IndexOf (".system") < 0)
151 CrawlRemoteDirectory (remote_dir
, index
);
155 private void CrawlRemoteDirectory (string remote_dir
, bool index
)
158 Inotify
.Subscribe (remote_dir
, OnInotifyNewConversation
, Inotify
.EventType
.CloseWrite
| Inotify
.EventType
.Modify
);
161 foreach (FileInfo file
in DirectoryWalker
.GetFileInfos (remote_dir
))
162 if (FileIsInteresting (file
.Name
))
163 IndexLog (file
.FullName
, Scheduler
.Priority
.Delayed
);
169 /////////////////////////////////////////////////
171 public string StatusName
{
172 get { return "GaimLogQueryable"; }
175 private IEnumerator log_files
= null;
177 public void PostFlushHook () { }
179 public bool HasNextIndexable ()
181 if (log_files
== null)
182 log_files
= DirectoryWalker
.GetFileInfosRecursive (log_dir
).GetEnumerator ();
184 if (log_files
.MoveNext ())
192 public Indexable
GetNextIndexable ()
194 FileInfo file
= (FileInfo
) log_files
.Current
;
199 if (IsUpToDate (file
.FullName
))
202 Indexable indexable
= ImLogToIndexable (file
.FullName
);
207 /////////////////////////////////////////////////
209 private bool CheckForExistence ()
211 if (!Directory
.Exists (log_dir
))
219 private bool FileIsInteresting (string filename
)
221 if (filename
.Length
< 21)
224 string ext
= Path
.GetExtension (filename
);
225 if (ext
!= ".txt" && ext
!= ".html")
228 // Pre-gaim 2.0.0 logs are in the format "2005-07-22.161521.txt". Afterward a
229 // timezone field as added, ie. "2005-07-22.161521-0500EST.txt".
231 // This is a lot uglier than a regexp, but they are so damn expensive.
233 return Char
.IsDigit (filename
[0]) && Char
.IsDigit (filename
[1])
234 && Char
.IsDigit (filename
[2]) && Char
.IsDigit (filename
[3])
235 && filename
[4] == '-'
236 && Char
.IsDigit (filename
[5]) && Char
.IsDigit (filename
[6])
237 && filename
[7] == '-'
238 && Char
.IsDigit (filename
[8]) && Char
.IsDigit (filename
[9])
239 && filename
[10] == '.'
240 && Char
.IsDigit (filename
[11]) && Char
.IsDigit (filename
[12])
241 && Char
.IsDigit (filename
[13]) && Char
.IsDigit (filename
[14])
242 && Char
.IsDigit (filename
[15]) && Char
.IsDigit (filename
[16])
243 && (filename
[17] == '+' || filename
[17] == '-' || filename
[17] == '.');
246 /////////////////////////////////////////////////
248 private void OnInotifyNewProtocol (Inotify
.Watch watch
,
249 string path
, string subitem
, string srcpath
,
250 Inotify
.EventType type
)
252 if (subitem
.Length
== 0 || (type
& Inotify
.EventType
.IsDirectory
) == 0)
255 CrawlProtocolDirectory (Path
.Combine (path
, subitem
), true);
258 private void OnInotifyNewAccount (Inotify
.Watch watch
,
259 string path
, string subitem
, string srcpath
,
260 Inotify
.EventType type
)
262 if (subitem
.Length
== 0 || (type
& Inotify
.EventType
.IsDirectory
) == 0)
265 CrawlAccountDirectory (Path
.Combine (path
, subitem
), true);
268 private void OnInotifyNewRemote (Inotify
.Watch watch
,
269 string path
, string subitem
, string srcpath
,
270 Inotify
.EventType type
)
272 if (subitem
.Length
== 0 || (type
& Inotify
.EventType
.IsDirectory
) == 0)
275 CrawlRemoteDirectory (Path
.Combine (path
, subitem
), true);
278 private void OnInotifyNewConversation (Inotify
.Watch watch
,
279 string path
, string subitem
, string srcpath
,
280 Inotify
.EventType type
)
282 if (subitem
.Length
== 0 || (type
& Inotify
.EventType
.IsDirectory
) != 0)
285 if (FileIsInteresting (subitem
))
286 IndexLog (Path
.Combine (path
, subitem
), Scheduler
.Priority
.Immediate
);
289 /////////////////////////////////////////////////
291 private static Indexable
ImLogToIndexable (string filename
)
293 Uri uri
= UriFu
.PathToFileUri (filename
);
294 Indexable indexable
= new Indexable (uri
);
295 indexable
.ContentUri
= uri
;
296 indexable
.Timestamp
= File
.GetLastWriteTimeUtc (filename
);
297 indexable
.MimeType
= GaimLog
.MimeType
;
298 indexable
.HitType
= "IMLog";
299 indexable
.CacheContent
= false;
304 private void IndexLog (string filename
, Scheduler
.Priority priority
)
306 if (! File
.Exists (filename
))
309 if (IsUpToDate (filename
))
312 Indexable indexable
= ImLogToIndexable (filename
);
313 Scheduler
.Task task
= NewAddTask (indexable
);
314 task
.Priority
= priority
;
315 task
.SubPriority
= 0;
316 ThisScheduler
.Add (task
);
319 override protected double RelevancyMultiplier (Hit hit
)
321 return HalfLifeMultiplierFromProperty (hit
, 0.25, "fixme:endtime", "fixme:starttime");
324 override protected bool HitFilter (Hit hit
)
326 // If the protocol isn't set (because maybe we got an
327 // exception while we were indexing), this isn't a
329 if (hit
["fixme:protocol"] == null) {
330 Log
.Warn ("Discarding IM log hit with missing protocol info: {0}", hit
.Uri
);
334 string speakingto
= hit
["fixme:speakingto"];
336 // We have no idea who we're speaking to. Bad, but we
337 // still want to present it.
338 if (speakingto
== null || speakingto
== String
.Empty
)
341 ImBuddy buddy
= list
.Search (speakingto
);
343 // We might still want to see a chat even if someone's
344 // not on our buddy list.
348 if (buddy
.Alias
!= "")
349 hit
.AddProperty (Beagle
.Property
.NewKeyword ("fixme:speakingto_alias", buddy
.Alias
));
351 if (buddy
.BuddyIconLocation
!= "")
352 hit
.AddProperty (Beagle
.Property
.NewUnsearched ("fixme:speakingto_icon", Path
.Combine (icons_dir
, buddy
.BuddyIconLocation
)));
357 override public string GetSnippet (string [] query_terms
, Hit hit
)
360 reader
= TextCache
.UserCache
.GetReader (hit
.Uri
);
363 HtmlRemovingReader html_removing_reader
= new HtmlRemovingReader (reader
);
364 string snippet
= SnippetFu
.GetSnippet (query_terms
, html_removing_reader
);
365 html_removing_reader
.Close ();