File Coverage

blib/lib/PAGI/Request/Upload.pm
Criterion Covered Total %
statement 81 90 90.0
branch 20 36 55.5
condition 6 16 37.5
subroutine 21 21 100.0
pod 13 14 92.8
total 141 177 79.6


line stmt bran cond sub pod time code
1             package PAGI::Request::Upload;
2 26     26   166846 use strict;
  26         40  
  26         852  
3 26     26   86 use warnings;
  26         31  
  26         1108  
4              
5 26     26   100 use File::Basename qw(fileparse);
  26         33  
  26         1842  
6 26     26   11126 use File::Copy qw(move);
  26         89966  
  26         2207  
7 26     26   143 use File::Spec;
  26         34  
  26         532  
8 26     26   82 use Carp qw(croak);
  26         28  
  26         30646  
9              
10              
11             # Constructor
12             sub new {
13 24     24 1 170078 my ($class, %args) = @_;
14             my $self = bless {
15             field_name => $args{field_name} // croak("field_name is required"),
16             filename => $args{filename} // '',
17             content_type => $args{content_type} // 'application/octet-stream',
18             data => $args{data}, # in-memory content
19             temp_path => $args{temp_path}, # on-disk path
20             size => $args{size},
21 24   33     244 _cleaned_up => 0,
      50        
      50        
22             }, $class;
23              
24             # Calculate size if not provided
25 24 100       75 if (!defined $self->{size}) {
26 21 50 0     49 if (defined $self->{data}) {
    0          
27 21         40 $self->{size} = length($self->{data});
28             } elsif (defined $self->{temp_path} && -f $self->{temp_path}) {
29 0         0 $self->{size} = -s $self->{temp_path};
30             } else {
31 0         0 $self->{size} = 0;
32             }
33             }
34              
35 24         74 return $self;
36             }
37              
38             # Accessors
39 1     1 1 5 sub field_name { my ($self) = @_; $self->{field_name} }
  1         6  
40 10     10 1 3423 sub filename { my ($self) = @_; $self->{filename} }
  10         39  
41 2     2 1 4 sub content_type { my ($self) = @_; $self->{content_type} }
  2         6  
42 3     3 1 698 sub size { my ($self) = @_; $self->{size} }
  3         14  
43 2     2 1 6 sub temp_path { my ($self) = @_; $self->{temp_path} }
  2         47  
44              
45             # Basename - strips Windows and Unix paths
46             sub basename {
47 3     3 1 11 my ($self) = @_;
48 3         5 my $filename = $self->{filename};
49 3 50       8 return '' unless $filename;
50              
51             # Strip Windows paths (C:\Users\... or \\server\share\...)
52 3         16 $filename =~ s/.*[\\\/]//;
53              
54 3         15 return $filename;
55             }
56              
57             # Predicates
58             sub is_empty {
59 2     2 1 7 my ($self) = @_;
60 2         13 return $self->{size} == 0;
61             }
62              
63             sub is_in_memory {
64 10     10 1 29 my ($self) = @_;
65 10         37 return defined($self->{data});
66             }
67              
68             sub is_on_disk {
69 28     28 1 168 my ($self) = @_;
70 28         115 return defined($self->{temp_path});
71             }
72              
73             # Content access - slurp
74             sub slurp {
75 5     5 1 474 my ($self) = @_;
76 5 100       11 if ($self->is_in_memory) {
    50          
77 3         28 return $self->{data};
78             } elsif ($self->is_on_disk) {
79             open my $fh, '<:raw', $self->{temp_path}
80 2 50       94 or croak("Cannot read $self->{temp_path}: $!");
81 2         6 my $content = do { local $/; <$fh> };
  2         10  
  2         72  
82 2         19 close $fh;
83 2         16 return $content;
84             }
85 0         0 return '';
86             }
87              
88             # Content access - filehandle
89             sub fh {
90 1     1 1 7 my ($self) = @_;
91 1 50       3 if ($self->is_in_memory) {
    0          
92             open my $fh, '<', \$self->{data}
93 1 50       15 or croak("Cannot create filehandle from memory: $!");
94 1         5 return $fh;
95             } elsif ($self->is_on_disk) {
96             open my $fh, '<:raw', $self->{temp_path}
97 0 0       0 or croak("Cannot open $self->{temp_path}: $!");
98 0         0 return $fh;
99             }
100 0         0 croak("No content available");
101             }
102              
103             # Move upload to destination (BLOCKING - performs synchronous file I/O)
104             sub move_to {
105 2     2 1 483 my ($self, $destination) = @_;
106              
107             # Ensure destination directory exists
108 2         52 my ($name, $dir) = fileparse($destination);
109 2 50 33     28 if ($dir && !-d $dir) {
110 0         0 require File::Path;
111 0         0 File::Path::make_path($dir);
112             }
113              
114 2 100       4 if ($self->is_in_memory) {
    50          
115             # Write data to destination (blocking I/O)
116 1 50       152 open my $fh, '>:raw', $destination
117             or croak("Cannot open $destination for writing: $!");
118 1         6 print $fh $self->{data};
119 1         34 close $fh;
120              
121             # Mark as cleaned up so destructor doesn't touch the saved file
122 1         3 delete $self->{data};
123 1         7 $self->{_cleaned_up} = 1;
124              
125 1         6 return $self;
126             } elsif ($self->is_on_disk) {
127             # Use File::Copy::move (typically a rename, very fast)
128 1 50       5 move($self->{temp_path}, $destination)
129             or croak("Cannot move to $destination: $!");
130              
131             # Mark as cleaned up so destructor doesn't touch the saved file
132 1         162 delete $self->{temp_path};
133 1         2 $self->{_cleaned_up} = 1;
134              
135 1         2 return $self;
136             }
137              
138 0         0 croak("No content to move");
139             }
140              
141             # Discard the upload
142             sub discard {
143 24     24 0 26 my ($self) = @_;
144 24 100       55 return if $self->{_cleaned_up};
145              
146 22 100 66     38 if ($self->is_on_disk && -f $self->{temp_path}) {
147 2         191 unlink $self->{temp_path};
148             }
149              
150 22         36 delete $self->{data};
151 22         23 delete $self->{temp_path};
152 22         86 $self->{_cleaned_up} = 1;
153             }
154              
155             # Destructor - cleanup temp files
156             sub DESTROY {
157 24     24   8145 my ($self) = @_;
158 24         50 $self->discard;
159             }
160              
161             1;
162              
163             __END__