public/assets
public/attachments
public/export
+storage
tmp
- psql -U postgres -c "CREATE FUNCTION tile_for_point(int4, int4) RETURNS int8 AS '/tmp/libpgosm', 'tile_for_point' LANGUAGE C STRICT" openstreetmap
- psql -U postgres -c "CREATE FUNCTION xid_to_int4(xid) RETURNS int4 AS '/tmp/libpgosm', 'xid_to_int4' LANGUAGE C STRICT" openstreetmap
- cp config/travis.database.yml config/database.yml
+ - cp config/example.storage.yml config/storage.yml
- touch config/settings.local.yml
- bundle exec rake db:migrate
- bundle exec rake i18n:js:export
# Used for browser detection
gem "browser"
+# Used for S3 object storage
+gem "aws-sdk-s3"
+
+# Used to resize user images
+gem "mini_magick"
+
# Gems useful for development
group :development do
gem "annotate"
ast (2.4.0)
autoprefixer-rails (8.6.5)
execjs
+ aws-eventstream (1.0.3)
+ aws-partitions (1.184.0)
+ aws-sdk-core (3.59.0)
+ aws-eventstream (~> 1.0, >= 1.0.2)
+ aws-partitions (~> 1.0)
+ aws-sigv4 (~> 1.1)
+ jmespath (~> 1.0)
+ aws-sdk-kms (1.23.0)
+ aws-sdk-core (~> 3, >= 3.58.0)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.45.0)
+ aws-sdk-core (~> 3, >= 3.58.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.1)
+ aws-sigv4 (1.1.0)
+ aws-eventstream (~> 1.0, >= 1.0.2)
better_errors (2.5.1)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
image_size (2.0.1)
in_threads (1.5.2)
jaro_winkler (1.5.3)
+ jmespath (1.4.0)
jquery-rails (4.3.5)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0331)
mimemagic (0.3.3)
+ mini_magick (4.9.3)
mini_mime (1.0.1)
mini_portile2 (2.4.0)
minitest (5.11.3)
activerecord-import
annotate
autoprefixer-rails (~> 8.6.3)
+ aws-sdk-s3
better_errors
bigdecimal (~> 1.1.0)
binding_of_caller
listen
logstasher
mimemagic
+ mini_magick
minitest (~> 5.1)
oauth-plugin (>= 0.5.1)
omniauth
bundle exec rake yarn:install
```
+## Storage setup
+
+The Rails port needs to be configured with an object storage facility - for
+development and testing purposes you can use the example configuration:
+
+```
+cp config/example.storage.yml config/storage.yml
+```
+
## Database setup
The Rails Port uses three databases - one for development, one for testing, and one for production. The database-specific configuration
$("select#user_auth_provider").on("change", updateAuthUID);
- $("input#user_image").on("change", function () {
- $("#image_action_new").prop("checked", true);
+ $("input#user_avatar").on("change", function () {
+ $("#avatar_action_new").prop("checked", true);
});
function enableAuth() {
user.languages = params[:user][:languages].split(",")
- case params[:image_action]
+ case params[:avatar_action]
when "new" then
- user.image = params[:user][:image]
+ user.avatar.attach(params[:user][:avatar])
user.image_use_gravatar = false
when "delete" then
- user.image = nil
+ user.avatar.purge
user.image_use_gravatar = false
when "gravatar" then
- user.image = nil
+ user.avatar.purge
user.image_use_gravatar = true
end
if user.image_use_gravatar
user_gravatar_tag(user, options)
- else
+ elsif user.avatar.attached?
+ image_tag user.avatar.variant(:resize => "100x100>"), options
+ elsif user.image.file?
image_tag user.image.url(:large), options
+ else
+ image_tag "avatar_large.png", options
end
end
if user.image_use_gravatar
user_gravatar_tag(user, options)
- else
+ elsif user.avatar.attached?
+ image_tag user.avatar.variant(:resize => "50x50>"), options
+ elsif user.image.file?
image_tag user.image.url(:small), options
+ else
+ image_tag "avatar_small.png", options
end
end
if user.image_use_gravatar
user_gravatar_tag(user, options)
- else
+ elsif user.avatar.attached?
+ image_tag user.avatar.variant(:resize => "50x50>"), options
+ elsif user.image.file?
image_tag user.image.url(:small), options
+ else
+ image_tag "avatar_small.png", options
end
end
def user_image_url(user, options = {})
if user.image_use_gravatar
user_gravatar_url(user, options)
- else
+ elsif user.avatar.attached?
+ url_for(user.avatar.variant(:resize => "100x100>"))
+ elsif user.image.file?
image_url(user.image.url(:large))
+ else
+ image_url("avatar_large.png")
end
end
def user_gravatar_url(user, options = {})
size = options[:size] || 100
hash = Digest::MD5.hexdigest(user.email.downcase)
- default_image_url = image_url("users/images/large.png")
+ default_image_url = image_url("avatar_large.png")
"#{request.protocol}www.gravatar.com/avatar/#{hash}.jpg?s=#{size}&d=#{u(default_image_url)}"
end
class Notifier < ActionMailer::Base
+ include ActionView::Helpers::AssetUrlHelper
+
default :from => Settings.email_from,
:return_path => Settings.email_return_path,
:auto_submitted => "auto-generated"
end
def attach_user_avatar(user)
- attachments.inline["avatar.png"] = File.read(user_avatar_file_path(user))
+ attachments.inline["avatar.png"] = user_avatar_file(user)
+ end
+
+ def user_avatar_file(user)
+ avatar = user&.avatar
+ if avatar&.attached?
+ return avatar.variant(:resize => "50x50>").blob.download
+ else
+ return File.read(user_avatar_file_path(user))
+ end
end
def user_avatar_file_path(user)
if image&.file?
return image.path(:small)
else
- return Rails.root.join("app", "assets", "images", "users", "images", "small.png")
+ return Rails.root.join("app", "assets", "images", "avatar_small.png")
end
end
scope :active, -> { where(:status => %w[active confirmed]) }
scope :identifiable, -> { where(:data_public => true) }
+ has_one_attached :avatar
+
has_attached_file :image,
:default_url => "/assets/:class/:attachment/:style.png",
:styles => { :large => "100x100>", :small => "50x50>" }
##
# delete a user - leave the account but purge most personal data
def delete
+ avatar.purge
+
self.display_name = "user_#{id}"
self.description = ""
self.home_lat = nil
self.auth_provider = nil
self.auth_uid = nil
self.status = "deleted"
+
save
end
else
xml.tag! "contributor-terms", :agreed => user.terms_agreed.present?
end
- xml.tag! "img", :href => user_image_url(user) if user.image.file? || user.image_use_gravatar
+ xml.tag! "img", :href => user_image_url(user) if user.avatar.attached? || user.image.file? || user.image_use_gravatar
xml.tag! "roles" do
user.roles.each do |role|
xml.tag! role.role
<label class="standard-label"><%= t ".image" %></label>
<%= user_image current_user %>
<ul class='form-list accountImage-options'>
- <% if current_user.image.file? %>
+ <% if current_user.avatar.attached? || current_user.image.file? %>
<li>
- <%= radio_button_tag "image_action", "keep", !current_user.image_use_gravatar %>
- <label class='standard-label' for='image_action_keep'><%= t ".keep image" %></label>
+ <%= radio_button_tag "avatar_action", "keep", !current_user.image_use_gravatar %>
+ <label class='standard-label' for='avatar_action_keep'><%= t ".keep image" %></label>
</li>
<% end %>
- <% if current_user.image.file? || current_user.image_use_gravatar? %>
+ <% if current_user.avatar.attached? || current_user.image.file? || current_user.image_use_gravatar? %>
<li>
- <%= radio_button_tag "image_action", "delete" %>
- <label class='standard-label' for='image_action_delete'><%= t ".delete image" %></label>
+ <%= radio_button_tag "avatar_action", "delete" %>
+ <label class='standard-label' for='avatar_action_delete'><%= t ".delete image" %></label>
</li>
<% end %>
- <% if current_user.image.file? %>
+ <% if current_user.avatar.attached? || current_user.image.file? %>
<li>
- <%= radio_button_tag "image_action", "new" %>
- <label class='standard-label' for='image_action_new'>
+ <%= radio_button_tag "avatar_action", "new" %>
+ <label class='standard-label' for='avatar_action_new'>
<%= t ".replace image" %>
<span class="form-help deemphasize"><%= t ".image size hint" %></span>
</label>
- <%= f.file_field :image %>
+ <%= f.file_field :avatar %>
</li>
<% else %>
<li>
- <%= radio_button_tag "image_action", "new" %>
- <label class='standard-label' for='image_action_new'>
+ <%= radio_button_tag "avatar_action", "new" %>
+ <label class='standard-label' for='avatar_action_new'>
<%= t ".new image" %>
<span class="form-help deemphasize"><%= t ".image size hint" %></span>
</label>
- <%= f.file_field :image %>
+ <%= f.file_field :avatar %>
</li>
<% end %>
<li>
- <%= radio_button_tag "image_action", "gravatar", current_user.image_use_gravatar %>
- <label class='standard-label' for='image_action_gravatar'>
+ <%= radio_button_tag "avatar_action", "gravatar", current_user.image_use_gravatar %>
+ <label class='standard-label' for='avatar_action_gravatar'>
<%= t ".gravatar.gravatar" %>
<span class='form-help deemphasize'> (<a href="<%= t ".gravatar.link" %>" target="_new"><%= t ".gravatar.link text" %></a>)</span>
</label>
database.yml
+storage.yml
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Store uploaded files on the local file system (see config/storage.yml for options)
- config.active_storage.service = :local
+ config.active_storage.service = Settings.storage_service.to_symbol
# Mount Action Cable outside main process or domain
# config.action_cable.mount_path = nil
--- /dev/null
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
--- /dev/null
+Rails.configuration.after_initialize do
+ require "active_storage/service/s3_service"
+ require_dependency "active_storage/variant"
+
+ module OpenStreetMap
+ module ActiveStorage
+ module Variant
+ private
+
+ def upload(image)
+ File.open(image.path, "r") { |file| service.upload(key, file, :content_type => content_type) }
+ end
+ end
+
+ module S3Service
+ def upload(key, io, content_type:, **options)
+ @upload_options[:content_type] = content_type
+ super(key, io, **options)
+ @upload_options.delete(:content_type)
+ end
+ end
+ end
+ end
+
+ ActiveStorage::Variant.prepend(OpenStreetMap::ActiveStorage::Variant)
+ ActiveStorage::Service::S3Service.prepend(OpenStreetMap::ActiveStorage::S3Service)
+
+ ActiveSupport::Reloader.to_complete do
+ ActiveStorage::Variant.prepend(OpenStreetMap::ActiveStorage::Variant)
+ end
+end
required(:api_timeout).filled(:int?)
required(:imagery_blacklist).maybe(:array?)
required(:status).filled(:str?, :included_in? => ALLOWED_STATUS)
+ required(:storage_service).filled(:str?)
end
end
csp_policy[:connect_src] << PIWIK["location"] if defined?(PIWIK)
csp_policy[:img_src] << PIWIK["location"] if defined?(PIWIK)
csp_policy[:script_src] << PIWIK["location"] if defined?(PIWIK)
+
+csp_policy[:img_src] << Settings.storage_url if Settings.key?(:storage_url)
+
csp_policy[:report_uri] << Settings.csp_report_url if Settings.key?(:csp_report_url)
cookie_policy = {
csp_enforce: false
# URL for reporting Content-Security-Policy violations
#csp_report_url: ""
+# Storage service to use in production mode
+storage_service: "local"
+# Root URL for storage service
+# storage_url:
+++ /dev/null
-test:
- service: Disk
- root: <%= Rails.root.join("tmp/storage") %>
-
-local:
- service: Disk
- root: <%= Rails.root.join("storage") %>
-
-# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
-# amazon:
-# service: S3
-# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
-# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
-# region: us-east-1
-# bucket: your_own_bucket
-
-# Remember not to checkin your GCS keyfile to a repository
-# google:
-# service: GCS
-# project: your_project
-# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
-# bucket: your_own_bucket
-
-# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
-# microsoft:
-# service: AzureStorage
-# storage_account_name: your_account_name
-# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
-# container: your_container_name
-
-# mirror:
-# service: Mirror
-# primary: local
-# mirrors: [ amazon, google, microsoft ]
--- /dev/null
+# This migration comes from active_storage (originally 20170806125915)
+class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
+ def change
+ create_table :active_storage_blobs do |t|
+ t.string :key, :null => false
+ t.string :filename, :null => false
+ t.string :content_type
+ t.text :metadata
+ t.bigint :byte_size, :null => false
+ t.string :checksum, :null => false
+ t.datetime :created_at, :null => false
+
+ t.index [:key], :unique => true
+ end
+
+ create_table :active_storage_attachments do |t|
+ t.string :name, :null => false
+ t.references :record, :null => false, :polymorphic => true, :index => false
+ t.references :blob, :null => false
+
+ t.datetime :created_at, :null => false
+
+ t.index [:record_type, :record_id, :name, :blob_id], :name => "index_active_storage_attachments_uniqueness", :unique => true
+ t.foreign_key :active_storage_blobs, :column => :blob_id
+ end
+ end
+end
ALTER SEQUENCE public.acls_id_seq OWNED BY public.acls.id;
+--
+-- Name: active_storage_attachments; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.active_storage_attachments (
+ id bigint NOT NULL,
+ name character varying NOT NULL,
+ record_type character varying NOT NULL,
+ record_id bigint NOT NULL,
+ blob_id bigint NOT NULL,
+ created_at timestamp without time zone NOT NULL
+);
+
+
+--
+-- Name: active_storage_attachments_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.active_storage_attachments_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: active_storage_attachments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.active_storage_attachments_id_seq OWNED BY public.active_storage_attachments.id;
+
+
+--
+-- Name: active_storage_blobs; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.active_storage_blobs (
+ id bigint NOT NULL,
+ key character varying NOT NULL,
+ filename character varying NOT NULL,
+ content_type character varying,
+ metadata text,
+ byte_size bigint NOT NULL,
+ checksum character varying NOT NULL,
+ created_at timestamp without time zone NOT NULL
+);
+
+
+--
+-- Name: active_storage_blobs_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.active_storage_blobs_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: active_storage_blobs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.active_storage_blobs_id_seq OWNED BY public.active_storage_blobs.id;
+
+
--
-- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: -
--
ALTER TABLE ONLY public.acls ALTER COLUMN id SET DEFAULT nextval('public.acls_id_seq'::regclass);
+--
+-- Name: active_storage_attachments id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.active_storage_attachments ALTER COLUMN id SET DEFAULT nextval('public.active_storage_attachments_id_seq'::regclass);
+
+
+--
+-- Name: active_storage_blobs id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.active_storage_blobs ALTER COLUMN id SET DEFAULT nextval('public.active_storage_blobs_id_seq'::regclass);
+
+
--
-- Name: changeset_comments id; Type: DEFAULT; Schema: public; Owner: -
--
ADD CONSTRAINT acls_pkey PRIMARY KEY (id);
+--
+-- Name: active_storage_attachments active_storage_attachments_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.active_storage_attachments
+ ADD CONSTRAINT active_storage_attachments_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: active_storage_blobs active_storage_blobs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.active_storage_blobs
+ ADD CONSTRAINT active_storage_blobs_pkey PRIMARY KEY (id);
+
+
--
-- Name: ar_internal_metadata ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
CREATE INDEX index_acls_on_mx ON public.acls USING btree (mx);
+--
+-- Name: index_active_storage_attachments_on_blob_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_active_storage_attachments_on_blob_id ON public.active_storage_attachments USING btree (blob_id);
+
+
+--
+-- Name: index_active_storage_attachments_uniqueness; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE UNIQUE INDEX index_active_storage_attachments_uniqueness ON public.active_storage_attachments USING btree (record_type, record_id, name, blob_id);
+
+
+--
+-- Name: index_active_storage_blobs_on_key; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE UNIQUE INDEX index_active_storage_blobs_on_key ON public.active_storage_blobs USING btree (key);
+
+
--
-- Name: index_changeset_comments_on_created_at; Type: INDEX; Schema: public; Owner: -
--
ADD CONSTRAINT diary_entry_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id);
+--
+-- Name: active_storage_attachments fk_rails_c3b3935057; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.active_storage_attachments
+ ADD CONSTRAINT fk_rails_c3b3935057 FOREIGN KEY (blob_id) REFERENCES public.active_storage_blobs(id);
+
+
--
-- Name: friends friends_friend_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
('20181031113522'),
('20190518115041'),
('20190623093642'),
+('20190702193519'),
('21'),
('22'),
('23'),
# Changing to an uploaded image should work
image = Rack::Test::UploadedFile.new("test/gpx/fixtures/a.gif", "image/gif")
- post :account, :params => { :display_name => user.display_name, :image_action => "new", :user => user.attributes.merge(:image => image) }, :session => { :user => user }
+ post :account, :params => { :display_name => user.display_name, :avatar_action => "new", :user => user.attributes.merge(:avatar => image) }, :session => { :user => user }
assert_response :success
assert_template :account
assert_select "div#errorExplanation", false
assert_select ".notice", /^User information updated successfully/
- assert_select "form#accountForm > fieldset > div.form-row.accountImage input[name=image_action][checked][value=?]", "keep"
+ assert_select "form#accountForm > fieldset > div.form-row.accountImage input[name=avatar_action][checked][value=?]", "keep"
# Changing to a gravatar image should work
- post :account, :params => { :display_name => user.display_name, :image_action => "gravatar", :user => user.attributes }, :session => { :user => user }
+ post :account, :params => { :display_name => user.display_name, :avatar_action => "gravatar", :user => user.attributes }, :session => { :user => user }
assert_response :success
assert_template :account
assert_select "div#errorExplanation", false
assert_select ".notice", /^User information updated successfully/
- assert_select "form#accountForm > fieldset > div.form-row.accountImage input[name=image_action][checked][value=?]", "gravatar"
+ assert_select "form#accountForm > fieldset > div.form-row.accountImage input[name=avatar_action][checked][value=?]", "gravatar"
# Removing the image should work
- post :account, :params => { :display_name => user.display_name, :image_action => "delete", :user => user.attributes }, :session => { :user => user }
+ post :account, :params => { :display_name => user.display_name, :avatar_action => "delete", :user => user.attributes }, :session => { :user => user }
assert_response :success
assert_template :account
assert_select "div#errorExplanation", false
assert_select ".notice", /^User information updated successfully/
- assert_select "form#accountForm > fieldset > div.form-row.accountImage input[name=image_action][checked]", false
+ assert_select "form#accountForm > fieldset > div.form-row.accountImage input[name=avatar_action][checked]", false
# Adding external authentication should redirect to the auth provider
post :account, :params => { :display_name => user.display_name, :user => user.attributes.merge(:auth_provider => "openid", :auth_uid => "gmail.com") }, :session => { :user => user }
gravatar_user = create(:user, :image_use_gravatar => true)
image = user_image(user)
- assert_match %r{^<img class="user_image" .* src="/assets/users/images/large-.*" />$}, image
+ assert_match %r{^<img class="user_image" .* src="/images/avatar_large.png" />$}, image
image = user_image(user, :class => "foo")
- assert_match %r{^<img class="foo" .* src="/assets/users/images/large-.*" />$}, image
+ assert_match %r{^<img class="foo" .* src="/images/avatar_large.png" />$}, image
image = user_image(gravatar_user)
assert_match %r{^<img class="user_image" .* src="http://www.gravatar.com/avatar/.*" />$}, image
gravatar_user = create(:user, :image_use_gravatar => true)
image = user_thumbnail(user)
- assert_match %r{^<img class="user_thumbnail" .* src="/assets/users/images/small-.*" />$}, image
+ assert_match %r{^<img class="user_thumbnail" .* src="/images/avatar_small.png" />$}, image
image = user_thumbnail(user, :class => "foo")
- assert_match %r{^<img class="foo" .* src="/assets/users/images/small-.*" />$}, image
+ assert_match %r{^<img class="foo" .* src="/images/avatar_small.png" />$}, image
image = user_thumbnail(gravatar_user)
assert_match %r{^<img class="user_thumbnail" .* src="http://www.gravatar.com/avatar/.*" />$}, image
gravatar_user = create(:user, :image_use_gravatar => true)
image = user_thumbnail_tiny(user)
- assert_match %r{^<img class="user_thumbnail_tiny" .* src="/assets/users/images/small-.*" />$}, image
+ assert_match %r{^<img class="user_thumbnail_tiny" .* src="/images/avatar_small.png" />$}, image
image = user_thumbnail_tiny(user, :class => "foo")
- assert_match %r{^<img class="foo" .* src="/assets/users/images/small-.*" />$}, image
+ assert_match %r{^<img class="foo" .* src="/images/avatar_small.png" />$}, image
image = user_thumbnail_tiny(gravatar_user)
assert_match %r{^<img class="user_thumbnail_tiny" .* src="http://www.gravatar.com/avatar/.*" />$}, image