1 package Sesse::pr0n::WebDAV;
5 use Sesse::pr0n::Common qw(error dberror);
11 my $dbh = Sesse::pr0n::Common::get_dbh();
13 my $res = Plack::Response->new(200);
14 my $io = IO::String->new;
15 $r->header('DAV' => "1,2");
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") {
22 $res->content_type('text/plain; charset="utf-8"');
23 $res->body("Invalid depth setting");
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');
35 my ($user,$takenby) = Sesse::pr0n::Common::check_access($r);
36 return Sesse::pr0n::Common::generate_401($r) if (!defined($user));
38 # Directory listings et al
39 if ($r->method eq "PROPFIND") {
40 $res->content_type('text/xml; charset="utf-8"');
43 if ($r->path_info =~ m#^/webdav/?$#) {
44 $res->header('content-location' => "/webdav/");
48 <?xml version="1.0" encoding="utf-8"?>
49 <multistatus xmlns="DAV:">
54 <resourcetype><collection/></resourcetype>
55 <getcontenttype>text/xml</getcontenttype>
57 <status>HTTP/1.1 200 OK</status>
62 # Optionally list the upload/ dir
66 <href>/webdav/upload/</href>
69 <resourcetype><collection/></resourcetype>
70 <getcontenttype>text/xml</getcontenttype>
72 <status>HTTP/1.1 200 OK</status>
77 $io->print("</multistatus>\n");
78 } elsif ($r->path_info =~ m#^/webdav/upload/?$#) {
79 $res->header('content-location' => "/webdav/upload/");
81 # Upload root directory
83 <?xml version="1.0" encoding="utf-8"?>
84 <multistatus xmlns="DAV:">
86 <href>/webdav/upload/</href>
89 <resourcetype><collection/></resourcetype>
90 <getcontenttype>text/xml</getcontenttype>
92 <status>HTTP/1.1 200 OK</status>
97 # Optionally list all events
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");
104 while (my $ref = $q->fetchrow_hashref()) {
105 my $id = Encode::encode_utf8($ref->{'event'});
106 my $name = Encode::encode_utf8($ref->{'name'});
108 $name =~ s/&/\&/g; # hack :-)
111 <href>/webdav/upload/$id/</href>
114 <resourcetype><collection/></resourcetype>
115 <getcontenttype>text/xml</getcontenttype>
116 <displayname>$name</displayname>
118 <status>HTTP/1.1 200 OK</status>
126 $io->print("</multistatus>\n");
127 } elsif ($r->path_info =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/?$#) {
130 $res->header('content-location' => "/webdav/upload/$event/");
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) {
137 $res->content_type('text/plain; charset=utf-8');
138 $res->body("Couldn't find event in database");
142 # OK, list the directory
144 <?xml version="1.0" encoding="utf-8"?>
145 <multistatus xmlns="DAV:">
147 <href>/webdav/upload/$event/</href>
150 <resourcetype><collection/></resourcetype>
151 <getcontenttype>text/xml</getcontenttype>
153 <status>HTTP/1.1 200 OK</status>
158 # List all the files within too, of course :-)
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");
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)
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);
176 <href>/webdav/upload/$event/$filename</href>
180 <getcontenttype>$mime_type</getcontenttype>
181 <getcontentlength>$size</getcontentlength>
182 <getlastmodified>$mtime</getlastmodified>
184 <status>HTTP/1.1 200 OK</status>
191 # And the magical autorename folder
194 <href>/webdav/upload/$event/autorename/</href>
197 <resourcetype><collection/></resourcetype>
198 <getcontenttype>text/xml</getcontenttype>
200 <status>HTTP/1.1 200 OK</status>
206 $io->print("</multistatus>\n");
210 } elsif ($r->path_info =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/autorename/?$#) {
211 # The autorename folder is always empty
214 $res->header('content-location' => "/webdav/upload/$event/autorename/");
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) {
221 $res->content_type('text/plain; charset=utf-8');
222 $res->body("Couldn't find event in database");
226 # OK, list the (empty) directory
228 <?xml version="1.0" encoding="utf-8"?>
229 <multistatus xmlns="DAV:">
231 <href>/webdav/upload/$event/autorename/</href>
234 <resourcetype><collection/></resourcetype>
235 <getcontenttype>text/xml</getcontenttype>
237 <status>HTTP/1.1 200 OK</status>
244 } elsif ($r->path_info =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/([a-zA-Z0-9._()-]+)$#) {
246 my ($event, $filename) = ($1, $2);
247 my ($fname, $size, $mtime);
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";
257 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image($r, $event, $filename);
260 if (!defined($fname)) {
262 $res->content_type('text/plain; charset=utf-8');
263 $res->body("Couldn't find file");
266 my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
268 $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
270 <?xml version="1.0" encoding="utf-8"?>
271 <multistatus xmlns="DAV:">
273 <href>/webdav/upload/$event/$filename</href>
277 <getcontenttype>$mime_type</getcontenttype>
278 <getcontentlength>$size</getcontentlength>
279 <getlastmodified>$mtime</getlastmodified>
281 <status>HTTP/1.1 200 OK</status>
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);
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";
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);
304 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image_from_id($r, $ref->{'id'});
308 if (!defined($fname)) {
310 $res->content_type('text/plain; charset=utf-8');
311 $res->body("Couldn't find file");
314 my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
316 $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
318 <?xml version="1.0" encoding="utf-8"?>
319 <multistatus xmlns="DAV:">
321 <href>/webdav/upload/$event/autorename/$filename</href>
325 <getcontenttype>$mime_type</getcontenttype>
326 <getcontentlength>$size</getcontentlength>
327 <getlastmodified>$mtime</getlastmodified>
329 <status>HTTP/1.1 200 OK</status>
336 $res->content_type('text/plain; charset=utf-8');
337 $res->body("Couldn't find file");
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})$#) {
348 $res->content_type('text/xml; charset=utf-8');
349 $res->body("<?xml version=\"1.0\"?>\n<p>Couldn't find file</p>");
353 my ($event, $autorename, $filename) = ($1, $2, $3);
355 # Check if this file really exists
356 my ($fname, $size, $mtime);
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";
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);
371 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image_from_id($r, $ref->{'id'});
373 } elsif (!defined($fname)) {
374 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image($r, $event, $filename);
378 if (!defined($fname)) {
380 $res->content_type('text/plain; charset=utf-8');
381 $res->body("Couldn't find file");
386 $res->set_content_length($size);
387 Sesse::pr0n::Common::set_last_modified($res, $mtime);
389 if ($r->method eq "GET") {
390 $res->content(IO::File::WithPath->new($fname));
395 if ($r->method eq "PUT") {
396 if ($r->path_info !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(.{1,250})$#) {
398 $res->content_type('text/plain; charset=utf-8');
399 $res->body("No access");
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');
408 my $orig_filename = $filename;
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;
416 $res->content_type('text/plain; charset=utf-8');
417 $res->body("Illegal characters in filename");
423 # gnome-vfs and mac os x love to make zero-byte files,
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"');
436 Sesse::pr0n::Common::log_info($r, "Fake upload of $event/$filename");
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");
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";
457 $res->content_type('text/plain; charset=utf-8');
458 $res->body("File $filename already exists in event $event, cannot overwrite");
464 # Enable transactions and error raising temporarily
465 local $dbh->{AutoCommit} = 0;
466 local $dbh->{RaiseError} = 1;
469 # Try to insert this new file
471 $dbh->do('DELETE FROM fake_files WHERE vhost=? AND event=? AND filename=?',
472 undef, Sesse::pr0n::Common::get_server_name($r), $event, $filename);
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/");
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);
482 open NEWFILE, ">", $fname
484 print NEWFILE $r->content;
485 close NEWFILE or die "close($fname): $!";
487 # Orient stuff correctly
488 system("/usr/bin/exifautotran", $fname) == 0
489 or die "/usr/bin/exifautotran: $!";
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, 320, 256);
499 # OK, we got this far, commit
502 Sesse::pr0n::Common::log_info($r, "Successfully wrote $event/$filename to $fname");
505 # Some error occurred, rollback and bomb out
508 return error($r, "Transaction aborted because $@");
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");
523 $res->content_type('text/plain; charset="utf-8"');
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._-]+)$#) {
533 $res->content_type('text/plain; charset=utf-8');
534 $res->body("No access");
538 my ($event, $autorename, $filename) = ($1, $2, $3);
539 $autorename = '' if (!defined($autorename));
540 my $sha1 = Digest::SHA::sha1_base64("/$event/$autorename$filename");
543 $res->content_type('text/xml; charset=utf-8');
546 <?xml version="1.0" encoding="utf-8"?>
550 <locktype><write/></locktype>
551 <lockscope><exclusive/></lockscope>
554 <href>/webdav/upload/$event/$autorename$filename</href>
556 <timeout>Second-3600</timeout>
558 <href>opaquelocktoken:$sha1</href>
569 if ($r->method eq "UNLOCK") {
570 $res->content_type('text/plain; charset="utf-8"');
576 if ($r->method eq "DELETE") {
577 if ($r->path_info !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(\._[a-zA-Z0-9._-]+)$#) {
579 $res->content_type('text/plain; charset=utf-8');
580 $res->body("No access");
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");
594 Sesse::pr0n::Common::log_info($r, "deleted $event/$filename");
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"');
606 $res->body("Sorry, you do not have access to that feature.");
610 $res->content_type('text/plain; charset=utf-8');
611 Sesse::pr0n::Common::log_error($r, "unknown method " . $r->method);
613 $res->body("Unknown method");