Co-authored-by: Oliver Günther <mail@oliverguenther.de>pull/8512/head
parent
1fbc75b96d
commit
c1b82bad00
@ -0,0 +1,40 @@ |
||||
require_relative 'fog_file_uploader' |
||||
|
||||
class DirectFogUploader < FogFileUploader |
||||
include CarrierWaveDirect::Uploader |
||||
|
||||
def self.for_attachment(attachment) |
||||
for_uploader attachment.file |
||||
end |
||||
|
||||
def self.for_uploader(fog_file_uploader) |
||||
raise ArgumentError, "FogFileUploader expected" unless fog_file_uploader.is_a? FogFileUploader |
||||
|
||||
uploader = self.new |
||||
|
||||
uploader.instance_variable_set "@file", fog_file_uploader.file |
||||
uploader.instance_variable_set "@key", fog_file_uploader.path |
||||
|
||||
uploader |
||||
end |
||||
|
||||
def self.direct_fog_hash(attachment:, success_action_redirect: nil, success_action_status: "201") |
||||
uploader = for_attachment attachment |
||||
|
||||
if success_action_redirect.present? |
||||
uploader.success_action_redirect = success_action_redirect |
||||
uploader.use_action_status = false |
||||
else |
||||
uploader.success_action_status = success_action_status |
||||
uploader.use_action_status = true |
||||
end |
||||
|
||||
hash = uploader.direct_fog_hash(enforce_utf8: false) |
||||
|
||||
if success_action_redirect.present? |
||||
hash.merge(success_action_redirect: success_action_redirect) |
||||
else |
||||
hash.merge(success_action_status: success_action_status) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,53 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Attachments::FinishDirectUploadJob < ApplicationJob |
||||
queue_with_priority :high |
||||
|
||||
def perform(attachment_id) |
||||
attachment = Attachment.pending_direct_uploads.where(id: attachment_id).first |
||||
local_file = attachment && attachment.file.local_file |
||||
|
||||
if local_file.nil? |
||||
return Rails.logger.error("File for attachment #{attachment_id} was not uploaded.") |
||||
end |
||||
|
||||
begin |
||||
attachment.downloads = 0 |
||||
attachment.set_file_size local_file unless attachment.filesize && attachment.filesize > 0 |
||||
attachment.set_content_type local_file unless attachment.content_type.present? |
||||
attachment.set_digest local_file unless attachment.digest.present? |
||||
|
||||
attachment.save! if attachment.changed? |
||||
ensure |
||||
File.unlink(local_file.path) if File.exist?(local_file.path) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,153 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {Injectable} from "@angular/core"; |
||||
import {HttpEvent, HttpResponse} from "@angular/common/http"; |
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {from, Observable, of} from "rxjs"; |
||||
import {share, switchMap} from "rxjs/operators"; |
||||
import {OpenProjectFileUploadService, UploadBlob, UploadFile, UploadInProgress} from './op-file-upload.service'; |
||||
|
||||
interface PrepareUploadResult { |
||||
url:string; |
||||
form:FormData; |
||||
response:any; |
||||
} |
||||
|
||||
@Injectable() |
||||
export class OpenProjectDirectFileUploadService extends OpenProjectFileUploadService { |
||||
/** |
||||
* Upload a single file, get an UploadResult observable |
||||
* @param {string} url |
||||
* @param {UploadFile} file |
||||
* @param {string} method |
||||
*/ |
||||
public uploadSingle(url:string, file:UploadFile|UploadBlob, method:string = 'post', responseType:'text'|'json' = 'text') { |
||||
const observable = from(this.getDirectUploadFormFrom(url, file)) |
||||
.pipe( |
||||
switchMap(this.uploadToExternal(file, method, responseType)), |
||||
share() |
||||
); |
||||
|
||||
return [file, observable] as UploadInProgress; |
||||
} |
||||
|
||||
private uploadToExternal(file:UploadFile|UploadBlob, method:string, responseType:string):(result:PrepareUploadResult) => Observable<HttpEvent<unknown>> { |
||||
return result => { |
||||
result.form.append('file', file, file.customName || file.name); |
||||
|
||||
return this |
||||
.http |
||||
.request<HalResource>( |
||||
method, |
||||
result.url, |
||||
{ |
||||
body: result.form, |
||||
// Observe the response, not the body
|
||||
observe: 'events', |
||||
// This is important as the CORS policy for the bucket is * and you can't use credentals then,
|
||||
// besides we don't need them here anyway.
|
||||
withCredentials: false, |
||||
responseType: responseType as any, |
||||
// Subscribe to progress events. subscribe() will fire multiple times!
|
||||
reportProgress: true |
||||
} |
||||
) |
||||
.pipe(switchMap(this.finishUpload(result))); |
||||
}; |
||||
} |
||||
|
||||
private finishUpload(result:PrepareUploadResult):(result:HttpEvent<unknown>) => Observable<HttpEvent<unknown>> { |
||||
return event => { |
||||
if (event instanceof HttpResponse) { |
||||
return this |
||||
.http |
||||
.get( |
||||
result.response._links.completeUpload.href, |
||||
{ |
||||
observe: 'response' |
||||
} |
||||
); |
||||
} |
||||
|
||||
// Return as new observable due to switchMap
|
||||
return of(event); |
||||
}; |
||||
} |
||||
|
||||
public getDirectUploadFormFrom(url:string, file:UploadFile|UploadBlob):Promise<PrepareUploadResult> { |
||||
const formData = new FormData(); |
||||
const metadata = { |
||||
description: file.description, |
||||
fileName: file.customName || file.name, |
||||
fileSize: file.size, |
||||
contentType: file.type |
||||
}; |
||||
|
||||
/* |
||||
* @TODO We could calculate the MD5 hash here too and pass that. |
||||
* The MD5 hash can be used as the `content-md5` option during the upload to S3 for instance. |
||||
* This way S3 can verify the integrity of the file which we currently don't do. |
||||
*/ |
||||
|
||||
// add the metadata object
|
||||
formData.append( |
||||
'metadata', |
||||
JSON.stringify(metadata), |
||||
); |
||||
|
||||
const result = this |
||||
.http |
||||
.request<HalResource>( |
||||
"post", |
||||
url, |
||||
{ |
||||
body: formData, |
||||
withCredentials: true, |
||||
responseType: "json" as any |
||||
} |
||||
) |
||||
.toPromise() |
||||
.then((res) => { |
||||
let form = new FormData(); |
||||
|
||||
_.each(res._links.addAttachment.form_fields, (value, key) => { |
||||
form.append(key, value); |
||||
}); |
||||
|
||||
return { url: res._links.addAttachment.href, form: form, response: res }; |
||||
}) |
||||
.catch((err) => { |
||||
console.log(err); |
||||
|
||||
return new FormData(); |
||||
}); |
||||
|
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,158 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'roar/decorator' |
||||
require 'roar/json/hal' |
||||
|
||||
module API |
||||
module V3 |
||||
module Attachments |
||||
class AttachmentUploadRepresenter < ::API::Decorators::Single |
||||
include API::Decorators::DateProperty |
||||
include API::Decorators::FormattableProperty |
||||
include API::Decorators::LinkedResource |
||||
|
||||
self_link title_getter: ->(*) { represented.filename } |
||||
|
||||
associated_resource :author, |
||||
v3_path: :user, |
||||
representer: ::API::V3::Users::UserRepresenter |
||||
|
||||
def self.associated_container_getter |
||||
->(*) do |
||||
next unless embed_links && container_representer |
||||
|
||||
container_representer |
||||
.new(represented.container, current_user: current_user) |
||||
end |
||||
end |
||||
|
||||
def self.associated_container_link |
||||
->(*) do |
||||
return nil unless v3_container_name == 'nil_class' || api_v3_paths.respond_to?(v3_container_name) |
||||
|
||||
::API::Decorators::LinkObject |
||||
.new(represented, |
||||
path: v3_container_name, |
||||
property_name: :container, |
||||
title_attribute: container_title_attribute) |
||||
.to_hash |
||||
end |
||||
end |
||||
|
||||
attr_reader :form_url |
||||
attr_reader :form_fields |
||||
|
||||
attr_reader :attachment |
||||
|
||||
def initialize(attachment, options = {}) |
||||
super |
||||
|
||||
fog_hash = DirectFogUploader.direct_fog_hash attachment: attachment |
||||
|
||||
@form_url = fog_hash[:uri] |
||||
@form_fields = fog_hash.except :uri |
||||
@attachment = attachment |
||||
end |
||||
|
||||
associated_resource :container, |
||||
getter: associated_container_getter, |
||||
link: associated_container_link |
||||
|
||||
link :addAttachment do |
||||
{ |
||||
href: form_url, |
||||
method: :post, |
||||
form_fields: form_fields |
||||
} |
||||
end |
||||
|
||||
link :delete do |
||||
{ |
||||
href: api_v3_paths.attachment_upload(represented.id), |
||||
method: :delete |
||||
} |
||||
end |
||||
|
||||
link :staticDownloadLocation do |
||||
{ |
||||
href: api_v3_paths.attachment_content(attachment.id) |
||||
} |
||||
end |
||||
|
||||
link :downloadLocation do |
||||
location = if attachment.external_storage? |
||||
attachment.external_url |
||||
else |
||||
api_v3_paths.attachment_content(attachment.id) |
||||
end |
||||
{ |
||||
href: location |
||||
} |
||||
end |
||||
|
||||
link :completeUpload do |
||||
{ |
||||
href: api_v3_paths.attachment_uploaded(attachment.id) |
||||
} |
||||
end |
||||
|
||||
property :id |
||||
property :file_name, |
||||
getter: ->(*) { filename } |
||||
|
||||
formattable_property :description, |
||||
plain: true |
||||
|
||||
date_time_property :created_at |
||||
|
||||
def _type |
||||
'AttachmentUpload' |
||||
end |
||||
|
||||
def container_representer |
||||
name = v3_container_name.camelcase |
||||
|
||||
"::API::V3::#{name.pluralize}::#{name}Representer".constantize |
||||
rescue NameError |
||||
nil |
||||
end |
||||
|
||||
def v3_container_name |
||||
::API::Utilities::PropertyNameConverter.from_ar_name(represented.container.class.name.underscore).underscore |
||||
end |
||||
|
||||
def container_title_attribute |
||||
represented.container.respond_to?(:subject) ? :subject : :title |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,57 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe 'direct IFC upload', type: :feature, js: true, with_direct_uploads: :redirect, with_config: { edition: 'bim' } do |
||||
let(:user) { FactoryBot.create :admin } |
||||
let(:project) { FactoryBot.create :project, enabled_module_names: %i[bim] } |
||||
let(:ifc_fixture) { Rails.root.join('modules/bim/spec/fixtures/files/minimal.ifc') } |
||||
|
||||
before do |
||||
login_as user |
||||
|
||||
allow_any_instance_of(Bim::IfcModels::BaseContract).to receive(:ifc_attachment_is_ifc).and_return true |
||||
end |
||||
|
||||
it 'should work' do |
||||
visit new_bcf_project_ifc_model_path(project_id: project.identifier) |
||||
|
||||
page.attach_file("file", ifc_fixture, visible: :all) |
||||
|
||||
click_on "Create" |
||||
|
||||
expect(page).to have_content("Upload succeeded") |
||||
|
||||
expect(Attachment.count).to eq 1 |
||||
expect(Attachment.first[:file]).to eq 'model.ifc' |
||||
|
||||
expect(Bim::IfcModels::IfcModel.count).to eq 1 |
||||
expect(Bim::IfcModels::IfcModel.first.title).to eq "minimal.ifc" |
||||
end |
||||
end |
@ -0,0 +1,103 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require_relative 'attachments/attachment_resource_shared_examples' |
||||
|
||||
describe API::V3::Attachments::AttachmentsAPI, type: :request do |
||||
include Rack::Test::Methods |
||||
include API::V3::Utilities::PathHelper |
||||
include FileHelpers |
||||
|
||||
let(:current_user) { FactoryBot.create(:user, member_in_project: project, member_through_role: role) } |
||||
|
||||
let(:project) { FactoryBot.create(:project, public: false) } |
||||
let(:role) { FactoryBot.create(:role, permissions: permissions) } |
||||
let(:permissions) { [:add_work_packages] } |
||||
|
||||
context( |
||||
'with missing permissions', |
||||
with_config: { |
||||
attachments_storage: :fog, |
||||
fog: { credentials: { provider: 'AWS' } } |
||||
} |
||||
) do |
||||
let(:permissions) { [] } |
||||
|
||||
let(:request_path) { api_v3_paths.prepare_new_attachment_upload } |
||||
let(:request_parts) { { metadata: metadata, file: file } } |
||||
let(:metadata) { { fileName: 'cat.png' }.to_json } |
||||
let(:file) { mock_uploaded_file(name: 'original-filename.txt') } |
||||
|
||||
before do |
||||
post request_path, request_parts |
||||
end |
||||
|
||||
it 'should forbid to prepare attachments' do |
||||
expect(last_response.status).to eq 403 |
||||
end |
||||
end |
||||
|
||||
it_behaves_like 'it supports direct uploads' do |
||||
let(:request_path) { api_v3_paths.prepare_new_attachment_upload } |
||||
let(:container_href) { nil } |
||||
|
||||
describe 'GET /uploaded' do |
||||
let(:digest) { "" } |
||||
let(:attachment) { FactoryBot.create :attachment, digest: digest, author: current_user, container: nil, container_type: nil, downloads: -1 } |
||||
|
||||
before do |
||||
get "/api/v3/attachments/#{attachment.id}/uploaded" |
||||
end |
||||
|
||||
context 'with no pending attachments' do |
||||
let(:digest) { "0xFF" } |
||||
|
||||
it 'should return 404' do |
||||
expect(last_response.status).to eq 404 |
||||
end |
||||
end |
||||
|
||||
context 'with a pending attachment' do |
||||
it 'should enqueue a FinishDirectUpload job' do |
||||
expect(::Attachments::FinishDirectUploadJob).to have_been_enqueued.at_least(1) |
||||
end |
||||
|
||||
it 'should respond with HTTP OK' do |
||||
expect(last_response.status).to eq 200 |
||||
end |
||||
|
||||
it 'should return the attachment representation' do |
||||
json = JSON.parse last_response.body |
||||
|
||||
expect(json["_type"]).to eq "Attachment" |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,199 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class WithDirectUploads |
||||
attr_reader :context |
||||
|
||||
def initialize(context) |
||||
@context = context |
||||
end |
||||
|
||||
## |
||||
# We need this so calls to rspec mocks (allow, expect etc.) will work here as expected. |
||||
def method_missing(method, *args, &block) |
||||
if context.respond_to?(method) |
||||
context.send method, *args, &block |
||||
else |
||||
super |
||||
end |
||||
end |
||||
|
||||
def before(example) |
||||
stub_config example |
||||
|
||||
mock_attachment |
||||
stub_frontend redirect: redirect?(example) if stub_frontend?(example) |
||||
|
||||
stub_uploader |
||||
end |
||||
|
||||
def stub_frontend?(example) |
||||
example.metadata[:js] |
||||
end |
||||
|
||||
def redirect?(example) |
||||
example.metadata[:with_direct_uploads] == :redirect |
||||
end |
||||
|
||||
def around(example) |
||||
example.metadata[:driver] = :headless_firefox_billy |
||||
|
||||
csp_config = SecureHeaders::Configuration.instance_variable_get("@default_config").csp |
||||
csp_config.connect_src = ["'self'", "my-bucket.s3.amazonaws.com"] |
||||
|
||||
begin |
||||
example.run |
||||
ensure |
||||
csp_config.connect_src = %w('self') |
||||
end |
||||
end |
||||
|
||||
def mock_attachment |
||||
allow(Attachment).to receive(:create) do |*args| |
||||
# We don't use create here because this would cause an infinite loop as FogAttachment's #create |
||||
# uses the base class's #create which is what we are mocking here. All this is necessary to begin |
||||
# with because the Attachment class is initialized with the LocalFileUploader before this test |
||||
# is ever run and we need remote attachments using the FogFileUploader in this scenario. |
||||
record = FogAttachment.new *args |
||||
record.save |
||||
record |
||||
end |
||||
|
||||
# This is so the uploaded callback works. Since we can't actually substitute the Attachment class |
||||
# used there we get a LocalFileUploader file for the attachment which is not readable when |
||||
# everything else is mocked to be remote. |
||||
allow_any_instance_of(FileUploader).to receive(:readable?).and_return true |
||||
end |
||||
|
||||
def stub_frontend(redirect: false) |
||||
proxy.stub("https://" + OpenProject::Configuration.remote_storage_host + ":443/", method: 'options').and_return( |
||||
headers: { |
||||
'Access-Control-Allow-Methods' => 'POST', |
||||
'Access-Control-Allow-Origin' => '*' |
||||
}, |
||||
code: 200 |
||||
) |
||||
|
||||
if redirect |
||||
stub_with_redirect |
||||
else # use status response instead of redirect by default |
||||
stub_with_status |
||||
end |
||||
end |
||||
|
||||
def stub_with_redirect |
||||
proxy |
||||
.stub("https://" + OpenProject::Configuration.remote_storage_host + ":443/", method: 'post') |
||||
.and_return(Proc.new { |params, headers, body, url, method| |
||||
key = body.scan(/key"\s*([^\s]+)\s/m).flatten.first |
||||
redirect_url = body.scan(/success_action_redirect"\s*(http[^\s]+)\s/m).flatten.first |
||||
ok = body =~ /X-Amz-Signature/ # check that the expected post to AWS was made with the form fields |
||||
|
||||
{ |
||||
code: ok ? 302 : 403, |
||||
headers: { |
||||
'Location' => ok ? redirect_url + '?key=' + CGI.escape(key) : nil, |
||||
'Access-Control-Allow-Methods' => 'POST', |
||||
'Access-Control-Allow-Origin' => '*' |
||||
} |
||||
} |
||||
}) |
||||
end |
||||
|
||||
def stub_with_status |
||||
proxy |
||||
.stub("https://" + OpenProject::Configuration.remote_storage_host + ":443/", method: 'post') |
||||
.and_return(Proc.new { |params, headers, body, url, method| |
||||
{ |
||||
code: (body =~ /X-Amz-Signature/) ? 201 : 403, # check that the expected post to AWS was made with the form fields |
||||
headers: { |
||||
'Access-Control-Allow-Methods' => 'POST', |
||||
'Access-Control-Allow-Origin' => '*' |
||||
} |
||||
} |
||||
}) |
||||
end |
||||
|
||||
def stub_uploader |
||||
creds = config[:fog][:credentials] |
||||
|
||||
allow_any_instance_of(FogFileUploader).to receive(:fog_credentials).and_return creds |
||||
|
||||
allow_any_instance_of(FogFileUploader).to receive(:aws_access_key_id).and_return creds[:aws_access_key_id] |
||||
allow_any_instance_of(FogFileUploader).to receive(:aws_secret_access_key).and_return creds[:aws_secret_access_key] |
||||
allow_any_instance_of(FogFileUploader).to receive(:provider).and_return creds[:provider] |
||||
allow_any_instance_of(FogFileUploader).to receive(:region).and_return creds[:region] |
||||
allow_any_instance_of(FogFileUploader).to receive(:directory).and_return config[:fog][:directory] |
||||
|
||||
allow(OpenProject::Configuration).to receive(:direct_uploads?).and_return(true) |
||||
end |
||||
|
||||
def stub_config(example) |
||||
WithConfig.new(context).before example, config |
||||
end |
||||
|
||||
def config |
||||
{ |
||||
attachments_storage: :fog, |
||||
fog: { |
||||
directory: MockCarrierwave.bucket, |
||||
credentials: MockCarrierwave.credentials |
||||
} |
||||
} |
||||
end |
||||
end |
||||
|
||||
RSpec.configure do |config| |
||||
config.before(:each) do |example| |
||||
next unless example.metadata[:with_direct_uploads] |
||||
|
||||
WithDirectUploads.new(self).before example |
||||
|
||||
class FogAttachment < Attachment |
||||
# Remounting the uploader overrides the original file setter taking care of setting, |
||||
# among other things, the content type. So we have to restore that original |
||||
# method this way. |
||||
# We do this in a new, separate class, as to not interfere with any other specs. |
||||
alias_method :set_file, :file= |
||||
mount_uploader :file, FogFileUploader |
||||
alias_method :file=, :set_file |
||||
end |
||||
end |
||||
|
||||
config.around(:each) do |example| |
||||
enabled = example.metadata[:with_direct_uploads] |
||||
|
||||
if enabled |
||||
WithDirectUploads.new(self).around example |
||||
else |
||||
example.run |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue