#!/usr/bin/perl -w # =head1 NAME debpaste - http://paste.debian.net/ XML-RPC client =cut # Author: Hanno Hecker # Licence: AGPL 3.0 (http://www.fsf.org/licensing/licenses/agpl-3.0.html) # Version: $Id: debpaste 25 2012-11-18 07:21:02Z vetinari $ # SVN: http://svn.ankh-morp.org:8080/tools/paste-dn/ # # Required: # deb: perl-base perl-modules # libtimedate-perl libfrontier-rpc-perl libtext-iconv-perl # # ToDo: # * "get" formatting? # * wishlist :) # use strict; use Getopt::Long; use Pod::Usage; my %config; my $VERSION = '1.1 ($Rev: 25 $)'; =head1 SYNOPSIS B ACTION [OPTIONS] [CODE|ID] =head1 ACTIONS =over 4 =item add Usage: debpaste add [OPTIONS] [CODE] Adds a new paste to L. If no code is given on the command line, it will read from stdin. Your paste infos are saved to I<~/.debpaste.history> =item del Usage: debpaste del [OPTIONS] ID Deletes paste with id ID. This must be an ID which you have pasted before (and is in your history file) =item get Usage: debpaste get [OPTIONS] ID Fetches the paste with id ID from L. To C a paste use something like debpaste get --noheader ID > OUTFILE =item lang Usage: debpaste lang [OPTIONS] Dumps the list of available languages for syntax highlighting, use the B<--lang=LANG> option when Bing a paste. =item edit Usage: debpaste edit [OPTIONS] ID Downloads the paste with id ID, spawns an editor, and sends the edited file as new paste. =item expire Usage: debpaste expire [OPTIONS] [ID] Removes the entry ID from history file. If no ID is given it removes all entries which are expired. =back =head1 OPTIONS =over 4 =item --user=USERNAME paste as USERNAME instead of C =item --server=URL use URL instead of http://paste.debian.net/server.pl =item --hidden post as hidden entry =item --noproxy do not use the http proxy given in the environment variable C =item --lang=LANG use LANG for syntax highlight ('debpaste lang' for available languages) =item --expires=SEC expires in SEC seconds (default: 259200 = 72h) =item --encoding=ENC when adding new paste, use ENC as encoding of file, default: UTF-8 =item --noheader when Bting entries, don't print header, just dump the paste to stdout. =item --version print version and exit =back =cut binmode(STDOUT, ":utf8"); binmode(STDERR, ":utf8"); $0 =~ s#.*/##; =head1 FILES =over 4 =item ~/.debpaste.rc The right place for setting default options like the username or expire values. Format is C, example: User: Vetinari Expires: 86400 =item ~/.debpaste.history All info about pastes done with B are recorded here. This file is used to keep a record for Beting entries after pasting. Use B to remove old entries. =back =cut my $settings = $ENV{HOME}."/.debpaste.rc"; ## Don't change, edit $settings file: ## KeYInAnyCaSE: value ## AnoThErKey: other-value my $history = $ENV{HOME}."/.debpaste.history"; %config = ( server => "http://paste.debian.net/server.pl", user => "anonymous", lang => "", expires => 86400 * 3, # history_file => $history, no_get_header => 0, hidden => 0, ); my $action = "help"; my %help = ( 'add' => "\n" ."Usage: $0 add [OPTIONS] [CODE]\n" ." Adds a new paste to http://paste.debian.net/\n" ." If no code is given on the command line, it will read from\n" ." stdin.\n" ." Your paste infos are saved to $history\n", 'get' => "\n" ."Usage: $0 get [OPTIONS] ID\n" ." Fetches the paste with id ID from paste.debian.net\n" ." To 'download' a paste use something like\n" ." $0 get --noheader ID > OUTFILE\n", 'del' => "\n" ."Usage: $0 del [OPTIONS] ID\n" ." Deletes paste with id ID. This must be an ID which you have\n" ." pasted before (and is in your history file)\n", 'lang' => "\n" ."Usage: $0 lang [OPTIONS]\n" ." Dumps the list of available languages for syntax highlighting\n", 'edit' => "\n" ."Usage: $0 edit [OPTIONS] ID\n" ." Downloads the paste with id ID, spawns an editor (\$EDITOR)\n" ." and sends the edited file as new paste\n", 'expire' => "\n" ."Usage: $0 expire [OPTIONS] [ID]\n" ." Removes the entry ID from history file. If no ID is given,\n" ." it removes all entries which are expired.\n", # 'help' => "FIXME: help", ); if (@ARGV and $ARGV[0] !~ /^-/) { $action = shift @ARGV; } &read_settings(); GetOptions( "user=s" => \$config{user}, "server=s" => \$config{server}, "expires=s" => \$config{expires}, "lang=s" => \$config{lang}, "encoding=s"=> \$config{encoding}, "noheader" => \$config{no_get_header}, "hidden" => \$config{hidden}, "help" => sub { pod2usage(-exitval => 0, -verbose => 2) }, "version" => sub { print "debpaste v$VERSION\n"; exit 0; }, ) or pod2usage(-exitval => 1, -verbose => 2); if ($action and $action eq "help") { $action = shift @ARGV if (@ARGV and $ARGV[0] !~ /^-/); &help($action); exit 0; } my $paste = PasteDN->new(%config); if ($paste->can($action) and $action ne "new" and $action !~ /^_/) { $paste->$action(); } else { die "$0: err... unknown action $action...\n"; } sub read_settings { open SET, $settings or return; while (defined (my $line = )) { next unless $line =~ /^(\w+):\s+(.*)$/; my ($key, $value) = (lc $1, $2); unless (exists $config{$key}) { warn "$0: unknown config key '$key' found\n"; next; } ($config{$key} = $value) =~ s/^\s*(.*?)\s*$/$1/; } close SET; } sub help { my $msg = ""; ($msg = $help{$_[0]}."\n") if (exists $help{$_[0]}); pod2usage(-exitval => 0, -verbose => 2, -message => $msg); } ################################################################### package PasteDN; use Frontier::Client; use Date::Parse; use POSIX qw(strftime); use File::Temp qw(tempfile); use Text::Iconv; sub new { my $me = shift; my %args = @_; my $type = ref($me) || $me; my $self = {}; bless $self, $type; foreach (keys %args) { $self->{$_} = $args{$_}; } unless (exists $self->{editor}) { $self->{editor} = $ENV{EDITOR} ? $ENV{EDITOR} : ($ENV{VISUAL} ? $ENV{VISUAL} : "/usr/bin/editor"); } $self->{encoding} = "UTF-8" unless $self->{encoding}; $self->{expires} += time; my %fc = ( url => $self->{server} ); unless ($self->{noproxy}) { $fc{proxy} = $ENV{http_proxy} if $ENV{http_proxy}; } $self->{_service} = Frontier::Client->new(%fc); $self; } sub _to_utf8 { my ($self,$txt) = @_; my $enc = $self->{encoding}; return $txt if $enc eq "UTF-8"; my $i = eval { Text::Iconv->new($enc, "UTF-8"); }; die "$0: unsupported encoding $enc\n" if $@; my $new = $i->convert($txt); return $txt unless $new; return $new; } sub _error { my ($self, $msg) = @_; unlink $self->{_tempfile} if $self->{_tempfile}; die "$0: $msg\n"; } sub lang { my $self = shift; my $rc = $self->{_service}->call("paste.getLanguages"); die $rc->{statusmessage},"\n" if $rc->{rc}; ## print $rc->{statusmessage},"\n"; print "Available syntax highlights:\n"; foreach (@{$rc->{langs}}) { print " $_\n"; } } sub get { my $self = shift; my $id = shift @ARGV; die "$0: no id given\n" unless $id; my $rc = $self->{_service}->call("paste.getPaste", $id); die $rc->{statusmessage},"\n" if $rc->{rc}; # ugly, but dates are ok then... # FIXME: probably only works with paste.d.n's timezone: my $stime = str2time($rc->{submitdate}, "CET") - 3600; my $sub_date = strftime('%Y-%m-%d %H:%M:%S', localtime $stime); my $exp_date = strftime('%Y-%m-%d %H:%M:%S', localtime($stime + $rc->{expiredate})); unless ($self->{no_get_header}) { print "User: ", $rc->{submitter}, "\n", "Date: $sub_date\n", "Expires: $exp_date\n", "---------------------------------\n"; } print $rc->{code},"\n"; } sub edit { my $self = shift; my $id = shift @ARGV; die "$0: no id given\n" unless $id; my $rc = $self->{_service}->call("paste.getPaste", $id); die $rc->{statusmessage},"\n" if $rc->{rc}; my $new = $self->_spawn_editor($rc->{code}); if (!$new or ($new eq $rc->{code})) { print "$0: not changed, aborting...\n"; exit 0; } ## FIXME: text from paste.debian.net is probably UTF-8 ## $new = $self->_to_utf8($new); $rc = $self->{_service}->call("paste.addPaste", $new, $self->{user}, $self->{expires} - time, $self->{lang}, $self->{hidden}); die $rc->{statusmessage},"\n" if $rc->{rc}; print $rc->{statusmessage},"\n"; print "To delete this entry, use: $0 del $rc->{id}\n"; $self->_save_entry($rc); } sub _spawn_editor { my ($self, $txt) = @_; my $fh; ($fh, $self->{_tempfile}) = tempfile("debpaste.XXXXXX", DIR => "/tmp"); $self->_error("Could not create temp file: $!") unless ($fh and $self->{_tempfile}); print $fh $txt or $self->_error("Could not print to tempfile: $!"); close $fh or $self->_error("Failed to close tempfile: $!"); if (system($self->{editor}, $self->{_tempfile}) != 0) { $self->_error("failed to execute: $!") if $? == -1; $self->_error(sprintf('child died with signal %d, %s coredump', ($? & 127), ($? & 128) ? 'with' : 'without')) if $? & 127; $self->_error(sprintf('editor exited with value %d', $? >> 8)); } open FH, $self->{_tempfile} or $self->_error("Failed to open temp file: $!"); { local $/ = undef; $txt = ; }; close FH; unlink $self->{_tempfile}; return $txt; } sub delete { $_[0]->del(); } sub del { my $self = shift; my %entry = (); my $id = shift @ARGV; die "$0: no id given\n" unless $id; open FILE, $self->{history_file} or die "$0: failed to open history file: $!\n"; { local $/ = "\n\n"; while () { s#^[\n\s]+##ms; s#[\n\s]+$##ms; next unless $_; %entry = map { /^(\S+):\s*(.*?)\s*$/; ($1, $2 ? $2 : "") } split /\n/, $_; last if ($entry{Entry} and $entry{Entry} eq $id); %entry = (); } } die "$0: Entry for $id not found...\n" unless $entry{Entry}; die "$0: No Digest for $id\n" unless $entry{Digest}; die "$0: Entry $id expired at ", scalar(localtime($entry{Expires})),"\n" if ($entry{Expires} and $entry{Expires} < time); my $rc = $self->{_service}->call("paste.deletePaste", $entry{Digest}); die $rc->{statusmessage},"\n" if $rc->{rc}; print $rc->{statusmessage},"\n", "$0: deleted paste id ",$rc->{id},"\n"; $self->_expire($rc->{id}); } sub expire { my $self = shift; my $id = shift @ARGV; $self->_expire($id); } sub _expire { my ($self, $id) = @_; my @history = (); my %entry; my @ids = (); open FILE, $self->{history_file} or return; { local $/ = "\n\n"; while () { s#^[\n\s]+##ms; s#[\n\s]+$##ms; next unless $_; %entry = map { /^(\S+):\s*(.*?)\s*$/; ($1, $2 ? $2 : "") } split /\n/, $_; ## print "ID: $entry{Entry}\n"; if ($id) { if ($entry{Entry} and $entry{Entry} eq $id) { push @ids, $entry{Entry}; next; } } elsif ($entry{Expires} and $entry{Expires} < time) { push @ids, $entry{Entry}; next; } push @history, { %entry }; } } close FILE; open FILE, ">", $self->{history_file} or die "$0: Failed to open history file: $!\n"; foreach my $h (@history) { foreach (keys %{$h}) { next unless $_; print FILE "$_: $h->{$_}\n"; } print FILE "\n"; } close FILE or die "$0: failed to write: $!\n"; print "$0: expired ", scalar(@ids), " entries from history", (@ids ? ": ".join(", ", @ids) : ""), "\n"; } sub add { my $self = shift; my $code = undef; if (@ARGV) { $code = join("\n", @ARGV); } else { { local $/ = undef; $code = ; } } die "$0: no code given\n" unless $code; $code = $self->_to_utf8($code); my $rc = $self->{_service}->call("paste.addPaste", $code, $self->{user}, $self->{expires} - time, $self->{lang}, $self->{hidden}); die $rc->{statusmessage},"\n" if $rc->{rc}; print $rc->{statusmessage},"\n"; print "To delete this entry, use: $0 del $rc->{id}\n"; $self->_save_entry($rc); } sub _save_entry { my ($self, $rc) = @_; # return unless $self->{save_pastes}; my $file = $self->{history_file} or return; open FILE, ">>", $file or die "$0: failed to open $file: $!\n"; seek FILE, 0, 2 or die "$0: Failed to seek: $!\n"; print FILE "Server: ", $self->{server}, "\n", "Entry: ", $rc->{id}, "\n", "Lang: ", $self->{lang}, "\n", "Expires: ", $self->{expires},"\n", "Hidden: ", $self->{hidden}, "\n", "Digest: ", $rc->{digest}, "\n\n" or die "$0: Failed to save paste: $!\n"; close FILE or die "$0: Failed to save paste: $!\n"; } =head1 DOWNLOAD L or L =head1 NOTES Renamed to C at Rev. 20 =head1 AUTHOR Hanno Hecker =cut # vim: ts=4 sw=4 expandtab syn=perl