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
= SqliteUtils
.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 (SqliteUtils
.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 SqliteUtils
.DoNonQuery (connection
,
121 "CREATE TABLE db_info ( " +
122 " version INTEGER NOT NULL, " +
123 " fingerprint STRING NOT NULL " +
126 SqliteUtils
.DoNonQuery (connection
,
127 "INSERT INTO db_info (version, fingerprint) VALUES ({0}, '{1}')",
128 VERSION
, index_fingerprint
);
130 SqliteUtils
.DoNonQuery (connection
,
131 "CREATE TABLE file_attributes ( " +
132 " unique_id STRING UNIQUE, " +
133 " directory STRING NOT NULL, " +
134 " filename STRING NOT NULL, " +
135 " last_mtime STRING NOT NULL, " +
136 " last_attrtime STRING NOT NULL, " +
137 " filter_name STRING NOT NULL, " +
138 " filter_version STRING NOT NULL " +
141 SqliteUtils
.DoNonQuery (connection
,
142 "CREATE INDEX file_path on file_attributes (" +
147 SqliteCommand command
;
148 SqliteDataReader reader
;
151 DateTime dt1
= DateTime
.Now
;
153 // Select all of the files and use them to populate our bit-vector.
154 command
= new SqliteCommand ();
155 command
.Connection
= connection
;
156 command
.CommandText
= "SELECT directory, filename FROM file_attributes";
158 reader
= SqliteUtils
.ExecuteReaderOrWait (command
);
160 while (SqliteUtils
.ReadOrWait (reader
)) {
162 string dir
= reader
.GetString (0);
163 string file
= reader
.GetString (1);
164 string path
= Path
.Combine (dir
, file
);
172 DateTime dt2
= DateTime
.Now
;
174 Logger
.Log
.Debug ("Loaded {0} records from {1} in {2:0.000}s",
175 count
, GetDbPath (directory
), (dt2
- dt1
).TotalSeconds
);
179 ///////////////////////////////////////////////////////////////////
181 private string GetDbPath (string directory
)
183 return Path
.Combine (directory
, "FileAttributesStore.db");
186 private SqliteConnection
Open (string directory
)
189 c
= new SqliteConnection ();
190 c
.ConnectionString
= "version=" + ExternalStringsHack
.SqliteVersion
191 + ",encoding=UTF-8,URI=file:" + GetDbPath (directory
);
196 private FileAttributes
GetFromReader (SqliteDataReader reader
)
198 FileAttributes attr
= new FileAttributes ();
200 attr
.UniqueId
= GuidFu
.FromShortString (reader
.GetString (0));
201 attr
.Path
= System
.IO
.Path
.Combine (reader
.GetString (1), reader
.GetString (2));
202 attr
.LastWriteTime
= StringFu
.StringToDateTime (reader
.GetString (3));
203 attr
.LastAttrTime
= StringFu
.StringToDateTime (reader
.GetString (4));
204 attr
.FilterName
= reader
.GetString (5);
205 attr
.FilterVersion
= int.Parse (reader
.GetString (6));
207 if (attr
.FilterName
== "")
208 attr
.FilterName
= null;
213 ///////////////////////////////////////////////////////////////////
215 private int GetPathHash (string path
)
217 uint hash
= 0xdeadbeef;
218 foreach (char c
in path
)
219 hash
= 17 * hash
+ (uint) c
;
220 // Fold the 32 bits in 16.
221 return (int) ((hash
& 0xffff) ^
(hash
>> 16));
224 private bool GetPathFlag (string path
)
226 int hash
= GetPathHash (path
);
227 return path_flags
[hash
];
230 private void SetPathFlag (string path
)
232 int hash
= GetPathHash (path
);
233 path_flags
[hash
] = true;
236 ///////////////////////////////////////////////////////////////////
238 public FileAttributes
Read (string path
)
240 // Sanitize the path; remove the last '/'
241 if (path
!= null && path
!= "/" && path
.EndsWith ("/"))
242 path
= path
.TrimEnd ('/');
244 SqliteCommand command
;
245 SqliteDataReader reader
;
247 if (! GetPathFlag (path
))
250 FileAttributes attr
= null;
251 bool found_too_many
= false;
253 // We need to quote any 's that appear in the strings
254 // (int particular, in the path)
255 string directory
= FileSystem
.GetDirectoryNameRootOk (path
).Replace ("'", "''");
256 string filename
= Path
.GetFileName (path
).Replace ("'", "''");
258 command
= SqliteUtils
.QueryCommand (connection
,
259 "directory='{0}' AND filename='{1}'",
260 directory
, filename
);
261 reader
= SqliteUtils
.ExecuteReaderOrWait (command
);
263 if (SqliteUtils
.ReadOrWait (reader
)) {
264 attr
= GetFromReader (reader
);
266 if (SqliteUtils
.ReadOrWait (reader
))
267 found_too_many
= true;
272 // If we found more than one matching record for a given
273 // directory and filename, something has gone wrong.
274 // Since we have no way of knowing which one is correct
275 // and which isn't, we delete them all and return
276 // null. (Which in most cases will force a re-index.
277 if (found_too_many
) {
278 SqliteUtils
.DoNonQuery (connection
,
279 "DELETE FROM file_attributes WHERE directory='{0}' AND filename='{1}'",
280 directory
, filename
);
288 public bool Write (FileAttributes fa
)
290 SetPathFlag (fa
.Path
);
294 // We need to quote any 's that appear in the strings
295 // (in particular, in the path)
298 // If a transaction has been requested, start it now.
299 MaybeStartTransaction ();
301 filter_name
= fa
.FilterName
;
302 if (filter_name
== null)
304 filter_name
= filter_name
.Replace ("'", "''");
306 ret
= SqliteUtils
.DoNonQuery (connection
,
307 "INSERT OR REPLACE INTO file_attributes " +
308 " (unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version) " +
309 " VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}')",
310 GuidFu
.ToShortString (fa
.UniqueId
),
311 fa
.Directory
.Replace ("'", "''"), fa
.Filename
.Replace ("'", "''"),
312 StringFu
.DateTimeToString (fa
.LastWriteTime
),
313 StringFu
.DateTimeToString (fa
.LastAttrTime
),
321 public void Drop (string path
)
323 // Sanitize the path; remove the last '/'
324 if (path
!= null && path
!= "/" && path
.EndsWith ("/"))
325 path
= path
.TrimEnd ('/');
327 // We don't want to "UnSetPathFlag" here, since we have no way of knowing
328 // if another path hashes to the same value as this one.
330 // We need to quote any 's that appear in the strings
331 // (in particular, in the path)
332 string directory
= FileSystem
.GetDirectoryNameRootOk (path
).Replace ("'", "''");
333 string filename
= Path
.GetFileName (path
).Replace ("'", "''");
336 // If a transaction has been requested, start it now.
337 MaybeStartTransaction ();
339 SqliteUtils
.DoNonQuery (connection
,
340 "DELETE FROM file_attributes WHERE directory='{0}' AND filename='{1}'",
341 directory
, filename
);
345 private void MaybeStartTransaction ()
347 if (transaction_state
== TransactionState
.Requested
) {
348 SqliteUtils
.DoNonQuery (connection
, "BEGIN");
349 transaction_state
= TransactionState
.Started
;
353 public void BeginTransaction ()
355 if (transaction_state
== TransactionState
.None
)
356 transaction_state
= TransactionState
.Requested
;
359 public void CommitTransaction ()
361 if (transaction_state
== TransactionState
.Started
) {
363 SqliteUtils
.DoNonQuery (connection
, "COMMIT");
365 transaction_state
= TransactionState
.None
;
371 if (transaction_count
> 0) {
372 Logger
.Log
.Debug ("Flushing requested -- committing sqlite transaction");
373 SqliteUtils
.DoNonQuery (connection
, "COMMIT");
374 transaction_count
= 0;
379 ///////////////////////////////////////////////////////////////////
381 // Return all attributes in the attributes database, used for merging
383 private ICollection
ReadAllAttributes ()
385 ArrayList attributes
= new ArrayList ();
387 SqliteCommand command
;
388 SqliteDataReader reader
;
391 command
= new SqliteCommand ();
392 command
.Connection
= connection
;
393 command
.CommandText
=
394 "SELECT unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version " +
395 "FROM file_attributes";
397 reader
= SqliteUtils
.ExecuteReaderOrWait (command
);
399 while (SqliteUtils
.ReadOrWait (reader
)) {
400 attributes
.Add (GetFromReader (reader
));
409 // FIXME: Might wanna do this a bit more intelligently
411 public void Merge (FileAttributesStore_Sqlite fa_sqlite_store_to_merge
)
413 ICollection attributes
= fa_sqlite_store_to_merge
.ReadAllAttributes ();
417 foreach (FileAttributes attribute
in attributes
)
420 CommitTransaction ();