2 // FileAttributesStore_Sqlite.cs
4 // Copyright (C) 2004 Novell, Inc.
8 // Permission is hereby granted, free of charge, to any person obtaining a copy
9 // of this software and associated documentation files (the "Software"), to deal
10 // in the Software without restriction, including without limitation the rights
11 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 // copies of the Software, and to permit persons to whom the Software is
13 // furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in all
16 // 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 FROM,
23 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 using System
.Collections
;
30 using System
.Threading
;
32 using Mono
.Data
.SqliteClient
;
36 namespace Beagle
.Daemon
{
38 public class FileAttributesStore_Sqlite
: IFileAttributesStore
{
41 // 1: Original version
42 // 2: Replaced LastIndexedTime with LastAttrTime
43 const int VERSION
= 2;
45 private SqliteConnection connection
;
46 private BitArray path_flags
;
47 private int transaction_count
= 0;
49 enum TransactionState
{
54 private TransactionState transaction_state
;
56 public FileAttributesStore_Sqlite (string directory
, string index_fingerprint
)
58 bool create_new_db
= false;
59 path_flags
= new BitArray (65536);
61 if (! File
.Exists (GetDbPath (directory
))) {
65 // Funky logic here to deal with sqlite versions.
67 // When sqlite 3 tries to open an sqlite 2 database,
68 // it will throw an SqliteException with SqliteError
69 // NOTADB when trying to execute a command.
71 // When sqlite 2 tries to open an sqlite 3 database,
72 // it will throw an ApplicationException when it
73 // tries to open the database.
76 connection
= Open (directory
);
77 } catch (ApplicationException
) {
78 Logger
.Log
.Warn ("Likely sqlite database version mismatch trying to open {0}. Purging.", GetDbPath (directory
));
82 if (! create_new_db
) {
83 SqliteCommand command
;
84 SqliteDataReader reader
= null;
85 int stored_version
= 0;
86 string stored_fingerprint
= null;
89 command
= new SqliteCommand ();
90 command
.Connection
= connection
;
92 "SELECT version, fingerprint FROM db_info";
94 reader
= ExecuteReaderOrWait (command
);
95 } catch (Exception ex
) {
96 Logger
.Log
.Warn ("Likely sqlite database version mismatch trying to read from {0}. Purging.", GetDbPath (directory
));
99 if (reader
!= null && ! create_new_db
) {
100 if (ReadOrWait (reader
)) {
101 stored_version
= reader
.GetInt32 (0);
102 stored_fingerprint
= reader
.GetString (1);
108 if (VERSION
!= stored_version
109 || (index_fingerprint
!= null && index_fingerprint
!= stored_fingerprint
))
110 create_new_db
= true;
115 if (connection
!= null)
116 connection
.Dispose ();
117 File
.Delete (GetDbPath (directory
));
118 connection
= Open (directory
);
120 DoNonQuery ("CREATE TABLE db_info ( " +
121 " version INTEGER NOT NULL, " +
122 " fingerprint STRING NOT NULL " +
125 DoNonQuery ("INSERT INTO db_info (version, fingerprint) VALUES ({0}, '{1}')",
126 VERSION
, index_fingerprint
);
128 DoNonQuery ("CREATE TABLE file_attributes ( " +
129 " unique_id STRING UNIQUE, " +
130 " directory STRING NOT NULL, " +
131 " filename STRING NOT NULL, " +
132 " last_mtime STRING NOT NULL, " +
133 " last_attrtime STRING NOT NULL, " +
134 " filter_name STRING NOT NULL, " +
135 " filter_version STRING NOT NULL " +
138 SqliteCommand command
;
139 SqliteDataReader reader
;
142 DateTime dt1
= DateTime
.Now
;
144 // Select all of the files and use them to populate our bit-vector.
145 command
= new SqliteCommand ();
146 command
.Connection
= connection
;
147 command
.CommandText
= "SELECT directory, filename FROM file_attributes";
149 reader
= ExecuteReaderOrWait (command
);
151 while (ReadOrWait (reader
)) {
153 string dir
= reader
.GetString (0);
154 string file
= reader
.GetString (1);
155 string path
= Path
.Combine (dir
, file
);
163 DateTime dt2
= DateTime
.Now
;
165 Logger
.Log
.Debug ("Loaded {0} records from {1} in {2:0.000}s",
166 count
, GetDbPath (directory
), (dt2
- dt1
).TotalSeconds
);
170 ///////////////////////////////////////////////////////////////////
172 private string GetDbPath (string directory
)
174 return Path
.Combine (directory
, "FileAttributesStore.db");
177 private SqliteConnection
Open (string directory
)
180 c
= new SqliteConnection ();
181 c
.ConnectionString
= "version=" + ExternalStringsHack
.SqliteVersion
182 + ",encoding=UTF-8,URI=file:" + GetDbPath (directory
);
187 private void DoNonQuery (string format
, params object [] args
)
189 SqliteCommand command
;
190 command
= new SqliteCommand ();
191 command
.Connection
= connection
;
192 command
.CommandText
= String
.Format (format
, args
);
196 command
.ExecuteNonQuery ();
198 } catch (SqliteBusyException ex
) {
206 private SqliteCommand
QueryCommand (string where_format
, params object [] where_args
)
208 SqliteCommand command
;
209 command
= new SqliteCommand ();
210 command
.Connection
= connection
;
211 command
.CommandText
=
212 "SELECT unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version " +
213 "FROM file_attributes WHERE " +
214 String
.Format (where_format
, where_args
);
218 static private SqliteDataReader
ExecuteReaderOrWait (SqliteCommand command
)
220 SqliteDataReader reader
= null;
221 while (reader
== null) {
223 reader
= command
.ExecuteReader ();
224 } catch (SqliteBusyException ex
) {
231 static private bool ReadOrWait (SqliteDataReader reader
)
235 return reader
.Read ();
236 } catch (SqliteBusyException ex
) {
242 private FileAttributes
GetFromReader (SqliteDataReader reader
)
244 FileAttributes attr
= new FileAttributes ();
246 attr
.UniqueId
= GuidFu
.FromShortString (reader
.GetString (0));
247 attr
.Path
= System
.IO
.Path
.Combine (reader
.GetString (1), reader
.GetString (2));
248 attr
.LastWriteTime
= StringFu
.StringToDateTime (reader
.GetString (3));
249 attr
.LastAttrTime
= StringFu
.StringToDateTime (reader
.GetString (4));
250 attr
.FilterName
= reader
.GetString (5);
251 attr
.FilterVersion
= int.Parse (reader
.GetString (6));
253 if (attr
.FilterName
== "")
254 attr
.FilterName
= null;
259 ///////////////////////////////////////////////////////////////////
261 private int GetPathHash (string path
)
263 uint hash
= 0xdeadbeef;
264 foreach (char c
in path
)
265 hash
= 17 * hash
+ (uint) c
;
266 // Fold the 32 bits in 16.
267 return (int) ((hash
& 0xffff) ^
(hash
>> 16));
270 private bool GetPathFlag (string path
)
272 int hash
= GetPathHash (path
);
273 return path_flags
[hash
];
276 private void SetPathFlag (string path
)
278 int hash
= GetPathHash (path
);
279 path_flags
[hash
] = true;
282 ///////////////////////////////////////////////////////////////////
284 public FileAttributes
Read (string path
)
286 SqliteCommand command
;
287 SqliteDataReader reader
;
289 if (! GetPathFlag (path
))
292 FileAttributes attr
= null;
293 bool found_too_many
= false;
295 // We need to quote any 's that appear in the strings
296 // (int particular, in the path)
297 string directory
= FileSystem
.GetDirectoryNameRootOk (path
).Replace ("'", "''");
298 string filename
= Path
.GetFileName (path
).Replace ("'", "''");
300 command
= QueryCommand ("directory='{0}' AND filename='{1}'",
301 directory
, filename
);
302 reader
= ExecuteReaderOrWait (command
);
304 if (ReadOrWait (reader
)) {
305 attr
= GetFromReader (reader
);
307 if (ReadOrWait (reader
))
308 found_too_many
= true;
313 // If we found more than one matching record for a given
314 // directory and filename, something has gone wrong.
315 // Since we have no way of knowing which one is correct
316 // and which isn't, we delete them all and return
317 // null. (Which in most cases will force a re-index.
318 if (found_too_many
) {
319 DoNonQuery ("DELETE FROM file_attributes WHERE directory='{0}' AND filename='{1}'",
320 directory
, filename
);
327 public bool Write (FileAttributes fa
)
329 SetPathFlag (fa
.Path
);
331 // We need to quote any 's that appear in the strings
332 // (in particular, in the path)
335 // If a transaction has been requested, start it now.
336 MaybeStartTransaction ();
339 filter_name
= fa
.FilterName
;
340 if (filter_name
== null)
342 filter_name
= filter_name
.Replace ("'", "''");
344 DoNonQuery ("INSERT OR REPLACE INTO file_attributes " +
345 " (unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version) " +
346 " VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}')",
347 GuidFu
.ToShortString (fa
.UniqueId
),
348 fa
.Directory
.Replace ("'", "''"), fa
.Filename
.Replace ("'", "''"),
349 StringFu
.DateTimeToString (fa
.LastWriteTime
),
350 StringFu
.DateTimeToString (fa
.LastAttrTime
),
357 public void Drop (string path
)
359 // We don't want to "UnSetPathFlag" here, since we have no way of knowing
360 // if another path hashes to the same value as this one.
362 // We need to quote any 's that appear in the strings
363 // (in particular, in the path)
364 string directory
= FileSystem
.GetDirectoryNameRootOk (path
).Replace ("'", "''");
365 string filename
= Path
.GetFileName (path
).Replace ("'", "''");
368 // If a transaction has been requested, start it now.
369 MaybeStartTransaction ();
371 DoNonQuery ("DELETE FROM file_attributes WHERE directory='{0}' AND filename='{1}'",
372 directory
, filename
);
376 private void MaybeStartTransaction ()
378 if (transaction_state
== TransactionState
.Requested
) {
379 DoNonQuery ("BEGIN");
380 transaction_state
= TransactionState
.Started
;
384 public void BeginTransaction ()
386 if (transaction_state
== TransactionState
.None
)
387 transaction_state
= TransactionState
.Requested
;
390 public void CommitTransaction ()
392 if (transaction_state
== TransactionState
.Started
) {
394 DoNonQuery ("COMMIT");
396 transaction_state
= TransactionState
.None
;
402 if (transaction_count
> 0) {
403 Logger
.Log
.Debug ("Flushing requested -- committing sqlite transaction");
404 DoNonQuery ("COMMIT");
405 transaction_count
= 0;
410 ///////////////////////////////////////////////////////////////////
412 // Return all attributes in the attributes database, used for merging
414 private ICollection
ReadAllAttributes ()
416 ArrayList attributes
= new ArrayList ();
418 SqliteCommand command
;
419 SqliteDataReader reader
;
422 command
= new SqliteCommand ();
423 command
.Connection
= connection
;
424 command
.CommandText
=
425 "SELECT unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version " +
426 "FROM file_attributes";
428 reader
= ExecuteReaderOrWait (command
);
430 while (ReadOrWait (reader
)) {
431 attributes
.Add (GetFromReader (reader
));
440 // FIXME: Might wanna do this a bit more intelligently
442 public void Merge (FileAttributesStore_Sqlite fa_sqlite_store_to_merge
)
444 ICollection attributes
= fa_sqlite_store_to_merge
.ReadAllAttributes ();
448 foreach (FileAttributes attribute
in attributes
)
451 CommitTransaction ();