package MJB::Backend::Jekyll; use Moo; use IPC::Run3 qw( run3 ); use Cwd qw( getcwd ); use File::Path qw( make_path ); use Storable qw( dclone ); use Mojo::File; use MJB::Backend::Jekyll::MarkdownFile; use MJB::Backend::Jekyll::ConfigFile; # The root path for the repositories has root => ( is => 'ro', required => 1, trigger => sub { my ( $self, $value ) = @_; make_path( $value ); }, ); # The domain name for this jekyll blog has domain => ( is => 'ro', required => 1, ); # The full path to the git repo this is backed by. has repo => ( is => 'ro', required => 1, ); has config => ( is => 'lazy', ); sub _build_config { my ( $self ) = @_; return MJB::Backend::Jekyll::ConfigFile->new( path => $self->repo_path . "/_config.yml", )->read; } has repo_path => ( is => 'lazy', ); sub _build_repo_path { my ( $self ) = @_; return $self->root . "/" . $self->domain; } # The full path to the git repo to clone when using # init on a new repository. has init_from => ( is => 'ro', required => 1, ); sub init { my ( $self ) = @_; # Refuse to overwrite an already-existing site. die "Error: Cannot init when the target directory already exists." if -d $self->repo_path; # Clone the template repo $self->system_command( [ qw( git clone ), $self->init_from, $self->repo_path ] ); # Update the origin that is set $self->system_command( [ qw( git remote set-url origin ), $self->repo ], { chdir => $self->repo_path, }); # Confirm the origin updated my $return = $self->system_command( [ qw( git remote get-url origin ) ], { chdir => $self->repo_path, }); if ( $return->{stdout} ne $self->repo . "\n" ) { die "Error: Unable to initialize and set repo."; } # Push the repo to the store $self->system_command( [ qw( git push origin master ) ], { chdir => $self->repo_path, }); return $self; } sub list_media { my ( $self ) = @_; $self->_ensure_repository_is_latest; my $media = Mojo::File->new( $self->repo_path . "/assets/media" ); my @return; # TODO: Sort by date for the listing on the front end. foreach my $file ( $media->list->each ) { push @return, { path => $file->to_string, filename => $file->basename, url => 'https://' . $self->domain . '/assets/media/' . $file->basename, markdown => '![Title for image](https://' . $self->domain . '/assets/media/' . $file->basename . ')', }; } return [ @return ]; } sub list_posts { my ( $self ) = @_; $self->_ensure_repository_is_latest; my $posts = Mojo::File->new( $self->repo_path . "/_posts" ); my @files; # TODO: Sort by date for the listing on the front end. foreach my $file ( $posts->list->each ) { push @files, MJB::Backend::Jekyll::MarkdownFile->new( path => $file->to_string, ); } return [ @files ]; } sub get_post { my ( $self, $filename ) = @_; return MJB::Backend::Jekyll::MarkdownFile->new( path => $self->repo_path . "/_posts/" . $filename, )->read; } sub new_post { my ( $self, $filename ) = @_; return MJB::Backend::Jekyll::MarkdownFile->new( path => $self->repo_path . "/_posts/" . $filename, ); } sub get_title_of_post { my ( $self, $file ) = @_; open my $lf, "<", $file or die "Failed to open $file for reading: $!"; while ( defined( my $line = <$lf> ) ) { if ( $line =~ /^title: (.+)$/ ) { close $lf; return $1; } } close $lf; return undef; } # Think about this.... # probably want 'slug: ' as an override for the file path sub _post_path { my ( $self, $headers ) = @_; my $title = $headers->{title}; $title = lc($title); $title =~ s/[^a-zA-Z0-9-_]+/_/g; $title =~ s/[_]+/_/g; $title =~ s/_$//g; $title =~ s/^_//g; return $self->repo_path . "/_posts/" . $title . ".markdown"; } sub write_config { my ( $self, $config ) = @_; $config ||= $self->config; $config->write; # Add the file to git $self->system_command( [ qw( git add ), $config->path ], { chdir => $self->repo_path, }); # Commit the file $self->system_command( [ qw( git commit -m ), "Updated Site Config" ], { chdir => $self->repo_path, }); # Push the repo to the store server $self->system_command( [ qw( git push origin master ) ], { chdir => $self->repo_path, }); return 1; } sub write_post { my ( $self, $md_file ) = @_; # Check that the repo exists and is latest. $self->_ensure_repository_is_latest; # Write the file $md_file->write; # Add the file to git $self->system_command( [ qw( git add ), $md_file->path ], { chdir => $self->repo_path, }); # Commit the file $self->system_command( [ qw( git commit -m ), "Created " . $md_file->headers->{title} ], { chdir => $self->repo_path, }); # Push the repo to the store server $self->system_command( [ qw( git push origin master ) ], { chdir => $self->repo_path, }); return 1; } sub commit_file { my ( $self, $file, $comment ) = @_; # Check that the repo exists and is latest. $self->_ensure_repository_is_latest; # Add the file to git $self->system_command( [ qw( git add ), $file ], { chdir => $self->repo_path, }); # Commit the file $self->system_command( [ qw( git commit -m ), $comment ], { chdir => $self->repo_path, }); # Push the repo to the store server $self->system_command( [ qw( git push origin master ) ], { chdir => $self->repo_path, }); return 1; } sub remove_file { my ( $self, $file, $comment ) = @_; # Check that the repo exists and is latest. $self->_ensure_repository_is_latest; # Add the file to git $self->system_command( [ qw( git rm ), $file ], { chdir => $self->repo_path, }); # Commit the file $self->system_command( [ qw( git commit -m ), $comment ], { chdir => $self->repo_path, }); # Push the repo to the store server $self->system_command( [ qw( git push origin master ) ], { chdir => $self->repo_path, }); return 1; } sub delete_post { my ( $self, $title, $file ) = @_; # Check if the repo exists and update the repo if needed $self->_ensure_repository_is_latest; # Ensure the post exists - irony die "Error: Cannot delete post that doesn't exists at " . $file if ! -f $file; # git rm the file $self->system_command( [ qw( git rm ), $file ], { chdir => $self->repo_path, }); # git commit the file $self->system_command( [ qw( git commit -m ), "Deleted post $title" ], { chdir => $self->repo_path, }); # Push the repo to the store server $self->system_command( [ qw( git push origin master ) ], { chdir => $self->repo_path, }); } sub history { # Check if the repo exists # Do a git history # Format the results into a data structure # Return the data structure } # Helper function to ensure the repo exists and has the latest # changes. sub _ensure_repository_is_latest { my ( $self ) = @_; # Check for the repo -- if it doesn't exist, clone it. if ( ! -d $self->repo_path ) { $self->system_command( [ qw( git clone ), $self->repo, $self->repo_path ] ); return 1; } # Run a git pull with fast forward $self->system_command( [ qw( git pull --ff-only origin master ) ], { chdir => $self->repo_path, }); return 1; } sub system_command { my ( $self, $cmd, $settings ) = @_; $settings ||= {}; # Change the directory, if requested. if ( $settings->{chdir} ) { # Throw an error if that directory doesn't exist. die "Error: directory " . $settings->{chdir} . "doesn't exist." unless -d $settings->{chdir}; # Change to that directory, or die with error. chdir $settings->{chdir} or die "Failed to chdir to " . $settings->{chdir} . ": $!"; $settings->{return_chdir} = getcwd(); } # Mask values we don't want exposed in the logs. my $masked_cmd = dclone($cmd); if ( ref $settings->{mask} eq 'HASH' ) { foreach my $key ( keys %{$settings->{mask}} ) { my $value = $settings->{mask}{$key}; $masked_cmd = [ map { s/\Q$key\E/$value/g; $_ } @{$masked_cmd} ]; } } # Log the lines my ( $out, $err ); my $ret = run3( $cmd, \undef, sub { chomp $_; # Mask values we don't want exposed in the logs. if ( ref $settings->{mask} eq 'HASH' ) { foreach my $key ( keys %{$settings->{mask}} ) { my $value = $settings->{mask}{$key}; s/\Q$key\E/$value/g; } } $out .= "$_\n"; }, sub { chomp $_; # Mask values we don't want exposed in the logs. if ( ref $settings->{mask} eq 'HASH' ) { foreach my $key ( keys %{$settings->{mask}} ) { my $value = $settings->{mask}{$key}; s/\Q$key\E/$value/g; } } $err .= "$_\n"; }); # Check stderr for errors to fail on. if ( $settings->{fail_on_stderr} ) { my @tests = @{$settings->{fail_on_stderr}}; while ( my $regex = shift @tests ) { my $reason = shift @tests; if ( $err =~ /$regex/ ) { die $reason; } } } # Return to the directory we started in if we chdir'ed. if ( $settings->{return_chdir} ) { chdir $settings->{return_chdir} or die "Failed to chdir to " . $settings->{chdir} . ": $!"; } if ( $ENV{MJB_DEBUG} ) { require Data::Dumper; print Data::Dumper::Dumper({ stdout => $out, stderr => $err, exitno => $ret, }); } return { stdout => $out, stderr => $err, exitno => $ret, }; } 1;