Merge branch 'staging' into docs_cleanup

pull/830/head
Rick Carlino 2018-05-04 16:15:47 -05:00 committed by GitHub
commit 27c730ea13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 140 additions and 60 deletions

1
.gitignore vendored
View File

@ -21,3 +21,4 @@ public/system
public/webpack
public/webpack/*
tmp
public/direct_upload/temp/*.jpg

View File

@ -4,12 +4,11 @@ module Api
# 2. POST the URL from step 1 (or any URL) to ImagesController#Create
# 3. Image is transfered to the "trusted bucket".
class ImagesController < Api::AbstractController
BUCKET = ENV.fetch("GCS_BUCKET") { "YOU_MUST_CONFIG_GOOGLE_CLOUD_STORAGE" }
KEY = ENV.fetch("GCS_KEY") { "YOU_MUST_CONFIG_GCS_KEY" }
SECRET = ENV.fetch("GCS_ID") { "YOU_MUST_CONFIG_GCS_ID" }
cattr_accessor :store_locally
self.store_locally = !ENV["GCS_BUCKET"]
def create
mutate Images::Create.run(raw_json, device: current_device)
mutate Images::Create.run(raw_json, device: current_device)
end
def index
@ -28,51 +27,17 @@ module Api
# Creates a "policy object" + meta data so that users may upload an image to
# Google Cloud Storage.
def storage_auth
# Creates a 1 hour authorization for a user to upload an image file to a
# Google Cloud Storage bucket.
# You probably want to POST that URL to Images#Create after that.
render json: {
verb: "POST",
url: "//storage.googleapis.com/#{BUCKET}/",
form_data: {
"key" => random_filename,
"acl" => "public-read",
"Content-Type" => "image/jpeg",
"policy" => policy,
"signature" => policy_signature,
"GoogleAccessId" => KEY,
"file" => "REPLACE_THIS_WITH_A_BINARY_JPEG_FILE"
},
instructions: "Send a 'from-data' request to the URL provided."\
"Then POST the resulting URL as an 'attachment_url' "\
"(json) to api/images/."
}
mutate policy_class.run
end
private
# The image URL in the "untrusted bucket" in Google Cloud Storage
def random_filename
@range ||= "temp1/#{SecureRandom.uuid}.jpg"
end
def policy
@policy ||= Base64.encode64(
{ 'expiration' => 1.hour.from_now.utc.xmlschema,
'conditions' => [
{ 'bucket' => BUCKET },
{ 'key' => random_filename},
{ 'acl' => 'public-read' },
{ 'Content-Type' => "image/jpeg"},
['content-length-range', 1, 7.megabytes]
]}.to_json).gsub(/\n/, '')
end
def policy_signature
@policy_signature ||= Base64.encode64(
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'),
SECRET,
policy)).gsub("\n",'')
def policy_class
if ImagesController.store_locally
Images::GeneratePolicy
else
Images::StubPolicy
end
end
def image

View File

@ -46,6 +46,16 @@ class DashboardController < ApplicationController
render json: report
end
# (for self hosted users) Direct image upload endpoint.
# Do not use this if you use GCS- it will slow your app down.
def direct_upload
raise "No." unless Api::ImagesController.store_locally
name = params.fetch(:key).split("/").last
path = File.join("public", "direct_upload", "temp", name)
File.open(path, "wb") { |f| f.write(params[:file]) }
render json: ""
end
private
def set_global_config

View File

@ -0,0 +1,50 @@
module Images
class GeneratePolicy < Mutations::Command
BUCKET = ENV.fetch("GCS_BUCKET") { "YOU_MUST_CONFIG_GOOGLE_CLOUD_STORAGE" }
KEY = ENV.fetch("GCS_KEY") { "YOU_MUST_CONFIG_GCS_KEY" }
SECRET = ENV.fetch("GCS_ID") { "YOU_MUST_CONFIG_GCS_ID" }
def execute
{
verb: "POST",
url: "//storage.googleapis.com/#{BUCKET}/",
form_data: {
"key" => random_filename,
"acl" => "public-read",
"Content-Type" => "image/jpeg",
"policy" => policy,
"signature" => policy_signature,
"GoogleAccessId" => KEY,
"file" => "REPLACE_THIS_WITH_A_BINARY_JPEG_FILE"
},
instructions: "Send a 'from-data' request to the URL provided."\
"Then POST the resulting URL as an 'attachment_url' "\
"(json) to api/images/."
}
end
private
# The image URL in the "untrusted bucket" in Google Cloud Storage
def random_filename
@range ||= "temp1/#{SecureRandom.uuid}.jpg"
end
def policy
@policy ||= Base64.encode64(
{ 'expiration' => 1.hour.from_now.utc.xmlschema,
'conditions' => [
{ 'bucket' => BUCKET },
{ 'key' => random_filename},
{ 'acl' => 'public-read' },
{ 'Content-Type' => "image/jpeg"},
['content-length-range', 1, 7.megabytes]
]}.to_json).gsub(/\n/, '')
end
def policy_signature
@policy_signature ||= Base64.encode64(
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'),
SECRET,
policy)).gsub("\n",'')
end
end
end

View File

@ -0,0 +1,29 @@
module Images
class StubPolicy < Mutations::Command
URL = "#{$API_URL}/direct_upload/"
def execute
{
verb: "POST",
url: URL,
form_data: {
"key" => random_filename,
"acl" => "public-read",
"Content-Type" => "image/jpeg",
"policy" => "N/A",
"signature" => "N/A",
"GoogleAccessId" => "N/A",
"file" => "REPLACE_THIS_WITH_A_BINARY_JPEG_FILE"
},
instructions: "Send a 'from-data' request to the URL provided."\
"Then POST the resulting URL as an 'attachment_url' "\
"(json) to api/images/."
}
end
private
# The image URL in the "untrusted bucket" in Google Cloud Storage
def random_filename
@range ||= "temp/#{SecureRandom.uuid}.jpg"
end
end
end

View File

@ -20,13 +20,13 @@ module Sequences
def execute
ActiveRecord::Base.transaction do
p = inputs
.merge(migrated_nodes: true)
.without(:body, :args, "body", "args")
p = inputs
.merge(migrated_nodes: true)
.without(:body, :args, "body", "args")
seq = Sequence.create!(p)
x = CeleryScript::FirstPass.run!(sequence: seq,
args: args || {},
body: body || [])
x = CeleryScript::FirstPass.run!(sequence: seq,
args: args || {},
body: body || [])
result = CeleryScript::FetchCelery.run!(sequence: seq)
seq.manually_sync! # We must manually sync this resource.
result

View File

@ -5,8 +5,6 @@ class ImageSerializer < ActiveModel::Serializer
def attachment_url
url_ = object.attachment.url("x640")
# Force google cloud users to use HTTPS://
x = Api::ImagesController::KEY.present? ?
url_.gsub("http://", "https://") : url_
return x
return ENV["GCS_KEY"].present? ? url_.gsub("http://", "https://") : url_
end
end

View File

@ -68,11 +68,12 @@ FarmBot::Application.routes.draw do
# =======================================================================
# NON-API (USER FACING) URLS:
# =======================================================================
get "/" => "dashboard#front_page", as: :front_page
get "/app" => "dashboard#main_app", as: :dashboard
get "/app/controls" => "dashboard#main_app", as: :app_landing_page
get "/tos_update" => "dashboard#tos_update", as: :tos_update
post "/csp_reports" => "dashboard#csp_reports", as: :csp_report
get "/" => "dashboard#front_page", as: :front_page
get "/app" => "dashboard#main_app", as: :dashboard
get "/app/controls" => "dashboard#main_app", as: :app_landing_page
get "/tos_update" => "dashboard#tos_update", as: :tos_update
post "/csp_reports" => "dashboard#csp_reports", as: :csp_report
post "/direct_upload" => "dashboard#direct_upload", as: :direct_upload
get "/password_reset/*token" => "dashboard#password_reset", as: :password_reset
get "/verify/:token" => "dashboard#verify", as: :verify_user

View File

@ -3,7 +3,7 @@ require 'spec_helper'
describe Api::ImagesController do
include Devise::Test::ControllerHelpers
let(:user) { FactoryBot.create(:user) }
it "Creates a polict object" do
it "Creates a policy object" do
sign_in user
get :storage_auth
@ -16,6 +16,21 @@ describe Api::ImagesController do
.to include("POST the resulting URL as an 'attachment_url'")
end
it "Creates a (stub) policy object" do
sign_in user
b4 = Api::ImagesController.store_locally
Api::ImagesController.store_locally = false
get :storage_auth
Api::ImagesController.store_locally = b4
expect(response.status).to eq(200)
expect(json).to be_kind_of(Hash)
expect(json[:verb]).to eq("POST")
expect(json[:url]).to include($API_URL)
[ :policy, :signature, :GoogleAccessId ]
.map { |key| expect(json.dig(:form_data, key)).to eq("N/A") }
expect(json[:form_data].keys.sort).to include(:signature)
end
describe '#index' do
it 'shows only the max images allowed' do
sign_in user

View File

@ -50,5 +50,16 @@ describe DashboardController do
expect(user.reload.unconfirmed_email).to be nil
expect(user.email).to eq email
end
it 'handles self hosted image uploads' do
name = "wow.jpg"
params = {key: "whatever/" + name,
file: StringIO.new(File.open("./spec/fixture.jpg").read)}
post :direct_upload, params: params
file = File.join("public", "direct_upload", "temp", name)
expect(File.file?(file)).to be(true)
expect(response.status).to be(200)
File.delete(file)
end
end
end