summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/amazon_s3.pm
blob: e181a84da04c6c152b74397fcf5d304a46d1344c (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki::Plugin::amazon_s3;
  3. use warnings;
  4. no warnings 'redefine';
  5. use strict;
  6. use IkiWiki 2.00;
  7. use IkiWiki::Render;
  8. use Net::Amazon::S3;
  9. # Store references to real subs before overriding them.
  10. our %subs;
  11. BEGIN {
  12. foreach my $sub (qw{IkiWiki::writefile IkiWiki::prune}) {
  13. $subs{$sub}=\&$sub;
  14. }
  15. };
  16. sub import { #{{{
  17. hook(type => "getopt", id => "amazon_s3", call => \&getopt);
  18. hook(type => "getsetup", id => "amazon_s3", call => \&getsetup);
  19. hook(type => "checkconfig", id => "amazon_s3", call => \&checkconfig);
  20. } # }}}
  21. sub getopt () { #{{{
  22. eval q{use Getopt::Long};
  23. error($@) if $@;
  24. Getopt::Long::Configure('pass_through');
  25. GetOptions("delete-bucket" => sub {
  26. my $bucket=getbucket();
  27. debug(gettext("deleting bucket.."));
  28. my $resp = $bucket->list_all or die $bucket->err . ": " . $bucket->errstr;
  29. foreach my $key (@{$resp->{keys}}) {
  30. debug("\t".$key->{key});
  31. $bucket->delete_key($key->{key}) or die $bucket->err . ": " . $bucket->errstr;
  32. }
  33. $bucket->delete_bucket or die $bucket->err . ": " . $bucket->errstr;
  34. debug(gettext("done"));
  35. exit(0);
  36. });
  37. } #}}}
  38. sub getsetup () { #{{{
  39. return
  40. amazon_s3_key_id => {
  41. type => "string",
  42. example => "XXXXXXXXXXXXXXXXXXXX",
  43. description => "public access key id",
  44. safe => 1,
  45. rebuild => 0,
  46. },
  47. amazon_s3_key_id => {
  48. type => "string",
  49. example => "$ENV{HOME}/.s3_key",
  50. description => "file holding secret key (must not be readable by others!)",
  51. safe => 0, # ikiwiki reads this file
  52. rebuild => 0,
  53. },
  54. amazon_s3_bucket => {
  55. type => "string",
  56. example => "mywiki",
  57. description => "globally unique name of bucket to store wiki in",
  58. safe => 1,
  59. rebuild => 1,
  60. },
  61. amazon_s3_prefix => {
  62. type => "string",
  63. example => "wiki/",
  64. description => "a prefix to prepend to each page name",
  65. safe => 1,
  66. rebuild => 1,
  67. },
  68. amazon_s3_location => {
  69. type => "string",
  70. example => "EU",
  71. description => "which S3 datacenter to use (leave blank for default)",
  72. safe => 1,
  73. rebuild => 1,
  74. },
  75. amazon_s3_dupindex => {
  76. type => "boolean",
  77. example => 0,
  78. description => "store each index file twice? (allows urls ending in \"/index.html\" and \"/\")",
  79. safe => 1,
  80. rebuild => 1,
  81. },
  82. } #}}}
  83. sub checkconfig { #{{{
  84. foreach my $field (qw{amazon_s3_key_id amazon_s3_key_file
  85. amazon_s3_bucket}) {
  86. if (! exists $config{$field} || ! defined $config{$field}) {
  87. error(sprintf(gettext("Must specify %s"), $field));
  88. }
  89. }
  90. if (! exists $config{amazon_s3_prefix} ||
  91. ! defined $config{amazon_s3_prefix}) {
  92. $config{amazon_s3_prefix}="wiki/";
  93. }
  94. } #}}}
  95. {
  96. my $bucket;
  97. sub getbucket { #{{{
  98. return $bucket if defined $bucket;
  99. open(IN, "<", $config{amazon_s3_key_file}) || error($config{amazon_s3_key_file}.": ".$!);
  100. my $key=<IN>;
  101. chomp $key;
  102. close IN;
  103. my $s3=Net::Amazon::S3->new({
  104. aws_access_key_id => $config{amazon_s3_key_id},
  105. aws_secret_access_key => $key,
  106. retry => 1,
  107. });
  108. # make sure the bucket exists
  109. if (exists $config{amazon_s3_location}) {
  110. $bucket=$s3->add_bucket({
  111. bucket => $config{amazon_s3_bucket},
  112. location_constraint => $config{amazon_s3_location},
  113. });
  114. }
  115. else {
  116. $bucket=$s3->add_bucket({
  117. bucket => $config{amazon_s3_bucket},
  118. });
  119. }
  120. if (! $bucket) {
  121. error(gettext("Failed to create bucket in S3: ").
  122. $s3->err.": ".$s3->errstr."\n");
  123. }
  124. return $bucket;
  125. } #}}}
  126. }
  127. # Given a file, return any S3 keys associated with it.
  128. sub file2keys ($) { #{{{
  129. my $file=shift;
  130. my @keys;
  131. if ($file =~ /^\Q$config{destdir}\/\E(.*)/) {
  132. push @keys, $config{amazon_s3_prefix}.$1;
  133. # Munge foo/index.html to foo/
  134. if ($keys[0]=~/(^|.*\/)index.$config{htmlext}$/) {
  135. # A duplicate might need to be stored under the
  136. # unmunged name too.
  137. if (!$config{usedirs} || $config{amazon_s3_dupindex}) {
  138. push @keys, $1;
  139. }
  140. else {
  141. @keys=($1);
  142. }
  143. }
  144. }
  145. return @keys;
  146. } #}}}
  147. package IkiWiki;
  148. use File::MimeInfo;
  149. use Encode;
  150. # This is a wrapper around the real writefile.
  151. sub writefile ($$$;$$) { #{{{
  152. my $file=shift;
  153. my $destdir=shift;
  154. my $content=shift;
  155. my $binary=shift;
  156. my $writer=shift;
  157. # First, write the file to disk.
  158. my $ret=$IkiWiki::Plugin::amazon_s3::subs{'IkiWiki::writefile'}->($file, $destdir, $content, $binary, $writer);
  159. my @keys=IkiWiki::Plugin::amazon_s3::file2keys("$destdir/$file");
  160. # Store the data in S3.
  161. if (@keys) {
  162. my $bucket=IkiWiki::Plugin::amazon_s3::getbucket();
  163. # The http layer tries to downgrade utf-8
  164. # content, but that can fail (see
  165. # http://rt.cpan.org/Ticket/Display.html?id=35710),
  166. # so force convert it to bytes.
  167. $content=encode_utf8($content) if defined $content;
  168. my %opts=(
  169. acl_short => 'public-read',
  170. content_type => mimetype("$destdir/$file"),
  171. );
  172. # If there are multiple keys to write, data is sent
  173. # multiple times.
  174. # TODO: investigate using the new copy operation.
  175. # (It may not be robust enough.)
  176. foreach my $key (@keys) {
  177. my $res;
  178. if (! $writer) {
  179. $res=$bucket->add_key($key, $content, \%opts);
  180. }
  181. else {
  182. # This test for empty files is a workaround
  183. # for this bug:
  184. # http://rt.cpan.org//Ticket/Display.html?id=35731
  185. if (-z "$destdir/$file") {
  186. $res=$bucket->add_key($key, "", \%opts);
  187. }
  188. else {
  189. # read back in the file that the writer emitted
  190. $res=$bucket->add_key_filename($key, "$destdir/$file", \%opts);
  191. }
  192. }
  193. if (! $res) {
  194. error(gettext("Failed to save file to S3: ").
  195. $bucket->err.": ".$bucket->errstr."\n");
  196. }
  197. }
  198. }
  199. return $ret;
  200. } #}}}
  201. # This is a wrapper around the real prune.
  202. sub prune ($) { #{{{
  203. my $file=shift;
  204. my @keys=IkiWiki::Plugin::amazon_s3::file2keys($file);
  205. # Prune files out of S3 too.
  206. if (@keys) {
  207. my $bucket=IkiWiki::Plugin::amazon_s3::getbucket();
  208. foreach my $key (@keys) {
  209. my $res=$bucket->delete_key($key);
  210. if (! $res) {
  211. error(gettext("Failed to delete file from S3: ").
  212. $bucket->err.": ".$bucket->errstr."\n");
  213. }
  214. }
  215. }
  216. return $IkiWiki::Plugin::amazon_s3::subs{'IkiWiki::prune'}->($file);
  217. } #}}}
  218. 1