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
);
287 public bool Write (FileAttributes fa
)
289 SetPathFlag (fa
.Path
);
291 // We need to quote any 's that appear in the strings
292 // (in particular, in the path)
295 // If a transaction has been requested, start it now.
296 MaybeStartTransaction ();
299 filter_name
= fa
.FilterName
;
300 if (filter_name
== null)
302 filter_name
= filter_name
.Replace ("'", "''");
304 SqliteUtils
.DoNonQuery (connection
,
305 "INSERT OR REPLACE INTO file_attributes " +
306 " (unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version) " +
307 " VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}')",
308 GuidFu
.ToShortString (fa
.UniqueId
),
309 fa
.Directory
.Replace ("'", "''"), fa
.Filename
.Replace ("'", "''"),
310 StringFu
.DateTimeToString (fa
.LastWriteTime
),
311 StringFu
.DateTimeToString (fa
.LastAttrTime
),
318 public void Drop (string path
)
320 // Sanitize the path; remove the last '/'
321 if (path
!= null && path
!= "/" && path
.EndsWith ("/"))
322 path
= path
.TrimEnd ('/');
324 // We don't want to "UnSetPathFlag" here, since we have no way of knowing
325 // if another path hashes to the same value as this one.
327 // We need to quote any 's that appear in the strings
328 // (in particular, in the path)
329 string directory
= FileSystem
.GetDirectoryNameRootOk (path
).Replace ("'", "''");
330 string filename
= Path
.GetFileName (path
).Replace ("'", "''");
333 // If a transaction has been requested, start it now.
334 MaybeStartTransaction ();
336 SqliteUtils
.DoNonQuery (connection
,
337 "DELETE FROM file_attributes WHERE directory='{0}' AND filename='{1}'",
338 directory
, filename
);
342 private void MaybeStartTransaction ()
344 if (transaction_state
== TransactionState
.Requested
) {
345 SqliteUtils
.DoNonQuery (connection
, "BEGIN");
346 transaction_state
= TransactionState
.Started
;
350 public void BeginTransaction ()
352 if (transaction_state
== TransactionState
.None
)
353 transaction_state
= TransactionState
.Requested
;
356 public void CommitTransaction ()
358 if (transaction_state
== TransactionState
.Started
) {
360 SqliteUtils
.DoNonQuery (connection
, "COMMIT");
362 transaction_state
= TransactionState
.None
;
368 if (transaction_count
> 0) {
369 Logger
.Log
.Debug ("Flushing requested -- committing sqlite transaction");
370 SqliteUtils
.DoNonQuery (connection
, "COMMIT");
371 transaction_count
= 0;
376 ///////////////////////////////////////////////////////////////////
378 // Return all attributes in the attributes database, used for merging
380 private ICollection
ReadAllAttributes ()
382 ArrayList attributes
= new ArrayList ();
384 SqliteCommand command
;
385 SqliteDataReader reader
;
388 command
= new SqliteCommand ();
389 command
.Connection
= connection
;
390 command
.CommandText
=
391 "SELECT unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version " +
392 "FROM file_attributes";
394 reader
= SqliteUtils
.ExecuteReaderOrWait (command
);
396 while (SqliteUtils
.ReadOrWait (reader
)) {
397 attributes
.Add (GetFromReader (reader
));
406 // FIXME: Might wanna do this a bit more intelligently
408 public void Merge (FileAttributesStore_Sqlite fa_sqlite_store_to_merge
)
410 ICollection attributes
= fa_sqlite_store_to_merge
.ReadAllAttributes ();
414 foreach (FileAttributes attribute
in attributes
)
417 CommitTransaction ();