File Coverage

blib/lib/File/Path/Redirect.pm
Criterion Covered Total %
statement 70 72 97.2
branch 17 26 65.3
condition 1 3 33.3
subroutine 11 11 100.0
pod 2 3 66.6
total 101 115 87.8


line stmt bran cond sub pod time code
1             package File::Path::Redirect;
2              
3 1     1   332670 use strict;
  1         2  
  1         47  
4 1     1   7 use warnings;
  1         8  
  1         151  
5              
6             our $VERSION="v0.1.2";
7              
8              
9              
10              
11             =head1 NAME
12              
13             File::Path::Redirect - Poor Man's Symbolic Link Path Redirection
14              
15             =head1 SYNOPSIS
16              
17             use File::Path::Redirect;
18              
19             # Run this example in 'examples' dir
20             # Create a test file to link to
21             #
22             my $source_path="path_to_file.txt";
23             my $contents="Probably a large file";
24              
25             open my $fh, ">", $source_path or die $!;
26             print $fh $contents;
27             close $fh;
28            
29            
30              
31             # 'Link' or redirect a file to another
32             #
33             my $link_path="my_link.txt";
34              
35             make_redirect($source_path, $link_path);
36              
37            
38             # Elsewhere in the application normal and redirect files are tested
39             my $path=follow_redirect($link_path);
40              
41             # open/process $path as normal
42             open my $f,"<", $path or die $!;
43             while(<$f>){
44             print $_;
45             }
46              
47              
48             =head1 DESCRIPTION
49              
50             This module implements a handful of functions implementing 'user space' file
51             path redirection, similar to a symbolic link on your file system.
52              
53             It supports chained redirect files, with recursion limit.
54              
55             =head1 WHY SHOULD I USE THIS?
56              
57             =over
58            
59             =item Not all File Systems support Sumbolic links
60              
61             For example FAT and exFAT variants do not support symbolic links.
62              
63             =item Symbolic links only work withing the same volume
64              
65             If you wanted to symbolic link to a file on a different volume, you can't
66              
67             =item Copying files my night be feasable
68              
69             Slow and size constrained external media means extra copies of large files
70             might not fit. Also slow devices would take too long to physically copy
71              
72             =back
73              
74             =head1 HOW IT WORKS
75              
76             The redirect ( or link ) file is just a basic text file. It contains a single
77             line of the format:
78            
79             !PATH
80              
81             ! is a magic header
82             PATH is the relative path to a file it links to. It can be a link to another link file.
83              
84              
85             Before using a path in an C function, the path can be passed to
86             C. The return value is the path of the first non link file
87             found. This is path can be used in the C call instead.
88              
89              
90             =head1 API
91              
92             =head2 Creating Redirects
93              
94             =head3 make_redirect
95              
96             my $path = make_redirect $existing_file, $link_file, $force;
97              
98             Creates a new redirect file at C<$link_file> containing a link to the file located at C<$existing_file>.
99              
100             C<$existing_file> can be a relative or absolute path.
101              
102             The file is only created if C<$link_file> doesn't already exist, or C<$force>
103             is a true value.
104              
105              
106              
107             Returns the relative path between the two files if possible, otherwise a
108             absolute path. Dies on any IO related errors in creating / opening / writing /
109             closing the link file.
110              
111             =head2 Using Redirects
112              
113             =head3 follow_redirect
114              
115             my $path = follow_redirect $file_path, $limit;
116              
117             Given a file path C<$path>, it attempts to open the file, check it is a
118             redirect file. If so it parses and follows the link path. The process is
119             recursive until the file does not look like a link path, or until the total
120             number of redirects is equal to or greater than C<$limit>.
121              
122             C<$limit> is an optional parameter and by default is 10.
123              
124             Returns the final redirect path. The path could be relative or absolute.
125              
126             Dies on any IO errors when processing a redirect chain.
127              
128             =head1 PERFORMANCE CONSIDERATIONS
129              
130             Each redirect file encountered is opened and read. For repeated access to the
131             same file, it is best to store the results of the C function.
132              
133             For more comprehensive solution, L (which uses this module)
134             might suit your needs
135              
136             =head1 REPOSITORY and BUG REPORTING
137              
138             Please report any bugs and feature requests on the repo page:
139             L
140              
141             =head1 AUTHOR
142              
143             Ruben Westerberg, Edrclaw@mac.comE
144              
145             =head1 COPYRIGHT AND LICENSE
146              
147             Copyright (C) 2026 by Ruben Westerberg
148              
149             This library is free software; you can redistribute it and/or modify
150             it under the same terms as Perl itself, or under the MIT license
151              
152             =cut
153              
154              
155             #use IO::FD;
156 1     1   11 use Fcntl qw(O_RDONLY);
  1         2  
  1         96  
