File Coverage

blib/lib/WWW/PAUSE/Simple.pm
Criterion Covered Total %
statement 17 347 4.9
branch 0 180 0.0
condition 0 78 0.0
subroutine 6 22 27.2
pod 8 10 80.0
total 31 637 4.8


!s; \s+\s+!gs) { !s; \s* \s* \s* \s* \s* !gsx) {
line stmt bran cond sub pod time code
1             package WWW::PAUSE::Simple;
2              
3 1     1   328594 use 5.010001;
  1         5  
4 1     1   6 use strict;
  1         3  
  1         39  
5 1     1   6 use warnings;
  1         3  
  1         70  
6 1     1   2338 use Log::ger;
  1         67  
  1         7  
7              
8 1     1   1101 use Perinci::Object;
  1         622  
  1         106  
9              
10 1     1   8 use Exporter qw(import);
  1         3  
  1         5332  
11             our @EXPORT_OK = qw(
12             upload_file
13             list_files
14             delete_files
15             undelete_files
16             reindex_files
17             list_dists
18             delete_old_releases
19             set_password
20             set_account_info
21             );
22              
23             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
24             our $DATE = '2024-02-18'; # DATE
25             our $DIST = 'WWW-PAUSE-Simple'; # DIST
26             our $VERSION = '0.457'; # VERSION
27              
28             our %SPEC;
29             my $access_log = Log::ger->get_logger(category => "_access");
30              
31             our $re_archive_ext = qr/(?:tar|tar\.(?:Z|gz|bz2|xz)|zip|rar)/;
32              
33             sub _access_log {
34 0     0     my ($args, $action, $obj, $description) = @_;
35             $access_log->info({
36             time => time(),
37             username => $args->{username},
38 0           action => $action,
39             object => $obj,
40             description => $description,
41             });
42             }
43              
44             our %common_args = (
45             username => {
46             summary => 'PAUSE ID',
47             schema => ['str*', match=>'\A\w{2,9}\z', max_len=>9], # see also: Regexp::Pattern::CPAN
48             description => <<'_',
49              
50             If unset, default value will be searched from `~/.pause`. Encrypted `.pause` is
51             not yet supported.
52              
53             _
54             tags => ['common'],
55             },
56             password => {
57             summary => 'PAUSE password',
58             schema => 'str*',
59             description => <<'_',
60              
61             If unset, default value will be searched from `~/.pause`. Encrypted `.pause` is
62             not yet supported.
63              
64             _
65             is_password => 1,
66             tags => ['common'],
67             },
68             # 2016-07-13 - for a few months now, PAUSE has been giving random 500 errors
69             # when uploading. i'm defaulting to a retries=2.
70             # 2017-06-28 - increase default to retries=7.
71             # 2017-06-28 - tune down retries to 5.
72             # 2019-06-05 - now uses exponential backoff, increase retries to 35 to try
73             # for a little over a day
74             # 2019-11-14 - PAUSE is now ok, tune down retries to 5
75             retries => {
76             summary => 'Number of retries when received 5xx HTTP error from server',
77             description => <<'_',
78              
79             The retry uses an exponential backoff strategy of delaying 3, 6, 12, 24, ...,
80             3600, 3600, ... seconds.
81              
82             _
83             schema => 'int*',
84             default => 5,
85             tags => ['common'],
86             },
87             );
88              
89             our %detail_arg = (
90             detail => {
91             summary => 'Whether to return detailed records',
92             schema => 'bool',
93             },
94             );
95              
96             our %detail_l_arg = (
97             detail => {
98             summary => 'Whether to return detailed records',
99             schema => 'bool',
100             cmdline_aliases => {l=>{}},
101             },
102             );
103              
104             our %files_arg = (
105             files => {
106             summary => 'File names/wildcard patterns',
107             'summary.alt.plurality.singular' => 'File name/wildcard pattern',
108             schema => ['array*', of=>'str*', min_len=>1],
109             'x.name.is_plural' => 1,
110             req => 1,
111             pos => 0,
112             greedy => 1,
113             },
114             );
115              
116             our %file_opt_arg = (
117             files => {
118             summary => 'File names/wildcard patterns',
119             'summary.alt.plurality.singular' => 'File name/wildcard pattern',
120             schema => ['array*', of=>'str*'],
121             'x.name.is_plural' => 1,
122             pos => 0,
123             greedy => 1,
124             tags => ['category:filtering'],
125             },
126             );
127              
128             our %mod_opt_arg = (
129             modules => {
130             summary => 'Module names/wildcard patterns',
131             'summary.alt.plurality.singular' => 'Module name/wildcard pattern',
132             schema => ['array*', of=>'str*'],
133             'x.name.is_plural' => 1,
134             pos => 0,
135             greedy => 1,
136             tags => ['category:filtering'],
137             },
138             );
139              
140             our %protect_files_arg = (
141             protect_files => {
142             summary => 'Protect some files/wildcard patterns from delete/cleanup',
143             schema => ['array*', of=>'str*'],
144             'x.name.is_plural' => 1,
145             tags => ['category:filtering'],
146             },
147             );
148              
149             our %argspecsopt_filter_dev = (
150             include_nondev => {
151             summary => 'Whether to include cleaning up non-dev releases',
152             schema => 'bool*',
153             default => 1,
154             },
155             include_dev => {
156             summary => 'Whether to include cleaning up non-dev releases',
157             schema => 'bool*',
158             default => 0,
159             },
160             );
161              
162             our %argspecsopt_filter_dists = (
163             include_dists => {
164             summary => 'Only include specified distributions',
165             "x.name.is_plural" => 1,
166             "x.name.singular" => "include_dist",
167             schema => ['array*', of=>'str*'], # XXX perl::distname
168             },
169             exclude_dists => {
170             summary => 'Exclude specified distributions',
171             "x.name.is_plural" => 1,
172             "x.name.singular" => "exclude_dist",
173             schema => ['array*', of=>'str*'], # XXX perl::distname
174             },
175             );
176              
177             $SPEC{':package'} = {
178             v => 1.1,
179             summary => 'An API for PAUSE',
180             };
181              
182             sub _parse_release_filename {
183 0     0     my $filename = shift;
184             ## no critic: Subroutines::ProhibitExplicitReturnUndef
185             return undef unless
186 0 0         $filename =~ /\A
187             (\w+(?:-\w+)*)
188             -v?(\d+(?:\.\d+){0,}(_\d+|-TRIAL)?)
189             \.$re_archive_ext
190             \z/ix;
191 0           return ($1, $2, $3); # (dist, version, is_dev)
192             }
193              
194             sub _common_args {
195 0     0     my $args = shift;
196 0           (username=>$args->{username}, password=>$args->{password});
197             }
198              
199             sub _request {
200 0     0     require HTTP::Request::Common;
201              
202 0           state $deprecation_warned = 0;
203              
204 0           my %args = @_;
205             # XXX schema
206 0   0       $args{retries} //= 5;
207 0           my $strategy;
208 0           require Algorithm::Backoff::Exponential;
209             $strategy = Algorithm::Backoff::Exponential->new(
210             max_attempts => $args{retries},
211 0           initial_delay => 3,
212             max_delay => 3600,
213             );
214              
215             # set default for username and password from ~/.pause
216 0           my $username = $args{username};
217 0           my $password = $args{password};
218             {
219 0 0 0       last if defined $username && defined $password;
  0            
220 0           my $path = "$ENV{HOME}/.pause";
221 0 0         last unless -f $path;
222 0 0         open my($fh), "<", $path or last;
223 0           while (defined(my $line = <$fh>)) {
224 0 0 0       if ($line =~ /^user\s+(.+)/) { $username //= $1 }
  0 0          
225 0   0       elsif ($line =~ /^password\s+(.+)/) { $password //= $1 }
226             }
227 0 0 0       unless (defined $username && defined $password) {
228 0           die "Please specify username/password\n";
229             }
230             }
231              
232 0           state $ua = do {
233 0           require LWP::UserAgent;
234 0           LWP::UserAgent->new;
235             };
236 0           my $url = "https://pause.perl.org/pause/authenquery";
237 0           my $req = HTTP::Request::Common::POST($url, @{ $args{post_data} });
  0            
238 0           $req->authorization_basic($username, $password);
239              
240 0           my $tries = 0;
241 0           my $resp;
242             RETRY:
243 0           while (1) {
244 0           $resp = $ua->request($req);
245 0 0         if ($resp->code =~ /^[5]/) {
246 0           $tries++;
247 0           my $delay = $strategy->failure;
248 0 0         if ($delay < 0) {
249 0           log_warn("Got error %s (%s) from server when POST-ing to %s%s, giving up");
250 0           last;
251             } else {
252             log_warn("Got error %s (%s) from server when POST-ing to %s%s, retrying (%d/%d) in %d second(s) ...",
253             $resp->code, $resp->message,
254             $url,
255             $args{note} ? " ($args{note})" : "",
256 0 0         $tries, $args{retries}, $delay);
257 0           sleep $delay;
258             }
259 0           next;
260             }
261 0           last;
262             }
263 0           $resp;
264             }
265              
266             sub _htres2envres {
267 0     0     my $res = shift;
268 0           [$res->code, $res->message, $res->content];
269             }
270              
271             $SPEC{upload_files} = {
272             v => 1.1,
273             summary => 'Upload file(s)',
274             args_rels => {
275             choose_one => [qw/delay/],
276             },
277             args => {
278             %common_args,
279             %files_arg,
280             subdir => {
281             summary => 'Subdirectory to put the file(s) into',
282             schema => 'str*',
283             default => '',
284             },
285             delay => {
286             summary => 'Pause a number of seconds between files',
287             schema => ['duration*'],
288             description => <<'_',
289              
290             If you upload a lot of files (e.g. 7-10 or more) at a time, the PAUSE indexer
291             currently might choke with SQLite database locking problem and thus fail to
292             index your releases. Giving a delay of say 2-3 minutes (120-180 seconds) between
293             files will alleviate this problem.
294              
295             _
296             },
297             },
298             features => {dry_run=>1},
299             };
300             sub upload_files {
301 0     0 1   require File::Basename;
302              
303 0           my %args = @_;
304             my $files = $args{files}
305 0 0         or return [400, "Please specify at least one file"];
306 0   0       my $subdir = $args{subdir} // '';
307              
308 0           my $envres = envresmulti();
309              
310 0           my $i = 0;
311 0           my $prev_group = 0;
312 0           for my $file (@$files) {
313 0           my $basename = File::Basename::basename($file);
314 0           my $res;
315             {
316 0 0         unless (-f $file) {
  0            
317 0           $res = [404, "No such file"];
318 0           last;
319             }
320              
321 0 0         if ($args{-dry_run}) {
322 0           log_trace("[dry-run] (%d/%d) Uploading %s ...", $i+1, scalar(@$files), $file);
323 0           goto DELAY;
324             }
325              
326 0           log_trace("(%d/%d) Uploading %s ...", $i+1, scalar(@$files), $file);
327             my $httpres = _request(
328             note => "upload $file",
329             %args,
330             post_data => [
331             Content_Type => 'form-data',
332             Content => {
333             HIDDENNAME => $args{username},
334 0 0         CAN_MULTIPART => 0,
335             pause99_add_uri_upload => $basename,
336             SUBMIT_pause99_add_uri_httpupload => " Upload this file from my disk ",
337             pause99_add_uri_uri => "",
338             pause99_add_uri_httpupload => [$file],
339             (length($subdir) ? (pause99_add_uri_subdirtext => $subdir) : ()),
340             },
341             ]
342             );
343 0 0         if (!$httpres->is_success) {
344 0           $res = _htres2envres($httpres);
345 0           last;
346             }
347 0           $res = [200, "OK"];
348             }
349 0   0       $res->[3] //= {};
350 0           $res->[3]{item_id} = $file;
351 0           log_trace("Result of upload: %s", $res);
352 0 0         if ($res->[0] =~ /^2/) {
353 0           _access_log(\%args, upload => {name=>$basename, size=>(-s $file), subdir=>$subdir});
354             } else {
355 0           log_warn("Upload of %s failed: %s - %s", $file, $res->[0], $res->[1])
356             }
357 0           $envres->add_result($res->[0], $res->[1], $res->[3]);
358              
359             DELAY:
360             {
361             # it's the last file, no point in delaying, just exit
362 0 0         last if ++$i >= @$files;
  0            
363 0 0         if ($args{delay}) {
364 0           log_trace("Sleeping between files for %d second(s) ...", $args{delay});
365 0           sleep $args{delay};
366 0           last;
367             }
368             }
369             }
370 0           $envres->as_struct;
371             }
372              
373             $SPEC{list_files} = {
374             v => 1.1,
375             summary => 'List files',
376             args => {
377             %common_args,
378             %detail_l_arg,
379             %file_opt_arg,
380             del => {
381             summary => 'Only list files which are scheduled for deletion',
382             'summary.alt.bool.not' => 'Only list files which are not scheduled for deletion',
383             schema => 'bool',
384             tags => ['category:filtering'],
385             },
386             size_min => {
387             #schema => 'filesize*',
388             schema => 'uint*',
389             tags => ['category:filtering'],
390             },
391             size_max => {
392             #schema => 'filesize*',
393             schema => 'uint*',
394             tags => ['category:filtering'],
395             },
396             mtime_min => {
397             schema => ['date*', 'x.perl.coerce_to'=>'float(epoch)'],
398             tags => ['category:filtering'],
399             },
400             mtime_max => {
401             schema => ['date*', 'x.perl.coerce_to'=>'float(epoch)'],
402             tags => ['category:filtering'],
403             },
404             },
405             };
406             sub list_files {
407 0     0 1   require Date::Parse;
408 0           require Regexp::Wildcards;
409 0           require String::Wildcard::Bash;
410              
411 0           my %args = @_;
412 0   0       my $q = $args{files} // [];
413 0           my $del = $args{del};
414              
415 0           my $httpres = _request(
416             note => "list files",
417             %args,
418             post_data => [{ACTION=>'show_files'}],
419             );
420              
421             # convert wildcard patterns in arguments to regexp
422 0           $q = [@$q];
423 0           for (@$q) {
424 0 0         next unless String::Wildcard::Bash::contains_wildcard($_);
425 0           my $re = Regexp::Wildcards->new(type=>'unix')->convert($_);
426 0           $re = qr/\A($re)\z/;
427 0           $_ = $re;
428             }
429              
430 0 0         return _htres2envres($httpres) unless $httpres->is_success;
431 0 0         return [543, "Can't scrape list of files from response",
432             $httpres->content]
433             unless $httpres->content =~ m!

Files in directory.+]*>(.+)

434 0           my $str = $1;
435 0           my @files;
436             REC:
437 0           while ($str =~ m!(.+?)(.+?)(.+?)
438 0           my $rec = {
439             name => $1,
440             size => $2,
441             };
442 0           my $time0 = $3;
443 0 0         if ($time0 =~ s/^Scheduled for deletion \(due at //) {
444 0           $rec->{is_scheduled_for_deletion} = 1;
445 0           $time0 =~ s/\)$//;
446             }
447 0           my $time = Date::Parse::str2time($time0, "UTC");
448 0 0         if ($rec->{is_scheduled_for_deletion}) {
449 0           $rec->{deletion_time} = $time;
450             } else {
451 0           $rec->{mtime} = $time;
452             }
453              
454             # filter by requested file/wildcard
455             FILTER_QUERY:
456             {
457 0 0         last unless @$q;
  0            
458 0           for (@$q) {
459 0 0         if (ref($_) eq 'Regexp') {
460 0 0         last FILTER_QUERY if $rec->{name} =~ $_;
461             } else {
462 0 0         last FILTER_QUERY if $rec->{name} eq $_;
463             }
464             }
465             # nothing matches
466 0           next REC;
467             }
468              
469             FILTER_SIZE:
470             {
471 0           next REC if defined $args{size_min} &&
472 0 0 0       $rec->{size} < $args{size_min};
473             next REC if defined $args{size_max} &&
474 0 0 0       $rec->{size} > $args{size_max};
475             }
476              
477             FILTER_MTIME:
478             {
479 0           next REC if defined $args{mtime_min} &&
480 0 0 0       $rec->{mtime} < $args{mtime_min};
481             next REC if defined $args{mtime_max} &&
482 0 0 0       $rec->{mtime} > $args{mtime_max};
483             }
484              
485             FILTER_DEL:
486             {
487 0 0         if (defined $del) {
  0            
488 0 0 0       next REC if $del xor $rec->{is_scheduled_for_deletion};
489             }
490             }
491              
492 0 0         push @files, $args{detail} ? $rec : $rec->{name};
493              
494             }
495 0           my %resmeta;
496 0 0         if ($args{detail}) {
497 0           $resmeta{'table.fields'} =
498             [qw/name size mtime is_scheduled_for_deletion deletion_time/];
499 0           $resmeta{'table.field_formats'} =
500             [undef, undef, 'iso8601_datetime', undef, 'iso8601_datetime'];
501             }
502 0           [200, "OK", \@files, \%resmeta];
503             }
504              
505             $SPEC{list_dists} = {
506             v => 1.1,
507             summary => 'List distributions',
508             description => <<'_',
509              
510             Distribution names will be extracted from tarball/zip filenames.
511              
512             Unknown/unparseable filenames will be skipped.
513              
514             _
515             args => {
516             %common_args,
517             %detail_l_arg,
518             newest => {
519             schema => 'bool',
520             summary => 'Only show newest non-dev version',
521             description => <<'_',
522              
523             Dev versions will be skipped.
524              
525             _
526             },
527             newest_n => {
528             schema => ['int*', min=>1],
529             summary => 'Only show this number of newest versions',
530             description => <<'_',
531              
532             Dev versions will be skipped.
533              
534             _
535             },
536             %argspecsopt_filter_dists,
537             %argspecsopt_filter_dev,
538             },
539             };
540             sub list_dists {
541 0     0 1   require List::MoreUtils;
542              
543 0           my %args = @_;
544              
545 0           my $res = list_files(_common_args(\%args), del=>0);
546 0 0         return [500, "Can't list files: $res->[0] - $res->[1]"] if $res->[0] != 200;
547              
548 0           my $newest_n;
549 0 0         if ($args{newest_n}) {
    0          
550 0           $newest_n = $args{newest_n};
551             } elsif ($args{newest}) {
552 0           $newest_n = 1;
553             }
554 0           my $include_dev = $args{include_dev};
555 0   0       my $include_nondev = $args{include_nondev} // 1;
556              
557 0           my @distrecs;
558 0           for my $file (@{$res->[2]}) {
  0            
559 0 0         if ($file =~ m!/!) {
560 0           log_debug("Skipping %s: under a subdirectory", $file);
561 0           next;
562             }
563 0           my ($distname, $version0, $dev) = _parse_release_filename($file);
564 0 0         unless (defined $distname) {
565 0           log_debug("Skipping %s: doesn't match release regex", $file);
566 0           next;
567             }
568 0 0 0       if ($args{include_dists} && @{$args{include_dists}} && !(grep {$distname eq $_} @{$args{include_dists}})) {
  0   0        
  0            
  0            
569 0           log_trace("Skipping %s: Distribution %s not in include_dists", $file, $distname);
570 0           next;
571             }
572 0 0 0       if ($args{exclude_dists} && @{$args{exclude_dists}} && (grep {$distname eq $_} @{$args{exclude_dists}})) {
  0   0        
  0            
  0            
573 0           log_trace("Skipping %s: Distribution %s in exclude_dists", $file, $distname);
574 0           next;
575             }
576              
577 0 0 0       next if $newest_n && (($dev && !$include_dev) || (!$dev && !$include_nondev));
      0        
578 0           (my $version = $version0) =~ s/-TRIAL$/_001/;
579 0           push @distrecs, {
580             name => $distname,
581             file => $file,
582             version0 => $version0,
583             version => $version,
584             };
585             } # for my $file
586              
587 0           my @old_files;
588 0 0         if ($newest_n) {
589 0           my %dist_versions;
590 0           for my $distrec (@distrecs) {
591 0           push @{ $dist_versions{$distrec->{name}} }, $distrec->{version};
  0            
592             }
593 0           for my $distname (keys %dist_versions) {
594             $dist_versions{$distname} = [
595 0           sort { version->parse($b) <=> version->parse($a) }
596 0           @{ $dist_versions{$distname} }];
  0            
597 0 0         if (@{ $dist_versions{$distname} } > $newest_n) {
  0            
598             $dist_versions{$distname} = [splice(
599 0           @{ $dist_versions{$distname} }, 0, $newest_n)];
  0            
600             }
601             }
602 0           my @old_distrecs = @distrecs;
603 0           @distrecs = ();
604 0           my %dist_seen;
605 0           for my $distrec (@old_distrecs) {
606             log_trace "Distribution %s: Keeping these newest versions: %s", $distrec->{name}, $dist_versions{$distrec->{name}}
607 0 0         unless $dist_seen{$distrec->{name}};
608 0 0         if (grep { $_ eq $distrec->{version} } @{ $dist_versions{$distrec->{name}} }) {
  0            
  0            
609 0           push @distrecs, $distrec;
610             } else {
611 0           push @old_files, $distrec->{file};
612             }
613             }
614             }
615              
616 0           my @distnames;
617 0 0         unless ($args{detail}) {
618 0           @distnames = List::MoreUtils::uniq(map { $_->{name} } @distrecs);
  0            
619             }
620              
621 0           my %resmeta;
622 0 0         if ($newest_n) {
623 0           $resmeta{"func.old_files"} = \@old_files;
624             }
625 0 0         if ($args{detail}) {
626 0           $resmeta{'table.fields'} = [qw/name version is_dev_version file/];
627             }
628 0 0         [200, "OK", ($args{detail} ? \@distrecs : \@distnames), \%resmeta];
629             }
630              
631             $SPEC{delete_old_releases} = {
632             v => 1.1,
633             summary => 'Delete older versions of distributions',
634             description => <<'_',
635              
636             Currently does not look for releases in subdirectories.
637              
638             By default does not include developer (trial) releases. To include that, use
639             `--include-dev`.
640              
641             To only cleanup developer releases, you can use `--include-dev` and
642             `--exclude-nondev`.
643              
644             _
645             args => {
646             %common_args,
647             %detail_l_arg,
648             %protect_files_arg,
649             %argspecsopt_filter_dists,
650             %argspecsopt_filter_dev,
651             num_keep => {
652             schema => ['int*', min=>1],
653             default => 1,
654             summary => 'Number of new versions (including newest) to keep',
655             cmdline_aliases => { n=>{} },
656             description => <<'_',
657              
658             1 means to only keep the newest version, 2 means to keep the newest and the
659             second newest, and so on.
660              
661             _
662             },
663             },
664             features => {dry_run=>1},
665             };
666             sub delete_old_releases {
667 0     0 1   my %args = @_;
668              
669             my $res = list_dists(
670             _common_args(\%args),
671             newest_n=>$args{num_keep}//1,
672             include_dev=>$args{include_dev},
673             include_nondev=>$args{include_nondev},
674             include_dists=>$args{include_dists},
675             exclude_dists=>$args{exclude_dists},
676 0   0       );
677 0 0         return [500, "Can't list dists: $res->[0] - $res->[1]"] if $res->[0] != 200;
678 0           my $old_files = $res->[3]{'func.old_files'};
679              
680 0 0         return [304, "No older releases", undef,
681             {'cmdline.result'=>'There are no older releases to delete'}]
682             unless @$old_files;
683 0           my @to_delete;
684 0           for my $file (@$old_files) {
685 0           $file =~ s/\.$re_archive_ext\z//;
686 0           push @to_delete, "$file.*";
687             }
688             $res = delete_files(
689             _common_args(\%args),
690             protect_files => $args{protect_files},
691             files=>\@to_delete,
692             -dry_run=>$args{-dry_run},
693 0           );
694 0 0 0       return $res if $res->[0] != 200 || $args{-dry_run};
695 0   0       my $deleted_files = $res->[3]{'func.files'} // [];
696 0 0         if (@$deleted_files) {
697 0           $res->[3]{'cmdline.result'} = $deleted_files;
698             } else {
699 0           $res->[3]{'cmdline.result'} = 'Deleted 0 files';
700             }
701 0           $res;
702             }
703              
704             sub _delete_or_undelete_or_reindex_files {
705 0     0     require Regexp::Wildcards;
706 0           require String::Wildcard::Bash;
707              
708 0           my $which = shift;
709 0           my %args = @_;
710              
711             # to supply to pause server
712 0           my $action;
713 0 0         if ($which eq 'delete') {
    0          
    0          
714 0           $action = 'delete_files';
715             } elsif ($which eq 'undelete') {
716 0           $action = 'delete_files'; # sic
717             } elsif ($which eq 'reindex') {
718 0           $action = 'reindex';
719             } else {
720 0           die "BUG: undefined action";
721             }
722              
723 0   0       my $files0 = $args{files} // [];
724 0 0         return [400, "Please specify at least one file"] unless @$files0;
725              
726 0   0       my $protect_files = $args{protect_files} // [];
727              
728 0           my @files;
729             {
730 0           my $listres;
  0            
731 0 0         if (grep {String::Wildcard::Bash::contains_wildcard($_)}
  0            
732             (@$files0, @$protect_files)) {
733              
734 0 0 0       if ($which eq 'delete' && (grep {$_ =~ /\A\*\.?/} @$files0)) {
  0 0 0        
735 0           log_warn "Please make sure that you really want to delete ALL/many files using the '*' wildcard! ".
736             "Delaying 10s to give you chance to cancel (Ctrl-C on the terminal) ...";
737 0           sleep 10;
738 0           log_warn "Continuing ...";
739 0           } elsif ($which eq 'reindex' && (grep {$_ =~ /\A\*(z|\.?gz|\.?tar\.gz)?\z/} @$files0)) {
740 0           log_warn "Please make sure that you really want to reindex ALL files or ALL tarballs! ".
741             "If you want to fix certain distributions that are missing from the index, ".
742             "you should reindex just those distribution files. ".
743             "Delaying 10s to give you chance to cancel (Ctrl-C on the terminal) ...";
744 0           sleep 10;
745 0           log_warn "Continuing ...";
746             }
747              
748 0           $listres = list_files(_common_args(\%args));
749 0 0         return [500, "Can't list files: $listres->[0] - $listres->[1]"]
750             unless $listres->[0] == 200;
751             }
752              
753 0           for my $file (@$files0) {
754 0 0         if (String::Wildcard::Bash::contains_wildcard($file)) {
755 0           my $re = Regexp::Wildcards->new(type=>'unix')->convert($file);
756 0           $re = qr/\A($re)\z/;
757 0           for my $f (@{$listres->[2]}) {
  0            
758 0 0 0       push @files, $f if $f =~ $re && !(grep { $_ eq $f } @files);
  0            
759             }
760             } else {
761 0           push @files, $file;
762             }
763             }
764              
765 0           for my $protect_file (@$protect_files) {
766 0 0         if (String::Wildcard::Bash::contains_wildcard($protect_file)) {
767 0           my $re = Regexp::Wildcards->new(type=>'unix')->convert(
768             $protect_file);
769 0           $re = qr/\A($re)\z/;
770             @files = grep {
771 0 0         if ($_ =~ $re) {
  0            
772 0           log_debug("Excluding %s (protected, wildcard %s)",
773             $_, $protect_file);
774 0           0;
775             } else {
776 0           1;
777             }
778             } @files;
779             } else {
780             @files = grep {
781 0 0         if ($_ eq $protect_file) {
  0            
782 0           log_debug("Excluding %s (protected)", $_);
783 0           0;
784             } else {
785 0           1;
786             }
787             } @files;
788             }
789             }
790             }
791              
792 0 0         unless (@files) {
793 0           return [304, "No files to process"];
794             }
795              
796 0 0         if ($args{-dry_run}) {
797 0           log_warn("[dry-run] %s %s", $which, \@files);
798 0           return [200, "OK (dry-run)"];
799             } else {
800 0           log_info("%s %s ...", $which, \@files);
801             }
802              
803             my $httpres = _request(
804             note => "$which files",
805             %args,
806             post_data => [
807             [
808             ACTION => $action,
809             HIDDENNAME => $args{username},
810 0 0         ($which eq 'delete' ? (SUBMIT_pause99_delete_files_delete => "Delete" ) : ()),
    0          
    0          
    0          
    0          
811             ($which eq 'undelete' ? (SUBMIT_pause99_delete_files_undelete => "Undelete") : ()),
812             ($which eq 'reindex' ? (SUBMIT_pause99_reindex_delete => "Reindex" ) : ()),
813             ($which =~ /delete/ ? (pause99_delete_files_FILE => \@files) : ()),
814             ($which eq 'reindex' ? (pause99_reindex_FILE => \@files) : ()),
815             ],
816             ],
817             );
818 0 0         return _htres2envres($httpres) unless $httpres->is_success;
819 0 0         _access_log(\%args, $which => {files=>\@files}) if $which =~ /delete|undelete/;
820 0           [200,"OK", undef, {'func.files'=>\@files}];
821             }
822              
823             $SPEC{delete_files} = {
824             v => 1.1,
825             summary => 'Delete files',
826             description => <<'_',
827              
828             When a file is deleted, it is not immediately deleted but has
829             scheduled_for_deletion status for 72 hours, then deleted. During that time, the
830             file can be undeleted.
831              
832             _
833             args => {
834             %common_args,
835             %files_arg,
836             %protect_files_arg,
837             },
838             features => {dry_run=>1},
839             };
840             sub delete_files {
841 0     0 1   my %args = @_; # only for DZP::Rinci::Wrap
842 0           _delete_or_undelete_or_reindex_files('delete', @_);
843             }
844              
845             $SPEC{undelete_files} = {
846             v => 1.1,
847             summary => 'Undelete files',
848             description => <<'_',
849              
850             When a file is deleted, it is not immediately deleted but has
851             scheduled_for_deletion status for 72 hours, then deleted. During that time, the
852             file can be undeleted.
853              
854             _
855             args => {
856             %common_args,
857             %files_arg,
858             },
859             features => {dry_run=>1},
860             };
861             sub undelete_files {
862 0     0 1   my %args = @_; # only for DZP::Rinci::Wrap
863 0           _delete_or_undelete_or_reindex_files('undelete', @_);
864             }
865              
866             $SPEC{reindex_files} = {
867             v => 1.1,
868             summary => 'Force reindexing',
869             args => {
870             %common_args,
871             %files_arg,
872             },
873             features => {dry_run=>1},
874             };
875             sub reindex_files {
876 0     0 1   my %args = @_; # only for DZP::Rinci::Wrap
877 0           _delete_or_undelete_or_reindex_files('reindex', @_);
878             }
879              
880             $SPEC{set_password} = {
881             v => 1.1,
882             args => {
883             %common_args,
884             },
885             'x.no_index' => 1,
886             };
887             sub set_password {
888 0     0 0   my %args = @_;
889 0           [501, "Not yet implemented"];
890             }
891              
892             $SPEC{set_account_info} = {
893             v => 1.1,
894             args => {
895             %common_args,
896             },
897             'x.no_index' => 1,
898             };
899             sub set_account_info {
900 0     0 0   my %args = @_;
901 0           [501, "Not yet implemented"];
902             }
903              
904             $SPEC{list_modules} = {
905             v => 1.1,
906             summary => 'List modules (permissions)',
907             args => {
908             %common_args,
909             %detail_l_arg,
910             %mod_opt_arg,
911             type => {
912             summary => 'Only list modules matching certain type',
913             schema => 'str*',
914             tags => ['category:filtering'],
915             },
916             },
917             };
918             sub list_modules {
919 0     0 1   my %args = @_;
920 0           require Regexp::Wildcards;
921 0           require String::Wildcard::Bash;
922              
923 0   0       my $q = $args{modules} // [];
924              
925 0           my %post_data = (ACTION=>'peek_perms');
926              
927             # optimize: the PAUSE server can do SQL LIKE, if there is only a single
928             # module argument we pass it to server to reduce traffic
929 0 0         if (@$q == 1) {
930 0           $post_data{pause99_peek_perms_by} = 'ml';
931             $post_data{pause99_peek_perms_query} =
932 0           String::Wildcard::Bash::convert_wildcard_to_sql($q->[0]);
933 0           $post_data{pause99_peek_perms_sub} = 'Submit';
934             }
935              
936 0           my $httpres = _request(
937             %args,
938             note => "list modules",
939             post_data => [\%post_data],
940             );
941              
942 0 0         return _htres2envres($httpres) unless $httpres->is_success;
943              
944             # convert wildcard patterns in arguments to regexp
945 0           for (@$q) {
946 0 0         next unless String::Wildcard::Bash::contains_wildcard($_);
947 0           my $re = Regexp::Wildcards->new(type=>'unix')->convert($_);
948 0           $re = qr/\A($re)\z/;
949 0           $_ = $re;
950             }
951              
952 0           my @mods;
953 0 0         goto NO_MODS if $httpres->content =~ /No records found/;
954 0 0         return [543, "Can't scrape list of modules from response",
955             $httpres->content]
956             unless $httpres->content =~ m!]*>module.+?]*>(.+?)
957 0           my $str = $1;
958              
959             REC:
960 0           while ($str =~ m!
961             ]+>(.+?)
962             ]+>(.+?)
963             (.+?)
964             (.*?)
965            
966 0           my $rec = {module=>$1, userid=>$2, type=>$3, owner=>$4};
967              
968             # filter by requested file/wildcard
969             FILTER_QUERY:
970             {
971 0 0         last unless @$q > 1;
  0            
972 0           for (@$q) {
973 0 0         if (ref($_) eq 'Regexp') {
974 0 0         last FILTER_QUERY if $rec->{module} =~ $_;
975             } else {
976 0 0         last FILTER_QUERY if $rec->{module} eq $_;
977             }
978             }
979             # nothing matches
980 0           next REC;
981             }
982              
983             FILTER_TYPE:
984 0 0         if ($args{type}) {
985 0 0         next REC unless $rec->{type} eq $args{type};
986             }
987              
988 0 0         push @mods, $args{detail} ? $rec : $rec->{module};
989             }
990              
991             NO_MODS:
992              
993 0           my %resmeta;
994 0 0         if ($args{detail}) {
995 0           $resmeta{'table.fields'} =[qw/module userid type owner/];
996             }
997 0           [200, "OK", \@mods, \%resmeta];
998             }
999              
1000             1;
1001             # ABSTRACT: An API for PAUSE
1002              
1003             __END__