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
;
37 namespace Beagle
.Daemon
{
39 public class TextCache
{
41 static public bool Debug
= false;
43 public const string SELF_CACHE_TAG
= "*self*";
45 private string text_cache_dir
;
46 private SqliteConnection connection
;
48 private enum TransactionState
{
53 private TransactionState transaction_state
;
55 private static TextCache user_cache
= null;
57 public static TextCache UserCache
{
59 if (user_cache
== null)
60 user_cache
= new TextCache (PathFinder
.StorageDir
);
66 public TextCache (string storage_dir
) : this (storage_dir
, false) { }
68 public TextCache (string storage_dir
, bool read_only
)
70 text_cache_dir
= Path
.Combine (storage_dir
, "TextCache");
71 if (! Directory
.Exists (text_cache_dir
)) {
72 Directory
.CreateDirectory (text_cache_dir
);
74 // Create our cache subdirectories.
75 for (int i
= 0; i
< 256; ++i
) {
76 string subdir
= i
.ToString ("x");
78 subdir
= "0" + subdir
;
79 subdir
= Path
.Combine (text_cache_dir
, subdir
);
80 Directory
.CreateDirectory (subdir
);
84 // Create our Sqlite database
85 string db_filename
= Path
.Combine (text_cache_dir
, "TextCache.db");
86 bool create_new_db
= false;
87 if (! File
.Exists (db_filename
))
90 // Funky logic here to deal with sqlite versions.
92 // When sqlite 3 tries to open an sqlite 2 database,
93 // it will throw an SqliteException with SqliteError
94 // NOTADB when trying to execute a command.
96 // When sqlite 2 tries to open an sqlite 3 database,
97 // it will throw an ApplicationException when it
98 // tries to open the database.
101 connection
= Open (db_filename
);
102 } catch (ApplicationException
) {
103 Logger
.Log
.Warn ("Likely sqlite database version mismatch trying to open {0}. Purging.", db_filename
);
104 create_new_db
= true;
107 if (!create_new_db
) {
108 // Run a dummy query to see if we get a NOTADB error. Sigh.
109 SqliteCommand command
;
110 SqliteDataReader reader
= null;
112 command
= new SqliteCommand ();
113 command
.Connection
= connection
;
114 command
.CommandText
=
115 "SELECT filename FROM uri_index WHERE uri='blah'";
118 reader
= ExecuteReaderOrWait (command
);
119 } catch (ApplicationException ex
) {
120 Logger
.Log
.Warn ("Likely sqlite database version mismatch trying to read from {0}. Purging.", db_filename
);
121 create_new_db
= true;
130 if (connection
!= null)
131 connection
.Dispose ();
134 throw new UnauthorizedAccessException (String
.Format ("Unable to create read only text cache {0}", db_filename
));
136 File
.Delete (db_filename
);
137 connection
= Open (db_filename
);
139 DoNonQuery ("CREATE TABLE uri_index ( " +
140 " uri STRING UNIQUE NOT NULL, " +
141 " filename STRING NOT NULL " +
146 private SqliteConnection
Open (string db_filename
)
148 SqliteConnection connection
= new SqliteConnection ();
149 connection
.ConnectionString
= "version=" + ExternalStringsHack
.SqliteVersion
150 + ",encoding=UTF-8,URI=file:" + db_filename
;
155 private static string UriToString (Uri uri
)
157 return UriFu
.UriToSerializableString (uri
).Replace ("'", "''");
160 private SqliteCommand
NewCommand (string format
, params object [] args
)
162 SqliteCommand command
;
163 command
= new SqliteCommand ();
164 command
.Connection
= connection
;
165 command
.CommandText
= String
.Format (format
, args
);
169 private void DoNonQuery (string format
, params object [] args
)
172 Logger
.Log
.Debug ("Executing command '{0}'", String
.Format (format
, args
));
173 SqliteCommand command
= NewCommand (format
, args
);
176 command
.ExecuteNonQuery ();
178 } catch (SqliteBusyException ex
) {
179 // FIXME: should we eventually time out?
186 private SqliteDataReader
ExecuteReaderOrWait (SqliteCommand command
)
188 SqliteDataReader reader
= null;
189 while (reader
== null) {
191 reader
= command
.ExecuteReader ();
192 } catch (SqliteBusyException ex
) {
199 private bool ReadOrWait (SqliteDataReader reader
)
203 return reader
.Read ();
204 } catch (SqliteBusyException ex
) {
210 private void Insert (Uri uri
, string filename
)
213 MaybeStartTransaction_Unlocked ();
214 DoNonQuery ("INSERT OR REPLACE INTO uri_index (uri, filename) VALUES ('{0}', '{1}')",
215 UriToString (uri
), filename
);
219 private string LookupPathRawUnlocked (Uri uri
, bool create_if_not_found
)
221 SqliteCommand command
;
222 SqliteDataReader reader
= null;
225 command
= NewCommand ("SELECT filename FROM uri_index WHERE uri='{0}'",
227 reader
= ExecuteReaderOrWait (command
);
228 if (ReadOrWait (reader
))
229 path
= reader
.GetString (0);
233 if (path
== null && create_if_not_found
) {
234 string guid
= Guid
.NewGuid ().ToString ();
235 path
= Path
.Combine (guid
.Substring (0, 2), guid
.Substring (2));
239 if (path
== SELF_CACHE_TAG
)
240 return SELF_CACHE_TAG
;
242 return path
!= null ? Path
.Combine (text_cache_dir
, path
) : null;
245 // Don't do this unless you know what you are doing! If you
246 // do anything to the path you get back other than open and
247 // read the file, you will almost certainly break something.
248 // And it will be evidence that you are a bad person and that
249 // you deserve whatever horrible fate befalls you.
250 public string LookupPathRaw (Uri uri
)
253 return LookupPathRawUnlocked (uri
, false);
256 private string LookupPath (Uri uri
, bool create_if_not_found
)
259 string path
= LookupPathRawUnlocked (uri
, create_if_not_found
);
260 if (path
== SELF_CACHE_TAG
) {
261 // FIXME: How do we handle URI remapping for self-cached items?
263 if (uri_remapper
!= null)
264 uri
= uri_remapper (uri
);
267 string msg
= String
.Format ("Non-file uri {0} flagged as self-cached", uri
);
268 throw new Exception (msg
);
270 return uri
.LocalPath
;
276 public void MarkAsSelfCached (Uri uri
)
279 Insert (uri
, SELF_CACHE_TAG
);
282 private bool world_readable
= false;
283 public bool WorldReadable
{
284 get { return this.world_readable; }
285 set { this.world_readable = value; }
288 public TextWriter
GetWriter (Uri uri
)
290 // FIXME: Uri remapping?
291 string path
= LookupPath (uri
, true);
294 stream
= new FileStream (path
, FileMode
.Create
, FileAccess
.Write
, FileShare
.ReadWrite
);
296 // We don't expect to need this again in the near future.
297 FileAdvise
.FlushCache (stream
);
299 if (! world_readable
) {
300 // Make files only readable by the owner.
301 Mono
.Unix
.Native
.Syscall
.chmod (path
, (Mono
.Unix
.Native
.FilePermissions
) 384);
305 writer
= new StreamWriter (stream
);
309 public void WriteFromReader (Uri uri
, TextReader reader
)
312 writer
= GetWriter (uri
);
314 while ((line
= reader
.ReadLine ()) != null)
315 writer
.WriteLine (line
);
319 public void WriteFromString (Uri uri
, string str
)
327 writer
= GetWriter (uri
);
328 writer
.WriteLine (str
);
332 // FIXME: Uri remapping?
333 public TextReader
GetReader (Uri uri
)
335 string path
= LookupPath (uri
, false);
341 stream
= new FileStream (path
, FileMode
.Open
, FileAccess
.Read
, FileShare
.ReadWrite
);
342 } catch (FileNotFoundException ex
) {
347 reader
= new StreamReader (stream
);
351 public void Delete (Uri uri
)
354 string path
= LookupPathRawUnlocked (uri
, false);
356 MaybeStartTransaction_Unlocked ();
357 DoNonQuery ("DELETE FROM uri_index WHERE uri='{0}' AND filename='{1}'",
358 UriToString (uri
), path
);
359 if (path
!= SELF_CACHE_TAG
)
365 private void MaybeStartTransaction_Unlocked ()
367 if (transaction_state
== TransactionState
.Requested
)
368 DoNonQuery ("BEGIN");
369 transaction_state
= TransactionState
.Started
;
372 public void BeginTransaction ()
375 if (transaction_state
== TransactionState
.None
)
376 transaction_state
= TransactionState
.Requested
;
380 public void CommitTransaction ()
383 if (transaction_state
== TransactionState
.Started
)
384 DoNonQuery ("COMMIT");
385 transaction_state
= TransactionState
.None
;