cvsimport
[beagle.git] / beagled / FileAttributesStore_Sqlite.cs
blob6cc8446e2946405bff010349eee2ef098a0d5f74
1 //
2 // FileAttributesStore_Sqlite.cs
3 //
4 // Copyright (C) 2004 Novell, Inc.
5 //
7 //
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
24 // SOFTWARE.
27 using System;
28 using System.Collections;
29 using System.IO;
30 using System.Threading;
32 using Mono.Data.SqliteClient;
34 using Beagle.Util;
36 namespace Beagle.Daemon {
38 public class FileAttributesStore_Sqlite : IFileAttributesStore {
40 // Version history:
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 {
50 None,
51 Requested,
52 Started
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))) {
62 create_new_db = true;
63 } else {
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.
75 try {
76 connection = Open (directory);
77 } catch (ApplicationException) {
78 Logger.Log.Warn ("Likely sqlite database version mismatch trying to open {0}. Purging.", GetDbPath (directory));
79 create_new_db = true;
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;
91 command.CommandText =
92 "SELECT version, fingerprint FROM db_info";
93 try {
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));
97 create_new_db = true;
99 if (reader != null && ! create_new_db) {
100 if (SqliteUtils.ReadOrWait (reader)) {
101 stored_version = reader.GetInt32 (0);
102 stored_fingerprint = reader.GetString (1);
104 reader.Close ();
106 command.Dispose ();
108 if (VERSION != stored_version
109 || (index_fingerprint != null && index_fingerprint != stored_fingerprint))
110 create_new_db = true;
114 if (create_new_db) {
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 " +
124 ")");
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 " +
139 ")");
141 SqliteUtils.DoNonQuery (connection,
142 "CREATE INDEX file_path on file_attributes (" +
143 " directory, " +
144 " filename " +
145 ")");
146 } else {
147 SqliteCommand command;
148 SqliteDataReader reader;
149 int count = 0;
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);
165 SetPathFlag (path);
166 ++count;
169 reader.Close ();
170 command.Dispose ();
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)
188 SqliteConnection c;
189 c = new SqliteConnection ();
190 c.ConnectionString = "version=" + ExternalStringsHack.SqliteVersion
191 + ",encoding=UTF-8,URI=file:" + GetDbPath (directory);
192 c.Open ();
193 return c;
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;
210 return attr;
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))
248 return null;
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 ("'", "''");
257 lock (connection) {
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;
269 reader.Close ();
270 command.Dispose ();
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);
281 attr = null;
285 return attr;
288 public bool Write (FileAttributes fa)
290 SetPathFlag (fa.Path);
291 int ret = 0;
292 string filter_name;
294 // We need to quote any 's that appear in the strings
295 // (in particular, in the path)
296 lock (connection) {
298 // If a transaction has been requested, start it now.
299 MaybeStartTransaction ();
301 filter_name = fa.FilterName;
302 if (filter_name == null)
303 filter_name = "";
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),
314 filter_name,
315 fa.FilterVersion);
318 return (ret != 0);
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 ("'", "''");
334 lock (connection) {
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) {
362 lock (connection)
363 SqliteUtils.DoNonQuery (connection, "COMMIT");
365 transaction_state = TransactionState.None;
368 public void Flush ()
370 lock (connection) {
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;
390 lock (connection) {
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));
402 reader.Close ();
403 command.Dispose ();
406 return attributes;
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 ();
415 BeginTransaction ();
417 foreach (FileAttributes attribute in attributes)
418 Write (attribute);
420 CommitTransaction ();