monotone

monotone Mtn Source Tree

Root/schema_migration.cc

1// copyright (C) 2002, 2003 graydon hoare <graydon@pobox.com>
2// all rights reserved.
3// licensed to the public under the terms of the GNU GPL (>= 2)
4// see the file COPYING for details
5
6#include <algorithm>
7#include <string>
8#include <vector>
9#include <locale>
10#include <stdexcept>
11#include <iostream>
12
13#include <boost/tokenizer.hpp>
14
15#include <sqlite3.h>
16
17#include "schema_migration.hh"
18#include "cryptopp/filters.h"
19#include "cryptopp/sha.h"
20#include "cryptopp/hex.h"
21
22// this file knows how to migrate schema databases. the general strategy is
23// to hash each schema we ever use, and make a list of the SQL commands
24// required to get from one hash value to the next. when you do a
25// migration, the migrator locates your current db's state on the list and
26// then runs all the migration functions between that point and the target
27// of the migration.
28
29// you will notice a little bit of duplicated code between here and
30// transforms.cc / database.cc; this was originally to facilitate inclusion of
31// migration capability into the depot code, which did not link against those
32// objects. the depot code is gone, but this isn't the sort of code that
33// should ever be touched after being written, so the duplication remains.
34
35using namespace std;
36
37typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
38
39extern "C" {
40// some wrappers to ease migration
41 int sqlite3_exec_printf(sqlite3*,const char *sqlFormat,sqlite3_callback,
42 void *,char **errmsg,...);
43 const char *sqlite3_value_text_s(sqlite3_value *v);
44}
45
46static string
47lowercase(string const & in)
48{
49 size_t const sz = in.size();
50 char buf[sz];
51 in.copy(buf, sz);
52 use_facet< ctype<char> >(locale::locale()).tolower(buf, buf+sz);
53 return string(buf,sz);
54}
55
56static void
57massage_sql_tokens(string const & in,
58 string & out)
59{
60 boost::char_separator<char> sep(" \r\n\t", "(),;");
61 tokenizer tokens(in, sep);
62 out.clear();
63 for (tokenizer::iterator i = tokens.begin();
64 i != tokens.end(); ++i)
65 {
66 if (i != tokens.begin())
67 out += " ";
68 out += *i;
69 }
70}
71
72static void
73calculate_id(string const & in,
74 string & ident)
75{
76 CryptoPP::SHA hash;
77 unsigned int const sz = 2 * CryptoPP::SHA::DIGESTSIZE;
78 char buffer[sz];
79 CryptoPP::StringSource
80 s(in, true, new CryptoPP::HashFilter
81 (hash, new CryptoPP::HexEncoder
82 (new CryptoPP::ArraySink(reinterpret_cast<byte *>(buffer), sz))));
83 ident = lowercase(string(buffer, sz));
84}
85
86
87struct
88is_ws
89{
90 bool operator()(char c) const
91 {
92 return c == '\r' || c == '\n' || c == '\t' || c == ' ';
93 }
94};
95
96static void
97sqlite_sha1_fn(sqlite3_context *f, int nargs, sqlite3_value ** args)
98{
99 string tmp, sha;
100 if (nargs <= 1)
101 {
102 sqlite3_result_error(f, "need at least 1 arg to sha1()", -1);
103 return;
104 }
105
106 if (nargs == 1)
107 {
108 string s = (sqlite3_value_text_s(args[0]));
109 s.erase(remove_if(s.begin(), s.end(), is_ws()),s.end());
110 tmp = s;
111 }
112 else
113 {
114 string sep = string(sqlite3_value_text_s(args[0]));
115 string s = (sqlite3_value_text_s(args[1]));
116 s.erase(remove_if(s.begin(), s.end(), is_ws()),s.end());
117 tmp = s;
118 for (int i = 2; i < nargs; ++i)
119 {
120 s = string(sqlite3_value_text_s(args[i]));
121 s.erase(remove_if(s.begin(), s.end(), is_ws()),s.end());
122 tmp += sep + s;
123 }
124 }
125 calculate_id(tmp, sha);
126 sqlite3_result_text(f,sha.c_str(),sha.size(),SQLITE_TRANSIENT);
127}
128
129int
130append_sql_stmt(void * vp,
131 int ncols,
132 char ** values,
133 char ** colnames)
134{
135 if (ncols != 1)
136 return 1;
137
138 if (vp == NULL)
139 return 1;
140
141 if (values == NULL)
142 return 1;
143
144 if (values[0] == NULL)
145 return 1;
146
147 string *str = reinterpret_cast<string *>(vp);
148 str->append(values[0]);
149 str->append("\n");
150 return 0;
151}
152
153void
154calculate_schema_id(sqlite3 *sql, string & id)
155{
156 id.clear();
157 string tmp, tmp2;
158 int res = sqlite3_exec_printf(sql,
159 "SELECT sql FROM sqlite_master "
160 "WHERE type = 'table' "
161 "ORDER BY name",
162 &append_sql_stmt, &tmp, NULL);
163 if (res != SQLITE_OK)
164 {
165 sqlite3_exec(sql, "ROLLBACK", NULL, NULL, NULL);
166 throw runtime_error("failure extracting schema from sqlite_master");
167 }
168 massage_sql_tokens(tmp, tmp2);
169 calculate_id(tmp2, id);
170}
171
172typedef bool (*migrator_cb)(sqlite3 *, char **);
173
174struct
175migrator
176{
177 vector< pair<string,migrator_cb> > migration_events;
178
179 void add(string schema_id, migrator_cb cb)
180 {
181 migration_events.push_back(make_pair(schema_id, cb));
182 }
183
184 void migrate(sqlite3 *sql, string target_id)
185 {
186 string init;
187 calculate_schema_id(sql, init);
188
189 if (sql == NULL)
190 throw runtime_error("NULL sqlite object given to migrate");
191
192 if (sqlite3_create_function(sql, "sha1", -1, SQLITE_UTF8, NULL,
193 &sqlite_sha1_fn, NULL, NULL))
194 throw runtime_error("error registering sha1 function with sqlite");
195
196 bool migrating = false;
197 for (vector< pair<string, migrator_cb> >::const_iterator i = migration_events.begin();
198 i != migration_events.end(); ++i)
199 {
200
201 if (i->first == init)
202 {
203 if (sqlite3_exec(sql, "BEGIN", NULL, NULL, NULL) != SQLITE_OK)
204 throw runtime_error("error at transaction BEGIN statement");
205 migrating = true;
206 }
207
208 if (migrating)
209 {
210 // confirm that we are where we ought to be
211 string curr;
212 char *errmsg = NULL;
213 calculate_schema_id(sql, curr);
214 if (curr != i->first)
215 {
216 if (migrating)
217 sqlite3_exec(sql, "ROLLBACK", NULL, NULL, NULL);
218 throw runtime_error("mismatched pre-state to migration step");
219 }
220
221 if (i->second == NULL)
222 {
223 sqlite3_exec(sql, "ROLLBACK", NULL, NULL, NULL);
224 throw runtime_error("NULL migration specifier");
225 }
226
227 // do this migration step
228 else if (! i->second(sql, &errmsg))
229 {
230 string e("migration step failed");
231 if (errmsg != NULL)
232 e.append(string(": ") + errmsg);
233 sqlite3_exec(sql, "ROLLBACK", NULL, NULL, NULL);
234 throw runtime_error(e);
235 }
236 }
237 }
238
239 // confirm that our target schema was met
240 if (migrating)
241 {
242 string curr;
243 calculate_schema_id(sql, curr);
244 if (curr != target_id)
245 {
246 sqlite3_exec(sql, "ROLLBACK", NULL, NULL, NULL);
247 throw runtime_error("mismatched result of migration, "
248 "got " + curr + ", wanted " + target_id);
249 }
250 if (sqlite3_exec(sql, "COMMIT", NULL, NULL, NULL) != SQLITE_OK)
251 {
252 throw runtime_error("failure on COMMIT");
253 }
254 }
255 }
256};
257
258static bool move_table(sqlite3 *sql, char **errmsg,
259 char const * srcname,
260 char const * dstname,
261 char const * dstschema)
262{
263 int res =
264 sqlite3_exec_printf(sql, "CREATE TABLE %s %s", NULL, NULL, errmsg,
265 dstname, dstschema);
266 if (res != SQLITE_OK)
267 return false;
268
269 res =
270 sqlite3_exec_printf(sql, "INSERT INTO %s SELECT * FROM %s",
271 NULL, NULL, errmsg, dstname, srcname);
272 if (res != SQLITE_OK)
273 return false;
274
275 res =
276 sqlite3_exec_printf(sql, "DROP TABLE %s",
277 NULL, NULL, errmsg, srcname);
278 if (res != SQLITE_OK)
279 return false;
280
281 return true;
282}
283
284
285static bool
286migrate_client_merge_url_and_group(sqlite3 * sql,
287 char ** errmsg)
288{
289
290 // migrate the posting_queue table
291 if (!move_table(sql, errmsg,
292 "posting_queue",
293 "tmp",
294 "("
295 "url not null,"
296 "groupname not null,"
297 "content not null"
298 ")"))
299 return false;
300
301 int res = sqlite3_exec_printf(sql, "CREATE TABLE posting_queue "
302 "("
303 "url not null, -- URL we are going to send this to\n"
304 "content not null -- the packets we're going to send\n"
305 ")", NULL, NULL, errmsg);
306 if (res != SQLITE_OK)
307 return false;
308
309 res = sqlite3_exec_printf(sql, "INSERT INTO posting_queue "
310 "SELECT "
311 "(url || '/' || groupname), "
312 "content "
313 "FROM tmp", NULL, NULL, errmsg);
314 if (res != SQLITE_OK)
315 return false;
316
317 res = sqlite3_exec_printf(sql, "DROP TABLE tmp", NULL, NULL, errmsg);
318 if (res != SQLITE_OK)
319 return false;
320
321
322 // migrate the incoming_queue table
323 if (!move_table(sql, errmsg,
324 "incoming_queue",
325 "tmp",
326 "("
327 "url not null,"
328 "groupname not null,"
329 "content not null"
330 ")"))
331 return false;
332
333 res = sqlite3_exec_printf(sql, "CREATE TABLE incoming_queue "
334 "("
335 "url not null, -- URL we got this bundle from\n"
336 "content not null -- the packets we're going to read\n"
337 ")", NULL, NULL, errmsg);
338 if (res != SQLITE_OK)
339 return false;
340
341 res = sqlite3_exec_printf(sql, "INSERT INTO incoming_queue "
342 "SELECT "
343 "(url || '/' || groupname), "
344 "content "
345 "FROM tmp", NULL, NULL, errmsg);
346 if (res != SQLITE_OK)
347 return false;
348
349 res = sqlite3_exec_printf(sql, "DROP TABLE tmp", NULL, NULL, errmsg);
350 if (res != SQLITE_OK)
351 return false;
352
353
354 // migrate the sequence_numbers table
355 if (!move_table(sql, errmsg,
356 "sequence_numbers",
357 "tmp",
358 "("
359 "url not null,"
360 "groupname not null,"
361 "major not null,"
362 "minor not null,"
363 "unique(url, groupname)"
364 ")"
365 ))
366 return false;
367
368 res = sqlite3_exec_printf(sql, "CREATE TABLE sequence_numbers "
369 "("
370 "url primary key, -- URL to read from\n"
371 "major not null, -- 0 in news servers, may be higher in depots\n"
372 "minor not null -- last article / packet sequence number we got\n"
373 ")", NULL, NULL, errmsg);
374 if (res != SQLITE_OK)
375 return false;
376
377 res = sqlite3_exec_printf(sql, "INSERT INTO sequence_numbers "
378 "SELECT "
379 "(url || '/' || groupname), "
380 "major, "
381 "minor "
382 "FROM tmp", NULL, NULL, errmsg);
383 if (res != SQLITE_OK)
384 return false;
385
386 res = sqlite3_exec_printf(sql, "DROP TABLE tmp", NULL, NULL, errmsg);
387 if (res != SQLITE_OK)
388 return false;
389
390
391 // migrate the netserver_manifests table
392 if (!move_table(sql, errmsg,
393 "netserver_manifests",
394 "tmp",
395 "("
396 "url not null,"
397 "groupname not null,"
398 "manifest not null,"
399 "unique(url, groupname, manifest)"
400 ")"
401 ))
402 return false;
403
404 res = sqlite3_exec_printf(sql, "CREATE TABLE netserver_manifests "
405 "("
406 "url not null, -- url of some server\n"
407 "manifest not null, -- manifest which exists on url\n"
408 "unique(url, manifest)"
409 ")", NULL, NULL, errmsg);
410 if (res != SQLITE_OK)
411 return false;
412
413 res = sqlite3_exec_printf(sql, "INSERT INTO netserver_manifests "
414 "SELECT "
415 "(url || '/' || groupname), "
416 "manifest "
417 "FROM tmp", NULL, NULL, errmsg);
418 if (res != SQLITE_OK)
419 return false;
420
421 res = sqlite3_exec_printf(sql, "DROP TABLE tmp", NULL, NULL, errmsg);
422 if (res != SQLITE_OK)
423 return false;
424
425 return true;
426}
427
428static bool
429migrate_client_add_hashes_and_merkle_trees(sqlite3 * sql,
430 char ** errmsg)
431{
432
433 // add the column to manifest_certs
434 if (!move_table(sql, errmsg,
435 "manifest_certs",
436 "tmp",
437 "("
438 "id not null,"
439 "name not null,"
440 "value not null,"
441 "keypair not null,"
442 "signature not null,"
443 "unique(name, id, value, keypair, signature)"
444 ")"))
445 return false;
446
447 int res = sqlite3_exec_printf(sql, "CREATE TABLE manifest_certs\n"
448 "(\n"
449 "hash not null unique, -- hash of remaining fields separated by \":\"\n"
450 "id not null, -- joins with manifests.id or manifest_deltas.id\n"
451 "name not null, -- opaque string chosen by user\n"
452 "value not null, -- opaque blob\n"
453 "keypair not null, -- joins with public_keys.id\n"
454 "signature not null, -- RSA/SHA1 signature of \"[name@id:val]\"\n"
455 "unique(name, id, value, keypair, signature)\n"
456 ")", NULL, NULL, errmsg);
457 if (res != SQLITE_OK)
458 return false;
459
460 res = sqlite3_exec_printf(sql, "INSERT INTO manifest_certs "
461 "SELECT "
462 "sha1(':', id, name, value, keypair, signature), "
463 "id, name, value, keypair, signature "
464 "FROM tmp", NULL, NULL, errmsg);
465 if (res != SQLITE_OK)
466 return false;
467
468 res = sqlite3_exec_printf(sql, "DROP TABLE tmp", NULL, NULL, errmsg);
469 if (res != SQLITE_OK)
470 return false;
471
472 // add the column to file_certs
473 if (!move_table(sql, errmsg,
474 "file_certs",
475 "tmp",
476 "("
477 "id not null,"
478 "name not null,"
479 "value not null,"
480 "keypair not null,"
481 "signature not null,"
482 "unique(name, id, value, keypair, signature)"
483 ")"))
484 return false;
485
486 res = sqlite3_exec_printf(sql, "CREATE TABLE file_certs\n"
487 "(\n"
488 "hash not null unique, -- hash of remaining fields separated by \":\"\n"
489 "id not null, -- joins with files.id or file_deltas.id\n"
490 "name not null, -- opaque string chosen by user\n"
491 "value not null, -- opaque blob\n"
492 "keypair not null, -- joins with public_keys.id\n"
493 "signature not null, -- RSA/SHA1 signature of \"[name@id:val]\"\n"
494 "unique(name, id, value, keypair, signature)\n"
495 ")", NULL, NULL, errmsg);
496 if (res != SQLITE_OK)
497 return false;
498
499 res = sqlite3_exec_printf(sql, "INSERT INTO file_certs "
500 "SELECT "
501 "sha1(':', id, name, value, keypair, signature), "
502 "id, name, value, keypair, signature "
503 "FROM tmp", NULL, NULL, errmsg);
504 if (res != SQLITE_OK)
505 return false;
506
507 res = sqlite3_exec_printf(sql, "DROP TABLE tmp", NULL, NULL, errmsg);
508 if (res != SQLITE_OK)
509 return false;
510
511 // add the column to public_keys
512 if (!move_table(sql, errmsg,
513 "public_keys",
514 "tmp",
515 "("
516 "id primary key,"
517 "keydata not null"
518 ")"))
519 return false;
520
521 res = sqlite3_exec_printf(sql, "CREATE TABLE public_keys\n"
522 "(\n"
523 "hash not null unique, -- hash of remaining fields separated by \":\"\n"
524 "id primary key, -- key identifier chosen by user\n"
525 "keydata not null -- RSA public params\n"
526 ")", NULL, NULL, errmsg);
527 if (res != SQLITE_OK)
528 return false;
529
530 res = sqlite3_exec_printf(sql, "INSERT INTO public_keys "
531 "SELECT "
532 "sha1(':', id, keydata), "
533 "id, keydata "
534 "FROM tmp", NULL, NULL, errmsg);
535 if (res != SQLITE_OK)
536 return false;
537
538 res = sqlite3_exec_printf(sql, "DROP TABLE tmp", NULL, NULL, errmsg);
539 if (res != SQLITE_OK)
540 return false;
541
542 // add the column to private_keys
543 if (!move_table(sql, errmsg,
544 "private_keys",
545 "tmp",
546 "("
547 "id primary key,"
548 "keydata not null"
549 ")"))
550 return false;
551
552 res = sqlite3_exec_printf(sql, "CREATE TABLE private_keys\n"
553 "(\n"
554 "hash not null unique, -- hash of remaining fields separated by \":\"\n"
555 "id primary key, -- as in public_keys (same identifiers, in fact)\n"
556 "keydata not null -- encrypted RSA private params\n"
557 ")", NULL, NULL, errmsg);
558 if (res != SQLITE_OK)
559 return false;
560
561 res = sqlite3_exec_printf(sql, "INSERT INTO private_keys "
562 "SELECT "
563 "sha1(':', id, keydata), "
564 "id, keydata "
565 "FROM tmp", NULL, NULL, errmsg);
566 if (res != SQLITE_OK)
567 return false;
568
569 res = sqlite3_exec_printf(sql, "DROP TABLE tmp", NULL, NULL, errmsg);
570 if (res != SQLITE_OK)
571 return false;
572
573 // add the merkle tree stuff
574
575 res = sqlite3_exec_printf(sql,
576 "CREATE TABLE merkle_nodes\n"
577 "(\n"
578 "type not null, -- \"key\", \"mcert\", \"fcert\", \"manifest\"\n"
579 "collection not null, -- name chosen by user\n"
580 "level not null, -- tree level this prefix encodes\n"
581 "prefix not null, -- label identifying node in tree\n"
582 "body not null, -- binary, base64'ed node contents\n"
583 "unique(type, collection, level, prefix)\n"
584 ")", NULL, NULL, errmsg);
585 if (res != SQLITE_OK)
586 return false;
587
588 return true;
589}
590
591static bool
592migrate_client_to_revisions(sqlite3 * sql,
593 char ** errmsg)
594{
595 int res;
596
597 res = sqlite3_exec_printf(sql, "DROP TABLE schema_version;", NULL, NULL, errmsg);
598 if (res != SQLITE_OK)
599 return false;
600
601 res = sqlite3_exec_printf(sql, "DROP TABLE posting_queue;", NULL, NULL, errmsg);
602 if (res != SQLITE_OK)
603 return false;
604
605 res = sqlite3_exec_printf(sql, "DROP TABLE incoming_queue;", NULL, NULL, errmsg);
606 if (res != SQLITE_OK)
607 return false;
608
609 res = sqlite3_exec_printf(sql, "DROP TABLE sequence_numbers;", NULL, NULL, errmsg);
610 if (res != SQLITE_OK)
611 return false;
612
613 res = sqlite3_exec_printf(sql, "DROP TABLE file_certs;", NULL, NULL, errmsg);
614 if (res != SQLITE_OK)
615 return false;
616
617 res = sqlite3_exec_printf(sql, "DROP TABLE netserver_manifests;", NULL, NULL, errmsg);
618 if (res != SQLITE_OK)
619 return false;
620
621 res = sqlite3_exec_printf(sql, "DROP TABLE merkle_nodes;", NULL, NULL, errmsg);
622 if (res != SQLITE_OK)
623 return false;
624
625 res = sqlite3_exec_printf(sql,
626 "CREATE TABLE merkle_nodes\n"
627 "(\n"
628 "type not null, -- \"key\", \"mcert\", \"fcert\", \"rcert\"\n"
629 "collection not null, -- name chosen by user\n"
630 "level not null, -- tree level this prefix encodes\n"
631 "prefix not null, -- label identifying node in tree\n"
632 "body not null, -- binary, base64'ed node contents\n"
633 "unique(type, collection, level, prefix)\n"
634 ")", NULL, NULL, errmsg);
635 if (res != SQLITE_OK)
636 return false;
637
638 res = sqlite3_exec_printf(sql, "CREATE TABLE revision_certs\n"
639 "(\n"
640 "hash not null unique, -- hash of remaining fields separated by \":\"\n"
641 "id not null, -- joins with revisions.id\n"
642 "name not null, -- opaque string chosen by user\n"
643 "value not null, -- opaque blob\n"
644 "keypair not null, -- joins with public_keys.id\n"
645 "signature not null, -- RSA/SHA1 signature of \"[name@id:val]\"\n"
646 "unique(name, id, value, keypair, signature)\n"
647 ")", NULL, NULL, errmsg);
648 if (res != SQLITE_OK)
649 return false;
650
651 res = sqlite3_exec_printf(sql, "CREATE TABLE revisions\n"
652 "(\n"
653 "id primary key, -- SHA1(text of revision)\n"
654 "data not null -- compressed, encoded contents of a revision\n"
655 ")", NULL, NULL, errmsg);
656 if (res != SQLITE_OK)
657 return false;
658
659 res = sqlite3_exec_printf(sql, "CREATE TABLE revision_ancestry\n"
660 "(\n"
661 "parent not null, -- joins with revisions.id\n"
662 "child not null, -- joins with revisions.id\n"
663 "unique(parent, child)\n"
664 ")", NULL, NULL, errmsg);
665 if (res != SQLITE_OK)
666 return false;
667
668 return true;
669}
670
671
672static bool
673migrate_client_to_epochs(sqlite3 * sql,
674 char ** errmsg)
675{
676 int res;
677
678 res = sqlite3_exec_printf(sql, "DROP TABLE merkle_nodes;", NULL, NULL, errmsg);
679 if (res != SQLITE_OK)
680 return false;
681
682
683 res = sqlite3_exec_printf(sql,
684
685 "CREATE TABLE branch_epochs\n"
686 "(\n"
687 "hash not null unique, -- hash of remaining fields separated by \":\"\n"
688 "branch not null unique, -- joins with revision_certs.value\n"
689 "epoch not null -- random hex-encoded id\n"
690 ");", NULL, NULL, errmsg);
691 if (res != SQLITE_OK)
692 return false;
693
694 return true;
695}
696
697static bool
698migrate_client_to_vars(sqlite3 * sql,
699 char ** errmsg)
700{
701 int res;
702
703 res = sqlite3_exec_printf(sql,
704 "CREATE TABLE db_vars\n"
705 "(\n"
706 "domain not null, -- scope of application of a var\n"
707 "name not null, -- var key\n"
708 "value not null, -- var value\n"
709 "unique(domain, name)\n"
710 ");", NULL, NULL, errmsg);
711 if (res != SQLITE_OK)
712 return false;
713
714 return true;
715}
716
717void
718migrate_monotone_schema(sqlite3 *sql)
719{
720
721 migrator m;
722
723 m.add("edb5fa6cef65bcb7d0c612023d267c3aeaa1e57a",
724 &migrate_client_merge_url_and_group);
725
726 m.add("f042f3c4d0a4f98f6658cbaf603d376acf88ff4b",
727 &migrate_client_add_hashes_and_merkle_trees);
728
729 m.add("8929e54f40bf4d3b4aea8b037d2c9263e82abdf4",
730 &migrate_client_to_revisions);
731
732 m.add("c1e86588e11ad07fa53e5d294edc043ce1d4005a",
733 &migrate_client_to_epochs);
734
735 m.add("40369a7bda66463c5785d160819ab6398b9d44f4",
736 &migrate_client_to_vars);
737
738 // IMPORTANT: whenever you modify this to add a new schema version, you must
739 // also add a new migration test for the new schema version. See
740 // tests/t_migrate_schema.at for details.
741
742 m.migrate(sql, "e372b508bea9b991816d1c74680f7ae10d2a6d94");
743
744 if (sqlite3_exec(sql, "VACUUM", NULL, NULL, NULL) != SQLITE_OK)
745 throw runtime_error("error vacuuming after migration");
746
747}

Archive Download this file

Branches

Tags

Quick Links:     www.monotone.ca    -     Downloads    -     Documentation    -     Wiki    -     Code Forge    -     Build Status