157 1     1   763 use POSIX;
  1         11844  
  1         8  
158 1     1   4017 use File::Spec::Functions qw;
  1         3  
  1         93  
159 1     1   9 use File::Basename qw;
  1         2  
  1         158  
160              
161             my $default_limit=10;
162             my $mode=O_RDONLY; # Read only while following links
163             my $magic="!";
164             my $max_size = $^O eq 'MSWin32' ? 260 : (POSIX::pathconf('/', &POSIX::_PC_PATH_MAX) // 4096);
165              
166 1     1   754 use constant::more qw;
  1         1420  
  1         8  
167 1     1   1175 use Export::These qw;
  1         1134  
  1         9  
168              
169              
170              
171              
172             # $existing is the path to link to
173             # $name is the path to the file we will create
174             sub make_redirect {
175 16     16 1 281144 my ($existing, $name, $force)=@_;
176            
177 16 50 33     73 if( $force or ! -e $name ){
178             # make relative $name to existing
179 16         30 my $path;
180 16 50       78 if(file_name_is_absolute $name){
181 0         0 $path=$name;
182             }
183             else {
184 16         843 $path=abs2rel($existing, dirname $name);
185             }
186 16 50       7397 open my $fh, ">", $name or die $!;
187 16 50       206 print $fh "$magic$path" or die $!;
188 16         1118 close $fh;
189 16         235 return $path;
190             }
191 0         0 die "Redirect/Link file already exists: $name";
192             }
193              
194             sub follow_redirect{
195 19     19 1 2085 my ($path, $limit, $trace)=@_;
196              
197 19 100       57 if(!defined $limit){
198 3         6 $limit=$default_limit;
199             }
200              
201 19 100       47 if($limit == 0){
202             # gone far enough. Error
203 1         5 $!=TOO_MANY; # mark as to many reidrects
204 1         35 return undef;
205             }
206              
207            
208 18         51 my $is_abs=file_name_is_absolute $path;
209              
210             # Open the file
211             #
212 18         645 my $fd=POSIX::open($path, $mode);
213 18 50       90 defined $fd or die $!;
214 18         38 my $buffer="";
215 18         53 my $count=0;
216             # Read the contents up to the max length of path for the current system + magic header size
217 18         27 my $res;
218 18         695 while(($res=POSIX::read $fd, my $data="", $max_size)!=0){
219 18         48 $count+=$res;
220            
221 18         201 $buffer.=$data;
222             }
223 18 50       54 defined $res or die $!;
224 18         208 POSIX::close $fd;
225              
226              
227             # Check for magic header
228 18 100       84 if((my $index=index($buffer, $magic))==0){
229             # Found attempt to read
230 16         47 my $new_path=substr $buffer, length $magic;
231              
232 16 50       54 if(file_name_is_absolute $new_path){
233             # Use as is
234             }
235             else {
236             # Build realtive to current file
237 16         721 $new_path= catfile dirname($path), $new_path;
238             }
239              
240 16 100       84 push @$trace, $path if $trace;
241 16         116 return follow_redirect($new_path, $limit-1, $trace);
242             }
243             else {
244             # Not a redirect file, this is the target
245 2         11 $!=NOT_A_REDIRECT;
246 2         35 return $path;
247             }
248             }
249              
250             sub is_redirect {
251 4     4 0 507 my ($path)=@_;
252              
253 4         153 my $fd=POSIX::open($path, $mode);
254 4 50       23 defined $fd or die $!;
255 4         19 my $buffer="";
256 4         8 my $count=0;
257             # Read the contents up to the max length of path for the current system + magic header size
258 4         7 my $res;
259 4         108 while(($res=POSIX::read $fd, my $data="", $max_size)!=0){
260 4         12 $count+=$res;
261 4         48 $buffer.=$data;
262             }
263 4 50       22 defined $res or die $!;
264 4         53 POSIX::close $fd;
265              
266              
267             # Check for magic header
268 4         42 (my $index=index($buffer, $magic))==0;
269             }
270              
271             1;