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   160470 use strict;
  26         48  
  26         929  
3 26     26   86 use warnings;
  26         30  
  26         1167  
4              
5 26     26   92 use File::Basename qw(fileparse);
  26         30  
  26         1989  
6 26     26   11067 use File::Copy qw(move);
  26         94130  
  26         2184  
7 26     26   154 use File::Spec;
  26         32  
  26         571  
8 26     26   87 use Carp qw(croak);
  26         30  
  26         30561  
9              
10              
11             # Constructor
12             sub new {
13 24     24 1 158627 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     183 _cleaned_up => 0,
      50        
      50        
22             }, $class;
23              
24             # Calculate size if not provided
25 24 100       60 if (!defined $self->{size}) {
26 21 50 0     31 if (defined $self->{data}) {
    0          
27 21         35 $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         52 return $self;
36             }
37              
38             # Accessors
39 1     1 1 5 sub field_name { my ($self) = @_; $self->{field_name} }
  1         8  
40 10     10 1 3294 sub filename { my ($self) = @_; $self->{filename} }
  10         41  
41 2     2 1 18 sub content_type { my ($self) = @_; $self->{content_type} }
  2         6  
42 3     3 1 608 sub size { my ($self) = @_; $self->{size} }
  3         13  
43 2     2 1 4 sub temp_path { my ($self) = @_; $self->{temp_path} }
  2         27  
44              
45             # Basename - strips Windows and Unix paths
46             sub basename {
47 3     3 1 7 my ($self) = @_;
48 3         4 my $filename = $self->{filename};
49 3 50       5 return '' unless $filename;
50              
51             # Strip Windows paths (C:\Users\... or \\server\share\...)
52 3         12 $filename =~ s/.*[\\\/]//;
53              
54 3         12 return $filename;
55             }
56              
57             # Predicates
58             sub is_empty {
59 2     2 1 5 my ($self) = @_;
60 2         12 return $self->{size} == 0;
61             }
62              
63             sub is_in_memory {
64 10     10 1 22 my ($self) = @_;
65 10         34 return defined($self->{data});
66             }
67              
68             sub is_on_disk {
69 28     28 1 161 my ($self) = @_;
70 28         104 return defined($self->{temp_path});
71             }
72              
73             # Content access - slurp
74             sub slurp {
75 5     5 1 459 my ($self) = @_;
76 5 100       10 if ($self->is_in_memory) {
    50          
77 3         11 return $self->{data};
78             } elsif ($self->is_on_disk) {
79             open my $fh, '<:raw', $self->{temp_path}
80 2 50       92 or croak("Cannot read $self->{temp_path}: $!");
81 2         4 my $content = do { local $/; <$fh> };
  2         8  
  2         72  
82 2         18 close $fh;
83 2         14 return $content;
84             }
85 0         0 return '';
86             }
87              
88             # Content access - filehandle
89             sub fh {
90 1     1 1 5 my ($self) = @_;
91 1 50       3 if ($self->is_in_memory) {
    0          
92             open my $fh, '<', \$self->{data}
93 1 50       10 or croak("Cannot create filehandle from memory: $!");
94 1         3 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 419 my ($self, $destination) = @_;
106              
107             # Ensure destination directory exists
108 2         52 my ($name, $dir) = fileparse($destination);
109 2 50 33     29 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       127 open my $fh, '>:raw', $destination
117             or croak("Cannot open $destination for writing: $!");
118 1         9 print $fh $self->{data};
119 1         36 close $fh;
120              
121             # Mark as cleaned up so destructor doesn't touch the saved file
122 1         3 delete $self->{data};
123 1         1 $self->{_cleaned_up} = 1;
124              
125 1         12 return $self;
126             } elsif ($self->is_on_disk) {
127             # Use File::Copy::move (typically a rename, very fast)
128 1 50       4 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         136 delete $self->{temp_path};
133 1         2 $self->{_cleaned_up} = 1;
134              
135 1         3 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 42 my ($self) = @_;
144 24 100       54 return if $self->{_cleaned_up};
145              
146 22 100 66     34 if ($self->is_on_disk && -f $self->{temp_path}) {
147 2         190 unlink $self->{temp_path};
148             }
149              
150 22         33 delete $self->{data};
151 22         22 delete $self->{temp_path};
152 22         102 $self->{_cleaned_up} = 1;
153             }
154              
155             # Destructor - cleanup temp files
156             sub DESTROY {
157 24     24   7476 my ($self) = @_;
158 24         46 $self->discard;
159             }
160              
161             1;
162              
163             __END__