Group
Extension

App-short/lib/App/short.pm

package App::short;

our $DATE = '2017-07-10'; # DATE
our $VERSION = '0.14'; # VERSION

use 5.010001;
use strict;
use warnings;
use Log::ger;

# just for testing DZP:Preload
use constant PRELOAD => $ENV{PRELOAD};
if (PRELOAD) { require Complete::Util; require Cwd; require File::Spec; require Perinci::Object } # INSERT_PRELOADS: PRELOAD

our %SPEC;

our %common_args = (
    short_dir => {
        schema => 'str*',
        cmdline_aliases => {S=>{}},
        req => 1,
    },
    long_dir => {
        schema => 'str*',
        cmdline_aliases => {L=>{}},
        req => 1,
    },
    long_include => {
        schema => ['array*', of=>'str*'],
    },
);

our %detail_l_arg = (
    detail => {
        schema => ['bool'],
        cmdline_aliases => {l=>{}},
    },
);

my $_completion_missing = sub {
    unless (PRELOAD) { require Complete::Util; } #PRELOAD
    my %args = @_;
    my $word    = $args{word} // '';
    my $cmdline = $args{cmdline};
    my $r       = $args{r};

    return undef unless $cmdline;

    $r->{read_config} = 1;
    my $res = $cmdline->parse_argv($r);
    return undef unless $res->[0] == 200;

    my $fargs = $res->[2];

    $res = _validate($fargs);
    return undef unless $res->[0] == 200;

    $res = list_missing(_common_args($fargs));
    return undef unless $res->[0] == 200;

    Complete::Util::complete_array_elem(
        array=>$res->[2], word=>$word,
    );
};

my $_completion_short = sub {
    unless (PRELOAD) { require Complete::Util; } #PRELOAD
    my %args = @_;
    my $word    = $args{word} // '';
    my $cmdline = $args{cmdline};
    my $r       = $args{r};

    return undef unless $cmdline;

    $r->{read_config} = 1;
    my $res = $cmdline->parse_argv($r);
    return undef unless $res->[0] == 200;

    my $fargs = $res->[2];

    $res = _validate($fargs);
    return undef unless $res->[0] == 200;

    $res = list_shorts(_common_args($fargs));
    return undef unless $res->[0] == 200;

    Complete::Util::complete_array_elem(
        array=>$res->[2], word=>$word,
    );
};

# (temporary) borrowed from PERLANCAR::Path::Util
sub _get_my_home_dir {
    if ($^O eq 'Win32') {
        # File::HomeDir always uses exists($ENV{x}) first, does it want to avoid
        # accidentally creating env vars?
        return $ENV{HOME} if $ENV{HOME};
        return $ENV{USERPROFILE} if $ENV{USERPROFILE};
        return join($ENV{HOMEDRIVE}, "\\", $ENV{HOMEPATH})
            if $ENV{HOMEDRIVE} && $ENV{HOMEPATH};
    } else {
        return $ENV{HOME} if $ENV{HOME};
        my @pw = getpwuid($>);
        return $pw[7] if @pw;
    }
    undef;
}

sub _common_args {
    my $args = shift;
    my %res;
    for (keys %common_args) {
        $res{$_} = $args->{$_} if exists $args->{$_};
    }
    %res;
}

sub _validate {
    my $args = shift;
    return [200] if $args->{-validated};

    if (defined $args->{long}) {
        return [400, "Invalid long name"] if $args->{long} =~ m![/\\]!;
    }
    if (defined $args->{short}) {
        my @shorts = ref($args->{short}) eq 'ARRAY' ?
            @{$args->{short}} : ($args->{short});
        for (@shorts) {
            return [400, "Invalid short name '$_'"] if m![/\\]!;
        }
    }

    $args->{-validated}++;
    [200];
}

$SPEC{':package'} = {
    v => 1.1,
    summary => 'Manage short directory symlinks',
};

