Commit ba608dc0 authored by Sean McGivern's avatar Sean McGivern

Merge branch '12910-uploader-pers-snippet' into 'master'

Prepare uploaders for personal snippets comments

See merge request !11022
parents ef71bf62 43ff7386
...@@ -5,7 +5,7 @@ require('./preview_markdown'); ...@@ -5,7 +5,7 @@ require('./preview_markdown');
window.DropzoneInput = (function() { window.DropzoneInput = (function() {
function DropzoneInput(form) { function DropzoneInput(form) {
var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress;
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
...@@ -16,7 +16,7 @@ window.DropzoneInput = (function() { ...@@ -16,7 +16,7 @@ window.DropzoneInput = (function() {
iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"; iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
uploadProgress = $("<div class=\"div-dropzone-progress\"></div>"); uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>"; btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
project_uploads_path = window.project_uploads_path || null; uploads_path = window.uploads_path || null;
max_file_size = gon.max_file_size || 10; max_file_size = gon.max_file_size || 10;
form_textarea = $(form).find(".js-gfm-input"); form_textarea = $(form).find(".js-gfm-input");
form_textarea.wrap("<div class=\"div-dropzone\"></div>"); form_textarea.wrap("<div class=\"div-dropzone\"></div>");
...@@ -39,10 +39,10 @@ window.DropzoneInput = (function() { ...@@ -39,10 +39,10 @@ window.DropzoneInput = (function() {
"display": "none" "display": "none"
}); });
if (!project_uploads_path) return; if (!uploads_path) return;
dropzone = form_dropzone.dropzone({ dropzone = form_dropzone.dropzone({
url: project_uploads_path, url: uploads_path,
dictDefaultMessage: "", dictDefaultMessage: "",
clickable: true, clickable: true,
paramName: "file", paramName: "file",
...@@ -159,7 +159,7 @@ window.DropzoneInput = (function() { ...@@ -159,7 +159,7 @@ window.DropzoneInput = (function() {
formData = new FormData(); formData = new FormData();
formData.append("file", item, filename); formData.append("file", item, filename);
return $.ajax({ return $.ajax({
url: project_uploads_path, url: uploads_path,
type: "POST", type: "POST",
data: formData, data: formData,
dataType: "json", dataType: "json",
......
module UploadsActions
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
respond_to do |format|
if link_to_file
format.json do
render json: { link: link_to_file }
end
else
format.json do
render json: 'Invalid file.', status: :unprocessable_entity
end
end
end
end
def show
return render_404 unless uploader.exists?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
end
class Projects::UploadsController < Projects::ApplicationController class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
skip_before_action :project, :repository, skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? } if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create] before_action :authorize_upload_file!, only: [:create]
def create
link_to_file = ::Projects::UploadService.new(project, params[:file]).
execute
respond_to do |format|
if link_to_file
format.json do
render json: { link: link_to_file }
end
else
format.json do
render json: 'Invalid file.', status: :unprocessable_entity
end
end
end
end
def show
return render_404 if uploader.nil? || !uploader.file.exists?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
send_file uploader.file.path, disposition: disposition
end
private private
def uploader def uploader
...@@ -52,4 +30,10 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -52,4 +30,10 @@ class Projects::UploadsController < Projects::ApplicationController
def image_or_video? def image_or_video?
uploader && uploader.file.exists? && uploader.image_or_video? uploader && uploader.file.exists? && uploader.image_or_video?
end end
def uploader_class
FileUploader
end
alias_method :model, :project
end end
class UploadsController < ApplicationController class UploadsController < ApplicationController
skip_before_action :authenticate_user! include UploadsActions
before_action :find_model, :authorize_access!
def show
uploader = @model.send(upload_mount)
unless uploader.file_storage?
return redirect_to uploader.url
end
unless uploader.file && uploader.file.exists? skip_before_action :authenticate_user!
return render_404 before_action :find_model
end before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create]
disposition = uploader.image? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
private private
def find_model def find_model
unless upload_model && upload_mount return render_404 unless upload_model && upload_mount
return render_404
end
@model = upload_model.find(params[:id]) @model = upload_model.find(params[:id])
end end
def authorize_access! def authorize_access!
authorized = authorized =
case @model case model
when Project
can?(current_user, :read_project, @model)
when Group
can?(current_user, :read_group, @model)
when Note when Note
can?(current_user, :read_project, @model.project) can?(current_user, :read_project, model.project)
else when User
# No authentication required for user avatars.
true true
else
permission = "read_#{model.class.to_s.underscore}".to_sym
can?(current_user, permission, model)
end
render_unauthorized unless authorized
end end
return if authorized def authorize_create_access!
# for now we support only personal snippets comments
authorized = can?(current_user, :comment_personal_snippet, model)
render_unauthorized unless authorized
end
def render_unauthorized
if current_user if current_user
render_404 render_404
else else
...@@ -58,17 +51,44 @@ class UploadsController < ApplicationController ...@@ -58,17 +51,44 @@ class UploadsController < ApplicationController
"project" => Project, "project" => Project,
"note" => Note, "note" => Note,
"group" => Group, "group" => Group,
"appearance" => Appearance "appearance" => Appearance,
"personal_snippet" => PersonalSnippet
} }
upload_models[params[:model]] upload_models[params[:model]]
end end
def upload_mount def upload_mount
return true unless params[:mounted_as]
upload_mounts = %w(avatar attachment file logo header_logo) upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as]) if upload_mounts.include?(params[:mounted_as])
params[:mounted_as] params[:mounted_as]
end end
end end
def uploader
return @uploader if defined?(@uploader)
if model.is_a?(PersonalSnippet)
@uploader = PersonalFileUploader.new(model, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
else
@uploader = @model.send(upload_mount)
redirect_to @uploader.url unless @uploader.file_storage?
end
@uploader
end
def uploader_class
PersonalFileUploader
end
def model
@model ||= find_model
end
end end
...@@ -3,11 +3,16 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -3,11 +3,16 @@ class PersonalSnippetPolicy < BasePolicy
can! :read_personal_snippet if @subject.public? can! :read_personal_snippet if @subject.public?
return unless @user return unless @user
if @subject.public?
can! :comment_personal_snippet
end
if @subject.author == @user if @subject.author == @user
can! :read_personal_snippet can! :read_personal_snippet
can! :update_personal_snippet can! :update_personal_snippet
can! :destroy_personal_snippet can! :destroy_personal_snippet
can! :admin_personal_snippet can! :admin_personal_snippet
can! :comment_personal_snippet
end end
unless @user.external? unless @user.external?
...@@ -16,6 +21,7 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -16,6 +21,7 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.internal? && !@user.external? if @subject.internal? && !@user.external?
can! :read_personal_snippet can! :read_personal_snippet
can! :comment_personal_snippet
end end
end end
end end
module Projects
class UploadService < BaseService
def initialize(project, file)
@project, @file = project, file
end
def execute
return nil unless @file && @file.size <= max_attachment_size
uploader = FileUploader.new(@project)
uploader.store!(@file)
uploader.to_h
end
private
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
end
end
class UploadService
def initialize(model, file, uploader_class = FileUploader)
@model, @file, @uploader_class = model, file, uploader_class
end
def execute
return nil unless @file && @file.size <= max_attachment_size
uploader = @uploader_class.new(@model)
uploader.store!(@file)
uploader.to_h
end
private
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
end
...@@ -30,8 +30,4 @@ class ArtifactUploader < GitlabUploader ...@@ -30,8 +30,4 @@ class ArtifactUploader < GitlabUploader
def filename def filename
file.try(:filename) file.try(:filename)
end end
def exists?
file.try(:exists?)
end
end end
...@@ -26,11 +26,11 @@ class FileUploader < GitlabUploader ...@@ -26,11 +26,11 @@ class FileUploader < GitlabUploader
File.join(CarrierWave.root, base_dir, model.path_with_namespace) File.join(CarrierWave.root, base_dir, model.path_with_namespace)
end end
attr_accessor :project attr_accessor :model
attr_reader :secret attr_reader :secret
def initialize(project, secret = nil) def initialize(model, secret = nil)
@project = project @model = model
@secret = secret || generate_secret @secret = secret || generate_secret
end end
...@@ -38,10 +38,6 @@ class FileUploader < GitlabUploader ...@@ -38,10 +38,6 @@ class FileUploader < GitlabUploader
File.join(dynamic_path_segment, @secret) File.join(dynamic_path_segment, @secret)
end end
def model
project
end
def relative_path def relative_path
self.file.path.sub("#{dynamic_path_segment}/", '') self.file.path.sub("#{dynamic_path_segment}/", '')
end end
......
...@@ -33,4 +33,8 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -33,4 +33,8 @@ class GitlabUploader < CarrierWave::Uploader::Base
def relative_path def relative_path
self.file.path.sub("#{root}/", '') self.file.path.sub("#{root}/", '')
end end
def exists?
file.try(:exists?)
end
end end
...@@ -9,10 +9,6 @@ class LfsObjectUploader < GitlabUploader ...@@ -9,10 +9,6 @@ class LfsObjectUploader < GitlabUploader
"#{Gitlab.config.lfs.storage_path}/tmp/cache" "#{Gitlab.config.lfs.storage_path}/tmp/cache"
end end
def exists?
file.try(:exists?)
end
def filename def filename
model.oid[4..-1] model.oid[4..-1]
end end
......
class PersonalFileUploader < FileUploader
def self.dynamic_path_segment(model)
File.join(CarrierWave.root, model_path(model))
end
private
def secure_url
File.join(self.class.model_path(model), secret, file.filename)
end
def self.model_path(model)
File.join("/#{base_dir}", model.class.to_s.underscore, model.id.to_s)
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
- preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project) - preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project)
- if current_user - if current_user
:javascript :javascript
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path}"; window.preview_markdown_path = "#{preview_markdown_path}";
- content_for :header_content do - content_for :header_content do
......
---
title: Support uploaders for personal snippets comments
merge_request:
author:
...@@ -4,6 +4,11 @@ scope path: :uploads do ...@@ -4,6 +4,11 @@ scope path: :uploads do
to: "uploads#show", to: "uploads#show",
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
# show uploads for models, snippets (notes) available for now
get ':model/:id/:secret/:filename',
to: 'uploads#show',
constraints: { model: /personal_snippet/, id: /\d+/, filename: /[^\/]+/ }
# Appearance # Appearance
get ":model/:mounted_as/:id/:filename", get ":model/:mounted_as/:id/:filename",
to: "uploads#show", to: "uploads#show",
...@@ -13,6 +18,12 @@ scope path: :uploads do ...@@ -13,6 +18,12 @@ scope path: :uploads do
get ":namespace_id/:project_id/:secret/:filename", get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show", to: "projects/uploads#show",
constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ } constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
# create uploads for models, snippets (notes) available for now
post ':model/:id/',
to: 'uploads#create',
constraints: { model: /personal_snippet/, id: /\d+/ },
as: 'upload'
end end
# Redirect old note attachments path to new uploads path. # Redirect old note attachments path to new uploads path.
......
...@@ -385,7 +385,7 @@ module API ...@@ -385,7 +385,7 @@ module API
requires :file, type: File, desc: 'The file to be uploaded' requires :file, type: File, desc: 'The file to be uploaded'
end end
post ":id/uploads" do post ":id/uploads" do
::Projects::UploadService.new(user_project, params[:file]).execute UploadService.new(user_project, params[:file]).execute
end end
desc 'Get the users list of a project' do desc 'Get the users list of a project' do
......
...@@ -452,7 +452,7 @@ module API ...@@ -452,7 +452,7 @@ module API
requires :file, type: File, desc: 'The file to be uploaded' requires :file, type: File, desc: 'The file to be uploaded'
end end
post ":id/uploads" do post ":id/uploads" do
::Projects::UploadService.new(user_project, params[:file]).execute UploadService.new(user_project, params[:file]).execute
end end
desc 'Get the users list of a project' do desc 'Get the users list of a project' do
......
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
content_type: attachment.content_type content_type: attachment.content_type
} }
link = ::Projects::UploadService.new(project, file).execute link = UploadService.new(project, file).execute
attachments << link if link attachments << link if link
ensure ensure
tmp.close! tmp.close!
......
...@@ -8,6 +8,93 @@ end ...@@ -8,6 +8,93 @@ end
describe UploadsController do describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
describe 'POST create' do
let(:model) { 'personal_snippet' }
let(:snippet) { create(:personal_snippet, :public) }
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
context 'when a user does not have permissions to upload a file' do
it "returns 401 when the user is not logged in" do
post :create, model: model, id: snippet.id, format: :json
expect(response).to have_http_status(401)
end
it "returns 404 when user can't comment on a snippet" do
private_snippet = create(:personal_snippet, :private)
sign_in(user)
post :create, model: model, id: private_snippet.id, format: :json
expect(response).to have_http_status(404)
end
end
context 'when a user is logged in' do
before do
sign_in(user)
end
it "returns an error without file" do
post :create, model: model, id: snippet.id, format: :json
expect(response).to have_http_status(422)
end
it "returns an error with invalid model" do
expect { post :create, model: 'invalid', id: snippet.id, format: :json }
.to raise_error(ActionController::UrlGenerationError)
end
it "returns 404 status when object not found" do
post :create, model: model, id: 9999, format: :json
expect(response).to have_http_status(404)
end
context 'with valid image' do
before do
post :create, model: 'personal_snippet', id: snippet.id, file: jpg, format: :json
end
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"/uploads"
end
it 'creates a corresponding Upload record' do
upload = Upload.last
aggregate_failures do
expect(upload).to exist
expect(upload.model).to eq snippet
end
end
end
context 'with valid non-image file' do
before do
post :create, model: 'personal_snippet', id: snippet.id, file: txt, format: :json
end
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
expect(response.body).to match "\"url\":\"/uploads"
end
it 'creates a corresponding Upload record' do
upload = Upload.last
aggregate_failures do
expect(upload).to exist
expect(upload.model).to eq snippet
end
end
end
end
end
describe "GET show" do describe "GET show" do
context 'Content-Disposition security measures' do context 'Content-Disposition security measures' do
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
......
require 'spec_helper'
describe PersonalSnippetPolicy, models: true do
let(:regular_user) { create(:user) }
let(:external_user) { create(:user, :external) }
let(:admin_user) { create(:user, :admin) }
let(:author_permissions) do
[
:update_personal_snippet,
:admin_personal_snippet,
:destroy_personal_snippet
]
end
def permissions(user)
described_class.abilities(user, snippet).to_set
end
context 'public snippet' do
let(:snippet) { create(:personal_snippet, :public) }
context 'no user' do
subject { permissions(nil) }
it do
is_expected.to include(:read_personal_snippet)
is_expected.not_to include(:comment_personal_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'regular user' do
subject { permissions(regular_user) }
it do
is_expected.to include(:read_personal_snippet)
is_expected.to include(:comment_personal_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'author' do
subject { permissions(snippet.author) }
it do
is_expected.to include(:read_personal_snippet)
is_expected.to include(:comment_personal_snippet)
is_expected.to include(*author_permissions)
end
end
end
context 'internal snippet' do
let(:snippet) { create(:personal_snippet, :internal) }
context 'no user' do
subject { permissions(nil) }
it do
is_expected.not_to include(:read_personal_snippet)
is_expected.not_to include(:comment_personal_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'regular user' do
subject { permissions(regular_user) }
it do
is_expected.to include(:read_personal_snippet)
is_expected.to include(:comment_personal_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'external user' do
subject { permissions(external_user) }
it do
is_expected.not_to include(:read_personal_snippet)
is_expected.not_to include(:comment_personal_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'snippet author' do
subject { permissions(snippet.author) }
it do
is_expected.to include(:read_personal_snippet)
is_expected.to include(:comment_personal_snippet)
is_expected.to include(*author_permissions)
end
end
end
context 'private snippet' do
let(:snippet) { create(:project_snippet, :private) }
context 'no user' do
subject { permissions(nil) }
it do
is_expected.not_to include(:read_personal_snippet)
is_expected.not_to include(:comment_personal_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'regular user' do
subject { permissions(regular_user) }
it do
is_expected.not_to include(:read_personal_snippet)
is_expected.not_to include(:comment_personal_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'external user' do
subject { permissions(external_user) }
it do
is_expected.not_to include(:read_personal_snippet)
is_expected.not_to include(:comment_personal_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'snippet author' do
subject { permissions(snippet.author) }
it do
is_expected.to include(:read_personal_snippet)
is_expected.to include(:comment_personal_snippet)
is_expected.to include(*author_permissions)
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Projects::UploadService, services: true do describe UploadService, services: true do
describe 'File service' do describe 'File service' do
before do before do
@user = create(:user) @user = create(:user)
...@@ -68,6 +68,6 @@ describe Projects::UploadService, services: true do ...@@ -68,6 +68,6 @@ describe Projects::UploadService, services: true do
end end
def upload_file(project, file) def upload_file(project, file)
Projects::UploadService.new(project, file).execute described_class.new(project, file, FileUploader).execute
end end
end end
require 'spec_helper'
describe PersonalFileUploader do
let(:uploader) { described_class.new(build_stubbed(:empty_project)) }
let(:snippet) { create(:personal_snippet) }
describe '.absolute_path' do
it 'returns the correct absolute path by building it dynamically' do
upload = double(model: snippet, path: 'secret/foo.jpg')
dynamic_segment = "personal_snippet/#{snippet.id}"
expect(described_class.absolute_path(upload)).to end_with("#{dynamic_segment}/secret/foo.jpg")
end
end
describe '#to_h' do
it 'returns the hass' do
uploader = described_class.new(snippet, 'secret')
allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
expected_url = "/uploads/personal_snippet/#{snippet.id}/secret/file_name"
expect(uploader.to_h).to eq(
alt: 'file_name',
url: expected_url,
markdown: "[file_name](#{expected_url})"
)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment