37 use File::Basename; |
37 use File::Basename; |
38 use File::Path qw(remove_tree make_path); |
38 use File::Path qw(remove_tree make_path); |
39 use File::Spec::Functions; |
39 use File::Spec::Functions; |
40 use File::MimeInfo qw(mimetype); |
40 use File::MimeInfo qw(mimetype); |
41 use Cwd qw(getcwd realpath); |
41 use Cwd qw(getcwd realpath); |
42 use Digest::MD5 qw(md5_hex); |
42 use Digest::MD5 qw(md5_hex md5); |
|
43 use experimental qw(smartmatch lexical_topic); |
43 |
44 |
44 sub humanize; # convert numbers to human readable format |
45 sub humanize; # convert numbers to human readable format |
45 sub deletedir; # safely delete directories |
46 sub deletedir; # safely delete directories |
46 sub confirm; # ask for user confirmation (HTML) |
47 sub confirm; # ask for user confirmation (HTML) |
47 sub deslash; # cleanup a path name |
48 sub deslash; # cleanup a path name |
48 sub gen_uuid; |
49 sub gen_uuid; # create a uniq identifier |
49 |
50 sub base62; |
50 my $uuid = qr/[[:xdigit:]-]{36}/; |
51 sub md5_base62 { ... } |
51 my $hash = qr/[[:xdigit:]]{32}/; |
52 |
|
53 my $rxVIEW = qr/[a-z\d]{6}-[a-z\d]+/i; # date-userhash |
|
54 my $rxFILE = qr/[a-z\d]{6}-[a-z\d]+-./i; # date-filehash-deletemode |
52 |
55 |
53 umask 077; |
56 umask 077; |
54 |
57 |
55 # The working (var) directory gets passed to us via ONCE_VAR environment |
58 # The working (var) directory gets passed to us via ONCE_VAR environment |
56 # FIXME: Should we allow the current directory as an alternative? |
59 # FIXME: Should we allow the current directory as an alternative? |
76 " © 2014 ", |
79 " © 2014 ", |
77 a({ -href => "http://www.schlittermann.de/" } => "Matthias Förste") |
80 a({ -href => "http://www.schlittermann.de/" } => "Matthias Förste") |
78 ) |
81 ) |
79 ); |
82 ); |
80 |
83 |
81 MAIN: { |
84 exit main() if not caller; |
|
85 |
|
86 sub main { |
82 |
87 |
83 # Download? |
88 # Download? |
|
89 # PATH_INFO is something like |
|
90 # /once/hDOPzMJMY0p/3fLjYmbYlGk-1450284310-d/boot.dot |
|
91 # |-VIEW-----| |-BASE-| |
|
92 # |-STORE----------------------------| |
|
93 # … |-PATH--------------------------------------| |
84 if ($ENV{PATH_INFO} =~ |
94 if ($ENV{PATH_INFO} =~ |
85 m{(?<path>(?<store>(?<view>/$hash|$uuid)/$uuid-\d+-.)/(?<base>.*))}) |
95 m{/(?<path>(?<store>(?<view>$rxVIEW)/$rxFILE)/(?<base>.*))}) |
86 { |
96 { |
|
97 # use Data::Dumper; |
|
98 # die Dumper \%+; |
87 my $view = deslash realpath catfile $ONCE_VAR, $+{view}; |
99 my $view = deslash realpath catfile $ONCE_VAR, $+{view}; |
88 my $store = deslash realpath catfile $ONCE_VAR, $+{store}; |
100 my $store = deslash realpath catfile $ONCE_VAR, $+{store}; |
89 my $file = deslash realpath catfile $ONCE_VAR, $+{path}; |
101 my $file = deslash realpath catfile $ONCE_VAR, $+{path}; |
90 my $base = $+{base}; |
102 my $base = $+{base}; |
91 |
103 |
121 |
133 |
122 -x -w $ONCE_VAR |
134 -x -w $ONCE_VAR |
123 or die "Can't write to $ONCE_VAR: $!\n"; |
135 or die "Can't write to $ONCE_VAR: $!\n"; |
124 |
136 |
125 my ($view, $user_dir) = do { |
137 my ($view, $user_dir) = do { |
|
138 # view: display name |
|
139 # anonymous | hans | … |
|
140 # user_dir: the directory name, becomes part of the |
|
141 # link, later |
|
142 # /var/lib/once/1AaIF9-1KF |
|
143 # `--> base62 of a random value, may |
|
144 # be shorter than 3 digits |
|
145 # `-----> base62 of a unix time stamp, |
|
146 # number of digits will be 6 for the |
|
147 # forseeable future |
|
148 # NOTE: if you change the generated user_dir string here, you may need |
|
149 # to adapt the patterns $rxVIEW and $rxFILE at the beginning of |
|
150 # the script. |
|
151 # |
126 my ($v, $d); |
152 my ($v, $d); |
127 if ($ENV{REMOTE_USER} =~ /^\s*(.+)\s*$/) { |
153 if ($ENV{REMOTE_USER} =~ /^\s*(.+)\s*$/) { |
|
154 # Known users get a directory name based user name. |
|
155 # Yes, if somebody can guess the user names, they can guess |
|
156 # the directory names too. But they can't guess the |
|
157 # completly randomly named files in there. |
|
158 $d = join '-' => base62(time), md5_base62($1); |
128 $v = $1; |
159 $v = $1; |
129 $d = md5_hex($1); |
|
130 } |
160 } |
131 else { |
161 else { |
132 $d = gen_uuid(); |
162 # Anonymous get an timestamp()-rand(1000) directory |
|
163 $d = join '-' => base62(time), base62(rand(10_000)); |
133 $v = 'anonymous'; |
164 $v = 'anonymous'; |
134 } |
165 } |
135 $v, deslash catfile($ONCE_VAR, $d); |
166 $v, deslash catfile($ONCE_VAR, $d); |
136 }; |
167 }; |
137 |
168 |
138 if (param('delete') =~ m{(?<store>(?<view>$uuid|$hash)/$uuid-\d+-./?)}) { |
169 if (param('delete') =~ m{(?<store>(?<view>$rxVIEW)/$rxFILE/?)}) { |
139 |
170 |
140 # FIXME: sanitization |
171 # FIXME: sanitization |
141 my $store = deslash catfile $ONCE_VAR, $+{store}; |
172 my $store = deslash catfile $ONCE_VAR, $+{store}; |
142 my $view = deslash catfile $ONCE_VAR, $+{view}; |
173 my $view = deslash catfile $ONCE_VAR, $+{view}; |
143 remove_tree $1 if $store =~ m(^(/.*)); |
174 remove_tree $1 if $store =~ m(^(/.*)); |
148 |
179 |
149 print header(-charset => "UTF-8"), |
180 print header(-charset => "UTF-8"), |
150 start_html(-title => "once"), |
181 start_html(-title => "once"), |
151 h1 "Ansicht: $view"; |
182 h1 "Ansicht: $view"; |
152 |
183 |
153 # print Dump; |
184 # calculate the file name for the uploaded file |
154 |
|
155 if (length(my $file = param('upload'))) { |
185 if (length(my $file = param('upload'))) { |
156 my $uuid = gen_uuid(); |
186 my $uuid = gen_uuid(); |
157 my $days = param('expires'); |
187 my ($delete, $expires, $days) = do { |
158 my ($delete, $expires); |
188 my ($d, $e); |
159 # sanitize expires |
189 my $days = param('expires'); |
160 $days =~ /.*?([+-]?\d+).*/; |
190 # sanitize expires |
161 $days = defined $1 ? $1 : 10; |
191 $days =~ /.*?([+-]?\d+).*/; |
162 |
192 $days = $1 // 10; |
163 $expires = time + $days * 86400; |
193 $e = base62 time + $days * 86400; |
164 $delete = 'l'; # on file[l]ist |
194 |
165 if ($days == 0) { |
195 if ($days == 0) { $d = 'd' } # at first [d]ownload |
166 $delete = 'd'; # on first [d]ownload |
196 elsif ($days < 0) { $d = 'm' } # only [m]anually |
167 } |
197 else { $d = 'e' } # if expired |
168 elsif ($days == -1) { |
198 ($d, $e, $days); |
169 $delete = 'm'; # only [m]anually |
199 }; |
170 } |
200 |
171 |
201 # sanitize the filename |
172 # sanitizing the filename |
202 my $filename = do { |
173 (my $filename = $file) =~ tr /\\/\//; |
203 $file =~ tr /\\/\//; |
174 $filename =~ /(.*)/; |
204 $file =~ /(.*)/; |
175 $filename = $1; |
205 $1; |
176 |
206 }; |
177 my $dir = catfile($user_dir, "$uuid-$expires-$delete"); |
207 |
|
208 my $dir = catfile($user_dir, "$expires-$uuid-$delete"); |
178 make_path($dir); |
209 make_path($dir); |
179 my $outfh = new IO::File ">$dir/$filename" |
210 my $outfh = new IO::File "$dir/$filename", 'w' |
180 or die "Can't create $dir/$filename: $!\n"; |
211 or die "Can't create $dir/$filename: $!\n"; |
181 print {$outfh} <$file>; |
212 print {$outfh} <$file>; |
182 |
213 |
183 if (not $delete ~~ [qw(d m)] |
214 if (not $delete ~~ [qw(d m)] |
184 and my $atfh = new IO::File("|at now + $days days")) |
215 and my $atfh = new IO::File("|at now + $days days")) |
185 { |
216 { |
186 print {$atfh} |
217 print {$atfh} |
187 "rm -f \"$dir/$filename\"\n", |
218 "rm -f \"$dir/$filename\"\n", |
188 "rmdir \"$dir\"\n"; |
219 "rmdir \"$dir\"\n"; |
189 close $atfh; |
220 close $atfh; |
190 system("cat /tmp/log"); |
|
191 } |
221 } |
192 |
222 |
193 } |
223 } |
194 print hr; |
224 print hr; |
195 |
225 |
210 foreach (map { /(.*)/ } sort { -M $a <=> -M $b } @files) { |
240 foreach (map { /(.*)/ } sort { -M $a <=> -M $b } @files) { |
211 my ($file, $dir) = fileparse($_); |
241 my ($file, $dir) = fileparse($_); |
212 $dir = substr $dir, |
242 $dir = substr $dir, |
213 length $ONCE_VAR; # make it relative to $ONCE_VAR |
243 length $ONCE_VAR; # make it relative to $ONCE_VAR |
214 |
244 |
215 $dir =~ /(\S+)-(\d+)-(.)\/?$/ or next; |
245 # FIXME: use the rx* patterns from above |
216 my ($hash, $expires, $delete) = ($1, $2, $3); |
246 $dir =~ m{/(?<expires>[a-z\d]{6})-(?<hash>[a-z\d]+)-(?<delete>.)}i or next; |
217 if (${expires} <= time and $delete eq 'l') { |
247 my ($hash, $expires, $delete) = @{+}{qw/hash expires delete/}; |
|
248 if (${expires} <= time and $delete eq 'e') { |
218 /(.*)/; |
249 /(.*)/; |
219 unlink $_ or die "Can't unlik $_: $!\n"; |
250 unlink $_ or die "Can't unlik $_: $!\n"; |
220 rmdir $dir or die "Can't rmdir $dir: $!\n"; |
251 rmdir $dir or die "Can't rmdir $dir: $!\n"; |
221 next; |
252 next; |
222 } |
253 } |
223 |
254 |
224 my $d; |
255 my $d; |
225 if ($delete eq 'l') { |
256 if ($delete eq 'e') { |
226 $d = localtime ${expires}; |
257 $d = localtime ${expires}; |
227 } |
258 } |
228 elsif ($delete eq 'd') { |
259 elsif ($delete eq 'd') { |
229 $d = 'unmittelbar nach Download'; |
260 $d = 'unmittelbar nach Download'; |
230 } |
261 } |
240 td(a({ href => "?delete=$dir" }, 'remove')) |
271 td(a({ href => "?delete=$dir" }, 'remove')) |
241 ); |
272 ); |
242 } |
273 } |
243 |
274 |
244 print end_table, hr; |
275 print end_table, hr; |
|
276 return 0; |
245 } |
277 } |
246 |
278 |
247 print start_multipart_form, start_table, |
279 print start_multipart_form, start_table, |
248 Tr(td("Dateiname: "), |
280 Tr(td("Dateiname: "), |
249 td(filefield(-name => "upload", -default => "nothing")), |
281 td(filefield(-name => "upload", -default => "nothing")), |
301 exit 0; |
335 exit 0; |
302 } |
336 } |
303 |
337 |
304 sub base62 { |
338 sub base62 { |
305 my $n = shift // $_; |
339 my $n = shift // $_; |
306 die 'left integer precision' if $n == $n - 1 or $n == $n + 1; |
340 die 'input is >MAXINT' if $n == $n - 1 or $n == $n + 1; |
307 state $digits = [0..9, 'a'..'z', 'A'..'Z']; |
341 state $digits = [0..9, 'a'..'z', 'A'..'Z']; |
308 state $base = @$digits; |
342 state $base = @$digits; |
309 my @result; |
343 my @result; |
310 |
344 |
311 for (;$n >= $base; $n = int($n/$base)) { |
345 for (;$n >= $base; $n = int($n/$base)) { |
314 } |
348 } |
315 unshift @result, $digits->[$n]; |
349 unshift @result, $digits->[$n]; |
316 join '', @result; |
350 join '', @result; |
317 } |
351 } |
318 |
352 |
|
353 |
319 sub gen_uuid { |
354 sub gen_uuid { |
320 open my $f, '/dev/random' or croak; |
355 #open my $f, '/dev/urandom' or croak; |
321 read $f, my $_, 64/8; |
356 #read $f, my($_), 128/8; |
322 /^(.*)$/; |
357 #/^(.*)$/; |
323 return join '-', map { base62 $_ } unpack 'Q*', $1; |
358 #die join '-', map { base62 $_ } unpack 'Q*', $1; |
324 } |
359 return base62 int rand(2**64); |
|
360 } |