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