For deserialization exception, debug print the actual XML message. Helps in debugging.
[beagle.git] / beagled / TextCache.cs
blobe0e51679cbc43dcbf6de02182f85d906d02ab04a
1 //
2 // TextCache.cs
3 //
4 // Copyright (C) 2004 Novell, Inc.
5 //
7 //
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;
29 using System.Collections;
30 using System.IO;
31 using System.Threading;
33 using Mono.Data.SqliteClient;
34 using ICSharpCode.SharpZipLib.GZip;
36 using Beagle.Util;
38 namespace Beagle.Daemon {
40 public class TextCache {
42 static public bool Debug = false;
44 public const string SELF_CACHE_TAG = "*self*";
46 private string text_cache_dir;
47 private SqliteConnection connection;
49 private enum TransactionState {
50 None,
51 Requested,
52 Started
54 private TransactionState transaction_state;
56 private static TextCache user_cache = null;
58 public static TextCache UserCache {
59 get {
60 if (user_cache == null)
61 user_cache = new TextCache (PathFinder.StorageDir);
63 return user_cache;
67 public TextCache (string storage_dir) : this (storage_dir, false) { }
69 public TextCache (string storage_dir, bool read_only)
71 text_cache_dir = Path.Combine (storage_dir, "TextCache");
72 if (! Directory.Exists (text_cache_dir)) {
73 Directory.CreateDirectory (text_cache_dir);
75 // Create our cache subdirectories.
76 for (int i = 0; i < 256; ++i) {
77 string subdir = i.ToString ("x");
78 if (i < 16)
79 subdir = "0" + subdir;
80 subdir = Path.Combine (text_cache_dir, subdir);
81 Directory.CreateDirectory (subdir);
85 // Create our Sqlite database
86 string db_filename = Path.Combine (text_cache_dir, "TextCache.db");
87 bool create_new_db = false;
88 if (! File.Exists (db_filename))
89 create_new_db = true;
91 // Funky logic here to deal with sqlite versions.
93 // When sqlite 3 tries to open an sqlite 2 database,
94 // it will throw an SqliteException with SqliteError
95 // NOTADB when trying to execute a command.
97 // When sqlite 2 tries to open an sqlite 3 database,
98 // it will throw an ApplicationException when it
99 // tries to open the database.
101 try {
102 connection = Open (db_filename);
103 } catch (ApplicationException) {
104 Logger.Log.Warn ("Likely sqlite database version mismatch trying to open {0}. Purging.", db_filename);
105 create_new_db = true;
108 if (!create_new_db) {
109 // Run a dummy query to see if we get a NOTADB error. Sigh.
110 SqliteCommand command;
111 SqliteDataReader reader = null;
113 command = new SqliteCommand ();
114 command.Connection = connection;
115 command.CommandText =
116 "SELECT filename FROM uri_index WHERE uri='blah'";
118 try {
119 reader = ExecuteReaderOrWait (command);
120 } catch (ApplicationException ex) {
121 Logger.Log.Warn ("Likely sqlite database version mismatch trying to read from {0}. Purging.", db_filename);
122 create_new_db = true;
125 if (reader != null)
126 reader.Dispose ();
127 command.Dispose ();
130 if (create_new_db) {
131 if (connection != null)
132 connection.Dispose ();
134 if (read_only)
135 throw new UnauthorizedAccessException (String.Format ("Unable to create read only text cache {0}", db_filename));
137 File.Delete (db_filename);
138 try{
139 connection = Open (db_filename);
140 }catch (Exception e){
141 Logger.Log.Debug ( e.Message);
143 DoNonQuery ("CREATE TABLE uri_index ( " +
144 " uri STRING UNIQUE NOT NULL, " +
145 " filename STRING NOT NULL " +
146 ")");
150 private SqliteConnection Open (string db_filename)
152 SqliteConnection connection = new SqliteConnection ();
153 connection.ConnectionString = "version=" + ExternalStringsHack.SqliteVersion
154 + ",encoding=UTF-8,URI=file:" + db_filename;
155 connection.Open ();
156 return connection;
159 private static string UriToString (Uri uri)
161 return UriFu.UriToSerializableString (uri).Replace ("'", "''");
164 private SqliteCommand NewCommand (string format, params object [] args)
166 SqliteCommand command;
167 command = new SqliteCommand ();
168 command.Connection = connection;
169 command.CommandText = String.Format (format, args);
170 return command;
173 private void DoNonQuery (string format, params object [] args)
175 if (Debug)
176 Logger.Log.Debug ("Executing command '{0}'", String.Format (format, args));
177 SqliteCommand command = NewCommand (format, args);
178 while (true) {
179 try {
180 command.ExecuteNonQuery ();
181 break;
182 } catch (SqliteBusyException ex) {
183 // FIXME: should we eventually time out?
184 Thread.Sleep (50);
187 command.Dispose ();
190 private SqliteDataReader ExecuteReaderOrWait (SqliteCommand command)
192 SqliteDataReader reader = null;
193 while (reader == null) {
194 try {
195 reader = command.ExecuteReader ();
196 } catch (SqliteBusyException ex) {
197 Thread.Sleep (50);
200 return reader;
203 private bool ReadOrWait (SqliteDataReader reader)
205 while (true) {
206 try {
207 return reader.Read ();
208 } catch (SqliteBusyException ex) {
209 Thread.Sleep (50);
214 private void Insert (Uri uri, string filename)
216 lock (connection) {
217 MaybeStartTransaction_Unlocked ();
218 DoNonQuery ("INSERT OR REPLACE INTO uri_index (uri, filename) VALUES ('{0}', '{1}')",
219 UriToString (uri), filename);
223 private string LookupPathRawUnlocked (Uri uri, bool create_if_not_found)
225 SqliteCommand command;
226 SqliteDataReader reader = null;
227 string path = null;
229 command = NewCommand ("SELECT filename FROM uri_index WHERE uri='{0}'",
230 UriToString (uri));
231 reader = ExecuteReaderOrWait (command);
232 if (ReadOrWait (reader))
233 path = reader.GetString (0);
234 reader.Close ();
235 command.Dispose ();
237 if (path == null && create_if_not_found) {
238 string guid = Guid.NewGuid ().ToString ();
239 path = Path.Combine (guid.Substring (0, 2), guid.Substring (2));
240 Insert (uri, path);
243 if (path == SELF_CACHE_TAG)
244 return SELF_CACHE_TAG;
246 return path != null ? Path.Combine (text_cache_dir, path) : null;
249 // Don't do this unless you know what you are doing! If you
250 // do anything to the path you get back other than open and
251 // read the file, you will almost certainly break something.
252 // And it will be evidence that you are a bad person and that
253 // you deserve whatever horrible fate befalls you.
254 public string LookupPathRaw (Uri uri)
256 lock (connection)
257 return LookupPathRawUnlocked (uri, false);
260 private string LookupPath (Uri uri, bool create_if_not_found)
262 lock (connection) {
263 string path = LookupPathRawUnlocked (uri, create_if_not_found);
264 if (path == SELF_CACHE_TAG) {
265 // FIXME: How do we handle URI remapping for self-cached items?
266 #if false
267 if (uri_remapper != null)
268 uri = uri_remapper (uri);
269 #endif
270 if (! uri.IsFile) {
271 string msg = String.Format ("Non-file uri {0} flagged as self-cached", uri);
272 throw new Exception (msg);
274 return uri.LocalPath;
276 return path;
280 public void MarkAsSelfCached (Uri uri)
282 lock (connection)
283 Insert (uri, SELF_CACHE_TAG);
286 private bool world_readable = false;
287 public bool WorldReadable {
288 get { return this.world_readable; }
289 set { this.world_readable = value; }
292 public TextWriter GetWriter (Uri uri)
294 // FIXME: Uri remapping?
295 string path = LookupPath (uri, true);
297 FileStream fs;
298 fs = new FileStream (path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
300 // We don't expect to need this again in the near future.
301 FileAdvise.FlushCache (fs);
303 GZipOutputStream stream;
304 stream = new GZipOutputStream (fs);
306 if (! world_readable) {
307 // Make files only readable by the owner.
308 Mono.Unix.Native.Syscall.chmod (path, (Mono.Unix.Native.FilePermissions) 384);
311 StreamWriter writer;
312 writer = new StreamWriter (stream);
313 return writer;
316 public void WriteFromReader (Uri uri, TextReader reader)
318 TextWriter writer;
319 writer = GetWriter (uri);
320 string line;
321 while ((line = reader.ReadLine ()) != null)
322 writer.WriteLine (line);
323 writer.Close ();
326 public void WriteFromString (Uri uri, string str)
328 if (str == null) {
329 Delete (uri);
330 return;
333 TextWriter writer;
334 writer = GetWriter (uri);
335 writer.WriteLine (str);
336 writer.Close ();
339 // FIXME: Uri remapping?
340 public TextReader GetReader (Uri uri)
342 string path = LookupPath (uri, false);
343 if (path == null)
344 return null;
346 return GetReader (path);
349 public TextReader GetReader (string path)
351 FileStream file_stream;
352 try {
353 file_stream = new FileStream (path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
354 } catch (FileNotFoundException ex) {
355 return null;
358 StreamReader reader = null;
359 try {
360 Stream stream = new GZipInputStream (file_stream);
361 reader = new StreamReader (stream);
363 // This throws an exception if the file isn't compressed as follows:
364 // 1.) IOException on older versions of SharpZipLib
365 // 2.) GZipException on newer versions of SharpZipLib
366 // FIXME: Change this to GZipException when we depend
367 // on a higer version of SharpZipLib
368 reader.Peek ();
369 } catch (Exception ex) {
370 reader = new StreamReader (file_stream);
373 return reader;
376 public void Delete (Uri uri)
378 lock (connection) {
379 string path = LookupPathRawUnlocked (uri, false);
380 if (path != null) {
381 MaybeStartTransaction_Unlocked ();
382 DoNonQuery ("DELETE FROM uri_index WHERE uri='{0}' AND filename='{1}'",
383 UriToString (uri), path);
384 if (path != SELF_CACHE_TAG)
385 File.Delete (path);
390 private void MaybeStartTransaction_Unlocked ()
392 if (transaction_state == TransactionState.Requested)
393 DoNonQuery ("BEGIN");
394 transaction_state = TransactionState.Started;
397 public void BeginTransaction ()
399 lock (connection) {
400 if (transaction_state == TransactionState.None)
401 transaction_state = TransactionState.Requested;
405 public void CommitTransaction ()
407 lock (connection) {
408 if (transaction_state == TransactionState.Started)
409 DoNonQuery ("COMMIT");
410 transaction_state = TransactionState.None;