* KNotesQueryable.cs: Dont re-index all the notes when the notes file changes. Since...
[beagle.git] / beagled / FileAttributesStore_Sqlite.cs
blob5ba195b1f8888e7b449bb060e1d2a7ad58c70e8c
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);
284 return attr;
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)
293 lock (connection) {
295 // If a transaction has been requested, start it now.
296 MaybeStartTransaction ();
298 string filter_name;
299 filter_name = fa.FilterName;
300 if (filter_name == null)
301 filter_name = "";
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),
312 filter_name,
313 fa.FilterVersion);
315 return true;
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 ("'", "''");
331 lock (connection) {
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) {
359 lock (connection)
360 SqliteUtils.DoNonQuery (connection, "COMMIT");
362 transaction_state = TransactionState.None;
365 public void Flush ()
367 lock (connection) {
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;
387 lock (connection) {
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));
399 reader.Close ();
400 command.Dispose ();
403 return attributes;
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 ();
412 BeginTransaction ();
414 foreach (FileAttributes attribute in attributes)
415 Write (attribute);
417 CommitTransaction ();