Move from mod_perl to being a PSGI app.
[pr0n] / perl / Sesse / pr0n / WebDAV.pm
1 package Sesse::pr0n::WebDAV;
2 use strict;
3 use warnings;
4
5 use Sesse::pr0n::Common qw(error dberror);
6 use Digest::SHA;
7 use MIME::Base64;
8
9 sub handler {
10         my $r = shift;
11         my $dbh = Sesse::pr0n::Common::get_dbh();
12
13         my $res = Plack::Response->new(200);
14         my $io = IO::String->new;
15         $r->header('DAV' => "1,2");
16
17         # We only handle depth=0, depth=1 (cf. the RFC)
18         my $depth = $r->header('depth');
19         $depth = 0 if (!defined($depth));
20         if (defined($depth) && $depth ne "0" && $depth ne "1") {
21                 $res->status(403);      
22                 $res->content_type('text/plain; charset="utf-8"');
23                 $res->body("Invalid depth setting");
24                 return $res;
25         }
26
27         # Just "ping, are you alive and do you speak WebDAV"
28         if ($r->method eq "OPTIONS") {
29                 $res->content_type('text/plain; charset="utf-8"');
30                 $res->header('allow' => 'OPTIONS,PUT');
31                 $res->header('ms-author-via' => 'DAV');
32                 return $res;
33         }
34         
35         my ($user,$takenby) = Sesse::pr0n::Common::check_access($r);
36         return Sesse::pr0n::Common::generate_401($r) if (!defined($user));
37
38         # Directory listings et al
39         if ($r->method eq "PROPFIND") {
40                 $res->content_type('text/xml; charset="utf-8"');
41                 $res->status(207);
42
43                 if ($r->path_info =~ m#^/webdav/?$#) {
44                         $res->header('content-location' => "/webdav/");
45                 
46                         # Root directory
47                         $io->print(<<"EOF");
48 <?xml version="1.0" encoding="utf-8"?>
49 <multistatus xmlns="DAV:">
50   <response>
51      <href>/webdav/</href>
52      <propstat>
53         <prop>
54           <resourcetype><collection/></resourcetype>
55           <getcontenttype>text/xml</getcontenttype>
56         </prop>
57         <status>HTTP/1.1 200 OK</status>
58      </propstat>
59   </response>
60 EOF
61
62                         # Optionally list the upload/ dir
63                         if ($depth >= 1) {
64                                 $io->print(<<"EOF");
65   <response>
66      <href>/webdav/upload/</href>
67      <propstat>
68         <prop>
69           <resourcetype><collection/></resourcetype>
70           <getcontenttype>text/xml</getcontenttype>
71         </prop>
72         <status>HTTP/1.1 200 OK</status>
73      </propstat>
74   </response>
75 EOF
76                         }
77                         $io->print("</multistatus>\n");
78                  } elsif ($r->path_info =~ m#^/webdav/upload/?$#) {
79                         $res->header('content-location' => "/webdav/upload/");
80                         
81                         # Upload root directory
82                         $io->print(<<"EOF");
83 <?xml version="1.0" encoding="utf-8"?>
84 <multistatus xmlns="DAV:">
85   <response>
86      <href>/webdav/upload/</href>
87      <propstat>
88         <prop>
89           <resourcetype><collection/></resourcetype>
90           <getcontenttype>text/xml</getcontenttype>
91         </prop>
92         <status>HTTP/1.1 200 OK</status>
93      </propstat>
94   </response>
95 EOF
96
97                         # Optionally list all events
98                         if ($depth >= 1) {
99                                 my $q = $dbh->prepare('SELECT * FROM events WHERE vhost=?') or
100                                         return dberror($r, "Couldn't list events");
101                                 $q->execute(Sesse::pr0n::Common::get_server_name($r)) or
102                                         return dberror($r, "Couldn't get events");
103                 
104                                 while (my $ref = $q->fetchrow_hashref()) {
105                                         my $id = Encode::encode_utf8($ref->{'event'});
106                                         my $name = Encode::encode_utf8($ref->{'name'});
107                                 
108                                         $name =~ s/&/\&amp;/g;  # hack :-)
109                                         $io->print(<<"EOF");
110   <response>
111      <href>/webdav/upload/$id/</href>
112      <propstat>
113         <prop>
114           <resourcetype><collection/></resourcetype>
115           <getcontenttype>text/xml</getcontenttype>
116           <displayname>$name</displayname> 
117         </prop>
118         <status>HTTP/1.1 200 OK</status>
119      </propstat>
120   </response>
121 EOF
122                                 }
123                                 $q->finish;
124                         }
125
126                         $io->print("</multistatus>\n");
127                 } elsif ($r->path_info =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/?$#) {
128                         my $event = $1;
129                         
130                         $res->header('content-location' => "/webdav/upload/$event/");
131                         
132                         # Check that we do indeed exist
133                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numev FROM events WHERE vhost=? AND event=?',
134                                 undef, Sesse::pr0n::Common::get_server_name($r), $event);
135                         if ($ref->{'numev'} != 1) {
136                                 $res->status(404);
137                                 $res->content_type('text/plain; charset=utf-8');
138                                 $res->body("Couldn't find event in database");
139                                 return $res;
140                         }
141                         
142                         # OK, list the directory
143                         $io->print(<<"EOF");
144 <?xml version="1.0" encoding="utf-8"?>
145 <multistatus xmlns="DAV:">
146   <response>
147      <href>/webdav/upload/$event/</href>
148      <propstat>
149         <prop>
150           <resourcetype><collection/></resourcetype>
151           <getcontenttype>text/xml</getcontenttype>
152         </prop>
153         <status>HTTP/1.1 200 OK</status>
154      </propstat>
155   </response>
156 EOF
157
158                         # List all the files within too, of course :-)
159                         if ($depth >= 1) {
160                                 my $q = $dbh->prepare('SELECT * FROM images WHERE vhost=? AND event=?') or
161                                         return dberror($r, "Couldn't list images");
162                                 $q->execute(Sesse::pr0n::Common::get_server_name($r), $event) or
163                                         return dberror($r, "Couldn't get events");
164                 
165                                 while (my $ref = $q->fetchrow_hashref()) {
166                                         my $id = $ref->{'id'};
167                                         my $filename = $ref->{'filename'};
168                                         my $fname = Sesse::pr0n::Common::get_disk_location($r, $id);
169                                         my (undef, undef, undef, undef, undef, undef, undef, $size, undef, $mtime) = stat($fname)
170                                                 or next;
171                                         $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
172                                         my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
173
174                                         $io->print(<<"EOF");
175   <response>
176      <href>/webdav/upload/$event/$filename</href>
177      <propstat>
178         <prop>
179           <resourcetype/>
180           <getcontenttype>$mime_type</getcontenttype>
181           <getcontentlength>$size</getcontentlength>
182           <getlastmodified>$mtime</getlastmodified>
183         </prop>
184         <status>HTTP/1.1 200 OK</status>
185      </propstat>
186   </response>
187 EOF
188                                 }
189                                 $q->finish;
190
191                                 # And the magical autorename folder
192                                 $io->print(<<"EOF");
193   <response>
194      <href>/webdav/upload/$event/autorename/</href>
195      <propstat>
196         <prop>
197           <resourcetype><collection/></resourcetype>
198           <getcontenttype>text/xml</getcontenttype>
199         </prop>
200         <status>HTTP/1.1 200 OK</status>
201      </propstat>
202   </response>
203 EOF
204                         }
205
206                         $io->print("</multistatus>\n");
207                         $io->setpos(0);
208                         $res->body($io);
209                         return $res;
210                 } elsif ($r->path_info =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/autorename/?$#) {
211                         # The autorename folder is always empty
212                         my $event = $1;
213                         
214                         $res->header('content-location' => "/webdav/upload/$event/autorename/");
215                         
216                         # Check that we do indeed exist
217                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numev FROM events WHERE vhost=? AND event=?',
218                                 undef, Sesse::pr0n::Common::get_server_name($r), $event);
219                         if ($ref->{'numev'} != 1) {
220                                 $res->status(404);
221                                 $res->content_type('text/plain; charset=utf-8');
222                                 $res->body("Couldn't find event in database");
223                                 return $res;
224                         }
225                         
226                         # OK, list the (empty) directory
227                         $res->body(<<"EOF");
228 <?xml version="1.0" encoding="utf-8"?>
229 <multistatus xmlns="DAV:">
230   <response>
231      <href>/webdav/upload/$event/autorename/</href>
232      <propstat>
233         <prop>
234           <resourcetype><collection/></resourcetype>
235           <getcontenttype>text/xml</getcontenttype>
236         </prop>
237         <status>HTTP/1.1 200 OK</status>
238      </propstat>
239   </response>
240 </multistatus>
241 EOF
242         
243                         return $res;
244                 } elsif ($r->path_info =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/([a-zA-Z0-9._()-]+)$#) {
245                         # stat a single file
246                         my ($event, $filename) = ($1, $2);
247                         my ($fname, $size, $mtime);
248                         
249                         # check if we have a pending fake file for this
250                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND vhost=? AND filename=? AND expires_at > now()',
251                                 undef, $event, Sesse::pr0n::Common::get_server_name($r), $filename);
252                         if ($ref->{'numfiles'} == 1) {
253                                 $fname = "/dev/null";
254                                 $size = 0;
255                                 $mtime = time;
256                         } else {
257                                 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image($r, $event, $filename);
258                         }
259                         
260                         if (!defined($fname)) {
261                                 $res->status(404);
262                                 $res->content_type('text/plain; charset=utf-8');
263                                 $res->body("Couldn't find file");
264                                 return $res;
265                         }
266                         my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
267                         
268                         $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
269                         $res->body(<<"EOF");
270 <?xml version="1.0" encoding="utf-8"?>
271 <multistatus xmlns="DAV:">
272   <response>
273     <href>/webdav/upload/$event/$filename</href>
274     <propstat>
275       <prop>
276         <resourcetype/>
277         <getcontenttype>$mime_type</getcontenttype>
278         <getcontentlength>$size</getcontentlength>
279         <getlastmodified>$mtime</getlastmodified>
280       </prop>
281       <status>HTTP/1.1 200 OK</status>
282     </propstat>
283   </response>
284 </multistatus>
285 EOF
286                         return $res;
287                 } elsif ($r->path_info =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/autorename/(.{1,250})$#) {
288                         # stat a single file in autorename
289                         my ($event, $filename) = ($1, $2);
290                         my ($fname, $size, $mtime);
291                         
292                         # check if we have a pending fake file for this
293                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND vhost=? AND filename=? AND expires_at > now()',
294                                 undef, $event, Sesse::pr0n::Common::get_server_name($r), $filename);
295                         if ($ref->{'numfiles'} == 1) {
296                                 $fname = "/dev/null";
297                                 $size = 0;
298                                 $mtime = time;
299                         } else {
300                                 # check if we have a "shadow file" for this
301                                 my $ref = $dbh->selectrow_hashref('SELECT id FROM shadow_files WHERE vhost=? AND event=? AND filename=? AND expires_at > now()',
302                                         undef, Sesse::pr0n::Common::get_server_name($r), $event, $filename);
303                                 if (defined($ref)) {
304                                         ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image_from_id($r, $ref->{'id'});
305                                 }
306                         }
307                         
308                         if (!defined($fname)) {
309                                 $res->status(404);
310                                 $res->content_type('text/plain; charset=utf-8');
311                                 $res->body("Couldn't find file");
312                                 return $res;
313                         }
314                         my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
315                         
316                         $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
317                         $io->print(<<"EOF");
318 <?xml version="1.0" encoding="utf-8"?>
319 <multistatus xmlns="DAV:">
320   <response>
321     <href>/webdav/upload/$event/autorename/$filename</href>
322     <propstat>
323       <prop>
324         <resourcetype/>
325         <getcontenttype>$mime_type</getcontenttype>
326         <getcontentlength>$size</getcontentlength>
327         <getlastmodified>$mtime</getlastmodified>
328       </prop>
329       <status>HTTP/1.1 200 OK</status>
330     </propstat>
331   </response>
332 </multistatus>
333 EOF
334                 } else {
335                         $res->status(404);
336                         $res->content_type('text/plain; charset=utf-8');
337                         $res->body("Couldn't find file");
338                         return $res;
339                 }
340                 $io->setpos(0);
341                 $res->body($io);
342                 return $res;
343         }
344         
345         if ($r->method eq "HEAD" or $r->method eq "GET") {
346                 if ($r->path_info !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(.{1,250})$#) {
347                         $res->status(404);
348                         $res->content_type('text/xml; charset=utf-8');
349                         $res->body("<?xml version=\"1.0\"?>\n<p>Couldn't find file</p>");
350                         return $res;
351                 }
352
353                 my ($event, $autorename, $filename) = ($1, $2, $3);
354                 
355                 # Check if this file really exists
356                 my ($fname, $size, $mtime);
357
358                 # check if we have a pending fake file for this
359                 my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND vhost=? AND filename=? AND expires_at > now()',
360                         undef, $event, Sesse::pr0n::Common::get_server_name($r), $filename);
361                 if ($ref->{'numfiles'} == 1) {
362                         $fname = "/dev/null";
363                         $size = 0;
364                         $mtime = time;
365                 } else {
366                         # check if we have a "shadow file" for this
367                         if (defined($autorename) && $autorename eq "autorename/") {
368                                 my $ref = $dbh->selectrow_hashref('SELECT id FROM shadow_files WHERE vhost=? AND event=? AND filename=? AND expires_at > now()',
369                                         undef, Sesse::pr0n::Common::get_server_name($r), $event, $filename);
370                                 if (defined($ref)) {
371                                         ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image_from_id($r, $ref->{'id'});
372                                 }
373                         } elsif (!defined($fname)) {
374                                 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image($r, $event, $filename);
375                         }
376                 }
377                 
378                 if (!defined($fname)) {
379                         $res->status(404);
380                         $res->content_type('text/plain; charset=utf-8');
381                         $res->body("Couldn't find file");
382                         return $res;
383                 }
384                 
385                 $res->status(200);
386                 $res->set_content_length($size);
387                 Sesse::pr0n::Common::set_last_modified($res, $mtime);
388         
389                 if ($r->method eq "GET") {
390                         $res->content(IO::File::WithPath->new($fname));
391                 }
392                 return $res;
393         }
394         
395         if ($r->method eq "PUT") {
396                 if ($r->path_info !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(.{1,250})$#) {
397                         $res->status(403);
398                         $res->content_type('text/plain; charset=utf-8');
399                         $res->body("No access");
400                         return $res;
401                 }
402                 
403                 my ($event, $autorename, $filename) = ($1, $2, $3);
404                 my $size = $r->header('content-length');
405                 if (!defined($size)) {
406                         $size = $r->header('x-expected-entity-length');
407                 }
408                 my $orig_filename = $filename;
409
410                 # Remove evil characters
411                 if ($filename =~ /[^a-zA-Z0-9._()-]/) {
412                         if (defined($autorename) && $autorename eq "autorename/") {
413                                 $filename =~ tr/a-zA-Z0-9.()-/_/c;
414                         } else {
415                                 $res->status(403);
416                                 $res->content_type('text/plain; charset=utf-8');
417                                 $res->body("Illegal characters in filename");
418                                 return $res;
419                         }
420                 }
421                 
422                 #
423                 # gnome-vfs and mac os x love to make zero-byte files,
424                 # make them happy
425                 # 
426                 if ($size == 0 || $filename =~ /^\.(_|DS_Store)/) {
427                         $dbh->do('DELETE FROM fake_files WHERE expires_at <= now() OR (event=? AND vhost=? AND filename=?);',
428                                 undef, $event, Sesse::pr0n::Common::get_server_name($r), $filename)
429                                 or return dberror($r, "Couldn't prune fake_files");
430                         $dbh->do('INSERT INTO fake_files (vhost,event,filename,expires_at) VALUES (?,?,?,now() + interval \'1 day\');',
431                                 undef, Sesse::pr0n::Common::get_server_name($r), $event, $filename)
432                                 or return dberror($r, "Couldn't add file");
433                         $res->content_type('text/plain; charset="utf-8"');
434                         $res->status(201);
435                         $res->body("OK");
436                         Sesse::pr0n::Common::log_info($r, "Fake upload of $event/$filename");
437                         return $res;
438                 }
439                         
440                 # Get the new ID
441                 my $ref = $dbh->selectrow_hashref("SELECT NEXTVAL('imageid_seq') AS id;");
442                 my $newid = $ref->{'id'};
443                 if (!defined($newid)) {
444                         return dberror($r, "Couldn't get new ID");
445                 }
446                 
447                 # Autorename if we need to
448                 $ref = $dbh->selectrow_hashref("SELECT COUNT(*) AS numfiles FROM images WHERE vhost=? AND event=? AND filename=?",
449                                                undef, Sesse::pr0n::Common::get_server_name($r), $event, $filename)
450                         or return dberror($r, "Couldn't check for existing files");
451                 if ($ref->{'numfiles'} > 0) {
452                         if (defined($autorename) && $autorename eq "autorename/") {
453                                 Sesse::pr0n::Common::log_info($r, "Renaming $filename to $newid.jpeg");
454                                 $filename = "$newid.jpeg";
455                         } else {
456                                 $res->status(403);
457                                 $res->content_type('text/plain; charset=utf-8');
458                                 $res->body("File $filename already exists in event $event, cannot overwrite");
459                                 return $res;
460                         }
461                 }
462                 
463                 {
464                         # Enable transactions and error raising temporarily
465                         local $dbh->{AutoCommit} = 0;
466                         local $dbh->{RaiseError} = 1;
467                         my $fname;
468
469                         # Try to insert this new file
470                         eval {
471                                 $dbh->do('DELETE FROM fake_files WHERE vhost=? AND event=? AND filename=?',
472                                         undef, Sesse::pr0n::Common::get_server_name($r), $event, $filename);
473                                         
474                                 $dbh->do('INSERT INTO images (id,vhost,event,uploadedby,takenby,filename) VALUES (?,?,?,?,?,?)',
475                                         undef, $newid, Sesse::pr0n::Common::get_server_name($r), $event, $user, $takenby, $filename);
476                                 Sesse::pr0n::Common::purge_cache($r, $res, "/$event/");
477
478                                 # Now save the file to disk
479                                 Sesse::pr0n::Common::ensure_disk_location_exists($r, $newid);   
480                                 $fname = Sesse::pr0n::Common::get_disk_location($r, $newid);
481
482                                 open NEWFILE, ">", $fname
483                                         or die "$fname: $!";
484                                 print NEWFILE $r->content;
485                                 close NEWFILE or die "close($fname): $!";
486                                 
487                                 # Orient stuff correctly
488                                 system("/usr/bin/exifautotran", $fname) == 0
489                                         or die "/usr/bin/exifautotran: $!";
490
491                                 # Make cache while we're at it.
492                                 # Don't do it for the resource forks Mac OS X loves to upload :-(
493                                 if ($filename !~ /^\.(_|DS_Store)/) {
494                                         # FIXME: Ideally we'd want to ensure cache of -1x-1 here as well (for NEFs), but that would
495                                         # preclude mipmapping in its current form.
496                                         Sesse::pr0n::Common::ensure_cached($r, $filename, $newid, undef, undef, "nobox", 1, 80, 64, 320, 256);
497                                 }
498                                 
499                                 # OK, we got this far, commit
500                                 $dbh->commit;
501
502                                 Sesse::pr0n::Common::log_info($r, "Successfully wrote $event/$filename to $fname");
503                         };
504                         if ($@) {
505                                 # Some error occurred, rollback and bomb out
506                                 $dbh->rollback;
507                                 unlink($fname);
508                                 return error($r, "Transaction aborted because $@");
509                         }
510                 }
511
512                 # Insert a `shadow file' we can stat the next day or so
513                 if (defined($autorename) && $autorename eq "autorename/") {
514                         $dbh->do('DELETE FROM shadow_files WHERE expires_at <= now() OR (vhost=? AND event=? AND filename=?);',
515                                 undef, Sesse::pr0n::Common::get_server_name($r), $event, $filename)
516                                 or return dberror($r, "Couldn't prune shadow_files");
517                         $dbh->do('INSERT INTO shadow_files (vhost,event,filename,id,expires_at) VALUES (?,?,?,?,now() + interval \'1 day\');',
518                                 undef, Sesse::pr0n::Common::get_server_name($r), $event, $orig_filename, $newid)
519                                 or return dberror($r, "Couldn't add shadow file");
520                         Sesse::pr0n::Common::log_info($r, "Added shadow entry for $event/$filename");
521                 }
522
523                 $res->content_type('text/plain; charset="utf-8"');
524                 $res->status(201);
525                 $res->body("OK");
526                 return $res;
527         }
528         
529         # Yes, we fake locks. :-)
530         if ($r->method eq "LOCK") {
531                 if ($r->path_info !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?([a-zA-Z0-9._-]+)$#) {
532                         $res->status(403);
533                         $res->content_type('text/plain; charset=utf-8');
534                         $res->body("No access");
535                         return $res;
536                 }
537
538                 my ($event, $autorename, $filename) = ($1, $2, $3);
539                 $autorename = '' if (!defined($autorename));
540                 my $sha1 = Digest::SHA::sha1_base64("/$event/$autorename$filename");
541
542                 $res->status(200);
543                 $res->content_type('text/xml; charset=utf-8');
544
545                 $io->print(<<"EOF");
546 <?xml version="1.0" encoding="utf-8"?>
547 <prop xmlns="DAV:">
548   <lockdiscovery>
549     <activelock>
550       <locktype><write/></locktype>
551       <lockscope><exclusive/></lockscope>
552       <depth>0</depth>
553       <owner>
554         <href>/webdav/upload/$event/$autorename$filename</href>
555       </owner>
556       <timeout>Second-3600</timeout>
557       <locktoken>
558         <href>opaquelocktoken:$sha1</href>
559       </locktoken>
560     </activelock>
561   </lockdiscovery>
562 </prop>
563 EOF
564                 $io->setpos(0);
565                 $res->body($io);
566                 return $res;
567         }
568         
569         if ($r->method eq "UNLOCK") {
570                 $res->content_type('text/plain; charset="utf-8"');
571                 $res->status(200);
572                 $res->body("OK");
573                 return $res;
574         }
575
576         if ($r->method eq "DELETE") {
577                 if ($r->path_info !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(\._[a-zA-Z0-9._-]+)$#) {
578                         $res->status(403);
579                         $res->content_type('text/plain; charset=utf-8');
580                         $res->body("No access");
581                         return $res;
582                 }
583                 
584                 my ($event, $autorename, $filename) = ($1, $2, $3);
585                 $dbh->do('DELETE FROM images WHERE vhost=? AND event=? AND filename=?',
586                         undef, Sesse::pr0n::Common::get_server_name($r), $event, $filename)
587                         or return dberror($r, "Couldn't remove file");
588                 $dbh->do('UPDATE last_picture_cache SET last_update=CURRENT_TIMESTAMP WHERE vhost=? AND event=?',
589                         undef, Sesse::pr0n::Common::get_server_name($r), $event)
590                         or return dberror($r, "Couldn't invalidate cache");
591                 $res->status(200);
592                 $res->body("OK");
593
594                 Sesse::pr0n::Common::log_info($r, "deleted $event/$filename");
595                 
596                 return $res;
597         }
598         
599         if ($r->method eq "MOVE" or
600             $r->method eq "MKCOL" or
601             $r->method eq "RMCOL" or
602             $r->method eq "RENAME" or
603             $r->method eq "COPY") {
604                 $res->content_type('text/plain; charset="utf-8"');
605                 $res->status(403);
606                 $res->body("Sorry, you do not have access to that feature.");
607                 return $res;
608         }
609
610         $res->content_type('text/plain; charset=utf-8');
611         Sesse::pr0n::Common::log_error($r, "unknown method " . $r->method);
612         $res->status(500);
613         $res->body("Unknown method");
614         return $res;
615 }
616
617 1;
618
619