Commit febf5782 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'fj-snippet-image-blob-render' into 'master'

Render snippet blobs

Closes #212584

See merge request gitlab-org/gitlab!28085
parents 74096b27 ed40d508
...@@ -8,16 +8,16 @@ module SendsBlob ...@@ -8,16 +8,16 @@ module SendsBlob
include SendFileUpload include SendFileUpload
end end
def send_blob(repository, blob, params = {}) def send_blob(repository, blob, inline: true, allow_caching: false)
if blob if blob
headers['X-Content-Type-Options'] = 'nosniff' headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?(blob) return if cached_blob?(blob, allow_caching: allow_caching)
if blob.stored_externally? if blob.stored_externally?
send_lfs_object(blob) send_lfs_object(blob, repository.project)
else else
send_git_blob(repository, blob, params) send_git_blob(repository, blob, inline: inline)
end end
else else
render_404 render_404
...@@ -26,11 +26,11 @@ module SendsBlob ...@@ -26,11 +26,11 @@ module SendsBlob
private private
def cached_blob?(blob) def cached_blob?(blob, allow_caching: false)
stale = stale?(etag: blob.id) # The #stale? method sets cache headers. stale = stale?(etag: blob.id) # The #stale? method sets cache headers.
# Because we are opinionated we set the cache headers ourselves. # Because we are opinionated we set the cache headers ourselves.
response.cache_control[:public] = project.public? response.cache_control[:public] = allow_caching
response.cache_control[:max_age] = response.cache_control[:max_age] =
if @ref && @commit && @ref == @commit.id # rubocop:disable Gitlab/ModuleWithInstanceVariables if @ref && @commit && @ref == @commit.id # rubocop:disable Gitlab/ModuleWithInstanceVariables
...@@ -48,7 +48,7 @@ module SendsBlob ...@@ -48,7 +48,7 @@ module SendsBlob
!stale !stale
end end
def send_lfs_object(blob) def send_lfs_object(blob, project)
lfs_object = find_lfs_object(blob) lfs_object = find_lfs_object(blob)
if lfs_object && lfs_object.project_allowed_access?(project) if lfs_object && lfs_object.project_allowed_access?(project)
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
module SnippetsActions module SnippetsActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
include SendsBlob
def edit def edit
# We need to load some info from the existing blob # We need to load some info from the existing blob
...@@ -12,16 +13,26 @@ module SnippetsActions ...@@ -12,16 +13,26 @@ module SnippetsActions
end end
def raw def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
workhorse_set_content_type! workhorse_set_content_type!
send_data( # Until we don't migrate all snippets to version
convert_line_endings(blob.data), # snippets we need to support old `SnippetBlob`
type: 'text/plain; charset=utf-8', # blobs
disposition: disposition, if defined?(blob.snippet)
filename: Snippet.sanitized_file_name(blob.name) send_data(
) convert_line_endings(blob.data),
type: 'text/plain; charset=utf-8',
disposition: content_disposition,
filename: Snippet.sanitized_file_name(blob.name)
)
else
send_blob(
snippet.repository,
blob,
inline: content_disposition == 'inline',
allow_caching: snippet.public?
)
end
end end
def js_request? def js_request?
...@@ -30,6 +41,10 @@ module SnippetsActions ...@@ -30,6 +41,10 @@ module SnippetsActions
private private
def content_disposition
@disposition ||= params[:inline] == 'false' ? 'attachment' : 'inline'
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def blob def blob
return unless snippet return unless snippet
......
...@@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController
def show def show
@blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git) @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
send_blob(@repository, @blob) send_blob(@repository, @blob, allow_caching: @project.public?)
end end
def destroy def destroy
......
...@@ -17,7 +17,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -17,7 +17,7 @@ class Projects::RawController < Projects::ApplicationController
def show def show
@blob = @repository.blob_at(@commit.id, @path) @blob = @repository.blob_at(@commit.id, @path)
send_blob(@repository, @blob, inline: (params[:inline] != 'false')) send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching: @project.public?)
end end
private private
......
...@@ -45,7 +45,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -45,7 +45,7 @@ class Projects::WikisController < Projects::ApplicationController
render 'show' render 'show'
elsif file_blob elsif file_blob
send_blob(@project_wiki.repository, file_blob) send_blob(@project_wiki.repository, file_blob, allow_caching: @project.public?)
elsif show_create_form? elsif show_create_form?
# Assign a title to the WikiPage unless `id` is a randomly generated slug from #new # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new
title = params[:id] unless params[:random_title].present? title = params[:id] unless params[:random_title].present?
......
---
title: Render snippet repository blobs
merge_request: 28085
author:
type: changed
...@@ -12,7 +12,7 @@ module Projects ...@@ -12,7 +12,7 @@ module Projects
def show def show
blob = design_repository.blob_at(ref, design.full_path) blob = design_repository.blob_at(ref, design.full_path)
send_blob(design_repository, blob, { inline: false }) send_blob(design_repository, blob, inline: false, allow_caching: project.public?)
end end
private private
......
...@@ -52,6 +52,8 @@ describe Projects::DesignManagement::Designs::RawImagesController do ...@@ -52,6 +52,8 @@ describe Projects::DesignManagement::Designs::RawImagesController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it_behaves_like 'project cache control headers'
context 'when the user does not have permission' do context 'when the user does not have permission' do
let_it_be(:viewer) { create(:user) } let_it_be(:viewer) { create(:user) }
......
...@@ -225,8 +225,8 @@ module Gitlab ...@@ -225,8 +225,8 @@ module Gitlab
def gitaly_server_hash(repository) def gitaly_server_hash(repository)
{ {
address: Gitlab::GitalyClient.address(repository.project.repository_storage), address: Gitlab::GitalyClient.address(repository.container.repository_storage),
token: Gitlab::GitalyClient.token(repository.project.repository_storage), token: Gitlab::GitalyClient.token(repository.container.repository_storage),
features: Feature::Gitaly.server_feature_flags features: Feature::Gitaly.server_feature_flags
} }
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe Projects::AvatarsController do describe Projects::AvatarsController do
let(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
before do before do
controller.instance_variable_set(:@project, project) controller.instance_variable_set(:@project, project)
...@@ -34,15 +34,18 @@ describe Projects::AvatarsController do ...@@ -34,15 +34,18 @@ describe Projects::AvatarsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.header['Content-Disposition']).to eq('inline') expect(response.header['Content-Disposition']).to eq('inline')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
end end
it_behaves_like 'project cache control headers'
end end
context 'when the avatar is stored in lfs' do context 'when the avatar is stored in lfs' do
it_behaves_like 'a controller that can serve LFS files' do let(:filename) { 'lfs_object.iso' }
let(:filename) { 'lfs_object.iso' } let(:filepath) { "files/lfs/#{filename}" }
let(:filepath) { "files/lfs/#{filename}" }
end it_behaves_like 'a controller that can serve LFS files'
it_behaves_like 'project cache control headers'
end end
end end
end end
......
...@@ -6,6 +6,7 @@ describe Projects::RawController do ...@@ -6,6 +6,7 @@ describe Projects::RawController do
include RepoHelpers include RepoHelpers
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:inline) { nil }
describe 'GET #show' do describe 'GET #show' do
subject do subject do
...@@ -13,7 +14,8 @@ describe Projects::RawController do ...@@ -13,7 +14,8 @@ describe Projects::RawController do
params: { params: {
namespace_id: project.namespace, namespace_id: project.namespace,
project_id: project, project_id: project,
id: filepath id: filepath,
inline: inline
}) })
end end
...@@ -25,10 +27,12 @@ describe Projects::RawController do ...@@ -25,10 +27,12 @@ describe Projects::RawController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).to eq('inline') expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end end
it_behaves_like 'project cache control headers'
it_behaves_like 'content disposition headers'
end end
context 'image header' do context 'image header' do
...@@ -38,15 +42,20 @@ describe Projects::RawController do ...@@ -38,15 +42,20 @@ describe Projects::RawController do
subject subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.header['Content-Disposition']).to eq('inline')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end end
it_behaves_like 'project cache control headers'
it_behaves_like 'content disposition headers'
end end
it_behaves_like 'a controller that can serve LFS files' do context 'with LFS files' do
let(:filename) { 'lfs_object.iso' } let(:filename) { 'lfs_object.iso' }
let(:filepath) { "be93687/files/lfs/#{filename}" } let(:filepath) { "be93687/files/lfs/#{filename}" }
it_behaves_like 'a controller that can serve LFS files'
it_behaves_like 'project cache control headers'
end end
context 'when the endpoint receives requests above the limit', :clean_gitlab_redis_cache do context 'when the endpoint receives requests above the limit', :clean_gitlab_redis_cache do
......
...@@ -449,44 +449,76 @@ describe Projects::SnippetsController do ...@@ -449,44 +449,76 @@ describe Projects::SnippetsController do
end end
describe 'GET #raw' do describe 'GET #raw' do
let(:content) { "first line\r\nsecond line\r\nthird line" } let(:inline) { nil }
let(:formatted_content) { content.gsub(/\r\n/, "\n") } let(:line_ending) { nil }
let(:project_snippet) do let(:params) do
create( {
:project_snippet, :public, :repository, namespace_id: project.namespace,
project: project, project_id: project,
author: user, id: project_snippet.to_param,
content: content inline: inline,
) line_ending: line_ending
}
end end
let(:blob) { project_snippet.blobs.first }
context 'CRLF line ending' do subject { get :raw, params: params }
let(:params) do
{ context 'when repository is empty' do
namespace_id: project.namespace, let(:content) { "first line\r\nsecond line\r\nthird line" }
project_id: project, let(:formatted_content) { content.gsub(/\r\n/, "\n") }
id: project_snippet.to_param let(:project_snippet) do
} create(
:project_snippet, :public, :empty_repo,
project: project,
author: user,
content: content
)
end end
before do context 'CRLF line ending' do
allow_next_instance_of(Blob) do |instance| before do
allow(instance).to receive(:data).and_return(content) allow_next_instance_of(Blob) do |instance|
allow(instance).to receive(:data).and_return(content)
end
end end
end
it 'returns LF line endings by default' do it 'returns LF line endings by default' do
get :raw, params: params subject
expect(response.body).to eq(formatted_content) expect(response.body).to eq(formatted_content)
end
context 'when line_ending parameter present' do
let(:line_ending) { :raw }
it 'does not convert line endings' do
subject
expect(response.body).to eq(content)
end
end
end
end
context 'when repository is not empty' do
let(:project_snippet) do
create(
:project_snippet, :public, :repository,
project: project,
author: user
)
end end
it 'does not convert line endings when parameter present' do it 'sends the blob' do
get :raw, params: params.merge(line_ending: :raw) subject
expect(response.body).to eq(content) expect(response).to have_gitlab_http_status(:ok)
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
end end
it_behaves_like 'project cache control headers'
it_behaves_like 'content disposition headers'
end end
end end
......
...@@ -144,14 +144,12 @@ describe Projects::WikisController do ...@@ -144,14 +144,12 @@ describe Projects::WikisController do
let(:id) { upload_file_to_wiki(project, user, file_name) } let(:id) { upload_file_to_wiki(project, user, file_name) }
before do
subject
end
context 'when file is an image' do context 'when file is an image' do
let(:file_name) { 'dk.png' } let(:file_name) { 'dk.png' }
it 'delivers the image' do it 'delivers the image' do
subject
expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end end
...@@ -160,19 +158,27 @@ describe Projects::WikisController do ...@@ -160,19 +158,27 @@ describe Projects::WikisController do
let(:file_name) { 'unsanitized.svg' } let(:file_name) { 'unsanitized.svg' }
it 'delivers the image' do it 'delivers the image' do
subject
expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end end
end end
it_behaves_like 'project cache control headers'
end end
context 'when file is a pdf' do context 'when file is a pdf' do
let(:file_name) { 'git-cheat-sheet.pdf' } let(:file_name) { 'git-cheat-sheet.pdf' }
it 'sets the content type to sets the content response headers' do it 'sets the content type to sets the content response headers' do
subject
expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end end
it_behaves_like 'project cache control headers'
end end
end end
end end
......
...@@ -501,6 +501,11 @@ describe SnippetsController do ...@@ -501,6 +501,11 @@ describe SnippetsController do
end end
describe "GET #raw" do describe "GET #raw" do
let(:inline) { nil }
let(:params) { { id: snippet.to_param, inline: inline } }
subject { get :raw, params: params }
shared_examples '200 status' do shared_examples '200 status' do
before do before do
subject subject
...@@ -511,11 +516,6 @@ describe SnippetsController do ...@@ -511,11 +516,6 @@ describe SnippetsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it 'has expected headers' do
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).to match(/inline/)
end
it "sets #{Gitlab::Workhorse::DETECT_HEADER} header" do it "sets #{Gitlab::Workhorse::DETECT_HEADER} header" do
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
end end
...@@ -551,12 +551,20 @@ describe SnippetsController do ...@@ -551,12 +551,20 @@ describe SnippetsController do
shared_examples 'successful response' do shared_examples 'successful response' do
it_behaves_like '200 status' it_behaves_like '200 status'
it_behaves_like 'CRLF line ending'
it 'returns snippet first blob data' do it 'has expected blob headers' do
subject subject
expect(response.body).to eq snippet.blobs.first.data expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
end
it_behaves_like 'content disposition headers'
it 'sets cache_control public header based on snippet visibility' do
subject
expect(response.cache_control[:public]).to eq snippet.public?
end end
context 'when feature flag version_snippets is disabled' do context 'when feature flag version_snippets is disabled' do
...@@ -571,12 +579,33 @@ describe SnippetsController do ...@@ -571,12 +579,33 @@ describe SnippetsController do
subject subject
expect(response.body).to eq snippet.content expect(response.body).to eq snippet.content
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
end end
it_behaves_like 'content disposition headers'
end
context 'when snippet repository is empty' do
before do
allow_any_instance_of(Repository).to receive(:empty?).and_return(true)
end
it_behaves_like '200 status'
it_behaves_like 'CRLF line ending'
it 'returns snippet database content' do
subject
expect(response.body).to eq snippet.content
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
end
it_behaves_like 'content disposition headers'
end end
end end
context 'when the personal snippet is private' do context 'when the personal snippet is private' do
let_it_be(:personal_snippet) { create(:personal_snippet, :private, :repository, author: user) } let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: user) }
context 'when signed in' do context 'when signed in' do
before do before do
...@@ -595,18 +624,13 @@ describe SnippetsController do ...@@ -595,18 +624,13 @@ describe SnippetsController do
end end
context 'when signed in user is the author' do context 'when signed in user is the author' do
it_behaves_like 'successful response' do it_behaves_like 'successful response'
let(:snippet) { personal_snippet }
let(:params) { { id: snippet.to_param } }
subject { get :raw, params: params }
end
end end
end end
context 'when not signed in' do context 'when not signed in' do
it 'redirects to the sign in page' do it 'redirects to the sign in page' do
get :raw, params: { id: personal_snippet.to_param } subject
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end end
...@@ -614,24 +638,19 @@ describe SnippetsController do ...@@ -614,24 +638,19 @@ describe SnippetsController do
end end
context 'when the personal snippet is internal' do context 'when the personal snippet is internal' do
let_it_be(:personal_snippet) { create(:personal_snippet, :internal, :repository, author: user) } let_it_be(:snippet) { create(:personal_snippet, :internal, :repository, author: user) }
context 'when signed in' do context 'when signed in' do
before do before do
sign_in(user) sign_in(user)
end end
it_behaves_like 'successful response' do it_behaves_like 'successful response'
let(:snippet) { personal_snippet }
let(:params) { { id: snippet.to_param } }
subject { get :raw, params: params }
end
end end
context 'when not signed in' do context 'when not signed in' do
it 'redirects to the sign in page' do it 'redirects to the sign in page' do
get :raw, params: { id: personal_snippet.to_param } subject
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end end
...@@ -639,26 +658,21 @@ describe SnippetsController do ...@@ -639,26 +658,21 @@ describe SnippetsController do
end end
context 'when the personal snippet is public' do context 'when the personal snippet is public' do
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository, author: user) } let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: user) }
context 'when signed in' do context 'when signed in' do
before do before do
sign_in(user) sign_in(user)
end end
it_behaves_like 'successful response' do it_behaves_like 'successful response'
let(:snippet) { personal_snippet }
let(:params) { { id: snippet.to_param } }
subject { get :raw, params: params }
end
end end
context 'when not signed in' do context 'when not signed in' do
it 'responds with status 200' do it 'responds with status 200' do
get :raw, params: { id: personal_snippet.to_param } subject
expect(assigns(:snippet)).to eq(personal_snippet) expect(assigns(:snippet)).to eq(snippet)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
end end
......
# frozen_string_literal: true
RSpec.shared_examples 'project cache control headers' do
before do
project.update(visibility_level: visibility_level)
end
context 'when project is public' do
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
it 'returns cache_control public header to true' do
subject
expect(response.cache_control[:public]).to be_truthy
end
end
context 'when project is private' do
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
it 'returns cache_control public header to true' do
subject
expect(response.cache_control[:public]).to be_falsey
end
end
context 'when project is internal' do
let(:visibility_level) { Gitlab::VisibilityLevel::INTERNAL }
it 'returns cache_control public header to true' do
subject
expect(response.cache_control[:public]).to be_falsey
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'content disposition headers' do
it 'sets content disposition to inline' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.header['Content-Disposition']).to match(/inline/)
end
context 'when inline param is false' do
let(:inline) { 'false' }
it 'sets content disposition to attachment' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.header['Content-Disposition']).to match(/attachment/)
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