$SPEC{list_shorts} = {
    v => 1.1,
    args => {
        %common_args,
        %detail_l_arg,
        broken => {
            schema => 'bool',
            tags => ['category:filtering'],
        },
        query => {
            schema => 'str*',
            tags => ['category:filtering'],
            pos => 0,
        },
    },
};
sub list_shorts {
    my %args = @_;
    my $res = _validate(\%args);
    return $res unless $res->[0] == 200;

    my $S = $args{short_dir};

    my $q = lc($args{query} // '');

    my @res;
    opendir my($dh), $S or
        return [500, "Can't open dir $S: $!"];
    for my $ent (sort readdir($dh)) {
        next if $ent eq '.' || $ent eq '..';
        my $path = "$S/$ent";
        next unless -l $path;

        my $target = readlink($path);
        $target =~ s!.+[/\\]!!;

        # XXX check that target refers to $L

        my $broken = (-d $path) ? 0 : 1;

        # filter
        if (defined $args{broken}) {
            next if $args{broken} xor $broken;
        }
        if (length($q)) {
            next unless index(lc($target), $q) >= 0 || index(lc($ent), $q) >= 0;
        }

        push @res, {
            name => $ent,
            is_broken => $broken,
            target => $target,
        };
    }

    my %resmeta;
    if ($args{detail}) {
        $resmeta{'table.fields'} = [qw/name target is_broken/];
    } else {
        @res = map {$_->{name}} @res;
    }

    [200, "OK", \@res, \%resmeta];
}

$SPEC{list_longs} = {
    v => 1.1,
    args => {
        %common_args,
        %detail_l_arg,
    },
};
sub list_longs {
    my %args = @_;
    my $res = _validate(\%args);
    return $res unless $res->[0] == 200;

    my $L = $args{long_dir};

    my @res;
    opendir my($dh), $L or
        return [500, "Can't open dir $L: $!"];
  ENTRY:
    for my $ent (sort readdir($dh)) {
        next if $ent eq '.' || $ent eq '..';
        my $path = "$L/$ent";
        next unless -d $path;

      FILTER_INCLUDE:
        {
            if ($args{long_include}) {
                for (@{ $args{long_include} }) {
                    last FILTER_INCLUDE if $ent =~ $_;
                }
                next ENTRY;
            }
        }

        push @res, {
            name => $ent,
        };
    }

    my %resmeta;
    if ($args{detail}) {
        $resmeta{'table.fields'} = [qw/name/];
    } else {
        @res = map {$_->{name}} @res;
    }

    [200, "OK", \@res, \%resmeta];
}

$SPEC{list_missing} = {
    v => 1.1,
    args => {
        %common_args,
        %detail_l_arg,
    },
};
sub list_missing {
    use experimental 'smartmatch';

    my %args = @_;
    my $res = _validate(\%args);
    return $res unless $res->[0] == 200;

    my $S = $args{short_dir};
    my $L = $args{long_dir};

    my $res_s = list_shorts(_common_args(\%args), broken=>0, detail=>1);
    my $res_l = list_longs(_common_args(\%args));

    my @mentioned_longs = map {$_->{target}} @{$res_s->[2]};

    my @res;
    for (@{ $res_l->[2] }) {
        next if $_ ~~ @mentioned_longs;
        push @res, {
            name => $_,
        };
    }

    my %resmeta;
    if ($args{detail}) {
        $resmeta{'table.fields'} = [qw/name/];
    } else {
        @res = map {$_->{name}} @res;
    }

    [200, "OK", \@res];
}

$SPEC{list_duplicates} = {
    v => 1.1,
    args => {
        %common_args,
        %detail_l_arg,
    },
};
sub list_duplicates {
    use experimental 'smartmatch';

    my %args = @_;
    my $res = _validate(\%args);
    return $res unless $res->[0] == 200;

    my $S = $args{short_dir};
    my $L = $args{long_dir};

    my $res_s = list_shorts(_common_args(\%args), broken=>0, detail=>1);

    my %mentioned_longs;
    for my $e (@{ $res_s->[2] }) {
        push @{ $mentioned_longs{ $e->{target} } }, $e->{name};
    }

    my @res;
    for (sort keys %mentioned_longs) {
        my $names = $mentioned_longs{$_};
        next unless @$names > 1;
        push @res, {
            target => $_,
            names => join(", ", @$names),
        };
    }

    my %resmeta;
    if ($args{detail}) {
        $resmeta{'table.fields'} = [qw/target names/];
    } else {
        @res = map {$_->{target}} @res;
    }

    [200, "OK", \@res, \%resmeta];
}

$SPEC{get_short_target} = {
    v => 1.1,
    args => {
        %common_args,
        short => {
            schema => 'str*',
            req => 1,
            pos => 0,
            completion => $_completion_short,
        },
    },
};
sub get_short_target {
    unless (PRELOAD) { require Cwd; } #PRELOAD
    unless (PRELOAD) { require File::Spec; } #PRELOAD
    my %args = @_;
    my $res = _validate(\%args);
    return [200,"Invalid input: $res->[0] - $res->[1]"]
        unless $res->[0] == 200;

    my $S = $args{short_dir};
    #my $L = $args{long_dir};

    my $dir = readlink("$S/$args{short}");
    return [200, "Short name not found"] unless $dir;
    $dir = Cwd::abs_path(
        File::Spec->rel2abs(
            $dir, Cwd::abs_path($S),
        ));
    return [200, "Can't abs_path"] unless $dir;
    [200, "OK", $dir];
}

$SPEC{add_short} = {
    v => 1.1,
    args => {
        %common_args,
        long => {
            schema => 'str*',
            req => 1,
            pos => 0,
            completion => $_completion_missing,
        },
        short => {
            schema => 'str*',
            req => 1,
            pos => 1,
        },
    },
};
sub add_short {
    use experimental 'smartmatch';
    unless (PRELOAD) { require Cwd; } #PRELOAD
    unless (PRELOAD) { require File::Spec; } #PRELOAD
    my %args = @_;
    my $res = _validate(\%args);
    return $res unless $res->[0] == 200;

    my $S = $args{short_dir};
    my $L = $args{long_dir};

    return [404, "No such long name '$args{long}'"]
        unless (-d "$L/$args{long}");
    return [412, "Short name '$args{short}' already exists"]
        if (-l "$S/$args{short}");

    symlink(File::Spec->abs2rel(
        Cwd::abs_path("$L/$args{long}"),
        Cwd::abs_path($S),
    ), "$S/$args{short}") or return [500, "Can't create symlink: $!"];

    [200, "OK"];
}

$SPEC{rm_short} = {
    v => 1.1,
    args => {
        %common_args,
        short => {
            schema => ['array*', of=>'str*', min_len=>1],
            req => 1,
            pos => 0,
            greedy => 1,
            element_completion => $_completion_short,
        },
    },
};
sub rm_short {
    unless (PRELOAD) { require Perinci::Object; } #PRELOAD
    my %args = @_;
    my $res = _validate(\%args);
    return $res unless $res->[0] == 200;

    my $S = $args{short_dir};

    my $envres = Perinci::Object::envresmulti();

    for my $s (@{ $args{short} }) {
        my $path = "$S/$s";

        if (!(-l $path)) {
            $envres->add_result(404, "Short name not found", {item_id=>$s});
        } elsif (!unlink($path)) {
            $envres->add_result(500, "Can't unlink: $!", {item_id=>$s});
        } else {
            $envres->add_result(200, "OK", {item_id=>$s});
        }
    }

    $envres->as_struct;
}

1;
# ABSTRACT: Manage short directory symlinks

__END__

=pod

=encoding UTF-8

=head1 NAME

App::short - Manage short directory symlinks

=head1 VERSION

This document describes version 0.14 of App::short (from Perl distribution App-short), released on 2017-07-10.

=head1 SYNOPSIS

Please see L<short> script.

=head1 FUNCTIONS


=head2 add_short

Usage:

 add_short(%args) -> [status, msg, result, meta]

This function is not exported.

Arguments ('*' denotes required arguments):

=over 4

=item * B<long>* => I<str>

=item * B<long_dir>* => I<str>

=item * B<long_include> => I<array[str]>

=item * B<short>* => I<str>

=item * B<short_dir>* => I<str>

=back

Returns an enveloped result (an array).

First element (status) is an integer containing HTTP status code
(200 means OK, 4xx caller error, 5xx function error). Second element
(msg) is a string containing error message, or 'OK' if status is
200. Third element (result) is optional, the actual result. Fourth
element (meta) is called result metadata and is optional, a hash
that contains extra information.

Return value:  (any)


=head2 get_short_target

Usage:

 get_short_target(%args) -> [status, msg, result, meta]

This function is not exported.

Arguments ('*' denotes required arguments):

=over 4

=item * B<long_dir>* => I<str>

=item * B<long_include> => I<array[str]>

=item * B<short>* => I<str>

=item * B<short_dir>* => I<str>

=back

Returns an enveloped result (an array).

First element (status) is an integer containing HTTP status code
(200 means OK, 4xx caller error, 5xx function error). Second element
(msg) is a string containing error message, or 'OK' if status is
200. Third element (result) is optional, the actual result. Fourth
element (meta) is called result metadata and is optional, a hash
that contains extra information.

Return value:  (any)


=head2 list_duplicates

Usage:

 list_duplicates(%args) -> [status, msg, result, meta]

This function is not exported.

Arguments ('*' denotes required arguments):

=over 4

=item * B<detail> => I<bool>

=item * B<long_dir>* => I<str>

=item * B<long_include> => I<array[str]>

=item * B<short_dir>* => I<str>

=back

Returns an enveloped result (an array).

First element (status) is an integer containing HTTP status code
(200 means OK, 4xx caller error, 5xx function error). Second element
(msg) is a string containing error message, or 'OK' if status is
200. Third element (result) is optional, the actual result. Fourth
element (meta) is called result metadata and is optional, a hash
that contains extra information.

Return value:  (any)


=head2 list_longs

Usage:

 list_longs(%args) -> [status, msg, result, meta]

This function is not exported.

Arguments ('*' denotes required arguments):

=over 4

=item * B<detail> => I<bool>

=item * B<long_dir>* => I<str>

=item * B<long_include> => I<array[str]>

=item * B<short_dir>* => I<str>

=back

Returns an enveloped result (an array).

First element (status) is an integer containing HTTP status code
(200 means OK, 4xx caller error, 5xx function error). Second element
(msg) is a string containing error message, or 'OK' if status is
200. Third element (result) is optional, the actual result. Fourth
element (meta) is called result metadata and is optional, a hash
that contains extra information.

Return value:  (any)


=head2 list_missing

Usage:

 list_missing(%args) -> [status, msg, result, meta]

This function is not exported.

Arguments ('*' denotes required arguments):

=over 4

=item * B<detail> => I<bool>

=item * B<long_dir>* => I<str>

=item * B<long_include> => I<array[str]>

=item * B<short_dir>* => I<str>

=back

Returns an enveloped result (an array).

First element (status) is an integer containing HTTP status code
(200 means OK, 4xx caller error, 5xx function error). Second element
(msg) is a string containing error message, or 'OK' if status is
200. Third element (result) is optional, the actual result. Fourth
element (meta) is called result metadata and is optional, a hash
that contains extra information.

Return value:  (any)


=head2 list_shorts

Usage:

 list_shorts(%args) -> [status, msg, result, meta]

This function is not exported.

Arguments ('*' denotes required arguments):

=over 4

=item * B<broken> => I<bool>

=item * B<detail> => I<bool>

=item * B<long_dir>* => I<str>

=item * B<long_include> => I<array[str]>

=item * B<query> => I<str>

=item * B<short_dir>* => I<str>

=back

Returns an enveloped result (an array).

First element (status) is an integer containing HTTP status code
(200 means OK, 4xx caller error, 5xx function error). Second element
(msg) is a string containing error message, or 'OK' if status is
200. Third element (result) is optional, the actual result. Fourth
element (meta) is called result metadata and is optional, a hash
that contains extra information.

Return value:  (any)


=head2 rm_short

Usage:

 rm_short(%args) -> [status, msg, result, meta]

This function is not exported.

Arguments ('*' denotes required arguments):

=over 4

=item * B<long_dir>* => I<str>

=item * B<long_include> => I<array[str]>

=item * B<short>* => I<array[str]>

=item * B<short_dir>* => I<str>

=back

Returns an enveloped result (an array).

First element (status) is an integer containing HTTP status code
(200 means OK, 4xx caller error, 5xx function error). Second element
(msg) is a string containing error message, or 'OK' if status is
200. Third element (result) is optional, the actual result. Fourth
element (meta) is called result metadata and is optional, a hash
that contains extra information.

Return value:  (any)

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/App-short>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-App-short>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-short>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017, 2015 by perlancar@cpan.org.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.