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.
29 using System
.Collections
;
31 using System
.Threading
;
33 using Mono
.Data
.SqliteClient
;
34 using ICSharpCode
.SharpZipLib
.GZip
;
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
{
54 private TransactionState transaction_state
;
56 private static TextCache user_cache
= null;
58 public static TextCache UserCache
{
60 if (user_cache
== null)
61 user_cache
= new TextCache (PathFinder
.StorageDir
);
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");
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
))
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.
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'";
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;
131 if (connection
!= null)
132 connection
.Dispose ();
135 throw new UnauthorizedAccessException (String
.Format ("Unable to create read only text cache {0}", db_filename
));
137 File
.Delete (db_filename
);
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 " +
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
;
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
);
173 private void DoNonQuery (string format
, params object [] args
)
176 Logger
.Log
.Debug ("Executing command '{0}'", String
.Format (format
, args
));
177 SqliteCommand command
= NewCommand (format
, args
);
180 command
.ExecuteNonQuery ();
182 } catch (SqliteBusyException ex
) {
183 // FIXME: should we eventually time out?
190 private SqliteDataReader
ExecuteReaderOrWait (SqliteCommand command
)
192 SqliteDataReader reader
= null;
193 while (reader
== null) {
195 reader
= command
.ExecuteReader ();
196 } catch (SqliteBusyException ex
) {
203 private bool ReadOrWait (SqliteDataReader reader
)
207 return reader
.Read ();
208 } catch (SqliteBusyException ex
) {
214 private void Insert (Uri uri
, string filename
)
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;
229 command
= NewCommand ("SELECT filename FROM uri_index WHERE uri='{0}'",
231 reader
= ExecuteReaderOrWait (command
);
232 if (ReadOrWait (reader
))
233 path
= reader
.GetString (0);
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));
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
)
257 return LookupPathRawUnlocked (uri
, false);
260 private string LookupPath (Uri uri
, bool create_if_not_found
)
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?
267 if (uri_remapper
!= null)
268 uri
= uri_remapper (uri
);
271 string msg
= String
.Format ("Non-file uri {0} flagged as self-cached", uri
);
272 throw new Exception (msg
);
274 return uri
.LocalPath
;
280 public void MarkAsSelfCached (Uri uri
)
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);
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);
312 writer
= new StreamWriter (stream
);
316 public void WriteFromReader (Uri uri
, TextReader reader
)
319 writer
= GetWriter (uri
);
321 while ((line
= reader
.ReadLine ()) != null)
322 writer
.WriteLine (line
);
326 public void WriteFromString (Uri uri
, string str
)
334 writer
= GetWriter (uri
);
335 writer
.WriteLine (str
);
339 // FIXME: Uri remapping?
340 public TextReader
GetReader (Uri uri
)
342 string path
= LookupPath (uri
, false);
346 return GetReader (path
);
349 public TextReader
GetReader (string path
)
351 FileStream file_stream
;
353 file_stream
= new FileStream (path
, FileMode
.Open
, FileAccess
.Read
, FileShare
.ReadWrite
);
354 } catch (FileNotFoundException ex
) {
358 StreamReader reader
= null;
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
369 } catch (Exception ex
) {
370 reader
= new StreamReader (file_stream
);
376 public void Delete (Uri uri
)
379 string path
= LookupPathRawUnlocked (uri
, false);
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
)
390 private void MaybeStartTransaction_Unlocked ()
392 if (transaction_state
== TransactionState
.Requested
)
393 DoNonQuery ("BEGIN");
394 transaction_state
= TransactionState
.Started
;
397 public void BeginTransaction ()
400 if (transaction_state
== TransactionState
.None
)
401 transaction_state
= TransactionState
.Requested
;
405 public void CommitTransaction ()
408 if (transaction_state
== TransactionState
.Started
)
409 DoNonQuery ("COMMIT");
410 transaction_state
= TransactionState
.None
;