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
= SqliteUtils
.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
);
140 connection
= Open (db_filename
);
141 } catch (Exception e
) {
142 Log
.Debug (e
, "Exception opening text cache {0}", db_filename
);
145 SqliteUtils
.DoNonQuery (connection
,
146 "CREATE TABLE uri_index ( " +
147 " uri STRING UNIQUE NOT NULL, " +
148 " filename STRING NOT NULL " +
153 private SqliteConnection
Open (string db_filename
)
155 SqliteConnection connection
= new SqliteConnection ();
156 connection
.ConnectionString
= "version=" + ExternalStringsHack
.SqliteVersion
157 + ",encoding=UTF-8,URI=file:" + db_filename
;
162 private static string UriToString (Uri uri
)
164 return UriFu
.UriToEscapedString (uri
).Replace ("'", "''");
167 private SqliteCommand
NewCommand (string format
, params object [] args
)
169 SqliteCommand command
;
170 command
= new SqliteCommand ();
171 command
.Connection
= connection
;
172 command
.CommandText
= String
.Format (format
, args
);
176 private void Insert (Uri uri
, string filename
)
179 MaybeStartTransaction_Unlocked ();
180 SqliteUtils
.DoNonQuery (connection
,
181 "INSERT OR REPLACE INTO uri_index (uri, filename) VALUES ('{0}', '{1}')",
182 UriToString (uri
), filename
);
186 private string LookupPathRawUnlocked (Uri uri
, bool create_if_not_found
)
188 SqliteCommand command
;
189 SqliteDataReader reader
= null;
192 command
= NewCommand ("SELECT filename FROM uri_index WHERE uri='{0}'",
194 reader
= SqliteUtils
.ExecuteReaderOrWait (command
);
195 if (SqliteUtils
.ReadOrWait (reader
))
196 path
= reader
.GetString (0);
200 if (path
== null && create_if_not_found
) {
201 string guid
= Guid
.NewGuid ().ToString ();
202 path
= Path
.Combine (guid
.Substring (0, 2), guid
.Substring (2));
206 if (path
== SELF_CACHE_TAG
)
207 return SELF_CACHE_TAG
;
209 return path
!= null ? Path
.Combine (text_cache_dir
, path
) : null;
212 // Don't do this unless you know what you are doing! If you
213 // do anything to the path you get back other than open and
214 // read the file, you will almost certainly break something.
215 // And it will be evidence that you are a bad person and that
216 // you deserve whatever horrible fate befalls you.
217 public string LookupPathRaw (Uri uri
)
220 return LookupPathRawUnlocked (uri
, false);
223 private string LookupPath (Uri uri
, bool create_if_not_found
)
226 string path
= LookupPathRawUnlocked (uri
, create_if_not_found
);
227 if (path
== SELF_CACHE_TAG
) {
228 // FIXME: How do we handle URI remapping for self-cached items?
230 if (uri_remapper
!= null)
231 uri
= uri_remapper (uri
);
234 string msg
= String
.Format ("Non-file uri {0} flagged as self-cached", uri
);
235 throw new Exception (msg
);
237 return uri
.LocalPath
;
243 public void MarkAsSelfCached (Uri uri
)
246 Insert (uri
, SELF_CACHE_TAG
);
249 private bool world_readable
= false;
250 public bool WorldReadable
{
251 get { return this.world_readable; }
252 set { this.world_readable = value; }
255 public TextWriter
GetWriter (Uri uri
)
257 // FIXME: Uri remapping?
258 string path
= LookupPath (uri
, true);
261 fs
= new FileStream (path
, FileMode
.Create
, FileAccess
.Write
, FileShare
.ReadWrite
);
263 // We don't expect to need this again in the near future.
264 FileAdvise
.FlushCache (fs
);
266 GZipOutputStream stream
;
267 stream
= new GZipOutputStream (fs
);
269 if (! world_readable
) {
270 // Make files only readable by the owner.
271 Mono
.Unix
.Native
.Syscall
.chmod (path
, (Mono
.Unix
.Native
.FilePermissions
) 384);
275 writer
= new StreamWriter (stream
);
279 public void WriteFromReader (Uri uri
, TextReader reader
)
282 writer
= GetWriter (uri
);
284 while ((line
= reader
.ReadLine ()) != null)
285 writer
.WriteLine (line
);
289 public void WriteFromString (Uri uri
, string str
)
297 writer
= GetWriter (uri
);
298 writer
.WriteLine (str
);
302 // FIXME: Uri remapping?
303 public TextReader
GetReader (Uri uri
)
305 string path
= LookupPath (uri
, false);
309 return GetReader (path
);
312 public TextReader
GetReader (string path
)
314 FileStream file_stream
;
316 file_stream
= new FileStream (path
, FileMode
.Open
, FileAccess
.Read
, FileShare
.ReadWrite
);
317 } catch (FileNotFoundException ex
) {
321 StreamReader reader
= null;
323 Stream stream
= new GZipInputStream (file_stream
);
324 reader
= new StreamReader (stream
);
326 // This throws an exception if the file isn't compressed as follows:
327 // 1.) IOException on older versions of SharpZipLib
328 // 2.) GZipException on newer versions of SharpZipLib
329 // FIXME: Change this to GZipException when we depend
330 // on a higer version of SharpZipLib
332 } catch (Exception ex
) {
333 reader
= new StreamReader (file_stream
);
339 public void Delete (Uri uri
)
342 string path
= LookupPathRawUnlocked (uri
, false);
344 MaybeStartTransaction_Unlocked ();
345 SqliteUtils
.DoNonQuery (connection
,
346 "DELETE FROM uri_index WHERE uri='{0}' AND filename='{1}'",
347 UriToString (uri
), path
);
348 if (path
!= SELF_CACHE_TAG
)
354 private void MaybeStartTransaction_Unlocked ()
356 if (transaction_state
== TransactionState
.Requested
)
357 SqliteUtils
.DoNonQuery (connection
, "BEGIN");
358 transaction_state
= TransactionState
.Started
;
361 public void BeginTransaction ()
364 if (transaction_state
== TransactionState
.None
)
365 transaction_state
= TransactionState
.Requested
;
369 public void CommitTransaction ()
372 if (transaction_state
== TransactionState
.Started
)
373 SqliteUtils
.DoNonQuery (connection
, "COMMIT");
374 transaction_state
= TransactionState
.None
;