add kvpairs2td
[hband-tools.git] / user-tools / dlnew
blob942bb2d1e8c2edbfe5fedef352d8bc09ea37f8e3
1 #!/usr/bin/env perl
3 use LWP::Simple;
4 use Date::Parse;
5 use Data::Dumper;
6 eval q{ use Linux::UserXAttr ':all'; };
7 $use_xattr = $@ ? 0 : 1;
8 use Getopt::Long;
9 use POSIX;
10 use Errno ':POSIX';
11 use constant {EC_DOWNLOADED=>0, EC_ERROR=>1, EC_CACHED=>2, EC_NOTOK=>3, EC_NOTMODIFIED=>4, EC_NOTUPDATED=>5,};
12 use constant {XATTR_URL=>"user.xdg.origin.url", XATTR_CACHECONTROL=>"user.cache.control", XATTR_MAXAGE=>"user.cache.control.max_age", XATTR_EXPIRES=>"user.cache.expires",};
14 $now = time;
15 $use_caching = 1;
16 GetOptions(
17 'C!' => sub{ $use_caching = 0; },
18 )or die;
20 ($url, $file) = @ARGV;
21 ($file) = $url =~ /([^\/]+)$/ if not $file;
23 exit main($url, $file);
27 sub delxattr
29 if(not removexattr(@_))
31 return 0 unless $!{ENOTSUP} or $!{ENOATTR} or $!{ENODATA};
36 sub file_mtime { return((stat $_[0])[9]); }
37 sub file_atime { return((stat $_[0])[8]); }
39 sub process_response
41 my ($data, $rsp, $prt) = @_;
42 my $file = $GLOB{file};
43 my $chunk_n = ++$GLOB{chunk_n};
45 sub cb_return { die join(' ', @_)."\n"; }
47 if($chunk_n == 1)
49 my $file_mtime;
51 if(not $rsp->is_success)
53 cb_return EC_NOTOK;
55 if(-e $file)
57 $file_mtime = file_mtime($file);
60 my $url_mtime = str2time($rsp->header("Last-Modified"));
61 if(defined $url_mtime)
63 if($url_mtime <= $file_mtime)
65 cb_return EC_NOTUPDATED;
67 $GLOB{url_mtime} = $url_mtime;
70 my $expires = $rsp->header("Expires");
71 my $cache_control = $rsp->header("Cache-Control");
72 my $maxage;
74 if($cache_control =~ /\b(s-maxage)=(\d+)/i)
76 $maxage = $2;
78 elsif($cache_control =~ /\b(max-age)=(\d+)/i)
80 $maxage = $2;
83 open $GLOB{'fh'}, '>', $file or cb_return EC_ERROR, "$file: $!";
85 if($use_xattr)
87 # https://www.freedesktop.org/wiki/CommonExtendedAttributes/
88 setxattr($GLOB{'fh'}, XATTR_URL, $rsp->request()->uri) or warn "setxattr: $!\n";
89 if(defined $cache_control)
91 setxattr($GLOB{'fh'}, XATTR_CACHECONTROL, $cache_control) or warn "setxattr: $!\n";
93 else
95 delxattr($GLOB{'fh'}, XATTR_CACHECONTROL) or warn "removexattr: $!\n";
97 if(defined $maxage)
99 setxattr($GLOB{'fh'}, XATTR_MAXAGE, $maxage) or warn "setxattr: $!\n";
101 else
103 delxattr($GLOB{'fh'}, XATTR_MAXAGE) or warn "removexattr: $!\n";
105 if(defined $expires)
107 setxattr($GLOB{'fh'}, XATTR_EXPIRES, $expires) or warn "setxattr: $!\n";
109 else
111 delxattr($GLOB{'fh'}, XATTR_EXPIRES) or warn "removexattr: $!\n";
116 print {$GLOB{'fh'}} $data or cb_return EC_ERROR, "$file: $!";
119 sub time2rfc
121 my ($localtime) = @_;
122 my $lc_time = POSIX::setlocale(POSIX::LC_TIME);
123 POSIX::setlocale(POSIX::LC_TIME, "C");
124 my $str = POSIX::strftime("%a, %d %b %Y %T %Z", localtime $localtime);
125 POSIX::setlocale(POSIX::LC_TIME, $lc_time);
126 return $str;
129 sub main
131 my ($url, $file) = @_;
132 my $local_expires;
133 my $local_maxage;
134 my %headers;
136 if(-e $file)
138 my $file_mtime = file_mtime($file);
139 if($use_xattr and $use_caching)
141 my $local_maxage = getxattr($file, XATTR_MAXAGE);
142 my $local_expires = getxattr($file, XATTR_EXPIRES);
143 if(defined $local_maxage)
145 if($file_mtime + $local_maxage > $now)
147 return EC_CACHED;
150 elsif(defined $local_expires)
152 if(str2time($local_expires) > $now)
154 return EC_CACHED;
158 $headers{"If-Modified-Since"} = time2rfc($file_mtime);
161 my $ua = LWP::UserAgent->new;
162 %GLOB = (file => $file);
163 my $rsp = $ua->get($url, ':content_cb' => \&process_response, %headers);
165 if($rsp->is_success)
167 my ($rc, $msg) = split /\s+/, $rsp->header("X-Died"), 2;
168 if(defined $rc)
170 warn "$msg\n" if $msg;
171 return $rc;
173 close $GLOB{'fh'};
174 if(defined $GLOB{url_mtime})
176 utime file_atime($file), $GLOB{url_mtime}, $file or warn "$file: $!\n";
178 return EC_DOWNLOADED;
180 else
182 warn "HTTP " . $rsp->code . " " . $rsp->message . "\n";
183 if($rsp->code == 304)
185 return EC_NOTMODIFIED;
187 return EC_NOTOK;
192 =pod
194 =head1 NAME
196 dlnew - Download web resource if local copy is older
198 =head1 SYNOPSIS
200 dlnew [-C] <B<url>> <B<file>>
202 =head1 DESCRIPTION
204 Download content from web if newer than local copy (based on Last-Modified and caching headers).
206 =head1 PARAMETERS
208 =over 8
210 =item B<-C>
212 Bypass validating cache.
214 =item B<url>
216 URL to be downloaded. Schema can be HTTP or HTTPS.
218 =item B<file>
220 Local file data have to written in. If omitted, last component (basename) of B<url> will be used.
222 =back
224 =head1 EXIT STATUS
226 =over 8
228 =item B<0>
230 B<Url> ia found and downloaded.
232 =item B<1>
234 General error, system errors.
236 =item B<2>
238 Local B<file>'s freshness validated by saved cache metadata, not downloaded.
240 =item B<3>
242 Download not OK. (usually Not Found)
244 =item B<4>
246 B<Url> found but not modified. (HTTP 304)
248 =item B<5>
250 B<Url> found but not updated, based on Last-Modified header.
252 =back
254 =cut