File Coverage

blib/lib/File/Symlink/Util.pm
Criterion Covered Total %
statement 38 123 30.8
branch 9 62 14.5
condition 0 10 0.0
subroutine 9 10 90.0
pod 4 4 100.0
total 60 209 28.7


line stmt bran cond sub pod time code
1             package File::Symlink::Util;
2              
3 1     1   75009 use 5.010001;
  1         17  
4 1     1   5 use strict;
  1         2  
  1         19  
5 1     1   4 use warnings;
  1         2  
  1         22  
6 1     1   1820 use Log::ger;
  1         51  
  1         5  
7              
8 1     1   262 use Exporter 'import';
  1         2  
  1         38  
9 1     1   7 use File::Spec;
  1         2  
  1         1167  
10              
11             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
12             our $DATE = '2023-06-30'; # DATE
13             our $DIST = 'File-Symlink-Util'; # DIST
14             our $VERSION = '0.002'; # VERSION
15              
16             our @EXPORT_OK = qw(
17             symlink_rel
18             symlink_abs
19             adjust_rel_symlink
20             check_symlink
21             );
22              
23              
24             sub symlink_rel {
25 2     2 1 4089 my ($dest_path, $link_path) = @_;
26 2         271 symlink(File::Spec->abs2rel($dest_path), $link_path);
27             }
28              
29             sub symlink_abs {
30 2     2 1 3993 my ($dest_path, $link_path) = @_;
31 2         199 symlink(File::Spec->rel2abs($dest_path), $link_path);
32             }
33              
34             sub adjust_rel_symlink {
35 1     1 1 2953 require File::Basename;
36 1         578 require Path::Naive;
37              
38 1         1233 my ($link_path1, $link_path2) = @_;
39              
40 1 50       21 unless (-l $link_path1) {
41 0         0 log_warn "First path '$link_path1' is not a symlink, skipping adjusting";
42 0         0 return;
43             }
44 1 50       15 unless (-l $link_path2) {
45 0         0 log_warn "Second path '$link_path2' is not a symlink, skipping adjusting";
46 0         0 return;
47             }
48              
49 1         15 my $dest_path1 = readlink $link_path1;
50 1 50       5 if (!defined $dest_path1) {
51 0         0 log_warn "Cannot read first symlink %s, skipping adjusting", $link_path1;
52 0         0 return;
53             }
54 1         24 my $dest_path2 = readlink $link_path2;
55 1 50       6 if (!defined $dest_path2) {
56 0         0 log_warn "Cannot read second symlink %s, skipping adjusting", $link_path2;
57 0         0 return;
58             }
59              
60 1 50       21 if (File::Spec->file_name_is_absolute($dest_path1)) {
61 0         0 log_trace "First symlink %s (target '%s') is not relative path, skipping adjusting", $link_path1, $dest_path1;
62 0         0 return;
63             }
64 1 50       6 if (File::Spec->file_name_is_absolute($dest_path2)) {
65 0         0 log_trace "Second symlink %s (target '%s') is not relative path, skipping adjusting", $link_path2, $dest_path2;
66 0         0 return;
67             }
68 1         220 my $new_dest_path2 = Path::Naive::normalize_path(
69             File::Spec->abs2rel(
70             (File::Spec->rel2abs($dest_path1, File::Basename::dirname($link_path1))),
71             File::Spec->rel2abs(File::Basename::dirname(File::Spec->rel2abs($link_path2)), "/"), # XXX "/" is unixism
72             )
73             );
74 1 50       77 if ($dest_path2 eq $new_dest_path2) {
75 0         0 log_trace "Skipping adjusting second symlink %s (no change: %s)", $link_path2, $new_dest_path2;
76 0         0 return;
77             }
78 1 50       66 unlink $link_path2 or do {
79 0         0 log_error "Cannot adjust second symlink %s (can't unlink: %s)", $link_path2, $!;
80 0         0 return;
81             };
82 1 50       47 symlink($new_dest_path2, $link_path2) or do {
83 0         0 log_error "Cannot adjust second symlink %s (can't symlink to '%s': %s)", $link_path2, $new_dest_path2, $!;
84 0         0 return;
85             };
86 1         16 log_trace "Adjusted symlink %s (from target '%s' to target '%s')", $link_path2, $dest_path2, $new_dest_path2;
87 1         6 1;
88             }
89              
90             sub check_symlink {
91 0     0 1   my %args = @_;
92 0           my $res = [200, "OK", []];
93              
94 0 0         my $symlink; defined($symlink = $args{symlink}) or return [400, "Please specify 'symlink' argument"];
  0            
95 0 0         (-l $symlink) or do { push @{ $res->[2] }, (-e _) ? "File is not a symlink" : "File does not exist"; goto END_CHECK };
  0 0          
  0            
  0            
96 0           my $target = readlink $symlink;
97 0 0         (-e $target) or do { push @{ $res->[2] }, "Broken symlink, target does not exist ($target)"; goto END_CHECK };
  0            
  0            
  0            
98 0 0         if (defined $args{is_abs}) {
99 0           require File::Spec;
100 0 0         if ($args{is_abs}) {
101 0 0         unless (File::Spec->file_name_is_absolute($target)) {
102 0           push @{ $res->[2] }, "Symlink target is not an absolute path";
  0            
103             }
104             } else {
105 0 0         if (File::Spec->file_name_is_absolute($target)) {
106 0           push @{ $res->[2] }, "Symlink target is not a relative path";
  0            
107             }
108             }
109             }
110 0 0         if (defined $args{target}) {
111 0           require Cwd;
112 0           my $wanted_abs_target = Cwd::abs_path($args{target});
113 0           my $abs_target = Cwd::abs_path($target);
114 0 0         unless ($wanted_abs_target eq $abs_target) {
115 0           push @{ $res->[2] }, "Symlink target is not the same as wanted ($args{target})";
  0            
116             }
117             }
118             CHECK_EXT_MATCHES: {
119 0 0         if ($args{ext_matches}) {
  0            
120 0           my ($symlink_ext) = $symlink =~ /\.(\w+)\z/;
121 0           my ($target_ext) = $target =~ /\.(\w+)\z/;
122 0 0 0       last CHECK_EXT_MATCHES unless defined $symlink_ext && defined $target_ext;
123 0 0         unless (lc($symlink_ext) eq lc($target_ext)) {
124 0           push @{ $res->[2] }, "Symlink extension ($symlink_ext) does not match target's ($target_ext)";
  0            
125             }
126             }
127             }
128             CHECK_CONTENT_MATCHES: {
129 0 0         if ($args{content_matches}) {
  0            
130 0           require File::MimeInfo::Magic;
131 0           my ($symlink_ext) = $symlink =~ /\.(\w+)\z/;
132 0 0         open my $fh, "<", $symlink or do { push @{ $res->[2] }, "Can't open symlink target for content checking: $!"; last CHECK_CONTENT_MATCHES };
  0            
  0            
  0            
133 0           my $type = File::MimeInfo::Magic::mimetype($fh);
134 0 0         my @exts; @exts = File::MimeInfo::Magic::extensions($type) if $type;
  0            
135 0 0 0       if (defined($symlink_ext) && @exts) {
    0 0        
136 0           my $found;
137 0           for my $ext (@exts) {
138 0 0         if (lc $ext eq lc $symlink_ext) { $found++; last }
  0            
  0            
139             }
140 0 0         unless ($found) {
141 0           push @{ $res->[2] }, "Symlink extension ($symlink_ext) does not match content type ($type, exts=".join("|", @exts).")";
  0            
142             }
143             } elsif (defined($symlink_ext) xor @exts) {
144 0 0         if (defined $symlink_ext) {
145 0           push @{ $res->[2] }, "Content type is unknown but symlink has extension ($symlink_ext)";
  0            
146             } else {
147 0           push @{ $res->[2] }, "Content type is $type but symlink does not have any extension";
  0            
148             }
149             } else {
150             # mime type is unknown and file does not have extension -> OK
151             }
152             }
153             }
154              
155             END_CHECK:
156 0 0         if (@{ $res->[2] }) { $res->[0] = 500; $res->[1] = "Errors" }
  0            
  0            
  0            
157 0           $res;
158             }
159              
160             1;
161             # ABSTRACT: Utilities related to symbolic links
162              
163             __END__