| blib/lib/File/Tabular/Web/Attachments.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 15 | 113 | 13.2 |
| branch | 0 | 46 | 0.0 |
| condition | 0 | 8 | 0.0 |
| subroutine | 5 | 18 | 27.7 |
| pod | 11 | 13 | 84.6 |
| total | 31 | 198 | 15.6 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | =begin TODO | ||||||
| 2 | |||||||
| 3 | - override "display" to accept V=UploadField | ||||||
| 4 | => would redirect to attached file | ||||||
| 5 | |||||||
| 6 | - support for multiple files under same field | ||||||
| 7 | |||||||
| 8 | =end TODO | ||||||
| 9 | |||||||
| 10 | =cut | ||||||
| 11 | |||||||
| 12 | |||||||
| 13 | package File::Tabular::Web::Attachments; | ||||||
| 14 | 1 | 1 | 1221 | use base 'File::Tabular::Web'; | |||
| 1 | 3 | ||||||
| 1 | 95 | ||||||
| 15 | 1 | 1 | 5 | use strict; | |||
| 1 | 2 | ||||||
| 1 | 20 | ||||||
| 16 | 1 | 1 | 4 | use warnings; | |||
| 1 | 2 | ||||||
| 1 | 38 | ||||||
| 17 | |||||||
| 18 | 1 | 1 | 7 | use File::Path; | |||
| 1 | 2 | ||||||
| 1 | 62 | ||||||
| 19 | 1 | 1 | 6 | use Scalar::Util qw/looks_like_number/; | |||
| 1 | 3 | ||||||
| 1 | 1538 | ||||||
| 20 | |||||||
| 21 | |||||||
| 22 | #---------------------------------------------------------------------- | ||||||
| 23 | sub app_initialize { | ||||||
| 24 | #---------------------------------------------------------------------- | ||||||
| 25 | 0 | 0 | 1 | my $self = shift; | |||
| 26 | |||||||
| 27 | 0 | $self->SUPER::app_initialize; | |||||
| 28 | |||||||
| 29 | # field names specified as "upload fields" in config | ||||||
| 30 | 0 | $self->{app}{upload_fields} = $self->{app}{cfg}->get('fields_upload'); | |||||
| 31 | } | ||||||
| 32 | |||||||
| 33 | |||||||
| 34 | #---------------------------------------------------------------------- | ||||||
| 35 | sub open_data { | ||||||
| 36 | #---------------------------------------------------------------------- | ||||||
| 37 | 0 | 0 | 1 | my $self = shift; | |||
| 38 | |||||||
| 39 | 0 | $self->SUPER::open_data; | |||||
| 40 | |||||||
| 41 | # upload fields must be present in the data file | ||||||
| 42 | 0 | my %data_headers = map {$_ => 1} $self->{data}->headers; | |||||
| 0 | |||||||
| 43 | 0 | my @upld = keys %{$self->{app}{upload_fields}}; | |||||
| 0 | |||||||
| 44 | 0 | my $invalid = join ", ", grep {not $data_headers{$_}} @upld; | |||||
| 0 | |||||||
| 45 | 0 | 0 | die "upload fields in config but not in data file: $invalid" if $invalid; | ||||
| 46 | } | ||||||
| 47 | |||||||
| 48 | |||||||
| 49 | #---------------------------------------------------------------------- | ||||||
| 50 | sub before_update { # | ||||||
| 51 | #---------------------------------------------------------------------- | ||||||
| 52 | 0 | 0 | 1 | my ($self, $record) = @_; | |||
| 53 | |||||||
| 54 | 0 | my @upld = keys %{$self->{app}{upload_fields}}; | |||||
| 0 | |||||||
| 55 | |||||||
| 56 | # remember paths and names of old files (in case we must delete them later) | ||||||
| 57 | 0 | foreach my $field (grep {$record->{$_}} @upld) { | |||||
| 0 | |||||||
| 58 | 0 | $self->{old_name}{$field} = $record->{$field}; | |||||
| 59 | 0 | $self->{old_path}{$field} = $self->upload_fullpath($record, $field); | |||||
| 60 | } | ||||||
| 61 | |||||||
| 62 | # call parent method | ||||||
| 63 | 0 | $self->SUPER::before_update($record); | |||||
| 64 | |||||||
| 65 | # find out about next autoNum (WARN: breaks encapsulation of File::Tabular!) | ||||||
| 66 | 0 | 0 | if ($self->{cfg}->get('fields_autoNum')) { | ||||
| 67 | 0 | $self->{next_autoNum} = $self->{data}{autoNum}; | |||||
| 68 | } | ||||||
| 69 | |||||||
| 70 | # now deal with file uploads | ||||||
| 71 | 0 | foreach my $field (@upld) { | |||||
| 72 | |||||||
| 73 | 0 | my $remote_name = $self->param($field); | |||||
| 74 | # If we have a Plack::Request , use the uploads func to | ||||||
| 75 | # retrieve the file that was uploaded | ||||||
| 76 | 0 | 0 | 0 | if(!$remote_name && $self->{req}) { | |||
| 77 | 0 | my $upload = $self->{req}->uploads->{$field}; | |||||
| 78 | 0 | 0 | $remote_name = $upload->basename if ($upload); | ||||
| 79 | } | ||||||
| 80 | 0 | 0 | if ($remote_name) { | ||||
| 81 | 0 | $self->do_upload_file($record, $field, $remote_name); | |||||
| 82 | } | ||||||
| 83 | else { # upload is "" ==> must restore old name in record | ||||||
| 84 | 0 | 0 | $record->{$field} = $self->{old_name}{$field} || ""; | ||||
| 85 | } | ||||||
| 86 | }; | ||||||
| 87 | } | ||||||
| 88 | |||||||
| 89 | #---------------------------------------------------------------------- | ||||||
| 90 | sub do_upload_file { # | ||||||
| 91 | #---------------------------------------------------------------------- | ||||||
| 92 | 0 | 0 | 1 | my ($self, $record, $field, $remote_name) = @_; | |||
| 93 | |||||||
| 94 | 0 | my $src_fh; | |||||
| 95 | |||||||
| 96 | 0 | 0 | if ($self->{modperl}) { | ||||
| 97 | 0 | require Apache2::Request; | |||||
| 98 | 0 | require Apache2::Upload; | |||||
| 99 | 0 | my $req = Apache2::Request->new($self->{modperl}); | |||||
| 100 | 0 | 0 | my $upld = $req->upload($field) or die "no upload object for field $field"; | ||||
| 101 | 0 | $src_fh = $upld->fh; | |||||
| 102 | } | ||||||
| 103 | else { | ||||||
| 104 | 0 | my @uploads = $self->{req}->upload($field); # may be an array | |||||
| 105 | 0 | my @upld_fh = map { $_->path } @uploads; | |||||
| 0 | |||||||
| 106 | |||||||
| 107 | # TODO : some convention for deleting an existing attached file | ||||||
| 108 | # if @upload_fh == 0 && $remote_name =~ /^( |del)/ {...} | ||||||
| 109 | |||||||
| 110 | # no support at the moment for multiple files under same field | ||||||
| 111 | 0 | 0 | @upld_fh < 2 or die "several files uploaded to $field"; | ||||
| 112 | |||||||
| 113 | # need to open the filehandle to reproduce Apache2::Upload's behaviour | ||||||
| 114 | 0 | 0 | open $src_fh, "<$upld_fh[0]" or die "open <$upld_fh[0] : $!"; | ||||
| 115 | |||||||
| 116 | } | ||||||
| 117 | |||||||
| 118 | # compute server name and server path | ||||||
| 119 | 0 | $record->{$field} | |||||
| 120 | = $self->generate_upload_name($record, $field, $remote_name); | ||||||
| 121 | 0 | my $path = $self->upload_fullpath($record, $field); | |||||
| 122 | 0 | my $old_path = $self->{old_path}{$field}; | |||||
| 123 | |||||||
| 124 | # avoid clobbering existing files | ||||||
| 125 | 0 | 0 | 0 | not -e $path or $path eq $old_path | |||
| 126 | or die "upload $field : file $path already exists"; | ||||||
| 127 | |||||||
| 128 | # check that upload path is unique | ||||||
| 129 | 0 | 0 | not exists $self->{results}{uploaded}{$path} | ||||
| 130 | or die "multiple uploads to same server location : $path"; | ||||||
| 131 | |||||||
| 132 | # remember new and old path | ||||||
| 133 | 0 | $self->{results}{uploaded}{$path} = {field => $field, | |||||
| 134 | old_path => $old_path}; | ||||||
| 135 | |||||||
| 136 | # do the transfer | ||||||
| 137 | 0 | my ($dir) = ($path =~ m[^(.*)[/\\]]); | |||||
| 138 | 0 | 0 | -d $dir or mkpath $dir; # will die if can't make path | ||||
| 139 | |||||||
| 140 | 0 | 0 | open my $dest_fh, ">$path.new" or die "open >$path.new : $!"; | ||||
| 141 | |||||||
| 142 | 0 | binmode($dest_fh), binmode($src_fh); | |||||
| 143 | 0 | my $buf; | |||||
| 144 | 0 | while (read($src_fh, $buf, 4096)) { print $dest_fh $buf;} | |||||
| 0 | |||||||
| 145 | |||||||
| 146 | 0 | $self->{msg} .= "file $remote_name uploaded to $path "; |
|||||
| 147 | } | ||||||
| 148 | |||||||
| 149 | |||||||
| 150 | #---------------------------------------------------------------------- | ||||||
| 151 | sub after_update { | ||||||
| 152 | #---------------------------------------------------------------------- | ||||||
| 153 | 0 | 0 | 1 | my ($self, $record) = @_; | |||
| 154 | |||||||
| 155 | 0 | my $uploaded = $self->{results}{uploaded}; | |||||
| 156 | |||||||
| 157 | # rename uploaded files and delete old versions | ||||||
| 158 | 0 | while (my ($path, $info) = each %$uploaded) { | |||||
| 159 | 0 | my $field = $info->{field}; | |||||
| 160 | 0 | my $old_path = $info->{old_path}; | |||||
| 161 | |||||||
| 162 | 0 | 0 | $self->before_delete_attachment($record, $field, $old_path) | ||||
| 163 | if $old_path; | ||||||
| 164 | |||||||
| 165 | 0 | 0 | rename "$path.new", "$path" or die "rename $path.new => $path : $!"; | ||||
| 166 | |||||||
| 167 | 0 | 0 | if ($old_path) { | ||||
| 168 | 0 | 0 | if ($old_path eq $path) { | ||||
| 169 | 0 | $self->{msg} .= "old file $old_path has been replaced "; |
|||||
| 170 | } | ||||||
| 171 | else { | ||||||
| 172 | 0 | my $unlink_ok = unlink $old_path; | |||||
| 173 | 0 | 0 | $self->{msg} .= $unlink_ok ? " removed old file $old_path " |
||||
| 174 | : " remove $old_path : $^E "; |
||||||
| 175 | } | ||||||
| 176 | } | ||||||
| 177 | 0 | $self->after_add_attachment($record, $field, $path); | |||||
| 178 | } | ||||||
| 179 | } | ||||||
| 180 | |||||||
| 181 | |||||||
| 182 | |||||||
| 183 | |||||||
| 184 | #---------------------------------------------------------------------- | ||||||
| 185 | sub rollback_update { # undo what was done by "before_update" | ||||||
| 186 | #---------------------------------------------------------------------- | ||||||
| 187 | 0 | 0 | 1 | my ($self, $record) = @_; | |||
| 188 | 0 | my $uploaded = $self->{results}{uploaded}; | |||||
| 189 | 0 | foreach my $path (keys %$uploaded) { | |||||
| 190 | 0 | unlink("$path.new"); | |||||
| 191 | } | ||||||
| 192 | } | ||||||
| 193 | |||||||
| 194 | |||||||
| 195 | |||||||
| 196 | |||||||
| 197 | #---------------------------------------------------------------------- | ||||||
| 198 | sub after_delete { | ||||||
| 199 | #---------------------------------------------------------------------- | ||||||
| 200 | 0 | 0 | 1 | my ($self, $record)= @_; | |||
| 201 | |||||||
| 202 | 0 | $self->SUPER::after_delete($record); | |||||
| 203 | |||||||
| 204 | # suppress files attached to deleted record | ||||||
| 205 | 0 | my @upld = keys %{$self->{app}{upload_fields}}; | |||||
| 0 | |||||||
| 206 | 0 | foreach my $field (@upld) { | |||||
| 207 | 0 | 0 | my $path = $self->upload_fullpath($record, $field) | ||||
| 208 | or next; | ||||||
| 209 | |||||||
| 210 | 0 | $self->before_delete_attachment($record, $path); | |||||
| 211 | 0 | my $unlink_ok = unlink "$path"; | |||||
| 212 | 0 | 0 | my $msg = $unlink_ok ? "was suppressed" : "couldn't be suppressed ($!)"; | ||||
| 213 | 0 | $self->{msg} .= " Attached file $path $msg"; |
|||||
| 214 | } | ||||||
| 215 | } | ||||||
| 216 | |||||||
| 217 | |||||||
| 218 | #---------------------------------------------------------------------- | ||||||
| 219 | sub generate_upload_name { | ||||||
| 220 | #---------------------------------------------------------------------- | ||||||
| 221 | 0 | 0 | 1 | my ($self, $record, $field, $remote_name)= @_; | |||
| 222 | |||||||
| 223 | # just keep the trailing part of the remote name | ||||||
| 224 | 0 | $remote_name =~ s{^.*[/\\]}{}; | |||||
| 225 | 0 | return $remote_name; | |||||
| 226 | } | ||||||
| 227 | |||||||
| 228 | |||||||
| 229 | #---------------------------------------------------------------------- | ||||||
| 230 | sub upload_path { | ||||||
| 231 | #---------------------------------------------------------------------- | ||||||
| 232 | 0 | 0 | 1 | my ($self, $record, $field)= @_; | |||
| 233 | |||||||
| 234 | 0 | 0 | return "" if not $record->{$field}; | ||||
| 235 | |||||||
| 236 | # get the id of that record; if creating, cheat by guessing next autoNum | ||||||
| 237 | 0 | my $autonum_char = $self->{data}{autoNumChar}; | |||||
| 238 | 0 | (my $key = $self->key($record)) =~ s/$autonum_char/$self->{next_autoNum}/; | |||||
| 239 | |||||||
| 240 | 0 | 0 | my $dir = looks_like_number($key) ? sprintf "%05d/", int($key / 100) | ||||
| 241 | : ""; | ||||||
| 242 | |||||||
| 243 | 0 | return "${field}/${dir}${key}_$record->{$field}"; | |||||
| 244 | } | ||||||
| 245 | |||||||
| 246 | |||||||
| 247 | #---------------------------------------------------------------------- | ||||||
| 248 | sub upload_fullpath { | ||||||
| 249 | #---------------------------------------------------------------------- | ||||||
| 250 | 0 | 0 | 1 | my ($self, $record, $field)= @_; | |||
| 251 | 0 | my $path = $self->upload_path($record, $field); | |||||
| 252 | 0 | 0 | return $path ? "$self->{app}{dir}$path" : ""; | ||||
| 253 | } | ||||||
| 254 | |||||||
| 255 | |||||||
| 256 | #---------------------------------------------------------------------- | ||||||
| 257 | sub download { # default implementation; override in subclasses | ||||||
| 258 | #---------------------------------------------------------------------- | ||||||
| 259 | 0 | 0 | 1 | my ($self, $record, $field)= @_; | |||
| 260 | |||||||
| 261 | 0 | return $self->upload_path($record, $field); # relative to app URL | |||||
| 262 | } | ||||||
| 263 | |||||||
| 264 | |||||||
| 265 | |||||||
| 266 | |||||||
| 267 | 0 | 0 | sub after_add_attachment {} | ||||
| 268 | 0 | 0 | sub before_delete_attachment {} | ||||
| 269 | |||||||
| 270 | |||||||
| 271 | |||||||
| 272 | |||||||
| 273 | |||||||
| 274 | |||||||
| 275 | |||||||
| 276 | 1; | ||||||
| 277 | |||||||
| 278 | __END__ |