File Coverage

blib/lib/File/Path/Redirect.pm
Criterion Covered Total %
statement 71 73 97.2
branch 18 28 64.2
condition 1 3 33.3
subroutine 11 11 100.0
pod 2 3 66.6
total 103 118 87.2


line stmt bran cond sub pod time code
1             package File::Path::Redirect;
2              
3 1     1   91370 use strict;
  1         2  
  1         28  
4 1     1   3 use warnings;
  1         4  
  1         66  
5              
6             our $VERSION="v0.1.3";
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   5 use Fcntl qw(O_RDONLY);
  1         1  
  1         48  
157 1     1   421 use POSIX;
  1         6672  
  1         3  
158 1     1   2128 use File::Spec::Functions qw;
  1         1  
  1         50  
159 1     1   3 use File::Basename qw;
  1         1  
  1         82  
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   429 use constant::more qw;
  1         753  
  1         7  
167 1     1   643 use Export::These qw;
  1         640  
  1         5  
168              
169             $max_size+=length $magic;
170              
171              
172              
173             # $existing is the path to link to
174             # $name is the path to the file we will create
175             sub make_redirect {
176 16     16 1 132372 my ($existing, $name, $force)=@_;
177            
178 16 50 33     40 if( $force or ! -e $name ){
179             # make relative $name to existing
180 16         87 my $path;
181 16 50       38 if(file_name_is_absolute $existing){
182 0         0 $path=$existing;
183             }
184             else {
185 16         400 $path=abs2rel($existing, dirname $name);
186             }
187 16 50       2018 open my $fh, ">", $name or die $!;
188 16 50       127 print $fh "$magic$path" or die $!;
189 16         606 close $fh;
190 16         141 return $path;
191             }
192 0         0 die "Redirect/Link file already exists: $name";
193             }
194              
195             sub follow_redirect{
196 19     19 1 572 my ($path, $limit, $trace)=@_;
197              
198 19 100       28 if(!defined $limit){
199 3         3 $limit=$default_limit;
200             }
201              
202 19 100       31 if($limit == 0){
203             # gone far enough. Error
204 1         3 $!=TOO_MANY; # mark as to many reidrects
205 1         11 return undef;
206             }
207              
208            
209 18         26 my $is_abs=file_name_is_absolute $path;
210              
211             # Open the file
212             #
213 18         294 my $fd=POSIX::open($path, $mode);
214 18 50       30 defined $fd or die "Error opening file $path: $!";
215 18         24 my $buffer="";
216 18         16 my $count=0;
217             # Read the contents up to the max length of path for the current system + magic header size
218 18         15 my $res;
219 18         282 while(($res=POSIX::read $fd, my $data="", $max_size)!=0){
220 18         22 $count+=$res;
221            
222 18         28 $buffer.=$data;
223 18 50       89 last if $count== $max_size;
224             }
225 18 50       26 defined $res or die $!;
226 18         92 POSIX::close $fd;
227              
228              
229             # Check for magic header
230 18 100       41 if((my $index=index($buffer, $magic))==0){
231             # Found attempt to read
232 16         24 my $new_path=substr $buffer, length $magic;
233              
234 16 50       26 if(file_name_is_absolute $new_path){
235             # Use as is
236             }
237             else {
238             # Build realtive to current file
239 16         365 $new_path= catfile dirname($path), $new_path;
240             }
241              
242 16 100       41 push @$trace, $path if $trace;
243 16         60 return follow_redirect($new_path, $limit-1, $trace);
244             }
245             else {
246             # Not a redirect file, this is the target
247 2         4 $!=NOT_A_REDIRECT;
248 2         11 return $path;
249             }
250             }
251              
252             sub is_redirect {
253 4     4 0 282 my ($path)=@_;
254              
255 4         73 my $fd=POSIX::open($path, $mode);
256 4 50       15 defined $fd or die $!;
257 4         5 my $buffer="";
258 4         5 my $count=0;
259             # Read the contents up to the max length of path for the current system + magic header size
260 4         4 my $res;
261 4         50 while(($res=POSIX::read $fd, my $data="", $max_size)!=0){
262 4         6 $count+=$res;
263 4         24 $buffer.=$data;
264             }
265 4 50       10 defined $res or die $!;
266 4         25 POSIX::close $fd;
267              
268              
269             # Check for magic header
270 4         24 (my $index=index($buffer, $magic))==0;
271             }
272              
273             1;