Move from HTTP::Tiny and threads to AnyEvent:HTTP
* Use AnyEvent::HTTP instead of HTTP::Tiny to decrease resource usage * Remove threads and threads::shared * Update --help * Update README.md * Update TODO * Add .gitignore * Add cpanfile with list of required files * Add bootstrap.sh to easily prepare run environment
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
vendor_perl/**
|
||||||
83
README.md
83
README.md
@@ -4,52 +4,87 @@
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
rrclone http://source/repo/ /absolute/path/to/destination/folder/
|
rrclone http://source/repo/ /absolute/path/to/destination/folder/
|
||||||
|
```
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
Linux or FreeBSD with recent perl (at least 5.20, on previous versions i didn't
|
Linux or FreeBSD with recent perl (at least 5.20, on previous versions i didn't
|
||||||
test it). There is no external packages required in order to run this program.
|
test it). Perl modules required: AnyEvent, AnyEvent::HTTP, JSON::PP,
|
||||||
The only thing that required is Perl. But many linux distributions have
|
XML::Parser, URI::URL
|
||||||
ill-minded practice to rip Perl into many sub-packages, in this case you have
|
|
||||||
to install some perl packages.
|
|
||||||
|
|
||||||
for ubuntu: libjson-pp-perl, libfile-spec-perl, libxml-parser-perl,
|
#### For ubuntu it is enough to perform
|
||||||
libio-compress-perl, liburi-perl
|
|
||||||
|
|
||||||
in case of Slackware Linux everything is already shipped with single Perl
|
```bash
|
||||||
package.
|
sudo apt install libjson-pp-perl libfile-spec-perl libxml-parser-perl \
|
||||||
|
liburi-perl libanyevent-http-perl
|
||||||
```
|
|
||||||
# apt install libjson-pp-perl libfile-spec-perl libxml-parser-perl \
|
|
||||||
libio-compress-perl liburi-perl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A correct path to repository. You need a string after "baseurl=" in repo-file
|
#### For centos 7+ it is enough to perform
|
||||||
(one that located in /etc/yum.repos.d, for example), it must end with trailng
|
|
||||||
slash. If it does not, just add it.
|
```bash
|
||||||
|
yum install perl-AnyEvent-HTTP perl-JSON-PP perl-XML-Parser perl-URI \
|
||||||
|
perl-IO-Zlib
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cpanminus way
|
||||||
|
|
||||||
|
There is a way to install all modules locally either to application dir or to
|
||||||
|
user profile. All you need in this case is cpanminus. Some **perl-App-cpanm**
|
||||||
|
or cpm or carton or so. In most bad case you can wget it from cpanminus
|
||||||
|
[github](https://github.com/miyagawa/cpanminus/raw/devel/App-cpanminus/cpanm)
|
||||||
|
directly.
|
||||||
|
|
||||||
|
Also you'll need analogue of centos **@development** metapackage (package group
|
||||||
|
Development Tools) or Ubuntu **build-essential** metapackage to build perl'
|
||||||
|
binary modules.
|
||||||
|
|
||||||
|
To prepare environment you need to run **./bootstrap.sh** in rrclone directory.
|
||||||
|
**Never run it as root**, or packages will be installed directly in system.
|
||||||
|
|
||||||
|
In case of package, providing perl' **local::lib** found in system dependencies
|
||||||
|
will be installed in script directory in **vendor_perl** catalogue, else it will
|
||||||
|
go to **~./perl5**.
|
||||||
|
|
||||||
|
To run script properly you should setup environment variables pointing to these
|
||||||
|
vendored modules.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PERL5LIB="/path/to/rrclone/dir/vendor_perl/lib/perl5"
|
||||||
|
```
|
||||||
|
|
||||||
|
and in case if perl' local::lib is not found in system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PERL5LIB="$HOME/.perl5/lib/perl5"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using rrclone
|
||||||
|
|
||||||
|
There are no additional effort required for one-shot sync.
|
||||||
|
|
||||||
|
You need correct path to repository. A string after "baseurl=" in repo-file
|
||||||
|
(one that located in **/etc/yum.repos.d**, for example), it must end with
|
||||||
|
**trailng slash**. If it does not, just add it.
|
||||||
|
|
||||||
To check that url is correct you can try to download part of repository xml
|
To check that url is correct you can try to download part of repository xml
|
||||||
metadata. Put your string into browser address bar and add
|
metadata. Put your string into browser address bar and add
|
||||||
"repodata/repomd.xml" (no quotes) and if you see xml text then - vua-la! -
|
"repodata/repomd.xml" (no quotes) and if you see xml text then - vua-la! -
|
||||||
your string is correct.
|
your string is correct.
|
||||||
|
|
||||||
### Installing
|
|
||||||
|
|
||||||
There are no additional effort required for one-shot sync.
|
|
||||||
|
|
||||||
But if you planning to keep repository in sync with upstream, you should add
|
But if you planning to keep repository in sync with upstream, you should add
|
||||||
cron job, that can be run without any additional privileges, i.e. web server
|
cron job, that can be run without any additional privileges, i.e. web server
|
||||||
user is enough for that.
|
user is enough for that.
|
||||||
|
|
||||||
to add cronjob invoke
|
To add cronjob invoke
|
||||||
|
|
||||||
```
|
```bash
|
||||||
su -s /bin/bash apache
|
su -s /bin/bash apache
|
||||||
crontab -e
|
crontab -e
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```text
|
||||||
30 4 * * 0 /path/to/rrclone http://repository/url/ /absolute/path/to/copy/
|
30 4 * * 0 /path/to/rrclone http://repository/url/ /absolute/path/to/copy/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -60,6 +95,8 @@ This will run job weekly, at Sunday 4:30.
|
|||||||
Obviously, before deploy this script on some production environment you should
|
Obviously, before deploy this script on some production environment you should
|
||||||
package it properly :)
|
package it properly :)
|
||||||
|
|
||||||
|
And as a good practice do not run it as root user.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you want contribute ideas or code - please feel free to contact me via
|
If you want contribute ideas or code - please feel free to contact me via
|
||||||
@@ -67,7 +104,7 @@ email eleksir at exs-elm.ru
|
|||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
* **Eleksir** - *Initial work* - [exs' corner](https://exs-elm.ru/hg/rrclone)
|
* **Eleksir** - *Initial work* - [exs' bitbucket](https://bitbucket.org/eleksir/rrclone/src/master/)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
2
TODO
2
TODO
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
* добавить возможность работы с sqlite-метаданными и перейти на этот метод по-умолчанию
|
* добавить возможность работы с sqlite-метаданными и перейти на этот метод по-умолчанию
|
||||||
* сделать fatpack скрипта
|
* сделать fatpack скрипта
|
||||||
* запользовать AnyEvent::HTTP для скачивания пакетов
|
|
||||||
* задействовать проверку по sha256 хэшу как скачиваемых пакетов так и тех, которые уже были скачаны
|
* задействовать проверку по sha256 хэшу как скачиваемых пакетов так и тех, которые уже были скачаны
|
||||||
* уточнить список возможных хэшей
|
* уточнить список возможных хэшей
|
||||||
* добавить пример spec-файла
|
* добавить пример spec-файла
|
||||||
@@ -15,4 +14,3 @@
|
|||||||
* добавить возможность клонирования ещё каких-нибудь репозиториев, навроде apt-rpm
|
* добавить возможность клонирования ещё каких-нибудь репозиториев, навроде apt-rpm
|
||||||
* добавить возможность исключения пакетов по регулярке (по подстроке, хотябы)
|
* добавить возможность исключения пакетов по регулярке (по подстроке, хотябы)
|
||||||
* добавить возможность перегенерации метаданных, по фэн-шую (описать этот фэн-шуй в документации)
|
* добавить возможность перегенерации метаданных, по фэн-шую (описать этот фэн-шуй в документации)
|
||||||
|
|
||||||
|
|||||||
5
bootstrap.sh
Executable file
5
bootstrap.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cpanm -L vendor_perl --force --installdeps .
|
||||||
|
|
||||||
|
# vim: set ft=sh noet ai ts=4 sw=4 sts=4:
|
||||||
6
cpanfile
Normal file
6
cpanfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
requires 'local::lib';
|
||||||
|
requires 'IO::Socket::SSL';
|
||||||
|
requires 'AnyEvent::HTTP';
|
||||||
|
requires 'XML::Parser';
|
||||||
|
requires 'IO::Zlib';
|
||||||
|
requires 'URI::URL';
|
||||||
168
rrclone
168
rrclone
@@ -3,25 +3,31 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings "all";
|
use warnings "all";
|
||||||
use Fcntl;
|
use Fcntl;
|
||||||
use threads;
|
|
||||||
use threads::shared;
|
|
||||||
use File::Basename;
|
use File::Basename;
|
||||||
use File::Path qw(make_path);
|
use File::Path qw(make_path);
|
||||||
use File::Spec;
|
use File::Spec;
|
||||||
use XML::Parser;
|
use XML::Parser;
|
||||||
use IO::Zlib;
|
use IO::Zlib;
|
||||||
use URI::URL;
|
use URI::URL;
|
||||||
use Data::Dumper;
|
use AnyEvent::Loop;
|
||||||
use HTTP::Tiny;
|
use AnyEvent::HTTP;
|
||||||
use IO::Socket::SSL; # implicit requirement in order to HTTP::Tiny work with https
|
use IO::Socket::SSL; # implicit requirement in order work with https
|
||||||
use Getopt::Long;
|
use Getopt::Long;
|
||||||
use utf8;
|
use utf8;
|
||||||
|
|
||||||
sub mythread();
|
# global vars
|
||||||
|
my $done = AnyEvent->condvar;
|
||||||
|
$AnyEvent::HTTP::MAX_PER_HOST = 4;
|
||||||
|
$AnyEvent::HTTP::TIMEOUT = 10;
|
||||||
|
my $batch = 0;
|
||||||
|
my $batchflag = 0;
|
||||||
|
my $savepath = '/dev/null';
|
||||||
|
|
||||||
sub dlfunc(@);
|
sub dlfunc(@);
|
||||||
sub urlencode($);
|
sub urlencode($);
|
||||||
sub getmetadata();
|
sub getmetadata();
|
||||||
sub help($);
|
sub help($);
|
||||||
|
sub download($$$); # borrowed directly from AnyEvent::HTTP docs
|
||||||
sub path_normalize_by_string_manipulation ($);
|
sub path_normalize_by_string_manipulation ($);
|
||||||
sub getfilenames($);
|
sub getfilenames($);
|
||||||
sub noslash($);
|
sub noslash($);
|
||||||
@@ -58,11 +64,8 @@ noslash($BASEURL);
|
|||||||
noslash($BASEDIR);
|
noslash($BASEDIR);
|
||||||
|
|
||||||
syswrite STDOUT, sprintf("Cloning %s to %s\n", $BASEURL, $BASEDIR);
|
syswrite STDOUT, sprintf("Cloning %s to %s\n", $BASEURL, $BASEDIR);
|
||||||
my @REPOFILES :shared;
|
my @REPOFILES;
|
||||||
my @FILES;
|
my @FILES;
|
||||||
my @LINKS :shared;
|
|
||||||
my @SIZES :shared;
|
|
||||||
my $MUTEX :shared = 0; # while $MUTEX = 1, noone picks links
|
|
||||||
|
|
||||||
my $primaryxmlgz = getmetadata();
|
my $primaryxmlgz = getmetadata();
|
||||||
my $fh = new IO::Zlib;
|
my $fh = new IO::Zlib;
|
||||||
@@ -82,7 +85,7 @@ if ($fh->open($BASEDIR . $primaryxmlgz, "rb")) {
|
|||||||
my $package;
|
my $package;
|
||||||
my $location;
|
my $location;
|
||||||
my $size;
|
my $size;
|
||||||
my %list :shared;
|
my %list;
|
||||||
my $p = XML::Parser->new( Style => 'Stream' );
|
my $p = XML::Parser->new( Style => 'Stream' );
|
||||||
$p->setHandlers (
|
$p->setHandlers (
|
||||||
Start => sub {
|
Start => sub {
|
||||||
@@ -111,24 +114,17 @@ $p->parse($xmldata);
|
|||||||
undef $xmldata;
|
undef $xmldata;
|
||||||
undef $p;
|
undef $p;
|
||||||
|
|
||||||
for (my $i = 1; $i < 4; $i++) {
|
$done = AnyEvent->condvar;
|
||||||
threads->create('mythread');
|
$batch = scalar(keys(%list));
|
||||||
|
|
||||||
|
for my $url (keys(%list)) {
|
||||||
|
dlfunc($url, $list{$url});
|
||||||
}
|
}
|
||||||
|
|
||||||
my $tcount = 0;
|
if ($batchflag) {
|
||||||
|
$done->recv;
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
|
||||||
$tcount = threads->list();
|
|
||||||
|
|
||||||
foreach my $thread (threads->list(threads::joinable)) {
|
|
||||||
$thread->join();
|
|
||||||
}
|
|
||||||
|
|
||||||
# list of threads can be polled once per second, it delays consecutive polls by one second
|
|
||||||
sleep(1);
|
|
||||||
} while ($tcount > 0);
|
|
||||||
|
|
||||||
undef $tcount;
|
|
||||||
syswrite STDOUT, "Cloning done.\n";
|
syswrite STDOUT, "Cloning done.\n";
|
||||||
exit 0 if (defined($noclean));
|
exit 0 if (defined($noclean));
|
||||||
|
|
||||||
@@ -167,6 +163,10 @@ Usage: rrclone.pl [--noclean] http://source/url/ /destination/dir/
|
|||||||
Where
|
Where
|
||||||
--noclean is optional argument, if it specified, no cleanup operation will
|
--noclean is optional argument, if it specified, no cleanup operation will
|
||||||
be performed after rpm downloading complete.
|
be performed after rpm downloading complete.
|
||||||
|
Note: because of new metadata arrives from repository old packages become
|
||||||
|
"dead", so you cannot simply "yum downgrade" unless old packages are saved
|
||||||
|
in yum cache. In order to make old packages "live" if --noclean given you
|
||||||
|
should run createrepo or createrepo_c command to re-generate metadata.
|
||||||
|
|
||||||
Source must be http(s) url which trimmed at repodata folder. If full url is
|
Source must be http(s) url which trimmed at repodata folder. If full url is
|
||||||
http://mirror.yandex.ru/centos/7.4.1708/os/x86_64/repodata/repomd.xml
|
http://mirror.yandex.ru/centos/7.4.1708/os/x86_64/repodata/repomd.xml
|
||||||
@@ -188,27 +188,77 @@ HELP
|
|||||||
exit $rc;
|
exit $rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub mythread() {
|
sub download($$$) {
|
||||||
while ( 1 ) {
|
my ($url, $file, $cb) = @_;
|
||||||
my $size;
|
open my $fh, "+>", $file or die "$file: $!";
|
||||||
my $link;
|
|
||||||
|
|
||||||
if ( 1 ) { # to properly remove lock
|
my %hdr;
|
||||||
lock %list;
|
my $ofs = 0;
|
||||||
my @l = keys(%list);
|
|
||||||
last if (@l == 0);
|
if (stat $fh and -s _) {
|
||||||
$link = $l[0];
|
$ofs = -s _;
|
||||||
$size = delete($list{$link});
|
warn "-s is ", $ofs;
|
||||||
|
$hdr{"if-unmodified-since"} = AnyEvent::HTTP::format_date +(stat _)[9];
|
||||||
|
$hdr{"range"} = "bytes=$ofs-";
|
||||||
}
|
}
|
||||||
|
|
||||||
dlfunc($link, $size);
|
http_get $url,
|
||||||
|
headers => \%hdr,
|
||||||
|
on_header => sub {
|
||||||
|
my ($hdr) = @_;
|
||||||
|
|
||||||
|
if ($hdr->{Status} == 200 && $ofs) {
|
||||||
|
# resume failed
|
||||||
|
truncate $fh, $ofs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sysseek $fh, $ofs, 0;
|
||||||
|
|
||||||
|
1
|
||||||
|
},
|
||||||
|
on_body => sub {
|
||||||
|
my ($data, $hdr) = @_;
|
||||||
|
|
||||||
|
if ($hdr->{Status} =~ /^2/) {
|
||||||
|
length $data == syswrite $fh, $data
|
||||||
|
or return; # abort on write errors
|
||||||
|
}
|
||||||
|
|
||||||
|
1
|
||||||
|
},
|
||||||
|
sub {
|
||||||
|
my (undef, $hdr) = @_;
|
||||||
|
my $status = $hdr->{Status};
|
||||||
|
|
||||||
|
if (my $time = AnyEvent::HTTP::parse_date $hdr->{"last-modified"}) {
|
||||||
|
utime $time, $time, $fh;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status == 200 || $status == 206 || $status == 416) {
|
||||||
|
# download ok || resume ok || file already fully downloaded
|
||||||
|
$cb->(1, $hdr);
|
||||||
|
|
||||||
|
} elsif ($status == 412) {
|
||||||
|
# file has changed while resuming, delete and retry
|
||||||
|
unlink $file;
|
||||||
|
$cb->(0, $hdr);
|
||||||
|
|
||||||
|
} elsif ($status == 500 or $status == 503 or $status =~ /^59/) {
|
||||||
|
# retry later
|
||||||
|
syswrite STDERR, "Unable to download $url. Server returns 5xx http status code.\n";
|
||||||
|
$cb->(0, $hdr);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
syswrite STDERR, "Unable to download $url.\n";
|
||||||
|
$cb->(undef, $hdr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub dlfunc(@) {
|
sub dlfunc(@) {
|
||||||
my $url = shift; # short (relative) URL without BASE part
|
my $url = shift; # short (relative) URL without BASE part
|
||||||
my $size = shift;
|
my $size = shift;
|
||||||
my $wgetpath;
|
|
||||||
my $savepath = path_normalize_by_string_manipulation($BASEDIR . $url);
|
my $savepath = path_normalize_by_string_manipulation($BASEDIR . $url);
|
||||||
|
|
||||||
if (substr($savepath, 0, length($BASEDIR)) ne $BASEDIR) {
|
if (substr($savepath, 0, length($BASEDIR)) ne $BASEDIR) {
|
||||||
@@ -220,18 +270,18 @@ sub dlfunc(@) {
|
|||||||
|
|
||||||
# do not re-download already downloaded rpm packages
|
# do not re-download already downloaded rpm packages
|
||||||
unless ($size == -1) {
|
unless ($size == -1) {
|
||||||
|
|
||||||
if (-f $savepath) {
|
if (-f $savepath) {
|
||||||
my $filesize = (stat($savepath))[7];
|
my $filesize = (stat($savepath))[7];
|
||||||
|
|
||||||
if ($size == $filesize) {
|
if ($size == $filesize) {
|
||||||
dbg(" * Skip $savepath file already exists and on-disk and in-repo sizes are match.");
|
dbg(" * Skip $savepath - file already exists and on-disk and in-repo sizes are match");
|
||||||
|
$batch--;
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
dbg(" + Downloading $savepath file exists but on-disk and in-repo sizes are mismatch. $filesize vs $size.")
|
dbg(" + Queue download of $savepath - file exists but on-disk and in-repo sizes are mismatch - $filesize vs $size")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dbg(" + Downloading $savepath");
|
dbg(" + Queue download of $savepath");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -249,10 +299,21 @@ sub dlfunc(@) {
|
|||||||
|
|
||||||
$url = $BASEURL . $url;
|
$url = $BASEURL . $url;
|
||||||
$url = urlencode($url);
|
$url = urlencode($url);
|
||||||
|
$batchflag = 1 if ($batchflag == 0);
|
||||||
|
|
||||||
|
download ($url, $savepath, sub {
|
||||||
|
if ($_[0]) {
|
||||||
|
#syswrite STDERR, "$url downloaded.\n";
|
||||||
|
} elsif (defined $_[0]) {
|
||||||
|
#syswrite STDERR, "Unable to download $url.\n";
|
||||||
|
} else {
|
||||||
|
#syswrite STDERR, "Unable to download $url. Server returns 5xx http status code.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$batch--;
|
||||||
|
$done->send if $batch == 0;
|
||||||
|
});
|
||||||
|
|
||||||
my $http = HTTP::Tiny->new();
|
|
||||||
$http->mirror($url, $savepath);
|
|
||||||
undef $http;
|
|
||||||
undef $savepath;
|
undef $savepath;
|
||||||
undef $url;
|
undef $url;
|
||||||
}
|
}
|
||||||
@@ -267,7 +328,14 @@ sub urlencode($) {
|
|||||||
|
|
||||||
sub getmetadata() {
|
sub getmetadata() {
|
||||||
my @metafiles;
|
my @metafiles;
|
||||||
|
$batch = 1;
|
||||||
dlfunc('repodata/repomd.xml', -1);
|
dlfunc('repodata/repomd.xml', -1);
|
||||||
|
|
||||||
|
if ($batchflag) {
|
||||||
|
$done->recv;
|
||||||
|
$batchflag = 0;
|
||||||
|
}
|
||||||
|
|
||||||
my $p = XML::Parser->new( Style => 'Stream' );
|
my $p = XML::Parser->new( Style => 'Stream' );
|
||||||
$p->setHandlers (
|
$p->setHandlers (
|
||||||
Start => sub {
|
Start => sub {
|
||||||
@@ -285,10 +353,20 @@ sub getmetadata() {
|
|||||||
|
|
||||||
$p->parsefile($BASEDIR . 'repodata/repomd.xml');
|
$p->parsefile($BASEDIR . 'repodata/repomd.xml');
|
||||||
|
|
||||||
|
$done = AnyEvent->condvar;
|
||||||
|
$batch = scalar(@metafiles);
|
||||||
|
|
||||||
|
die "Incorrect metadata in repository\n" if ($batch < 1);
|
||||||
|
|
||||||
foreach (@metafiles) {
|
foreach (@metafiles) {
|
||||||
dlfunc($_, -1);
|
dlfunc($_, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($batchflag) {
|
||||||
|
$done->recv;
|
||||||
|
$batchflag = 0;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (@metafiles) {
|
foreach (@metafiles) {
|
||||||
return $_ if ($_ =~ /primary\.xml\.gz$/);
|
return $_ if ($_ =~ /primary\.xml\.gz$/);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user