aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rwxr-xr-xbin/fixmystreet.com/fixture4
-rw-r--r--conf/general.yml-example26
-rw-r--r--cpanfile1
-rw-r--r--cpanfile.snapshot768
-rw-r--r--docs/customising/config.md197
-rw-r--r--docs/install/manual-install.md2
-rw-r--r--perllib/FixMyStreet.pm2
-rw-r--r--perllib/FixMyStreet/App.pm9
-rw-r--r--perllib/FixMyStreet/App/Controller/Photo.pm10
-rw-r--r--perllib/FixMyStreet/App/Model/PhotoSet.pm64
-rw-r--r--perllib/FixMyStreet/PhotoStorage.pm41
-rw-r--r--perllib/FixMyStreet/PhotoStorage/FileSystem.pm111
-rw-r--r--perllib/FixMyStreet/PhotoStorage/S3.pm122
-rw-r--r--t/app/controller/photo.t10
-rw-r--r--t/app/model/photoset.t30
-rw-r--r--t/cobrand/zurich.t5
-rw-r--r--t/open311.t5
-rw-r--r--t/photostorage/s3.t164
-rw-r--r--t/sendreport/open311.t8
20 files changed, 1433 insertions, 148 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10e0776b3..4bbc7604f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,8 @@
## Releases
* Unreleased
+ - New features:
+ - Support for storing photos in AWS S3. #2253
- Front end improvements:
- Import end point can optionally return a web page #2225
- Clicking the "Report" header links on the homepage now focusses
diff --git a/bin/fixmystreet.com/fixture b/bin/fixmystreet.com/fixture
index 091fcab9d..2f882381a 100755
--- a/bin/fixmystreet.com/fixture
+++ b/bin/fixmystreet.com/fixture
@@ -19,6 +19,7 @@ use List::Util qw(shuffle);
use Path::Tiny;
use FixMyStreet;
use FixMyStreet::Cobrand;
+use FixMyStreet::PhotoStorage;
use FixMyStreet::DB::Factories;
use Getopt::Long::Descriptive;
@@ -111,8 +112,7 @@ foreach (FixMyStreet::Cobrand->available_cobrand_classes) {
}
}
-my $cache_dir = path(FixMyStreet->config('UPLOAD_DIR'));
-$cache_dir->mkpath;
+FixMyStreet::PhotoStorage::backend->init()
my $user = $users{'user@example.org'};
my $num = 20;
diff --git a/conf/general.yml-example b/conf/general.yml-example
index 1f85b6fe7..11902c0b3 100644
--- a/conf/general.yml-example
+++ b/conf/general.yml-example
@@ -68,11 +68,31 @@ LANGUAGES:
# from the server, you can set the time zone here (standard time zone string)
TIME_ZONE: ""
-# File locations for uploaded photos and cached geocoding results.
-# Absolute paths, or relative to the project's root directory
-UPLOAD_DIR: '../upload/'
+# File locations for cached geocoding results.
GEO_CACHE: '../cache/'
+# Photo storage options.
+# Which storage backend to use. Options are 'FileSystem' and 'S3'.
+PHOTO_STORAGE_BACKEND: 'FileSystem'
+
+# FileSystem-specific options
+PHOTO_STORAGE_OPTIONS:
+ # Where uploaded photos will be stored
+ UPLOAD_DIR: '../upload/'
+
+# If using the S3 backend, you'll need to set the following options instead:
+#PHOTO_STORAGE_OPTIONS:
+# BUCKET: 'fixmystreet-photos'
+# ACCESS_KEY: ''
+# SECRET_KEY: ''
+# PREFIX: '' # optional prefix for key names in bucket, e.g. if you
+ # have multiple FMS sites storing photos in the same bucket.
+# CREATE_BUCKET: 0 # optional, set to 1 if the S3 bucket should be created if
+ # it doesn't already exist. Requires the appropriate AWS
+ # permissions.
+# REGION: 'eu-west-1' # optional, only used if CREATE_BUCKET is set. Controls
+ # which AWS region the S3 bucket will be created in.
+
# Location of MapIt, to map points to administrative areas, and what types of
# area from it you want to use. If left blank, a default area will be used
# everywhere (a URL needs to be given for non-web things, like sending of
diff --git a/cpanfile b/cpanfile
index 1bc2d8f09..71b3fc93e 100644
--- a/cpanfile
+++ b/cpanfile
@@ -79,6 +79,7 @@ requires 'Module::Pluggable';
requires 'Moose';
requires 'MooX::Types::MooseLike';
requires 'namespace::autoclean';
+requires 'Net::Amazon::S3';
requires 'Net::DNS::Resolver';
requires 'Net::Domain::TLD', '1.75';
requires 'Net::Facebook::Oauth2', '0.10';
diff --git a/cpanfile.snapshot b/cpanfile.snapshot
index 082be0ac0..565c389a6 100644
--- a/cpanfile.snapshot
+++ b/cpanfile.snapshot
@@ -877,6 +877,18 @@ DISTRIBUTIONS
Test::Fatal 0
Test::More 0
perl 5.006
+ Class-MethodMaker-2.24
+ pathname: S/SC/SCHWIGON/class-methodmaker/Class-MethodMaker-2.24.tar.gz
+ provides:
+ Class::MethodMaker 2.24
+ Class::MethodMaker::Constants undef
+ Class::MethodMaker::Engine 2.24
+ Class::MethodMaker::OptExt undef
+ Class::MethodMaker::V1Compat undef
+ Generate undef
+ requirements:
+ ExtUtils::MakeMaker 0
+ perl 5.006
Class-Mix-0.005
pathname: Z/ZE/ZEFRAM/Class-Mix-0.005.tar.gz
provides:
@@ -1595,6 +1607,30 @@ DISTRIBUTIONS
autodie 2.00
strict 0
warnings 0
+ Data-Stream-Bulk-0.11
+ pathname: D/DO/DOY/Data-Stream-Bulk-0.11.tar.gz
+ provides:
+ Data::Stream::Bulk 0.11
+ Data::Stream::Bulk::Array 0.11
+ Data::Stream::Bulk::Callback 0.11
+ Data::Stream::Bulk::Cat 0.11
+ Data::Stream::Bulk::Chunked 0.11
+ Data::Stream::Bulk::DBI 0.11
+ Data::Stream::Bulk::DBIC 0.11
+ Data::Stream::Bulk::DoneFlag 0.11
+ Data::Stream::Bulk::FileHandle 0.11
+ Data::Stream::Bulk::Filter 0.11
+ Data::Stream::Bulk::Nil 0.11
+ Data::Stream::Bulk::Path::Class 0.11
+ Data::Stream::Bulk::Util 0.11
+ requirements:
+ ExtUtils::MakeMaker 6.30
+ Moose 0.90
+ Path::Class 0
+ Sub::Exporter 0
+ Test::More 0.88
+ Test::Requires 0
+ namespace::clean 0
Data-Visitor-0.28
pathname: D/DO/DOY/Data-Visitor-0.28.tar.gz
provides:
@@ -1641,6 +1677,23 @@ DISTRIBUTIONS
utf8 0
vars 0
warnings 0
+ DateTime-Event-ICal-0.13
+ pathname: F/FG/FGLOCK/DateTime-Event-ICal-0.13.tar.gz
+ provides:
+ DateTime::Event::ICal 0.13
+ requirements:
+ DateTime 0
+ DateTime::Event::Recurrence 0.11
+ ExtUtils::MakeMaker 0
+ DateTime-Event-Recurrence-0.19
+ pathname: F/FG/FGLOCK/DateTime-Event-Recurrence-0.19.tar.gz
+ provides:
+ DateTime::Event::Recurrence 0.19
+ DateTime::Set::ICal 0.19
+ requirements:
+ DateTime 0.27
+ DateTime::Set 0.3600
+ ExtUtils::MakeMaker 0
DateTime-Format-Builder-0.80
pathname: D/DR/DROLSKY/DateTime-Format-Builder-0.80.tar.gz
provides:
@@ -1655,9 +1708,24 @@ DISTRIBUTIONS
Class::Factory::Util 1.6
DateTime 0.12
DateTime::Format::Strptime 1.04
- Module::Build 0.36
+ Module::Build 0
Params::Validate 0.72
Task::Weaken 0
+ DateTime-Format-Flexible-0.31
+ pathname: T/TH/THINC/DateTime-Format-Flexible-0.31.tar.gz
+ provides:
+ DateTime::Format::Flexible 0.31
+ DateTime::Format::Flexible::lang undef
+ DateTime::Format::Flexible::lang::de undef
+ DateTime::Format::Flexible::lang::en undef
+ DateTime::Format::Flexible::lang::es undef
+ requirements:
+ DateTime 0
+ DateTime::Format::Builder 0.74
+ DateTime::TimeZone 0
+ ExtUtils::MakeMaker 0
+ List::MoreUtils 0
+ Module::Pluggable 0
DateTime-Format-HTTP-0.40
pathname: C/CK/CKRAS/DateTime-Format-HTTP-0.40.tar.gz
provides:
@@ -1667,6 +1735,17 @@ DISTRIBUTIONS
HTTP::Date 1.44
Module::Build 0.36
Test::More 0.47
+ DateTime-Format-ICal-0.09
+ pathname: D/DR/DROLSKY/DateTime-Format-ICal-0.09.tar.gz
+ provides:
+ DateTime::Format::ICal 0.09
+ requirements:
+ DateTime 0.17
+ DateTime::Event::ICal 0.03
+ DateTime::Set 0.1
+ DateTime::TimeZone 0.22
+ Module::Build 0
+ Params::Validate 0.59
DateTime-Format-ISO8601-0.08
pathname: J/JH/JHOBLITT/DateTime-Format-ISO8601-0.08.tar.gz
provides:
@@ -1684,6 +1763,44 @@ DISTRIBUTIONS
Module::Build 0
Params::Validate 0.67
Test::More 0.47
+ DateTime-Format-Natural-1.05
+ pathname: S/SC/SCHUBIGER/DateTime-Format-Natural-1.05.tar.gz
+ provides:
+ DateTime::Format::Natural 1.05
+ DateTime::Format::Natural::Calc 1.41
+ DateTime::Format::Natural::Compat 0.07
+ DateTime::Format::Natural::Duration 0.06
+ DateTime::Format::Natural::Duration::Checks 0.04
+ DateTime::Format::Natural::Expand 0.03
+ DateTime::Format::Natural::Extract 0.11
+ DateTime::Format::Natural::Formatted 0.07
+ DateTime::Format::Natural::Helpers 0.06
+ DateTime::Format::Natural::Lang::Base 1.08
+ DateTime::Format::Natural::Lang::EN 1.62
+ DateTime::Format::Natural::Rewrite 0.06
+ DateTime::Format::Natural::Test 0.10
+ DateTime::Format::Natural::Utils 0.05
+ DateTime::Format::Natural::Wrappers 0.03
+ requirements:
+ Carp 0
+ Clone 0
+ Cwd 0
+ DateTime 0
+ DateTime::TimeZone 0
+ Exporter 0
+ File::Find 0
+ File::Spec 0
+ FindBin 0
+ Getopt::Long 0
+ List::MoreUtils 0
+ Module::Util 0
+ Params::Validate 0
+ Scalar::Util 0
+ Storable 0
+ Term::ReadLine 0
+ Test::MockTime 0
+ Test::More 0
+ boolean 0
DateTime-Format-Pg-0.16008
pathname: D/DM/DMAKI/DateTime-Format-Pg-0.16008.tar.gz
provides:
@@ -2191,9 +2308,21 @@ DISTRIBUTIONS
DateTime::Locale::zu_ZA undef
requirements:
List::MoreUtils 0
- Module::Build 0
+ Module::Build 0.36
Params::Validate 0.91
perl 5.006
+ DateTime-Set-0.3900
+ pathname: F/FG/FGLOCK/DateTime-Set-0.3900.tar.gz
+ provides:
+ DateTime::Set 0.3900
+ DateTime::Span undef
+ DateTime::SpanSet undef
+ Set::Infinite::_recurrence undef
+ requirements:
+ DateTime 0.12
+ Params::Validate 0
+ Set::Infinite 0.59
+ Test::More 0
DateTime-TimeZone-2.18
pathname: D/DR/DROLSKY/DateTime-TimeZone-2.18.tar.gz
provides:
@@ -2589,6 +2718,20 @@ DISTRIBUTIONS
perl 5.008004
strict 0
warnings 0
+ DateTimeX-Easy-0.089
+ pathname: R/RO/ROKR/DateTimeX-Easy-0.089.tar.gz
+ provides:
+ DateTimeX::Easy 0.089
+ requirements:
+ Date::Parse 0
+ DateTime 0
+ DateTime::Format::Flexible 0
+ DateTime::Format::ICal 0
+ DateTime::Format::Natural 0
+ ExtUtils::MakeMaker 6.31
+ Scalar::Util 0
+ Test::Most 0
+ Time::Zone 0
Devel-Caller-2.06
pathname: R/RC/RCLAMP/Devel-Caller-2.06.tar.gz
provides:
@@ -2648,6 +2791,14 @@ DISTRIBUTIONS
Digest::SHA 1
ExtUtils::MakeMaker 0
perl 5.004
+ Digest-MD5-File-0.08
+ pathname: D/DM/DMUEY/Digest-MD5-File-0.08.tar.gz
+ provides:
+ Digest::MD5::File 0.08
+ requirements:
+ Digest::MD5 0
+ ExtUtils::MakeMaker 0
+ LWP::UserAgent 0
Digest-Perl-MD5-1.9
pathname: D/DE/DELTA/Digest-Perl-MD5-1.9.tar.gz
provides:
@@ -2934,6 +3085,16 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
+ Exporter-Lite-0.08
+ pathname: N/NE/NEILB/Exporter-Lite-0.08.tar.gz
+ provides:
+ Exporter::Lite 0.08
+ requirements:
+ Carp 0
+ ExtUtils::MakeMaker 6.3
+ perl 5.006
+ strict 0
+ warnings 0
Exporter-Tiny-0.042
pathname: T/TO/TOBYINK/Exporter-Tiny-0.042.tar.gz
provides:
@@ -3472,6 +3633,14 @@ DISTRIBUTIONS
File::Temp 0
Scalar::Util 0
Test::More 0.88
+ IO-Interactive-1.022
+ pathname: B/BD/BDFOY/IO-Interactive-1.022.tar.gz
+ provides:
+ IO::Interactive 1.022
+ requirements:
+ ExtUtils::MakeMaker 6.64
+ File::Spec::Functions 0
+ perl 5.008
IO-SessionData-1.03
pathname: P/PH/PHRED/IO-SessionData-1.03.tar.gz
provides:
@@ -3601,6 +3770,13 @@ DISTRIBUTIONS
Mozilla::CA 20110101
Net::HTTPS 6
perl 5.008001
+ LWP-UserAgent-Determined-1.07
+ pathname: A/AL/ALEXMV/LWP-UserAgent-Determined-1.07.tar.gz
+ provides:
+ LWP::UserAgent::Determined 1.07
+ requirements:
+ ExtUtils::MakeMaker 0
+ LWP 0
LWPx-Profile-0.2
pathname: C/CR/CREIN/LWPx-Profile-0.2.tar.gz
provides:
@@ -4074,6 +4250,14 @@ DISTRIBUTIONS
Text::ParseWords 0
perl 5.008001
version 0
+ Module-Util-1.09
+ pathname: M/MA/MATTLAW/Module-Util-1.09.tar.gz
+ provides:
+ Module::Util 1.09
+ requirements:
+ Module::Build 0.40
+ Test::More 0
+ perl v5.5.3
Moo-2.003001
pathname: H/HA/HAARG/Moo-2.003001.tar.gz
provides:
@@ -4391,6 +4575,22 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 6.31
Moose 1.16
Test::More 0.88
+ MooseX-StrictConstructor-0.21
+ pathname: D/DR/DROLSKY/MooseX-StrictConstructor-0.21.tar.gz
+ provides:
+ MooseX::StrictConstructor 0.21
+ MooseX::StrictConstructor::Trait::Class 0.21
+ MooseX::StrictConstructor::Trait::Method::Constructor 0.21
+ requirements:
+ B 0
+ ExtUtils::MakeMaker 0
+ Moose 0.94
+ Moose::Exporter 0
+ Moose::Role 0
+ Moose::Util::MetaRole 0
+ namespace::autoclean 0
+ strict 0
+ warnings 0
MooseX-Traits-Pluggable-0.10
pathname: R/RK/RKITOVER/MooseX-Traits-Pluggable-0.10.tar.gz
provides:
@@ -4431,6 +4631,43 @@ DISTRIBUTIONS
Test::More 0.88
Test::Requires 0
namespace::clean 0.19
+ MooseX-Types-DateTime-0.13
+ pathname: E/ET/ETHER/MooseX-Types-DateTime-0.13.tar.gz
+ provides:
+ MooseX::Types::DateTime 0.13
+ requirements:
+ DateTime 0.4302
+ DateTime::Duration 0.4302
+ DateTime::Locale 0.4001
+ DateTime::TimeZone 0.95
+ Module::Build::Tiny 0.034
+ Moose 0.41
+ MooseX::Types 0.30
+ MooseX::Types::Moose 0.30
+ if 0
+ namespace::clean 0.19
+ perl 5.008003
+ strict 0
+ warnings 0
+ MooseX-Types-DateTime-MoreCoercions-0.15
+ pathname: E/ET/ETHER/MooseX-Types-DateTime-MoreCoercions-0.15.tar.gz
+ provides:
+ MooseX::Types::DateTime::MoreCoercions 0.15
+ requirements:
+ DateTime 0.4302
+ DateTime::Duration 0.4302
+ DateTimeX::Easy 0.085
+ Module::Build::Tiny 0.007
+ Moose 0.41
+ MooseX::Types 0.04
+ MooseX::Types::DateTime 0.07
+ MooseX::Types::Moose 0.04
+ Time::Duration::Parse 0.06
+ if 0
+ namespace::clean 0.19
+ perl 5.006
+ strict 0
+ warnings 0
MooseX-Types-Path-Class-0.06
pathname: T/TH/THEPLER/MooseX-Types-Path-Class-0.06.tar.gz
provides:
@@ -4450,6 +4687,127 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 0
Test 0
perl 5.006
+ Net-Amazon-S3-0.85
+ pathname: L/LL/LLAP/Net-Amazon-S3-0.85.tar.gz
+ provides:
+ Net::Amazon::S3 0.85
+ Net::Amazon::S3::Bucket 0.85
+ Net::Amazon::S3::Client 0.85
+ Net::Amazon::S3::Client::Bucket 0.85
+ Net::Amazon::S3::Client::Object 0.85
+ Net::Amazon::S3::HTTPRequest 0.85
+ Net::Amazon::S3::Request 0.85
+ Net::Amazon::S3::Request::AbortMultipartUpload 0.85
+ Net::Amazon::S3::Request::Bucket 0.85
+ Net::Amazon::S3::Request::CompleteMultipartUpload 0.85
+ Net::Amazon::S3::Request::CreateBucket 0.85
+ Net::Amazon::S3::Request::DeleteBucket 0.85
+ Net::Amazon::S3::Request::DeleteMultiObject 0.85
+ Net::Amazon::S3::Request::DeleteObject 0.85
+ Net::Amazon::S3::Request::GetBucketAccessControl 0.85
+ Net::Amazon::S3::Request::GetBucketLocationConstraint 0.85
+ Net::Amazon::S3::Request::GetObject 0.85
+ Net::Amazon::S3::Request::GetObjectAccessControl 0.85
+ Net::Amazon::S3::Request::InitiateMultipartUpload 0.85
+ Net::Amazon::S3::Request::ListAllMyBuckets 0.85
+ Net::Amazon::S3::Request::ListBucket 0.85
+ Net::Amazon::S3::Request::ListParts 0.85
+ Net::Amazon::S3::Request::Object 0.85
+ Net::Amazon::S3::Request::PutObject 0.85
+ Net::Amazon::S3::Request::PutPart 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Header 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Header::Acl_short 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Header::Content_length 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Header::Content_md5 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Header::Content_type 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Header::Copy_source 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Header::Encryption 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Method 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Method::DELETE 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Method::GET 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Method::POST 0.85
+ Net::Amazon::S3::Request::Role::HTTP::Method::PUT 0.85
+ Net::Amazon::S3::Request::Role::Query::Action 0.85
+ Net::Amazon::S3::Request::Role::Query::Action::Acl 0.85
+ Net::Amazon::S3::Request::Role::Query::Action::Delete 0.85
+ Net::Amazon::S3::Request::Role::Query::Action::Location 0.85
+ Net::Amazon::S3::Request::Role::Query::Action::Uploads 0.85
+ Net::Amazon::S3::Request::Role::Query::Param 0.85
+ Net::Amazon::S3::Request::Role::Query::Param::Delimiter 0.85
+ Net::Amazon::S3::Request::Role::Query::Param::Marker 0.85
+ Net::Amazon::S3::Request::Role::Query::Param::Max_keys 0.85
+ Net::Amazon::S3::Request::Role::Query::Param::Part_number 0.85
+ Net::Amazon::S3::Request::Role::Query::Param::Prefix 0.85
+ Net::Amazon::S3::Request::Role::Query::Param::Upload_id 0.85
+ Net::Amazon::S3::Request::Service 0.85
+ Net::Amazon::S3::Request::SetBucketAccessControl 0.85
+ Net::Amazon::S3::Request::SetObjectAccessControl 0.85
+ Net::Amazon::S3::Role::Bucket 0.85
+ Net::Amazon::S3::Signature 0.85
+ Net::Amazon::S3::Signature::V2 0.85
+ Net::Amazon::S3::Signature::V4 0.85
+ Net::Amazon::S3::Signature::V4Implementation 0.19
+ Shared::Examples::Net::Amazon::S3 0.85
+ Shared::Examples::Net::Amazon::S3::ACL 0.85
+ Shared::Examples::Net::Amazon::S3::API 0.85
+ Shared::Examples::Net::Amazon::S3::Client 0.85
+ Shared::Examples::Net::Amazon::S3::Error 0.85
+ Shared::Examples::Net::Amazon::S3::Operation::Bucket::Create 0.85
+ Shared::Examples::Net::Amazon::S3::Operation::Bucket::Objects::Delete 0.85
+ Shared::Examples::Net::Amazon::S3::Operation::Bucket::Objects::List 0.85
+ Shared::Examples::Net::Amazon::S3::Operation::Service::Buckets::List 0.85
+ Shared::Examples::Net::Amazon::S3::Request 0.85
+ requirements:
+ Carp 0
+ Data::Stream::Bulk::Callback 0
+ DateTime::Format::HTTP 0
+ Digest::HMAC_SHA1 0
+ Digest::MD5 0
+ Digest::MD5::File 0
+ Digest::SHA 0
+ Exporter::Tiny 0
+ ExtUtils::MakeMaker 0
+ File::Find::Rule 0
+ File::stat 0
+ Getopt::Long 0
+ HTTP::Date 0
+ HTTP::Response 0
+ HTTP::Status 0
+ Hash::Util 0
+ IO::File 1.14
+ LWP 6.03
+ LWP::UserAgent::Determined 0
+ MIME::Base64 0
+ MIME::Types 0
+ Moose 0.85
+ Moose::Object 0
+ Moose::Role 0
+ Moose::Util 0
+ Moose::Util::TypeConstraints 0
+ MooseX::Role::Parameterized 0
+ MooseX::StrictConstructor 0.16
+ MooseX::Types::DateTime::MoreCoercions 0.07
+ Path::Class 0
+ Pod::Usage 0
+ Ref::Util 0
+ Regexp::Common 0
+ Scalar::Util 0
+ Sub::Override 0
+ Term::Encoding 0
+ Term::ProgressBar::Simple 0
+ Test::Deep 0
+ Test::More 0
+ Time::Piece 0
+ URI 0
+ URI::Escape 0
+ URI::QueryParam 0
+ XML::LibXML 0
+ XML::LibXML::XPathContext 0
+ namespace::clean 0
+ parent 0
+ sort 0
+ strict 0
+ warnings 0
Net-DNS-0.72
pathname: N/NL/NLNETLABS/Net-DNS-0.72.tar.gz
provides:
@@ -4671,7 +5029,7 @@ DISTRIBUTIONS
LWP::Protocol::https 0
LWP::UserAgent 2.032
Module::Build::Tiny 0.034
- Net::HTTP 0
+ Net::HTTP >= 0, != 6.04, != 6.05
Net::Netrc 0
URI 1.40
URI::Escape 0
@@ -5491,6 +5849,26 @@ DISTRIBUTIONS
Readonly::Scalar 1.03
requirements:
ExtUtils::MakeMaker 0
+ Ref-Util-0.204
+ pathname: A/AR/ARC/Ref-Util-0.204.tar.gz
+ provides:
+ Ref::Util 0.204
+ Ref::Util::PP 0.204
+ requirements:
+ Exporter 5.57
+ ExtUtils::MakeMaker 0
+ Ref::Util::XS 0
+ Text::ParseWords 0
+ perl 5.006
+ Ref-Util-XS-0.117
+ pathname: X/XS/XSAWYERX/Ref-Util-XS-0.117.tar.gz
+ provides:
+ Ref::Util::XS 0.117
+ requirements:
+ Exporter 5.57
+ ExtUtils::MakeMaker 0
+ XSLoader 0
+ perl 5.006
Regexp-Common-2013031301
pathname: A/AB/ABIGAIL/Regexp-Common-2013031301.tar.gz
provides:
@@ -5789,6 +6167,16 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 0
Test::More 0
perl 5.006001
+ Set-Infinite-0.65
+ pathname: F/FG/FGLOCK/Set-Infinite-0.65.tar.gz
+ provides:
+ Set::Infinite 0.65
+ Set::Infinite::Arithmetic undef
+ Set::Infinite::Basic undef
+ requirements:
+ ExtUtils::MakeMaker 0
+ Test::More 0
+ Time::Local 0
Snowball-Norwegian-1.2
pathname: A/AS/ASKSH/Snowball-Norwegian-1.2.tar.gz
provides:
@@ -6208,6 +6596,53 @@ DISTRIBUTIONS
File::Spec 0.8
File::Temp 0.12
Scalar::Util 0
+ Term-Encoding-0.02
+ pathname: M/MI/MIYAGAWA/Term-Encoding-0.02.tar.gz
+ provides:
+ Term::Encoding 0.02
+ requirements:
+ ExtUtils::MakeMaker 0
+ Term-ProgressBar-2.22
+ pathname: M/MA/MANWAR/Term-ProgressBar-2.22.tar.gz
+ provides:
+ Term::ProgressBar 2.22
+ Term::ProgressBar::IO 2.22
+ requirements:
+ Capture::Tiny 0.13
+ Carp 0
+ Class::MethodMaker 1.02
+ ExtUtils::MakeMaker 0
+ Fatal 0
+ File::Temp 0
+ POSIX 0
+ Term::ReadKey 2.14
+ Test::Exception 0.31
+ Test::More 0.80
+ Test::Warnings 0
+ perl 5.006
+ Term-ProgressBar-Quiet-0.31
+ pathname: L/LB/LBROCARD/Term-ProgressBar-Quiet-0.31.tar.gz
+ provides:
+ Term::ProgressBar::Quiet 0.31
+ requirements:
+ ExtUtils::MakeMaker 0
+ IO::Interactive 0
+ Term::ProgressBar 0
+ Test::MockObject 0
+ Test::More 0
+ Term-ProgressBar-Simple-0.03
+ pathname: E/EV/EVDB/Term-ProgressBar-Simple-0.03.tar.gz
+ provides:
+ Term::ProgressBar::Simple 0.03
+ requirements:
+ ExtUtils::MakeMaker 0
+ Term::ProgressBar::Quiet 0
+ TermReadKey-2.37
+ pathname: J/JS/JSTOWE/TermReadKey-2.37.tar.gz
+ provides:
+ Term::ReadKey 2.37
+ requirements:
+ ExtUtils::MakeMaker 6.58
Test-Base-0.60
pathname: I/IN/INGY/Test-Base-0.60.tar.gz
provides:
@@ -6239,10 +6674,10 @@ DISTRIBUTIONS
Test::More 0.88
strict 0
warnings 0
- Test-Deep-0.110
- pathname: R/RJ/RJBS/Test-Deep-0.110.tar.gz
+ Test-Deep-1.128
+ pathname: R/RJ/RJBS/Test-Deep-1.128.tar.gz
provides:
- Test::Deep 0.110
+ Test::Deep 1.128
Test::Deep::All undef
Test::Deep::Any undef
Test::Deep::Array undef
@@ -6268,11 +6703,14 @@ DISTRIBUTIONS
Test::Deep::MM undef
Test::Deep::Methods undef
Test::Deep::NoTest undef
+ Test::Deep::None undef
Test::Deep::Number undef
+ Test::Deep::Obj undef
Test::Deep::Ref undef
Test::Deep::RefType undef
Test::Deep::Regexp undef
Test::Deep::RegexpMatches undef
+ Test::Deep::RegexpOnly undef
Test::Deep::RegexpRef undef
Test::Deep::RegexpRefOnly undef
Test::Deep::RegexpVersion undef
@@ -6294,30 +6732,32 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 0
List::Util 1.09
Scalar::Util 1.09
- Test::More 0
- Test::NoWarnings 0.02
- Test::Tester 0.04
- Test-Differences-0.61
- pathname: O/OV/OVID/Test-Differences-0.61.tar.gz
+ Test::Builder 0
+ Test-Differences-0.64
+ pathname: D/DC/DCANTRELL/Test-Differences-0.64.tar.gz
provides:
- Test::Differences 0.61
+ Test::Differences 0.64
requirements:
+ Capture::Tiny 0.24
Data::Dumper 2.126
- Module::Build 0.36
- Test::More 0
+ Test::More 0.88
Text::Diff 0.35
- Test-Exception-0.31
- pathname: A/AD/ADIE/Test-Exception-0.31.tar.gz
+ Test-Exception-0.43
+ pathname: E/EX/EXODIST/Test-Exception-0.43.tar.gz
provides:
- Test::Exception 0.31
+ Test::Exception 0.43
requirements:
- Module::Build 0.36
+ Carp 0
+ Exporter 0
+ ExtUtils::MakeMaker 0
Sub::Uplevel 0.18
Test::Builder 0.7
Test::Builder::Tester 1.07
Test::Harness 2.03
- Test::More 0.7
- Test::Simple 0.7
+ base 0
+ perl 5.006001
+ strict 0
+ warnings 0
Test-Fatal-0.010
pathname: R/RJ/RJBS/Test-Fatal-0.010.tar.gz
provides:
@@ -6333,57 +6773,58 @@ DISTRIBUTIONS
overload 0
strict 0
warnings 0
- Test-Harness-3.32
- pathname: L/LE/LEONT/Test-Harness-3.32.tar.gz
- provides:
- App::Prove 3.32
- App::Prove::State 3.32
- App::Prove::State::Result 3.32
- App::Prove::State::Result::Test 3.32
- TAP::Base 3.32
- TAP::Formatter::Base 3.32
- TAP::Formatter::Color 3.32
- TAP::Formatter::Console 3.32
- TAP::Formatter::Console::ParallelSession 3.32
- TAP::Formatter::Console::Session 3.32
- TAP::Formatter::File 3.32
- TAP::Formatter::File::Session 3.32
- TAP::Formatter::Session 3.32
- TAP::Harness 3.32
- TAP::Harness::Env 3.32
- TAP::Object 3.32
- TAP::Parser 3.32
- TAP::Parser::Aggregator 3.32
- TAP::Parser::Grammar 3.32
- TAP::Parser::Iterator 3.32
- TAP::Parser::Iterator::Array 3.32
- TAP::Parser::Iterator::Process 3.32
- TAP::Parser::Iterator::Stream 3.32
- TAP::Parser::IteratorFactory 3.32
- TAP::Parser::Multiplexer 3.32
- TAP::Parser::Result 3.32
- TAP::Parser::Result::Bailout 3.32
- TAP::Parser::Result::Comment 3.32
- TAP::Parser::Result::Plan 3.32
- TAP::Parser::Result::Pragma 3.32
- TAP::Parser::Result::Test 3.32
- TAP::Parser::Result::Unknown 3.32
- TAP::Parser::Result::Version 3.32
- TAP::Parser::Result::YAML 3.32
- TAP::Parser::ResultFactory 3.32
- TAP::Parser::Scheduler 3.32
- TAP::Parser::Scheduler::Job 3.32
- TAP::Parser::Scheduler::Spinner 3.32
- TAP::Parser::Source 3.32
- TAP::Parser::SourceHandler 3.32
- TAP::Parser::SourceHandler::Executable 3.32
- TAP::Parser::SourceHandler::File 3.32
- TAP::Parser::SourceHandler::Handle 3.32
- TAP::Parser::SourceHandler::Perl 3.32
- TAP::Parser::SourceHandler::RawTAP 3.32
- TAP::Parser::YAMLish::Reader 3.32
- TAP::Parser::YAMLish::Writer 3.32
- Test::Harness 3.32
+ Test-Harness-3.42
+ pathname: L/LE/LEONT/Test-Harness-3.42.tar.gz
+ provides:
+ App::Prove 3.42
+ App::Prove::State 3.42
+ App::Prove::State::Result 3.42
+ App::Prove::State::Result::Test 3.42
+ Harness::Hook undef
+ TAP::Base 3.42
+ TAP::Formatter::Base 3.42
+ TAP::Formatter::Color 3.42
+ TAP::Formatter::Console 3.42
+ TAP::Formatter::Console::ParallelSession 3.42
+ TAP::Formatter::Console::Session 3.42
+ TAP::Formatter::File 3.42
+ TAP::Formatter::File::Session 3.42
+ TAP::Formatter::Session 3.42
+ TAP::Harness 3.42
+ TAP::Harness::Env 3.42
+ TAP::Object 3.42
+ TAP::Parser 3.42
+ TAP::Parser::Aggregator 3.42
+ TAP::Parser::Grammar 3.42
+ TAP::Parser::Iterator 3.42
+ TAP::Parser::Iterator::Array 3.42
+ TAP::Parser::Iterator::Process 3.42
+ TAP::Parser::Iterator::Stream 3.42
+ TAP::Parser::IteratorFactory 3.42
+ TAP::Parser::Multiplexer 3.42
+ TAP::Parser::Result 3.42
+ TAP::Parser::Result::Bailout 3.42
+ TAP::Parser::Result::Comment 3.42
+ TAP::Parser::Result::Plan 3.42
+ TAP::Parser::Result::Pragma 3.42
+ TAP::Parser::Result::Test 3.42
+ TAP::Parser::Result::Unknown 3.42
+ TAP::Parser::Result::Version 3.42
+ TAP::Parser::Result::YAML 3.42
+ TAP::Parser::ResultFactory 3.42
+ TAP::Parser::Scheduler 3.42
+ TAP::Parser::Scheduler::Job 3.42
+ TAP::Parser::Scheduler::Spinner 3.42
+ TAP::Parser::Source 3.42
+ TAP::Parser::SourceHandler 3.42
+ TAP::Parser::SourceHandler::Executable 3.42
+ TAP::Parser::SourceHandler::File 3.42
+ TAP::Parser::SourceHandler::Handle 3.42
+ TAP::Parser::SourceHandler::Perl 3.42
+ TAP::Parser::SourceHandler::RawTAP 3.42
+ TAP::Parser::YAMLish::Reader 3.42
+ TAP::Parser::YAMLish::Writer 3.42
+ Test::Harness 3.42
requirements:
ExtUtils::MakeMaker 0
Test-LongString-0.15
@@ -6437,6 +6878,20 @@ DISTRIBUTIONS
Test::More 0
Time::Local 0
Time::Piece 1.08
+ Test-Most-0.35
+ pathname: O/OV/OVID/Test-Most-0.35.tar.gz
+ provides:
+ Test::Most 0.35
+ Test::Most::Exception 0.35
+ requirements:
+ Exception::Class 1.14
+ ExtUtils::MakeMaker 0
+ Test::Deep 0.119
+ Test::Differences 0.64
+ Test::Exception 0.43
+ Test::Harness 3.35
+ Test::More 1.302047
+ Test::Warn 0.30
Test-NoWarnings-1.04
pathname: A/AD/ADAMK/Test-NoWarnings-1.04.tar.gz
provides:
@@ -6514,6 +6969,83 @@ DISTRIBUTIONS
Test::Requires 0
Time::HiRes 0
perl 5.008
+ Test-Simple-1.302140
+ pathname: E/EX/EXODIST/Test-Simple-1.302140.tar.gz
+ provides:
+ Test2 1.302140
+ Test2::API 1.302140
+ Test2::API::Breakage 1.302140
+ Test2::API::Context 1.302140
+ Test2::API::Instance 1.302140
+ Test2::API::Stack 1.302140
+ Test2::Event 1.302140
+ Test2::Event::Bail 1.302140
+ Test2::Event::Diag 1.302140
+ Test2::Event::Encoding 1.302140
+ Test2::Event::Exception 1.302140
+ Test2::Event::Fail 1.302140
+ Test2::Event::Generic 1.302140
+ Test2::Event::Note 1.302140
+ Test2::Event::Ok 1.302140
+ Test2::Event::Pass 1.302140
+ Test2::Event::Plan 1.302140
+ Test2::Event::Skip 1.302140
+ Test2::Event::Subtest 1.302140
+ Test2::Event::TAP::Version 1.302140
+ Test2::Event::V2 1.302140
+ Test2::Event::Waiting 1.302140
+ Test2::EventFacet 1.302140
+ Test2::EventFacet::About 1.302140
+ Test2::EventFacet::Amnesty 1.302140
+ Test2::EventFacet::Assert 1.302140
+ Test2::EventFacet::Control 1.302140
+ Test2::EventFacet::Error 1.302140
+ Test2::EventFacet::Hub 1.302140
+ Test2::EventFacet::Info 1.302140
+ Test2::EventFacet::Meta 1.302140
+ Test2::EventFacet::Parent 1.302140
+ Test2::EventFacet::Plan 1.302140
+ Test2::EventFacet::Render 1.302140
+ Test2::EventFacet::Trace 1.302140
+ Test2::Formatter 1.302140
+ Test2::Formatter::TAP 1.302140
+ Test2::Hub 1.302140
+ Test2::Hub::Interceptor 1.302140
+ Test2::Hub::Interceptor::Terminator 1.302140
+ Test2::Hub::Subtest 1.302140
+ Test2::IPC 1.302140
+ Test2::IPC::Driver 1.302140
+ Test2::IPC::Driver::Files 1.302140
+ Test2::Tools::Tiny 1.302140
+ Test2::Util 1.302140
+ Test2::Util::ExternalMeta 1.302140
+ Test2::Util::Facets2Legacy 1.302140
+ Test2::Util::HashBase 1.302140
+ Test2::Util::Trace 1.302140
+ Test::Builder 1.302140
+ Test::Builder::Formatter 1.302140
+ Test::Builder::IO::Scalar 2.114
+ Test::Builder::Module 1.302140
+ Test::Builder::Tester 1.302140
+ Test::Builder::Tester::Color 1.302140
+ Test::Builder::Tester::Tie 1.302140
+ Test::Builder::TodoDiag 1.302140
+ Test::More 1.302140
+ Test::Simple 1.302140
+ Test::Tester 1.302140
+ Test::Tester::Capture 1.302140
+ Test::Tester::CaptureRunner 1.302140
+ Test::Tester::Delegate 1.302140
+ Test::use::ok 1.302140
+ ok 1.302140
+ requirements:
+ ExtUtils::MakeMaker 0
+ File::Spec 0
+ File::Temp 0
+ Scalar::Util 1.13
+ Storable 0
+ perl 5.006002
+ utf8 0
Test-TCP-1.21
pathname: T/TO/TOKUHIROM/Test-TCP-1.21.tar.gz
provides:
@@ -6606,22 +7138,30 @@ DISTRIBUTIONS
Test::More 0
Test::WWW::Mechanize 0
Try::Tiny 0
- Test-Warn-0.24
- pathname: C/CH/CHORNY/Test-Warn-0.24.tar.gz
+ Test-Warn-0.36
+ pathname: B/BI/BIGJ/Test-Warn-0.36.tar.gz
provides:
- Test::Warn 0.24
- Test::Warn::Categorization 0.24
- Test::Warn::DAG_Node_Tree 0.24
+ Test::Warn 0.36
requirements:
Carp 1.22
ExtUtils::MakeMaker 0
- File::Spec 0
Sub::Uplevel 0.12
Test::Builder 0.13
Test::Builder::Tester 1.02
- Test::More 0
- Tree::DAG_Node 1.02
perl 5.006
+ Test-Warnings-0.026
+ pathname: E/ET/ETHER/Test-Warnings-0.026.tar.gz
+ provides:
+ Test::Warnings 0.026
+ requirements:
+ Carp 0
+ Exporter 0
+ ExtUtils::MakeMaker 0
+ Test::Builder 0
+ parent 0
+ perl 5.006
+ strict 0
+ warnings 0
Test-use-ok-0.11
pathname: A/AU/AUDREYT/Test-use-ok-0.11.tar.gz
provides:
@@ -6746,6 +7286,17 @@ DISTRIBUTIONS
Test::More 0
Test::use::ok 0
Tie::RefHash 0
+ Time-Duration-Parse-0.14
+ pathname: N/NE/NEILB/Time-Duration-Parse-0.14.tar.gz
+ provides:
+ Time::Duration::Parse 0.14
+ requirements:
+ Carp 0
+ Exporter::Lite 0
+ ExtUtils::MakeMaker 0
+ perl 5.006
+ strict 0
+ warnings 0
TimeDate-2.30
pathname: G/GB/GBARR/TimeDate-2.30.tar.gz
provides:
@@ -7122,6 +7673,58 @@ DISTRIBUTIONS
Plack 0.9968
Syntax::Keyword::Gather 1.001
warnings::illegalproto 0.001
+ XML-LibXML-2.0132
+ pathname: S/SH/SHLOMIF/XML-LibXML-2.0132.tar.gz
+ provides:
+ XML::LibXML 2.0132
+ XML::LibXML::Attr 2.0132
+ XML::LibXML::AttributeHash 2.0132
+ XML::LibXML::Boolean 2.0132
+ XML::LibXML::CDATASection 2.0132
+ XML::LibXML::Comment 2.0132
+ XML::LibXML::Common 2.0132
+ XML::LibXML::Devel 2.0132
+ XML::LibXML::Document 2.0132
+ XML::LibXML::DocumentFragment 2.0132
+ XML::LibXML::Dtd 2.0132
+ XML::LibXML::Element 2.0132
+ XML::LibXML::ErrNo 2.0132
+ XML::LibXML::Error 2.0132
+ XML::LibXML::InputCallback 2.0132
+ XML::LibXML::Literal 2.0132
+ XML::LibXML::NamedNodeMap 2.0132
+ XML::LibXML::Namespace 2.0132
+ XML::LibXML::Node 2.0132
+ XML::LibXML::NodeList 2.0132
+ XML::LibXML::Number 2.0132
+ XML::LibXML::PI 2.0132
+ XML::LibXML::Pattern 2.0132
+ XML::LibXML::Reader 2.0132
+ XML::LibXML::RegExp 2.0132
+ XML::LibXML::RelaxNG 2.0132
+ XML::LibXML::SAX 2.0132
+ XML::LibXML::SAX::AttributeNode 2.0132
+ XML::LibXML::SAX::Builder 2.0132
+ XML::LibXML::SAX::Generator 2.0132
+ XML::LibXML::SAX::Parser 2.0132
+ XML::LibXML::Schema 2.0132
+ XML::LibXML::Text 2.0132
+ XML::LibXML::XPathContext 2.0132
+ XML::LibXML::XPathExpression 2.0132
+ XML::LibXML::_SAXParser 2.0132
+ requirements:
+ ExtUtils::MakeMaker 0
+ Test::More 0
+ XML::NamespaceSupport 1.07
+ XML::SAX 0.11
+ XML::SAX::Base 0
+ XML::SAX::Exception 0
+ base 0
+ parent 0
+ perl 5.008
+ strict 0
+ vars 0
+ warnings 0
XML-NamespaceSupport-1.11
pathname: P/PE/PERIGRIN/XML-NamespaceSupport-1.11.tar.gz
provides:
@@ -7341,6 +7944,13 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
+ boolean-0.46
+ pathname: I/IN/INGY/boolean-0.46.tar.gz
+ provides:
+ boolean 0.46
+ requirements:
+ ExtUtils::MakeMaker 0
+ perl 5.008001
gettext-1.05
pathname: P/PV/PVANDRY/gettext-1.05.tar.gz
provides:
diff --git a/docs/customising/config.md b/docs/customising/config.md
index 9f48bad7e..e0761cb8e 100644
--- a/docs/customising/config.md
+++ b/docs/customising/config.md
@@ -55,10 +55,23 @@ The following are all the configuration settings that you can change in `conf/ge
* <code><a href="#base_url">BASE_URL</a></code>
* <code><a href="#secure_proxy_ssl_header">SECURE_PROXY_SSL_HEADER</a></code>
-* <code><a href="#upload_dir">UPLOAD_DIR</a></code>
* <code><a href="#geo_cache">GEO_CACHE</a></code>
* <code><a href="#admin_base_url">ADMIN_BASE_URL</a></code>
+### Photo storage
+
+* <code><a href="#photo_storage_backend">PHOTO_STORAGE_BACKEND</a></code>
+* <code><a href="#photo_storage_options">PHOTO_STORAGE_OPTIONS</a></code>
+ * For local filesystem storage:
+ * <code><a href="#upload_dir">UPLOAD_DIR</a></code>
+ * For Amazon S3 storage:
+ * <code><a href="#bucket">BUCKET</a></code>
+ * <code><a href="#access_key">ACCESS_KEY</a></code>
+ * <code><a href="#secret_key">SECRET_KEY</a></code>
+ * <code><a href="#prefix">PREFIX</a></code>
+ * <code><a href="#create_bucket">CREATE_BUCKET</a></code>
+ * <code><a href="#region">REGION</a></code>
+
### Emailing
* <code><a href="#email_domain">EMAIL_DOMAIN</a></code>
@@ -404,18 +417,16 @@ LANGUAGES:
</dd>
<dt>
- <a name="upload_dir"><code>UPLOAD_DIR</code></a> &amp;
<a name="geo_cache"><code>GEO_CACHE</code></a>
</dt>
<dd>
- The file locations for uploaded photos and cached geocoding results.
- Normally you don't need to change these settings from the examples.
+ The file location for cached geocoding results.
+ Normally you don't need to change this setting from the example.
<div class="more-info">
<p>Example:</p>
<ul class="examples">
<li>
<code>
- UPLOAD_DIR: '../upload/'<br>
GEO_CACHE: '../cache/'
</code>
</li>
@@ -1118,5 +1129,179 @@ ALLOWED_COBRANDS:
</ul>
</div>
</dd>
-
+
+ <dt>
+ <a name="photo_storage_backend"><code>PHOTO_STORAGE_BACKEND</code></a>
+ </dt>
+ <dd>
+ The storage backend to use for uploaded photos.
+ <p>
+ Possible choices are <code>FileSystem</code> or <code>S3</code>.
+ By default, FixMyStreet will use <code>FileSystem</code>.
+ </p>
+ <p>
+ The chosen backend can be configured via the
+ <code><a href="#photo_storage_options">PHOTO_STORAGE_OPTIONS</a></code>
+ setting, see below.
+ </p>
+ </dd>
+
+ <dt>
+ <a name="photo_storage_options"><code>PHOTO_STORAGE_OPTIONS</code></a>
+ </dt>
+ <dd>
+ <p>
+ Contains backend-specific configuration options for photo storage.
+ </p>
+ <p>
+ For the <code>FileSystem</code> backend, the following apply:
+ </p>
+ <ul>
+ <li><code><a href="#upload_dir">UPLOAD_DIR</a></code></li>
+ </ul>
+ <p>
+ For the <code>S3</code> backend, the following apply:
+ </p>
+ <ul>
+ <li><code><a href="#bucket">BUCKET</a></code></li>
+ <li><code><a href="#access_key">ACCESS_KEY</a></code></li>
+ <li><code><a href="#secret_key">SECRET_KEY</a></code></li>
+ <li><code><a href="#prefix">PREFIX</a></code></li>
+ <li><code><a href="#create_bucket">CREATE_BUCKET</a></code></li>
+ <li><code><a href="#region">REGION</a></code></li>
+ </ul>
+ </dd>
+
+ <dt>
+ <a name="upload_dir"><code>UPLOAD_DIR</code></a>
+ </dt>
+ <dd>
+ <p>
+ The file location for uploaded photos.
+ Normally you don't need to change this setting from the example.
+ </p>
+ <p>
+ Only applies when <code>PHOTO_STORAGE_BACKEND</code> is <code>FileSystem</code>.
+ </p>
+ <div class="more-info">
+ <p>Example:</p>
+ <ul class="examples">
+ <li>
+ <pre>
+PHOTO_STORAGE_OPTIONS:
+ UPLOAD_DIR: '../upload/'
+ </pre>
+ </li>
+ </ul>
+ </div>
+ </dd>
+
+ <dt>
+ <a name="bucket"><code>BUCKET</code></a>
+ </dt>
+ <dd>
+ <p>
+ The name of the S3 bucket to store photos in.
+ </p>
+ <p>
+ <strong>Required</strong> when <code>PHOTO_STORAGE_BACKEND</code> is <code>S3</code>.
+ </p>
+ <div class="more-info">
+ <p>Example:</p>
+ <ul class="examples">
+ <li>
+ <pre>
+PHOTO_STORAGE_OPTIONS:
+ BUCKET: 'fixmystreet-photos'
+ </pre>
+ </li>
+ </ul>
+ </div>
+ </dd>
+
+ <dt>
+ <a name="access_key"><code>ACCESS_KEY</code></a> &amp;
+ <a name="secret_key"><code>SECRET_KEY</code></a>
+ </dt>
+ <dd>
+ <p>
+ The AWS access & secret keys to use when connecting to S3.
+ You should use a role with minimal privileges to manage objects in a specific S3 bucket, not your root keys.
+ </p>
+ <p>
+ <strong>Required</strong> when <code>PHOTO_STORAGE_BACKEND</code> is <code>S3</code>.
+ </p>
+ <div class="more-info">
+ <p>Example:</p>
+ <ul class="examples">
+ <li>
+ <pre>
+PHOTO_STORAGE_OPTIONS:
+ ACCESS_KEY: 'AKIAMYSUPERCOOLKEY'
+ SECRET_KEY: '12345/AbCdEFgHIJ98765'
+ </pre>
+ </li>
+ </ul>
+ </div>
+ </dd>
+
+ <dt>
+ <a name="prefix"><code>PREFIX</code></a>
+ </dt>
+ <dd>
+ <p>
+ An optional directory prefix to prepended to S3 filenames. Useful if, for example, you are using a bucket shared between other projects or FixMyStreet instances.
+ </p>
+ <p>
+ <strong>Optional</strong>. Only applies when <code>PHOTO_STORAGE_BACKEND</code> is <code>S3</code>.
+ </p>
+ <div class="more-info">
+ <p>Example:</p>
+ <ul class="examples">
+ <li>
+ <pre>
+PHOTO_STORAGE_OPTIONS:
+ PREFIX: '/fixmystreet_photos/'
+ </pre>
+ </li>
+ </ul>
+ </div>
+ </dd>
+
+ <dt>
+ <a name="create_bucket"><code>CREATE_BUCKET</code></a>
+ </dt>
+ <dd>
+ <p>
+ Set to <code>1</code> (or <code>true</code>) if FixMyStreet should create the S3 bucket specified in <code>BUCKET</code> if it doesn't already exist.
+ </p>
+ <p>
+ <strong>Optional</strong>. Only applies when <code>PHOTO_STORAGE_BACKEND</code> is <code>S3</code>.
+ </p>
+ </dd>
+
+ <dt>
+ <a name="region"><code>REGION</code></a>
+ </dt>
+ <dd>
+ <p>
+ The AWS region to create the S3 bucket in.
+ </p>
+ <p>
+ <strong>Optional</strong>. Only applies when <code>CREATE_BUCKET</code> is enabled.
+ </p>
+ <div class="more-info">
+ <p>Example:</p>
+ <ul class="examples">
+ <li>
+ <pre>
+PHOTO_STORAGE_OPTIONS:
+ CREATE_BUCKET: 1
+ REGION: 'eu-west-2'
+ </pre>
+ </li>
+ </ul>
+ </div>
+ </dd>
+
</dl>
diff --git a/docs/install/manual-install.md b/docs/install/manual-install.md
index 8acccdddf..84594f660 100644
--- a/docs/install/manual-install.md
+++ b/docs/install/manual-install.md
@@ -165,7 +165,7 @@ Some others you might want to look at, though the defaults are enough for it to
* [CONTACT_EMAIL]({{ "/customising/config/#contact_email" | relative_url }}) -- the email address to be used on the site for the contact us form.
* [DO_NOT_REPLY_EMAIL]({{ "/customising/config/#do_not_reply_email" | relative_url }}) -- the email address to be used on the site for e.g. confirmation emails.
* [STAGING_SITE]({{ "/customising/config/#staging_site" | relative_url }}) -- if this is 1 then all email (alerts and reports) will be sent to the contact email address. Use this for development sites.
-* [UPLOAD_DIR]({{ "/customising/config/#upload_dir" | relative_url }}) -- this is the location where images will be stored when they are uploaded. It should be accessible by and writeable by the FixMyStreet process.
+* [PHOTO_STORAGE_OPTIONS.UPLOAD_DIR]({{ "/customising/config/#upload_dir" | relative_url }}) -- this is the location where images will be stored when they are uploaded. It should be accessible by and writeable by the FixMyStreet process.
* [GEO_CACHE]({{ "/customising/config/#geo_cache" | relative_url }}) -- this is the location where Geolocation data will be cached. It should be accessible by and writeable by the FixMyStreet process.
If you are using Bing or Google maps you should also set one of
diff --git a/perllib/FixMyStreet.pm b/perllib/FixMyStreet.pm
index c8d22fe50..d10ce93aa 100644
--- a/perllib/FixMyStreet.pm
+++ b/perllib/FixMyStreet.pm
@@ -113,12 +113,14 @@ sub override_config($&) {
);
FixMyStreet::Map::reload_allowed_maps() if $config->{MAP_TYPE};
+ $FixMyStreet::PhotoStorage::instance = undef if $config->{PHOTO_STORAGE_BACKEND};
$code->();
$override_guard->restore();
mySociety::MaPit::configure() if $config->{MAPIT_URL};
FixMyStreet::Map::reload_allowed_maps() if $config->{MAP_TYPE};
+ $FixMyStreet::PhotoStorage::instance = undef if $config->{PHOTO_STORAGE_BACKEND};
}
=head2 dbic_connect_info
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm
index 160d2851e..051308920 100644
--- a/perllib/FixMyStreet/App.pm
+++ b/perllib/FixMyStreet/App.pm
@@ -10,6 +10,7 @@ use Memcached;
use FixMyStreet::Map;
use FixMyStreet::Email;
use FixMyStreet::Email::Sender;
+use FixMyStreet::PhotoStorage;
use Utils;
use Path::Tiny 'path';
@@ -128,11 +129,9 @@ after 'prepare_headers' => sub {
__PACKAGE__->log->disable('debug') #
unless __PACKAGE__->debug;
-# Check upload_dir
-my $cache_dir = path(FixMyStreet->config('UPLOAD_DIR'))->absolute(FixMyStreet->path_to());
-$cache_dir->mkpath;
-unless ( -d $cache_dir && -w $cache_dir ) {
- warn "\x1b[31mCan't find/write to photo cache directory '$cache_dir'\x1b[0m\n";
+# Set up photo storage
+unless ( FixMyStreet::PhotoStorage::backend->init() ) {
+ warn "\x1b[31mCan't set up photo storage backend\x1b[0m\n";
}
=head1 NAME
diff --git a/perllib/FixMyStreet/App/Controller/Photo.pm b/perllib/FixMyStreet/App/Controller/Photo.pm
index f41702dcf..1884b9f26 100644
--- a/perllib/FixMyStreet/App/Controller/Photo.pm
+++ b/perllib/FixMyStreet/App/Controller/Photo.pm
@@ -7,6 +7,7 @@ BEGIN {extends 'Catalyst::Controller'; }
use JSON::MaybeXS;
use File::Path;
use File::Slurp;
+use Try::Tiny;
use FixMyStreet::App::Model::PhotoSet;
=head1 NAME
@@ -101,8 +102,13 @@ sub upload : Local {
c => $c,
data_items => \@items,
});
-
- my $fileid = $photoset->data;
+ my $fileid = try {
+ $photoset->data;
+ } catch {
+ $c->log->debug("Photo upload failed.");
+ $c->stash->{photo_error} = _("Photo upload failed.");
+ return undef;
+ };
my $out;
if ($c->stash->{photo_error} || !$fileid) {
$c->res->status(500);
diff --git a/perllib/FixMyStreet/App/Model/PhotoSet.pm b/perllib/FixMyStreet/App/Model/PhotoSet.pm
index 8fcc1700e..21bde52d8 100644
--- a/perllib/FixMyStreet/App/Model/PhotoSet.pm
+++ b/perllib/FixMyStreet/App/Model/PhotoSet.pm
@@ -3,7 +3,6 @@ package FixMyStreet::App::Model::PhotoSet;
# TODO this isn't a Cat model, rename to something else
use Moose;
-use Path::Tiny 'path';
my $IM = eval {
require Image::Magick;
@@ -12,12 +11,13 @@ my $IM = eval {
};
use Scalar::Util 'openhandle', 'blessed';
-use Digest::SHA qw(sha1_hex);
use Image::Size;
use IPC::Cmd qw(can_run);
use IPC::Open3;
use MIME::Base64;
+use FixMyStreet::PhotoStorage;
+
has c => (
is => 'ro',
);
@@ -57,28 +57,20 @@ has data_items => ( # either a) split from db_data or b) provided by photo uploa
my $self = shift;
my $data = $self->db_data or return [];
- return [$data] if (detect_type($data));
+ return [$data] if ($self->storage->detect_type($data));
return [ split ',' => $data ];
},
);
-has upload_dir => (
+has storage => (
is => 'ro',
lazy => 1,
default => sub {
- path(FixMyStreet->config('UPLOAD_DIR'))->absolute(FixMyStreet->path_to());
- },
+ return FixMyStreet::PhotoStorage::backend;
+ }
);
-sub detect_type {
- return 'jpeg' if $_[0] =~ /^\x{ff}\x{d8}/;
- return 'png' if $_[0] =~ /^\x{89}\x{50}/;
- return 'tiff' if $_[0] =~ /^II/;
- return 'gif' if $_[0] =~ /^GIF/;
- return '';
-}
-
=head2 C<ids>, C<num_images>, C<get_id>, C<all_ids>
C<$photoset-E<GT>ids> is an arrayref containing the fileid data.
@@ -166,25 +158,20 @@ has ids => ( # Arrayref of $fileid tuples (always, so post upload/raw data proc
return ();
}
- # we have an image we can use - save it to the upload dir for storage
- my $fileid = $self->get_fileid($photo_blob);
- my $file = $self->get_file($fileid, $type);
- $upload->copy_to( $file );
- return $file->basename;
-
+ # we have an image we can use - save it to storage
+ return $self->storage->store_photo($photo_blob);
}
- if (my $type = detect_type($part)) {
+
+ # It might be a raw file stored in the DB column...
+ if (my $type = $self->storage->detect_type($part)) {
my $photo_blob = $part;
- my $fileid = $self->get_fileid($photo_blob);
- my $file = $self->get_file($fileid, $type);
- $file->spew_raw($photo_blob);
- return $file->basename;
+ return $self->storage->store_photo($photo_blob);
+ # TODO: Should this update the DB record with a pointer to the
+ # newly-stored file, instead of leaving it in the DB?
}
- my ($fileid, $type) = split /\./, $part;
- $type ||= 'jpeg';
- if ($fileid && length($fileid) == 40) {
- my $file = $self->get_file($fileid, $type);
- $file->basename;
+
+ if (my $key = $self->storage->validate_key($part)) {
+ $key;
} else {
# A bad hash, probably a bot spamming with bad data.
();
@@ -194,24 +181,11 @@ has ids => ( # Arrayref of $fileid tuples (always, so post upload/raw data proc
},
);
-sub get_fileid {
- my ($self, $photo_blob) = @_;
- return sha1_hex($photo_blob);
-}
-
-sub get_file {
- my ($self, $fileid, $type) = @_;
- my $cache_dir = $self->upload_dir;
- return path( $cache_dir, "$fileid.$type" );
-}
-
sub get_raw_image {
my ($self, $index) = @_;
my $filename = $self->get_id($index);
- my ($fileid, $type) = split /\./, $filename;
- my $file = $self->get_file($fileid, $type);
- if ($file->exists) {
- my $photo = $file->slurp_raw;
+ my ($photo, $type) = $self->storage->retrieve_photo($filename);
+ if ($photo) {
return {
data => $photo,
content_type => "image/$type",
diff --git a/perllib/FixMyStreet/PhotoStorage.pm b/perllib/FixMyStreet/PhotoStorage.pm
new file mode 100644
index 000000000..a441fb718
--- /dev/null
+++ b/perllib/FixMyStreet/PhotoStorage.pm
@@ -0,0 +1,41 @@
+package FixMyStreet::PhotoStorage;
+
+use Moose;
+use Digest::SHA qw(sha1_hex);
+use Module::Load;
+use FixMyStreet;
+
+our $instance; # our, so tests can set to undef when testing different backends
+sub backend {
+ return $instance if $instance;
+ my $class = 'FixMyStreet::PhotoStorage::';
+ $class .= FixMyStreet->config('PHOTO_STORAGE_BACKEND') || 'FileSystem';
+ load $class;
+ $instance = $class->new();
+ return $instance;
+}
+
+sub detect_type {
+ my ($self, $photo) = @_;
+ return 'jpeg' if $photo =~ /^\x{ff}\x{d8}/;
+ return 'png' if $photo =~ /^\x{89}\x{50}/;
+ return 'tiff' if $photo =~ /^II/;
+ return 'gif' if $photo =~ /^GIF/;
+ return '';
+}
+
+=head2 get_fileid
+
+Calculates an identifier for a binary blob of photo data.
+This is just the SHA1 hash of the blob currently.
+
+=cut
+
+sub get_fileid {
+ my ($self, $photo_blob) = @_;
+ return sha1_hex($photo_blob);
+}
+
+
+
+1;
diff --git a/perllib/FixMyStreet/PhotoStorage/FileSystem.pm b/perllib/FixMyStreet/PhotoStorage/FileSystem.pm
new file mode 100644
index 000000000..d61a26c7a
--- /dev/null
+++ b/perllib/FixMyStreet/PhotoStorage/FileSystem.pm
@@ -0,0 +1,111 @@
+package FixMyStreet::PhotoStorage::FileSystem;
+
+use Moose;
+use parent 'FixMyStreet::PhotoStorage';
+
+use Path::Tiny 'path';
+
+
+has upload_dir => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $dir = FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{UPLOAD_DIR} ||
+ FixMyStreet->config('UPLOAD_DIR');
+ return path($dir)->absolute(FixMyStreet->path_to());
+ },
+);
+
+=head2 init
+
+Creates UPLOAD_DIR and checks it's writeable.
+
+=cut
+
+sub init {
+ my $self = shift;
+ my $cache_dir = $self->upload_dir;
+ $cache_dir->mkpath;
+ unless ( -d $cache_dir && -w $cache_dir ) {
+ warn "\x1b[31mCan't find/write to photo cache directory '$cache_dir'\x1b[0m\n";
+ return;
+ }
+ return 1;
+}
+
+=head2 get_file
+
+Returns a Path::Tiny path to a file on disk identified by an ID and type.
+File may or may not exist. This handle is then used to read photo data or
+write to disk.
+
+=cut
+
+sub get_file {
+ my ($self, $fileid, $type) = @_;
+ my $cache_dir = $self->upload_dir;
+ return path( $cache_dir, "$fileid.$type" );
+}
+
+
+=head2 store_photo
+
+Stores a blob of binary data representing a photo on disk.
+Returns a key which is used in the future to get the contents of the file.
+
+=cut
+
+sub store_photo {
+ my ($self, $photo_blob) = @_;
+
+ my $type = $self->detect_type($photo_blob) || 'jpeg';
+ my $fileid = $self->get_fileid($photo_blob);
+ my $file = $self->get_file($fileid, $type);
+ $file->spew_raw($photo_blob);
+
+ return $file->basename;
+}
+
+
+=head2 retrieve_photo
+
+Fetches the file content of a particular photo from storage.
+Returns the binary blob and the filetype, if the photo exists in storage.
+
+=cut
+
+sub retrieve_photo {
+ my ($self, $filename) = @_;
+
+ my ($fileid, $type) = split /\./, $filename;
+ my $file = $self->get_file($fileid, $type);
+ if ($file->exists) {
+ my $photo = $file->slurp_raw;
+ return ($photo, $type);
+ }
+}
+
+
+=head2 validate_key
+
+A long-running FMS instance might have reports whose photo IDs in the DB
+don't include the file extension. This function takes a value from the DB and
+returns a 'tidied' version that can be used when calling photo_exists
+or retrieve_photo.
+
+If the passed key doesn't seem like it'll result in a valid filename (i.e.
+it's not a 40-char SHA1 hash) returns undef.
+
+=cut
+
+sub validate_key {
+ my ($self, $key) = @_;
+
+ my ($fileid, $type) = split /\./, $key;
+ $type ||= 'jpeg';
+ if ($fileid && length($fileid) == 40) {
+ return "$fileid.$type";
+ }
+}
+
+1;
diff --git a/perllib/FixMyStreet/PhotoStorage/S3.pm b/perllib/FixMyStreet/PhotoStorage/S3.pm
new file mode 100644
index 000000000..45325e9dc
--- /dev/null
+++ b/perllib/FixMyStreet/PhotoStorage/S3.pm
@@ -0,0 +1,122 @@
+package FixMyStreet::PhotoStorage::S3;
+
+use Moose;
+use parent 'FixMyStreet::PhotoStorage';
+
+use Net::Amazon::S3;
+use Try::Tiny;
+
+
+has client => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $key = FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{ACCESS_KEY};
+ my $secret = FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{SECRET_KEY};
+
+ my $s3 = Net::Amazon::S3->new(
+ aws_access_key_id => $key,
+ aws_secret_access_key => $secret,
+ retry => 1,
+ );
+ return Net::Amazon::S3::Client->new( s3 => $s3 );
+ },
+);
+
+has bucket => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ shift->client->bucket( name => FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{BUCKET} );
+ },
+);
+
+has region => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ return FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{REGION};
+ },
+);
+
+has prefix => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ my $prefix = FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{PREFIX};
+ return "" unless $prefix;
+ $prefix =~ s#/$##;
+ return "$prefix/";
+ },
+);
+
+sub init {
+ my $self = shift;
+
+ return 1 if $self->_bucket_exists();
+
+ if ( FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{CREATE_BUCKET} ) {
+ my $name = $self->bucket->name;
+ try {
+ $self->client->create_bucket(
+ name => $name,
+ location_constraint => $self->region,
+ );
+ } catch {
+ warn "\x1b[31mCouldn't create S3 bucket '$name'\x1b[0m\n";
+ return;
+ };
+
+ return 1 if $self->_bucket_exists();
+
+ warn "\x1b[31mCouldn't create S3 bucket '$name'\x1b[0m\n";
+ return;
+ } else {
+ my $bucket = $self->bucket->name;
+ warn "\x1b[31mS3 bucket '$bucket' doesn't exist and CREATE_BUCKET is not set.\x1b[0m\n";
+ return;
+ }
+}
+
+sub _bucket_exists {
+ my $self = shift;
+ my $name = $self->bucket->name;
+ my @buckets = $self->client->buckets;
+ return grep { $_->name eq $name } @buckets;
+}
+
+sub get_object {
+ my ($self, $key) = @_;
+ return $self->bucket->object( key => $key );
+}
+
+sub store_photo {
+ my ($self, $photo_blob) = @_;
+
+ my $type = $self->detect_type($photo_blob) || 'jpeg';
+ my $fileid = $self->get_fileid($photo_blob);
+ my $key = $self->prefix . "$fileid.$type";
+
+ my $object = $self->get_object($key);
+ $object->put($photo_blob);
+
+ return $key;
+}
+
+
+sub retrieve_photo {
+ my ($self, $key) = @_;
+
+ my $object = $self->get_object($key);
+ if ($object->exists) {
+ my ($fileid, $type) = split /\./, $key;
+ return ($object->get, $type);
+ }
+
+}
+
+sub validate_key { $_[1] }
+
+
+1;
diff --git a/t/app/controller/photo.t b/t/app/controller/photo.t
index e9183836b..e28ee1946 100644
--- a/t/app/controller/photo.t
+++ b/t/app/controller/photo.t
@@ -24,7 +24,10 @@ subtest "Check multiple upload worked" => sub {
FixMyStreet::override_config {
ALLOWED_COBRANDS => [ { fixmystreet => '.' } ],
MAPIT_URL => 'http://mapit.uk/',
- UPLOAD_DIR => $UPLOAD_DIR,
+ PHOTO_STORAGE_BACKEND => 'FileSystem',
+ PHOTO_STORAGE_OPTIONS => {
+ UPLOAD_DIR => $UPLOAD_DIR,
+ },
}, sub {
$mech->log_in_ok('test@example.com');
@@ -77,7 +80,10 @@ subtest "Check photo uploading URL works" => sub {
# submit initial pc form
FixMyStreet::override_config {
- UPLOAD_DIR => $UPLOAD_DIR,
+ PHOTO_STORAGE_BACKEND => 'FileSystem',
+ PHOTO_STORAGE_OPTIONS => {
+ UPLOAD_DIR => $UPLOAD_DIR,
+ },
}, sub {
$mech->post( '/photo/upload',
Content_Type => 'form-data',
diff --git a/t/app/model/photoset.t b/t/app/model/photoset.t
index d171ba88b..29a28d232 100644
--- a/t/app/model/photoset.t
+++ b/t/app/model/photoset.t
@@ -15,7 +15,10 @@ my $db = FixMyStreet::DB->schema;
my $user = $db->resultset('User')->find_or_create({ name => 'Bob', email => 'bob@example.com' });
FixMyStreet::override_config {
- UPLOAD_DIR => $UPLOAD_DIR,
+ PHOTO_STORAGE_BACKEND => 'FileSystem',
+ PHOTO_STORAGE_OPTIONS => {
+ UPLOAD_DIR => $UPLOAD_DIR,
+ },
}, sub {
my $image_path = path('t/app/controller/sample.jpg');
@@ -69,4 +72,29 @@ subtest 'Photoset with 3 referenced photo' => sub {
};
+subtest 'Correct storage backends are instantiated' => sub {
+ FixMyStreet::override_config {
+ PHOTO_STORAGE_BACKEND => 'FileSystem'
+ }, sub {
+ my $photoset = FixMyStreet::App::Model::PhotoSet->new;
+ isa_ok $photoset->storage, 'FixMyStreet::PhotoStorage::FileSystem';
+ };
+
+ FixMyStreet::override_config {
+ PHOTO_STORAGE_BACKEND => undef
+ }, sub {
+ my $photoset = FixMyStreet::App::Model::PhotoSet->new;
+ isa_ok $photoset->storage, 'FixMyStreet::PhotoStorage::FileSystem';
+ };
+
+ FixMyStreet::override_config {
+ PHOTO_STORAGE_BACKEND => 'S3'
+ }, sub {
+ my $photoset = FixMyStreet::App::Model::PhotoSet->new;
+ isa_ok $photoset->storage, 'FixMyStreet::PhotoStorage::S3';
+ };
+
+};
+
+
done_testing();
diff --git a/t/cobrand/zurich.t b/t/cobrand/zurich.t
index eccb0c8eb..dbd6ec882 100644
--- a/t/cobrand/zurich.t
+++ b/t/cobrand/zurich.t
@@ -416,7 +416,10 @@ FixMyStreet::override_config {
ALLOWED_COBRANDS => [ 'zurich' ],
MAPIT_URL => 'http://mapit.zurich/',
MAP_TYPE => 'Zurich,OSM',
- UPLOAD_DIR => $UPLOAD_DIR,
+ PHOTO_STORAGE_BACKEND => 'FileSystem',
+ PHOTO_STORAGE_OPTIONS => {
+ UPLOAD_DIR => $UPLOAD_DIR,
+ },
}, sub {
# Photo publishing
$mech->get_ok( '/admin/report_edit/' . $report->id );
diff --git a/t/open311.t b/t/open311.t
index 4dc1b2959..e1ad578d7 100644
--- a/t/open311.t
+++ b/t/open311.t
@@ -322,7 +322,10 @@ subtest 'check media url set' => sub {
$comment->cobrand('fixmystreet');
FixMyStreet::override_config {
- UPLOAD_DIR => $UPLOAD_DIR,
+ PHOTO_STORAGE_BACKEND => 'FileSystem',
+ PHOTO_STORAGE_OPTIONS => {
+ UPLOAD_DIR => $UPLOAD_DIR,
+ },
}, sub {
my $results = make_update_req( $comment, '<?xml version="1.0" encoding="utf-8"?><service_request_updates><request_update><update_id>248</update_id></request_update></service_request_updates>' );
diff --git a/t/photostorage/s3.t b/t/photostorage/s3.t
new file mode 100644
index 000000000..fa989374f
--- /dev/null
+++ b/t/photostorage/s3.t
@@ -0,0 +1,164 @@
+#!/usr/bin/env perl
+use FixMyStreet::Test;
+
+use Test::MockModule;
+use Path::Tiny 'path';
+use Net::Amazon::S3::Client::Bucket;
+
+use_ok( 'FixMyStreet::PhotoStorage::S3' );
+
+FixMyStreet::override_config {
+ PHOTO_STORAGE_OPTIONS => {
+ ACCESS_KEY => 'AKIAMYFAKEACCESSKEY',
+ SECRET_KEY => '1234/fAk35eCrETkEy',
+ BUCKET => 'fms-test-photos',
+ PREFIX => '/uploads',
+ },
+}, sub {
+
+ my $s3 = FixMyStreet::PhotoStorage::S3->new();
+
+ subtest "basic attributes are configured correctly" => sub {
+ ok $s3->client, "N::A::S3::Client created";
+ is $s3->client->s3->aws_access_key_id, 'AKIAMYFAKEACCESSKEY', "Correct access key used";
+ is $s3->client->s3->aws_secret_access_key, '1234/fAk35eCrETkEy', "Correct secret key used";
+
+ ok $s3->bucket, "N::A::S3::Bucket created";
+ is $s3->bucket->name, 'fms-test-photos', "Correct bucket name configured";
+
+ is $s3->prefix, '/uploads/', "Correct key prefix with trailing slash";
+ };
+
+ subtest "photos can be stored in S3" => sub {
+ my $photo_blob = path('t/app/controller/sample.jpg')->slurp;
+ is $s3->get_fileid($photo_blob), '74e3362283b6ef0c48686fb0e161da4043bbcc97', "File ID calculated correctly";
+ is $s3->detect_type($photo_blob), 'jpeg', "File type calculated correctly";
+
+ my $s3_object = Test::MockModule->new('Net::Amazon::S3::Client::Object');
+ my $put_called = 0;
+ $s3_object->mock('put', sub {
+ my ($self, $photo) = @_;
+ is $self->key, '/uploads/74e3362283b6ef0c48686fb0e161da4043bbcc97.jpeg', 'Object created with correct key';
+ is $self->bucket->name, 'fms-test-photos', 'Object stored in correct bucket';
+ is $photo, $photo_blob, 'Correct photo uploaded';
+ $put_called = 1;
+ });
+
+ is $s3->store_photo($photo_blob), '/uploads/74e3362283b6ef0c48686fb0e161da4043bbcc97.jpeg', 'Photo uploaded and correct key returned';
+ ok $put_called, "Object::put called";
+ };
+
+ subtest "photos can be retrieved from S3" => sub {
+ my $photo_blob = path('t/app/controller/sample.jpg')->slurp;
+ my $key = '/uploads/74e3362283b6ef0c48686fb0e161da4043bbcc97.jpeg';
+
+ my $s3_object = Test::MockModule->new('Net::Amazon::S3::Client::Object');
+ my $exists_called = 0;
+ $s3_object->mock('exists', sub {
+ my ($self) = @_;
+ is $self->key, $key, 'Object::exists called with correct key';
+ $exists_called = 1;
+ return 1;
+ });
+ my $get_called = 0;
+ $s3_object->mock('get', sub {
+ my ($self) = @_;
+ is $self->key, $key, 'Object::get called with correct key';
+ is $self->bucket->name, 'fms-test-photos', 'Object fetched from correct bucket';
+ $get_called = 1;
+ return $photo_blob;
+ });
+
+ my ($photo, $type) = $s3->retrieve_photo($key);
+ ok $exists_called, "Object::exists called";
+ ok $get_called, "Object::get called";
+ is $photo, $photo_blob, 'Correct file content returned';
+ is $type, 'jpeg', 'Correct file type returned';
+ };
+
+ subtest "init passes if bucket exists" => sub {
+ my $s3_client = Test::MockModule->new('Net::Amazon::S3::Client');
+ my $buckets_called = 0;
+ $s3_client->mock('buckets', sub {
+ my $self = shift;
+ $buckets_called = 1;
+ return (
+ Net::Amazon::S3::Client::Bucket->new(
+ client => $self,
+ name => 'fms-test-photos'
+ )
+ );
+ });
+
+ ok $s3->init(), "PhotoStorage::S3::init succeeded";
+ ok $buckets_called, "Client::buckets called";
+ };
+
+ subtest "init fails if bucket doesn't exist" => sub {
+ my $s3_client = Test::MockModule->new('Net::Amazon::S3::Client');
+ my $buckets_called = 0;
+ $s3_client->mock('buckets', sub {
+ my $self = shift;
+ $buckets_called = 1;
+ return (
+ Net::Amazon::S3::Client::Bucket->new(
+ client => $self,
+ name => 'not-your-bucket'
+ )
+ );
+ });
+ my $create_bucket_called = 0;
+ $s3_client->mock('create_bucket', sub {
+ $create_bucket_called = 1;
+ });
+
+ ok !$s3->init(), "PhotoStorage::S3::init failed";
+ ok $buckets_called, "Client::buckets called";
+ ok !$create_bucket_called, "Client::create_bucket not called";
+ };
+};
+
+FixMyStreet::override_config {
+ PHOTO_STORAGE_OPTIONS => {
+ ACCESS_KEY => 'AKIAMYFAKEACCESSKEY',
+ SECRET_KEY => '1234/fAk35eCrETkEy',
+ BUCKET => 'fms-test-photos',
+ CREATE_BUCKET => 1,
+ REGION => 'eu-west-3',
+ },
+}, sub {
+
+ my $s3 = FixMyStreet::PhotoStorage::S3->new();
+
+ subtest "init creates bucket if CREATE_BUCKET set" => sub {
+ my $s3_client = Test::MockModule->new('Net::Amazon::S3::Client');
+ my $create_bucket_called = 0;
+ $s3_client->mock('create_bucket', sub {
+ my ( $self, %conf ) = @_;
+ $create_bucket_called = 1;
+ is $conf{name}, "fms-test-photos", "Bucket created with correct name";
+ is $conf{location_constraint}, "eu-west-3", "Bucket created in correct region";
+ });
+ my $buckets_called = 0;
+ $s3_client->mock('buckets', sub {
+ my $self = shift;
+ $buckets_called = 1;
+ return (
+ Net::Amazon::S3::Client::Bucket->new(
+ client => $self,
+ name => 'not-your-bucket'
+ ),
+ $create_bucket_called ? Net::Amazon::S3::Client::Bucket->new(
+ client => $self,
+ name => 'fms-test-photos'
+ ) : (),
+ );
+ });
+
+ ok $s3->init(), "PhotoStorage::S3::init succeeded";
+ ok $buckets_called, "Client::buckets called";
+ ok $create_bucket_called, "Client::create_bucket called";
+ };
+};
+
+done_testing();
diff --git a/t/sendreport/open311.t b/t/sendreport/open311.t
index 26764dc19..e68a0aa3c 100644
--- a/t/sendreport/open311.t
+++ b/t/sendreport/open311.t
@@ -74,6 +74,10 @@ subtest 'test report with multiple photos only sends one', sub {
STAGING_FLAGS => { send_reports => 1 },
ALLOWED_COBRANDS => [ 'fixmystreet' ],
MAPIT_URL => 'http://mapit.uk/',
+ PHOTO_STORAGE_BACKEND => 'FileSystem',
+ PHOTO_STORAGE_OPTIONS => {
+ UPLOAD_DIR => $UPLOAD_DIR,
+ },
}, sub {
$test_data = FixMyStreet::Script::Reports::send();
};
@@ -107,6 +111,10 @@ subtest 'test sending multiple photos', sub {
STAGING_FLAGS => { send_reports => 1 },
ALLOWED_COBRANDS => [ 'tester' ],
MAPIT_URL => 'http://mapit.uk/',
+ PHOTO_STORAGE_BACKEND => 'FileSystem',
+ PHOTO_STORAGE_OPTIONS => {
+ UPLOAD_DIR => $UPLOAD_DIR,
+ },
}, sub {
$test_data = FixMyStreet::Script::Reports::send();
};