diff --git a/.env.example b/.env.example index 882dfe72f..85efb4416 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,10 @@ ALLOWED_ORIGINS=localhost:3002,localhost:3000 + +AWS_ACCESS_KEY_ID=changeme +AWS_S3_ACTIVE_STORAGE_BUCKET=changeme +AWS_S3_REGION=changeme +AWS_SECRET_ACCESS_KEY=changeme + POSTGRES_HOST=changeme POSTGRES_USER=changeme POSTGRES_PASSWORD=changeme diff --git a/Gemfile b/Gemfile index 5a049ca33..6f9eeb837 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.0.3' +gem 'aws-sdk-s3', require: false gem 'bootsnap', require: false gem 'cancancan', '~> 3.3' gem 'faraday' diff --git a/Gemfile.lock b/Gemfile.lock index 900479e9f..037f3b90f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,6 +69,22 @@ GEM addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) ast (2.4.2) + aws-eventstream (1.2.0) + aws-partitions (1.567.0) + aws-sdk-core (3.130.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.55.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.113.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.4.0) + aws-eventstream (~> 1, >= 1.0.2) bootsnap (1.9.3) msgpack (~> 1.0) builder (3.2.4) @@ -109,6 +125,7 @@ GEM io-wait (0.2.1) jbuilder (2.11.4) activesupport (>= 5.0.0) + jmespath (1.6.1) loofah (2.14.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -273,6 +290,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + aws-sdk-s3 bootsnap cancancan (~> 3.3) dotenv-rails diff --git a/app/controllers/api/projects/images_controller.rb b/app/controllers/api/projects/images_controller.rb new file mode 100644 index 000000000..88b1b1ff8 --- /dev/null +++ b/app/controllers/api/projects/images_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Api + module Projects + class ImagesController < ApiController + before_action :require_oauth_user + + def create + @project = Project.find_by!(identifier: params[:project_id]) + @project.images.attach(params[:images]) + render '/api/projects/images', formats: [:json] + end + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 094d3c7d5..05efd551d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -8,6 +8,7 @@ class Project < ApplicationRecord belongs_to :parent, class_name: 'Project', foreign_key: 'remixed_from_id', optional: true, inverse_of: :children has_many :components, -> { order(:index) }, dependent: :destroy, inverse_of: :project has_many :children, class_name: 'Project', foreign_key: 'remixed_from_id', dependent: :nullify, inverse_of: :parent + has_many_attached :images accepts_nested_attributes_for :components private diff --git a/app/views/api/projects/images.json.jbuilder b/app/views/api/projects/images.json.jbuilder new file mode 100644 index 000000000..37ad9f485 --- /dev/null +++ b/app/views/api/projects/images.json.jbuilder @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +json.call(@project) + +json.image_list @project.images do |image| + json.filename image.filename + json.url rails_blob_url(image) +end diff --git a/app/views/api/projects/show.json.jbuilder b/app/views/api/projects/show.json.jbuilder index da085ad97..75dbff2c3 100644 --- a/app/views/api/projects/show.json.jbuilder +++ b/app/views/api/projects/show.json.jbuilder @@ -5,3 +5,8 @@ json.call(@project, :identifier, :project_type, :name, :user_id) json.parent(@project.parent, :name, :identifier) if @project.parent json.components @project.components, :id, :name, :extension, :content, :index + +json.image_list @project.images do |image| + json.filename image.filename + json.url rails_blob_url(image) +end diff --git a/config/environments/production.rb b/config/environments/production.rb index 085222138..d16ec99e3 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -39,8 +39,7 @@ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # 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 = :amazon # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil diff --git a/config/routes.rb b/config/routes.rb index aa0d8afc9..bbdbb4200 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,6 +9,7 @@ resources :projects, only: %i[show update] do resource :remix, only: %i[create], controller: 'projects/remixes' + resource :images, only: %i[create], controller: 'projects/images' end end end diff --git a/config/storage.yml b/config/storage.yml index 4942ab669..e91939e78 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -6,13 +6,12 @@ local: service: Disk root: <%= Rails.root.join("storage") %> -# Use bin/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-<%= Rails.env %> +amazon: + service: S3 + access_key_id: <%= ENV.fetch('AWS_ACCESS_KEY_ID', nil) %> + secret_access_key: <%= ENV.fetch('AWS_SECRET_ACCESS_KEY', nil) %> + region: <%= ENV.fetch('AWS_S3_REGION', 'eu-west-2') %> + bucket: <%= ENV.fetch('AWS_S3_ACTIVE_STORAGE_BUCKET', nil) %> # Remember not to checkin your GCS keyfile to a repository # google: diff --git a/db/migrate/20220311121518_create_active_storage_tables.active_storage.rb b/db/migrate/20220311121518_create_active_storage_tables.active_storage.rb new file mode 100644 index 000000000..8a7bfe189 --- /dev/null +++ b/db/migrate/20220311121518_create_active_storage_tables.active_storage.rb @@ -0,0 +1,57 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + 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 + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index 8d5dbec3b..bfbd2fb20 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,12 +10,40 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_03_10_120419) do +ActiveRecord::Schema.define(version: 2022_03_11_121518) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" + create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.uuid "record_id", null: false + t.uuid "blob_id", null: false + t.datetime "created_at", precision: 6, null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", precision: 6, null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + create_table "components", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "project_id" t.string "name", null: false @@ -46,5 +74,7 @@ t.index ["word"], name: "index_words_on_word" end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "components", "projects" end diff --git a/docker-compose.yml b/docker-compose.yml index 662fc10ce..ad84cf096 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,11 +2,11 @@ version: "3.9" services: db: image: postgres:14.1 - volumes: - - pg-data:/var/lib/postgresql/data command: postgres -c listen_addresses='*' environment: - - POSTGRES_PASSWORD=password + - POSTGRES_USER + - POSTGRES_PASSWORD + - POSTGRES_DB ports: - "5434:5432" web: diff --git a/spec/factories/project.rb b/spec/factories/project.rb index fbcc39fd1..590f4adbf 100644 --- a/spec/factories/project.rb +++ b/spec/factories/project.rb @@ -24,5 +24,11 @@ object.components << FactoryBot.create(:default_python_component, project: object) end end + + trait :with_attached_images do + after(:create) do |object| + object.images.attach(fixture_file_upload(Rails.root.join('spec/fixtures/test_image_1.png'), 'image/png')) + end + end end end diff --git a/spec/fixtures/files/test_image_1.png b/spec/fixtures/files/test_image_1.png new file mode 100644 index 000000000..e700b4459 Binary files /dev/null and b/spec/fixtures/files/test_image_1.png differ diff --git a/spec/fixtures/files/test_image_2.jpeg b/spec/fixtures/files/test_image_2.jpeg new file mode 100644 index 000000000..bab62aeac Binary files /dev/null and b/spec/fixtures/files/test_image_2.jpeg differ diff --git a/spec/fixtures/files/test_image_3.jpeg b/spec/fixtures/files/test_image_3.jpeg new file mode 100644 index 000000000..c6c40fbe0 Binary files /dev/null and b/spec/fixtures/files/test_image_3.jpeg differ diff --git a/spec/fixtures/files/test_image_4.jpeg b/spec/fixtures/files/test_image_4.jpeg new file mode 100644 index 000000000..243aba18d Binary files /dev/null and b/spec/fixtures/files/test_image_4.jpeg differ diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ab890d4d1..a1a4b53a2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -7,6 +7,7 @@ it { is_expected.to have_many(:components) } it { is_expected.to have_many(:children) } it { is_expected.to belong_to(:parent).optional(true) } + it { is_expected.to have_many_attached(:images) } end describe 'identifier not nil' do diff --git a/spec/request/projects/images_spec.rb b/spec/request/projects/images_spec.rb new file mode 100644 index 000000000..c0d5fba69 --- /dev/null +++ b/spec/request/projects/images_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Images requests', type: :request do + let!(:project) { create(:project) } + let(:user_id) { 'e0675b6c-dc48-4cd6-8c04-0f7ac05af51a' } + let(:image_filename) { 'test_image_1.png' } + let(:params) { { images: [fixture_file_upload(image_filename, 'image/png')] } } + let(:expected_json) do + { + image_list: [ + { + filename: image_filename, + url: rails_blob_url(project.images[0]) + } + ] + }.to_json + end + + describe 'create' do + context 'when auth is correct' do + before do + mock_oauth_user + end + + it 'attaches file to project' do + expect { post "/api/projects/#{project.identifier}/images", params: params }.to change { project.images.count }.by(1) + end + + it 'returns file list' do + post "/api/projects/#{project.identifier}/images", params: params + + expect(response.body).to eq(expected_json) + end + + it 'returns success response' do + post "/api/projects/#{project.identifier}/images", params: params + + expect(response.status).to eq(200) + end + + it 'returns 404 response if invalid project' do + post '/api/projects/no-such-project/images' + + expect(response.status).to eq(404) + end + end + + context 'when auth is invalid' do + it 'returns unauthorized' do + post "/api/projects/#{project.identifier}/images" + + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/request/projects/show_spec.rb b/spec/request/projects/show_spec.rb index 02ac6aba9..90df52a09 100644 --- a/spec/request/projects/show_spec.rb +++ b/spec/request/projects/show_spec.rb @@ -10,7 +10,8 @@ project_type: 'python', name: project.name, user_id: project.user_id, - components: [] + components: [], + image_list: [] }.to_json end