1package Prophet::Replica::prophet; 2{ 3 $Prophet::Replica::prophet::VERSION = '0.751'; 4} 5use Any::Moose; 6extends 'Prophet::FilesystemReplica'; 7 8use Params::Validate qw(:all); 9use LWP::UserAgent; 10use LWP::ConnCache; 11use File::Spec (); 12use File::Path; 13use Cwd (); 14use File::Find; 15use Prophet::Util; 16use POSIX qw(); 17use Memoize; 18use Prophet::ContentAddressedStore; 19 20use JSON; 21use Digest::SHA qw(sha1_hex); 22 23has '+db_uuid' => ( 24 lazy => 1, 25 default => sub { shift->_read_file('database-uuid') }, 26); 27 28has _uuid => ( is => 'rw', ); 29 30has _replica_version => ( 31 is => 'rw', 32 isa => 'Int', 33 lazy => 1, 34 default => sub { shift->_read_file('replica-version') || 0 } 35); 36 37has fs_root_parent => ( 38 is => 'rw', 39 lazy => 1, 40 default => sub { 41 my $self = shift; 42 if ( $self->url =~ m{^file://(.*)} ) { 43 my $path = $1; 44 return File::Spec->catdir( 45 ( File::Spec->splitpath($path) )[ 0, -2 ] ); 46 } 47 }, 48); 49 50has fs_root => ( 51 is => 'rw', 52 lazy => 1, 53 default => sub { 54 my $self = shift; 55 return $self->url =~ m{^file://(.*)$} ? $1 : undef; 56 }, 57); 58 59has record_cas => ( 60 is => 'rw', 61 isa => 'Prophet::ContentAddressedStore', 62 lazy => 1, 63 default => sub { 64 my $self = shift; 65 Prophet::ContentAddressedStore->new( 66 { 67 fs_root => $self->fs_root, 68 root => $self->record_cas_dir 69 } 70 ); 71 }, 72); 73 74has changeset_cas => ( 75 is => 'rw', 76 isa => 'Prophet::ContentAddressedStore', 77 lazy => 1, 78 default => sub { 79 my $self = shift; 80 Prophet::ContentAddressedStore->new( 81 { 82 fs_root => $self->fs_root, 83 root => $self->changeset_cas_dir 84 } 85 ); 86 }, 87); 88 89has current_edit => ( 90 is => 'rw', 91 isa => 'Maybe[Prophet::ChangeSet]', 92); 93 94has current_edit_records => ( 95 is => 'rw', 96 isa => 'ArrayRef', 97 default => sub { [] }, 98); 99 100has '+resolution_db_handle' => ( 101 isa => 'Prophet::Replica | Undef', 102 lazy => 1, 103 default => sub { 104 my $self = shift; 105 return if $self->is_resdb; 106 return Prophet::Replica->get_handle( 107 { 108 url => "prophet:" . $self->url . '/resolutions', 109 app_handle => $self->app_handle, 110 is_resdb => 1, 111 } 112 ); 113 }, 114); 115 116has backend => ( 117 lazy => 1, 118 is => 'rw', 119 default => sub { 120 my $self = shift; 121 my $be; 122 if ( $self->url =~ /^http/i ) { 123 $be = 'Prophet::Replica::FS::Backend::LWP'; 124 } else { 125 $be = 'Prophet::Replica::FS::Backend::File'; 126 } 127 128 Prophet::App->require($be); 129 return $be->new( url => $self->url, fs_root => $self->fs_root ); 130 } 131 132); 133 134use constant scheme => 'prophet'; 135use constant cas_root => 'cas'; 136use constant record_cas_dir => 137 File::Spec->catdir( __PACKAGE__->cas_root => 'records' ); 138use constant changeset_cas_dir => 139 File::Spec->catdir( __PACKAGE__->cas_root => 'changesets' ); 140use constant record_dir => 'records'; 141use constant userdata_dir => 'userdata'; 142use constant changeset_index => 'changesets.idx'; 143use constant local_metadata_dir => 'local_metadata'; 144 145sub BUILD { 146 my $self = shift; 147 my $args = shift; 148 Carp::cluck() unless ( $args->{app_handle} ); 149 for ( $self->{url} ) { 150 s/^prophet://; # url-based constructor in ::replica should do better 151 s{/$}{}; 152 } 153 154} 155 156 157sub replica_version { 158 die "replica_version is read-only; you want set_replica_version." 159 if @_ > 1; 160 shift->_replica_version; 161} 162 163 164sub set_replica_version { 165 my $self = shift; 166 my $version = shift; 167 168 $self->_replica_version($version); 169 170 $self->_write_file( 171 path => 'replica-version', 172 content => $version, 173 ); 174 175 return $version; 176} 177 178sub can_initialize { 179 my $self = shift; 180 if ( $self->fs_root_parent && -w $self->fs_root_parent ) { 181 return 1; 182 183 } 184 return 0; 185} 186 187use constant can_read_records => 1; 188use constant can_read_changesets => 1; 189sub can_write_changesets { return ( shift->fs_root ? 1 : 0 ) } 190sub can_write_records { return ( shift->fs_root ? 1 : 0 ) } 191 192sub _on_initialize_create_paths { 193 my $self = shift; 194 return ( 195 $self->record_dir, $self->cas_root, 196 $self->record_cas_dir, $self->changeset_cas_dir, 197 $self->userdata_dir 198 ); 199 200} 201 202sub initialize_backend { 203 my $self = shift; 204 my %args = validate( 205 @_, 206 { 207 db_uuid => 0, 208 resdb_uuid => 0, 209 } 210 ); 211 212 $self->set_db_uuid( $args{'db_uuid'} 213 || $self->uuid_generator->create_str ); 214 $self->set_latest_sequence_no("0"); 215 $self->set_replica_uuid( $self->uuid_generator->create_str ); 216 217 $self->set_replica_version(1); 218 219 $self->resolution_db_handle->initialize( db_uuid => $args{resdb_uuid} ) 220 if !$self->is_resdb; 221} 222 223sub latest_sequence_no { 224 my $self = shift; 225 $self->_read_file('latest-sequence-no'); 226} 227 228sub set_latest_sequence_no { 229 my $self = shift; 230 my $id = shift; 231 $self->_write_file( 232 path => 'latest-sequence-no', 233 content => scalar($id) 234 ); 235} 236 237sub _increment_sequence_no { 238 my $self = shift; 239 my $seq = $self->latest_sequence_no + 1; 240 $self->set_latest_sequence_no($seq); 241 return $seq; 242} 243 244 245sub uuid { 246 my $self = shift; 247 $self->_uuid( $self->_read_file('replica-uuid') ) unless $self->_uuid; 248 249 # die $@ if $@; 250 return $self->_uuid; 251} 252 253sub set_replica_uuid { 254 my $self = shift; 255 my $uuid = shift; 256 $self->_write_file( 257 path => 'replica-uuid', 258 content => $uuid 259 ); 260 261} 262 263sub set_db_uuid { 264 my $self = shift; 265 my $uuid = shift; 266 $self->_write_file( 267 path => 'database-uuid', 268 content => $uuid 269 ); 270 $self->SUPER::set_db_uuid($uuid); 271} 272 273# Working with records { 274 275sub _write_record { 276 my $self = shift; 277 my %args = validate( @_, { record => { isa => 'Prophet::Record' }, } ); 278 my $record = $args{'record'}; 279 280 $self->_write_serialized_record( 281 type => $record->type, 282 uuid => $record->uuid, 283 props => $record->get_props, 284 ); 285} 286 287sub _write_serialized_record { 288 my $self = shift; 289 my %args = validate( @_, { type => 1, uuid => 1, props => 1 } ); 290 291 for ( keys %{ $args{'props'} } ) { 292 delete $args{'props'}->{$_} 293 if ( !defined $args{'props'}->{$_} || $args{'props'}->{$_} eq '' ); 294 } 295 my $cas_key = $self->record_cas->write( $args{props} ); 296 297 my $record = { 298 uuid => $args{uuid}, 299 type => $args{type}, 300 cas_key => $cas_key 301 }; 302 303 $self->_prepare_record_index_update( 304 uuid => $args{uuid}, 305 type => $args{type}, 306 cas_key => $cas_key 307 ); 308} 309 310sub _prepare_record_index_update { 311 my $self = shift; 312 my %record = (@_); 313 314 # If we're inside an edit, we can record the changeset info into the index 315 if ( $self->current_edit ) { 316 push @{ $self->current_edit_records }, \%record; 317 318 } else { 319 320 # If we're not inside an edit, we're likely exporting the replica 321 # TODO: the replica exporter code should probably be retooled 322 $self->_write_record_index_entry(%record); 323 } 324 325} 326 327use constant RECORD_INDEX_SIZE => ( 4 + 20 ); 328 329sub _write_record_index_entry { 330 my $self = shift; 331 my %args = 332 validate( @_, 333 { type => 1, uuid => 1, cas_key => 1, changeset_id => 0 } ); 334 my $idx_filename = $self->_record_index_filename( 335 uuid => $args{uuid}, 336 type => $args{type} 337 ); 338 339 my $index_path = Prophet::Util->catfile( $self->fs_root, $idx_filename ); 340 my ( undef, $parent, $filename ) = File::Spec->splitpath($index_path); 341 mkpath( [$parent] ); 342 343 open( my $record_index, ">>", $index_path ); 344 345 # XXX TODO: skip if the index already has this version of the record; 346 # XXX TODO FETCH THAT 347 my $record_last_changed_changeset = $args{'changeset_id'} || 0; 348 my $index_row = 349 pack( 'NH40', $record_last_changed_changeset, $args{cas_key} ); 350 print $record_index $index_row || die $!; 351 close $record_index; 352} 353 354sub _read_file_range { 355 my $self = shift; 356 my %args = validate( @_, { path => 1, position => 1, length => 1 } ); 357 358 return $self->backend->read_file_range(%args); 359 360} 361 362sub _last_record_index_entry { 363 my $self = shift; 364 my %args = ( type => undef, uuid => undef, @_ ); 365 366 my $idx_filename; 367 my $record = $self->_read_file_range( 368 path => $self->_record_index_filename( 369 uuid => $args{uuid}, 370 type => $args{type} 371 ), 372 position => ( 0 - RECORD_INDEX_SIZE ), 373 length => RECORD_INDEX_SIZE 374 ) || return; 375 376 my ( $seq, $key ) = unpack( "NH40", $record ); 377 return ( $seq, $key ); 378} 379 380sub _read_record_index { 381 my $self = shift; 382 my %args = validate( @_, { type => 1, uuid => 1 } ); 383 384 my $idx_filename = $self->_record_index_filename( 385 uuid => $args{uuid}, 386 type => $args{type} 387 ); 388 389 my $index = $self->backend->read_file($idx_filename); 390 return unless $index; 391 392 my $count = length($index) / RECORD_INDEX_SIZE; 393 my @entries; 394 for my $record ( 1 .. $count ) { 395 my ( $seq, $key ) = unpack( 396 'NH40', 397 substr( 398 $index, ( $record - 1 ) * RECORD_INDEX_SIZE, 399 RECORD_INDEX_SIZE 400 ) 401 ); 402 push @entries, [ $seq => $key ]; 403 } 404 return @entries; 405} 406 407sub _delete_record_index { 408 my $self = shift; 409 my %args = validate( @_, { type => 1, uuid => 1 } ); 410 my $idx_filename = $self->_record_index_filename( 411 uuid => $args{uuid}, 412 type => $args{type} 413 ); 414 unlink Prophet::Util->catfile( $self->fs_root => $idx_filename ) 415 || die "Could not delete record $idx_filename: " . $!; 416} 417 418sub _read_serialized_record { 419 my $self = shift; 420 my %args = validate( @_, { type => 1, uuid => 1 } ); 421 422 my $casfile = $self->_record_cas_filename( 423 type => $args{'type'}, 424 uuid => $args{'uuid'} 425 ); 426 427 return unless $casfile; 428 return from_json( $self->_read_file($casfile), { utf8 => 1 } ); 429} 430 431# XXX TODO: memoize doesn't work on win: 432# t\resty-server will issue the following error: 433# Anonymous function called in forbidden list context; faulting 434memoize '_record_index_filename' unless $^O =~ /MSWin/; 435 436sub _record_index_filename { 437 my $self = shift; 438 my %args = validate( @_, { uuid => 1, type => 1 } ); 439 return Prophet::Util->catfile( 440 $self->_record_type_dir( $args{'type'} ), 441 Prophet::Util::hashed_dir_name( $args{uuid} ) 442 ); 443} 444 445sub _record_cas_filename { 446 my $self = shift; 447 my %args = ( type => undef, uuid => undef, @_ ); 448 449 my ( $seq, $key ) = $self->_last_record_index_entry( 450 type => $args{'type'}, 451 uuid => $args{'uuid'} 452 ); 453 454 return unless ( $key and ( $key ne '0' x 40 ) ); 455 return $self->record_cas->filename($key); 456} 457 458sub _record_type_dir { 459 my $self = shift; 460 my $type = shift; 461 return File::Spec->catdir( $self->record_dir, $type ); 462} 463 464# } 465 466 467sub changesets_for_record { 468 my $self = shift; 469 my %args = validate( @_, { uuid => 1, type => 1, limit => 0 } ); 470 471 my @record_index = $self->_read_record_index( 472 type => $args{'type'}, 473 uuid => $args{'uuid'} 474 ); 475 476 my $changeset_index = $self->read_changeset_index(); 477 478 my @changesets; 479 for my $item (@record_index) { 480 my $sequence = $item->[0]; 481 push @changesets, 482 $self->_get_changeset_via_index( 483 sequence_no => $sequence, 484 index_file => $changeset_index 485 ); 486 last if ( defined $args{limit} && --$args{limit} ); 487 } 488 489 return @changesets; 490 491} 492 493 494sub begin_edit { 495 my $self = shift; 496 my %args = validate( 497 @_, 498 { 499 source => 0, # the changeset that we're replaying, if applicable 500 } 501 ); 502 503 my $source = $args{source}; 504 505 my $creator = $source ? $source->creator : $self->changeset_creator; 506 my $created = $source && $source->created; 507 508 require Prophet::ChangeSet; 509 my $changeset = Prophet::ChangeSet->new( 510 { 511 source_uuid => $self->uuid, 512 creator => $creator, 513 $created ? ( created => $created ) : (), 514 } 515 ); 516 $self->current_edit($changeset); 517 $self->current_edit_records( [] ); 518} 519 520sub _set_original_source_metadata_for_current_edit { 521 my $self = shift; 522 my ($changeset) = validate_pos( @_, { isa => 'Prophet::ChangeSet' } ); 523 524 $self->current_edit->original_source_uuid( 525 $changeset->original_source_uuid ); 526 $self->current_edit->original_sequence_no( 527 $changeset->original_sequence_no ); 528} 529 530sub commit_edit { 531 my $self = shift; 532 my $sequence = $self->_increment_sequence_no; 533 $self->current_edit->original_sequence_no($sequence) 534 unless ( defined $self->current_edit->original_sequence_no ); 535 $self->current_edit->original_source_uuid( $self->uuid ) 536 unless ( $self->current_edit->original_source_uuid ); 537 $self->current_edit->sequence_no($sequence); 538 for my $record ( @{ $self->current_edit_records } ) { 539 $self->_write_record_index_entry( 540 changeset_id => $sequence, 541 %$record 542 ); 543 } 544 $self->_write_changeset_to_index( $self->current_edit ); 545} 546 547sub _write_changeset_to_index { 548 my $self = shift; 549 my $changeset = shift; 550 $self->_write_changeset( changeset => $changeset ); 551 $self->current_edit(undef); 552} 553 554sub _after_record_changes { 555 my $self = shift; 556 my ($changeset) = validate_pos( @_, { isa => 'Prophet::ChangeSet' } ); 557 558 $self->current_edit->is_nullification( $changeset->is_nullification ); 559 $self->current_edit->is_resolution( $changeset->is_resolution ); 560} 561 562sub create_record { 563 my $self = shift; 564 my %args = validate( @_, { uuid => 1, props => 1, type => 1 } ); 565 566 my $inside_edit = $self->current_edit ? 1 : 0; 567 $self->begin_edit() unless ($inside_edit); 568 569 $self->_write_serialized_record( 570 type => $args{'type'}, 571 uuid => $args{'uuid'}, 572 props => $args{'props'} 573 ); 574 575 my $change = Prophet::Change->new( 576 { 577 record_type => $args{'type'}, 578 record_uuid => $args{'uuid'}, 579 change_type => 'add_file' 580 } 581 ); 582 583 for my $name ( keys %{ $args{props} } ) { 584 $change->add_prop_change( 585 name => $name, 586 old => undef, 587 new => $args{props}->{$name} 588 ); 589 } 590 591 $self->current_edit->add_change( change => $change ); 592 593 $self->commit_edit unless ($inside_edit); 594} 595 596sub delete_record { 597 my $self = shift; 598 my %args = validate( @_, { uuid => 1, type => 1 } ); 599 600 my $inside_edit = $self->current_edit ? 1 : 0; 601 $self->begin_edit() unless ($inside_edit); 602 603 my $change = Prophet::Change->new( 604 { 605 record_type => $args{'type'}, 606 record_uuid => $args{'uuid'}, 607 change_type => 'delete' 608 } 609 ); 610 $self->current_edit->add_change( change => $change ); 611 612 $self->_prepare_record_index_update( 613 uuid => $args{uuid}, 614 type => $args{type}, 615 cas_key => '0' x 40 616 ); 617 618 $self->commit_edit() unless ($inside_edit); 619 return 1; 620} 621 622sub set_record_props { 623 my $self = shift; 624 my %args = validate( @_, { uuid => 1, props => 1, type => 1 } ); 625 626 my $inside_edit = $self->current_edit ? 1 : 0; 627 $self->begin_edit() unless ($inside_edit); 628 629 my $old_props = $self->get_record_props( 630 uuid => $args{'uuid'}, 631 type => $args{'type'} 632 ); 633 my %new_props = %$old_props; 634 for my $prop ( keys %{ $args{props} } ) { 635 if ( !defined $args{props}->{$prop} ) { 636 delete $new_props{$prop}; 637 } else { 638 $new_props{$prop} = $args{props}->{$prop}; 639 } 640 } 641 $self->_write_serialized_record( 642 type => $args{'type'}, 643 uuid => $args{'uuid'}, 644 props => \%new_props 645 ); 646 647 my $change = Prophet::Change->new( 648 { 649 record_type => $args{'type'}, 650 record_uuid => $args{'uuid'}, 651 change_type => 'update_file' 652 } 653 ); 654 655 for my $name ( keys %{ $args{props} } ) { 656 $change->add_prop_change( 657 name => $name, 658 old => $old_props->{$name}, 659 new => $args{props}->{$name} 660 ); 661 } 662 $self->current_edit->add_change( change => $change ); 663 664 $self->commit_edit() unless ($inside_edit); 665 return 1; 666} 667 668sub get_record_props { 669 my $self = shift; 670 my %args = validate( @_, { uuid => 1, type => 1 } ); 671 return $self->_read_serialized_record( 672 uuid => $args{'uuid'}, 673 type => $args{'type'} 674 ); 675} 676 677sub record_exists { 678 my $self = shift; 679 my %args = validate( @_, { uuid => 1, type => 1 } ); 680 return unless $args{'uuid'}; 681 return $self->_record_cas_filename( 682 type => $args{'type'}, 683 uuid => $args{'uuid'} 684 ) ? 1 : 0; 685 686} 687 688sub list_records { 689 my $self = shift; 690 my %args = validate( @_ => { type => 1, record_class => 1 } ); 691 692 return [] unless $self->type_exists( type => $args{type} ); 693 694 #return just the filenames, which, File::Find::Rule doesn't seem capable of 695 my @record_uuids; 696 find sub { return unless -f $_; push @record_uuids, $_ }, 697 File::Spec->catdir( 698 $self->fs_root => $self->_record_type_dir( $args{'type'} ) ); 699 700 return [ 701 map { 702 my $record = $args{record_class}->new( 703 { 704 app_handle => $self->app_handle, 705 handle => $self, 706 type => $args{type} 707 } 708 ); 709 $record->_instantiate_from_hash( uuid => $_ ); 710 $record; 711 } 712 grep { 713 $self->_record_cas_filename( type => $args{'type'}, uuid => $_ ) 714 } @record_uuids 715 ]; 716 717} 718 719sub list_types { 720 my $self = shift; 721 opendir( my $dh, 722 File::Spec->catdir( $self->fs_root => $self->record_dir ) ) 723 || die "can't open type directory $!"; 724 my @types = grep { $_ !~ /^\./ } readdir($dh); 725 closedir $dh; 726 return \@types; 727} 728 729sub type_exists { 730 my $self = shift; 731 my %args = validate( @_, { type => 1 } ); 732 return $self->_file_exists( $self->_record_type_dir( $args{'type'} ) ); 733} 734 735__PACKAGE__->meta->make_immutable(); 736no Any::Moose; 737 7381; 739 740__END__ 741 742=pod 743 744=head1 NAME 745 746Prophet::Replica::prophet 747 748=head1 VERSION 749 750version 0.751 751 752=head1 METHODS 753 754=head2 replica_version 755 756Returns this replica's version. 757 758=head2 set_replica_version 759 760Sets the replica's version to the given integer. 761 762=head2 uuid 763 764Return the replica's UUID 765 766=head2 changesets_for_record { uuid => $uuid, type => $type, limit => $int } 767 768Returns an ordered set of changeset objects for all changesets containing 769changes to this object. 770 771Note that changesets may include changes to other records 772 773If "limit" is specified, only returns that many changesets (starting from 774record creation). 775 776=head2 begin_edit 777 778Creates a new L<Prophet::ChangeSet>, which new changes will be added to. 779 780=head1 Replica Format 781 782=head4 overview 783 784 $URL 785 /<db-uuid>/ 786 /replica-uuid 787 /latest-sequence-no 788 /replica-version 789 /cas/records/<substr(sha1,0,1)>/substr(sha1,1,1)/<sha1> 790 /cas/changesets/<substr(sha1,0,1)>/substr(sha1,1,1)/<sha1> 791 /records (optional?) 792 /<record type> (for resolution is actually _prophet-resolution-<cas-key>) 793 /<record uuid> which is a file containing a list of 0 or more rows 794 last-changed-sequence-no : cas key 795 796 /changesets.idx 797 798 index which has records: 799 each record is : local-replica-seq-no : original-uuid : original-seq-no : cas key 800 ... 801 802 /resolutions/ 803 /replica-uuid 804 /latest-sequence-no 805 /cas/<substr(sha1,0,1)>/substr(sha1,1,1)/<sha1> 806 /content (optional?) 807 /_prophet-resolution-<cas-key> (cas-key == a hash the conflicting change) 808 /<record uuid> (record uuid == the originating replica) 809 last-changed-sequence-no : <cas key to the content of the resolution> 810 811 /changesets.idx 812 index which has records: 813 each record is : local-replica-seq-no : original-uuid : original-seq-no : cas key 814 ... 815 816Inside the top level directory for the mirror, you'll find a directory named as 817B<a hex-encoded UUID>. This directory is the root of the published replica. The 818uuid uniquely identifes the database being replicated. All replicas of this 819database will share the same UUID. 820 821Inside the B<<db-uuid>> directory, are a set of files and directories that make 822up the actual content of the database replica: 823 824=over 2 825 826=item C<replica-uuid> 827 828Contains the replica's hex-encoded UUID. 829 830=item C<replica-version> 831 832Contains a single integer that defines the replica format. 833 834The current replica version is 1. 835 836=item C<latest-sequence-no> 837 838Contains a single integer, the replica's most recent sequence number. 839 840=item C<cas/records> 841 842=item C<cas/changesets> 843 844The C<cas> directory holds changesets and records, each keyed by a hex-encoded 845hash of the item's content. Inside the C<cas> directory, you'll find a 846two-level deep directory tree of single-character hex digits. You'll find the 847changeset with the sha1 digest C<f4b7489b21f8d107ad8df78750a410c028abbf6c> 848inside C<cas/changesets/f/4/f4b7489b21f8d107ad8df78750a410c028abbf6c>. 849 850You'll find the record with the sha1 digest 851C<dd6fb674de879a1a4762d690141cdfee138daf65> inside 852C<cas/records/d/d/dd6fb674de879a1a4762d690141cdfee138daf65>. 853 854TODO: define the format for changesets and records 855 856=item C<records> 857 858Files inside the C<records> directory are index files which list off all 859published versions of a record and the key necessary to retrieve the record 860from the I<content-addressed store>. 861 862Inside the C<records> directory, you'll find directories named for each C<type> 863in your database. Inside each C<type> directory, you'll find a two-level 864directory tree of single hexadecimal digits. You'll find the record with the 865type <Foo> and the UUID C<29A3CA16-03C5-11DD-9AE0-E25CFCEE7EC4> stored in 866 867 records/Foo/2/9/29A3CA16-03C5-11DD-9AE0-E25CFCEE7EC4 868 869The format of record files is: 870 871 <unsigned-long-int: last-changed-sequence-no><40 chars of hex: cas key> 872 873The file is sorted in asecnding order by revision id. 874 875=item C<changesets.idx> 876 877The C<changesets.idx> file lists each changeset in this replica and provides an 878index into the B<content-addressed storage> to fetch the content of the 879changeset. 880 881The format of record files is: 882 883 <unsigned-long-int: sequence-no><16 bytes: changeset original source uuid><unsigned-long-int: changeset original source sequence no><16 bytes: cas key - sha1 sum of the changeset's content> 884 885The file is sorted in ascending order by revision id. 886 887=item C<resolutions> 888 889=over 2 890 891=item TODO DOC RESOLUTIONS 892 893=back 894 895=back 896 897=head1 AUTHORS 898 899=over 4 900 901=item * 902 903Jesse Vincent <jesse@bestpractical.com> 904 905=item * 906 907Chia-Liang Kao <clkao@bestpractical.com> 908 909=item * 910 911Christine Spang <christine@spang.cc> 912 913=back 914 915=head1 COPYRIGHT AND LICENSE 916 917This software is Copyright (c) 2009 by Best Practical Solutions. 918 919This is free software, licensed under: 920 921 The MIT (X11) License 922 923=head1 BUGS AND LIMITATIONS 924 925You can make new bug reports, and view existing ones, through the 926web interface at L<https://rt.cpan.org/Public/Dist/Display.html?Name=Prophet>. 927 928=head1 CONTRIBUTORS 929 930=over 4 931 932=item * 933 934Alex Vandiver <alexmv@bestpractical.com> 935 936=item * 937 938Casey West <casey@geeknest.com> 939 940=item * 941 942Cyril Brulebois <kibi@debian.org> 943 944=item * 945 946Florian Ragwitz <rafl@debian.org> 947 948=item * 949 950Ioan Rogers <ioanr@cpan.org> 951 952=item * 953 954Jonas Smedegaard <dr@jones.dk> 955 956=item * 957 958Kevin Falcone <falcone@bestpractical.com> 959 960=item * 961 962Lance Wicks <lw@judocoach.com> 963 964=item * 965 966Nelson Elhage <nelhage@mit.edu> 967 968=item * 969 970Pedro Melo <melo@simplicidade.org> 971 972=item * 973 974Rob Hoelz <rob@hoelz.ro> 975 976=item * 977 978Ruslan Zakirov <ruz@bestpractical.com> 979 980=item * 981 982Shawn M Moore <sartak@bestpractical.com> 983 984=item * 985 986Simon Wistow <simon@thegestalt.org> 987 988=item * 989 990Stephane Alnet <stephane@shimaore.net> 991 992=item * 993 994Unknown user <nobody@localhost> 995 996=item * 997 998Yanick Champoux <yanick@babyl.dyndns.org> 999 1000=item * 1001 1002franck cuny <franck@lumberjaph.net> 1003 1004=item * 1005 1006robertkrimen <robertkrimen@gmail.com> 1007 1008=item * 1009 1010sunnavy <sunnavy@bestpractical.com> 1011 1012=back 1013 1014=cut 1015