Revision 388 (by dpavlin, 2009/09/07 10:30:19) fix push so it works now
#!/usr/bin/perl

use warnings;
use strict;

use LWP::UserAgent;
use JSON;
use Data::Dump qw/dump/;
use File::Path qw/mkpath/;
use File::Slurp qw//;
use File::Find;
use HTTP::Request::Common;
use MIME::Base64;
use autodie;

use lib qw(lib ../lib);
use Media::Type::Simple;


# design-couch.pl
#
# 04/26/09 21:12:28 CEST Dobrica Pavlinusic <dpavlin@rot13.org>

our $couchdb = $ENV{COUCHDB} || 'http://localhost:5984/';

our ( $command, $database, $design ) = @ARGV;

die "usage: $0 [push|pull] database [design]\n" unless $database;


my $ua = LWP::UserAgent->new;

sub create_path {
	my $path = shift;
	if ( $path =~ m{/} ) {
		my $dir = $path;
		$dir =~ s{/[^/]+$}{};
		mkpath $dir if ! -e $dir;
		#warn "# dir $dir";
	}
}

sub svn {
	my $path = shift;
	return if $path =~ m{(_.*|.*\.(push|pull)\.js)$};
	system "svn add --parents $path";
}

sub write_file {
	my ( $path, $content ) = @_;
	$path =~ s{^/+}{};
	create_path $path;
	File::Slurp::write_file $path, $content;
	print "$path ", -s $path, " bytes created\n";
	svn $path;
}

sub write_attachment {
	my ( $path ) = @_;
	my $file = "_attachemnts/$path";
	create_path $file;
	$ua->mirror( "$couchdb/$database/_design/$design/$path", $file );
	print "detached $file ", -s $file, " bytes\n";
	svn $file;
}


sub unroll {
	my ( $tree, $path ) = @_;

	my $ref = ref $tree;
	if ( $ref eq 'HASH' ) {
		foreach my $child ( keys %$tree ) {
			if ( $child eq '_attachments' ) {
				write_attachment $_ foreach keys %{ $tree->{$child} };
			} else {
				unroll( $tree->{$child}, $path ? "$path/$child" : $child );
			}
		}
	} elsif ( $ref ) {
		warn "UNSUPPORTED $path $ref ", dump( $tree );
		write_file "$path.json", to_json $tree;
	} elsif ( $ref eq '' ) {

		if ( $tree =~ m[^\s*(function(.*){.*}|/\*|//|var)]is ) {
			$path .= '.js';
		} elsif ( $tree =~ m{<%=.*%>} ) { # couchapp template
			$path .= '.html';
		} else {
			warn "# can't detect type of $path\n";
		}

		write_file $path, $tree;
	}

}

sub pull_design {
	$design = shift;

	my $url = "$couchdb/$database/_design/$design";

	warn "# get $url\n";
	my $response = $ua->get( $url );
	die $response->status_line if $response->is_error;

	my $json = $response->decoded_content;
	write_file "../$database-$design.pull.js", $json;

	unroll( from_json $json, '' );

}

sub push_design {
	$design = shift;

	$ua->request( HTTP::Request::Common::PUT( "http://localhost:5984/$database" ) ) && warn "# created database $database\n";

	my $json;

	find({ no_chdir => 1, wanted => sub {
		my $path = $File::Find::name;
		return unless -f $path;
		return if $path =~ m{/\.svn};

warn "## $path\n";

		$path =~ s{^\./}{};

		if ( $path =~ m{_attachemnts/(.+)} ) {

			my $filename = $1;
			my $content_type = 'text/plain';
			$content_type = type_from_ext($1) if $filename =~ m{\.(\w+)$};

			my $data = File::Slurp::read_file( $path );
			$data = encode_base64( $data );
			# XXX inline attachments must be single line
			# XXX http://wiki.apache.org/couchdb/HTTP_Document_API
			$data =~ s/[\n\r]+//gs;
			$json->{_attachments}->{ $filename } = {
				content_type => $content_type,
				data         => $data,
			};
			return;
		}

		my $data = File::Slurp::read_file( $path );
		$path =~ s[/]['}->{']g;
		$path =~ s{\.\w+$}{};
		my $code = "\$json->{'$path'} = \$data;";
		eval $code;
		die "ERROR in $code: $@" if $@;
#		warn "## json = ",dump( $json );
	}}, '.' );

	if ( ! defined $json->{_id} ) {
		warn "creating _id for document\n";
		$json->{_id} = $$ . '-' . time();
	}
	delete( $json->{_rev} ) && warn "removing _rev from document\n";

	print "push $database/_design/$design\n";
	write_file "../$database-$design.push.js", to_json $json;

	my $url = "$couchdb/$database/_design/$design";
	warn "# put $url\n";
	my $response = $ua->request(
		HTTP::Request::Common::PUT(
			$url,
			'Content-Type' => 'application/json',
			Content => to_json $json,
		)
	);

	if ( $response->code == 409 ) {
		warn "## update $url\n";
		my $response = $ua->get( $url );
		die $response->status_line if $response->is_error;

		my $data = from_json $response->decoded_content;
		$json->{$_} = $data->{$_} foreach ( '_rev', '_id' );

		$response = $ua->request( HTTP::Request::Common::PUT($url, 'Content-Type' => 'application/json', Content => to_json $json ) );
		die $response->status_line if $response->is_error;
		warn "push updated $url\n";
	} else {
		die $response->status_line if $response->is_error;
		warn "push new $url\n";
	}
}


# XXX main


if ( $command eq 'push' ) {

	my @designs = map { s{/.+$}{}; $_ } glob '*/views';
	@designs = ( $design ) if $design;

	foreach my $design ( @designs ) {

		chdir $design;
		push_design( $design );
		chdir '..';

	}

} elsif ( $command eq 'pull' ) {

	my $designs = from_json $ua->get( "$couchdb/$database/_all_docs?startkey=%22_design%2F%22&endkey=%22_design0%22" )->decoded_content;
	my @designs =
		map {
			my $name = $_->{id};
			$name =~ s{^_design/}{};
			$name;
		} @{ $designs->{rows} }
	;

	warn "# $database/_design ",dump( @designs );

	@designs = ( $design ) if $design;

	foreach my $design ( @designs ) {

		mkdir $design unless -e $design;
		chdir $design;
		pull_design( $design );
		chdir '..';

	}

} else {
	die "$0: unknown command $command";
}