309 lines
8.0 KiB
Ruby
309 lines
8.0 KiB
Ruby
|
module Gitlab
|
||
|
module Lfs
|
||
|
class Response
|
||
|
|
||
|
def initialize(project, user, request)
|
||
|
@origin_project = project
|
||
|
@project = storage_project(project)
|
||
|
@user = user
|
||
|
@env = request.env
|
||
|
@request = request
|
||
|
end
|
||
|
|
||
|
# Return a response for a download request
|
||
|
# Can be a response to:
|
||
|
# Request from a user to get the file
|
||
|
# Request from gitlab-workhorse which file to serve to the user
|
||
|
def render_download_hypermedia_response(oid)
|
||
|
render_response_to_download do
|
||
|
if check_download_accept_header?
|
||
|
render_lfs_download_hypermedia(oid)
|
||
|
else
|
||
|
render_not_found
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def render_download_object_response(oid)
|
||
|
render_response_to_download do
|
||
|
if check_download_sendfile_header? && check_download_accept_header?
|
||
|
render_lfs_sendfile(oid)
|
||
|
else
|
||
|
render_not_found
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def render_lfs_api_auth
|
||
|
render_response_to_push do
|
||
|
request_body = JSON.parse(@request.body.read)
|
||
|
return render_not_found if request_body.empty? || request_body['objects'].empty?
|
||
|
|
||
|
response = build_response(request_body['objects'])
|
||
|
[
|
||
|
200,
|
||
|
{
|
||
|
"Content-Type" => "application/json; charset=utf-8",
|
||
|
"Cache-Control" => "private",
|
||
|
},
|
||
|
[JSON.dump(response)]
|
||
|
]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def render_storage_upload_authorize_response(oid, size)
|
||
|
render_response_to_push do
|
||
|
[
|
||
|
200,
|
||
|
{ "Content-Type" => "application/json; charset=utf-8" },
|
||
|
[JSON.dump({
|
||
|
'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload",
|
||
|
'LfsOid' => oid,
|
||
|
'LfsSize' => size
|
||
|
})]
|
||
|
]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def render_storage_upload_store_response(oid, size, tmp_file_name)
|
||
|
render_response_to_push do
|
||
|
render_lfs_upload_ok(oid, size, tmp_file_name)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def render_not_enabled
|
||
|
[
|
||
|
501,
|
||
|
{
|
||
|
"Content-Type" => "application/vnd.git-lfs+json",
|
||
|
},
|
||
|
[JSON.dump({
|
||
|
'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
|
||
|
'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
|
||
|
})]
|
||
|
]
|
||
|
end
|
||
|
|
||
|
def render_unauthorized
|
||
|
[
|
||
|
401,
|
||
|
{
|
||
|
'Content-Type' => 'text/plain'
|
||
|
},
|
||
|
['Unauthorized']
|
||
|
]
|
||
|
end
|
||
|
|
||
|
def render_not_found
|
||
|
[
|
||
|
404,
|
||
|
{
|
||
|
"Content-Type" => "application/vnd.git-lfs+json"
|
||
|
},
|
||
|
[JSON.dump({
|
||
|
'message' => 'Not found.',
|
||
|
'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
|
||
|
})]
|
||
|
]
|
||
|
end
|
||
|
|
||
|
def render_forbidden
|
||
|
[
|
||
|
403,
|
||
|
{
|
||
|
"Content-Type" => "application/vnd.git-lfs+json"
|
||
|
},
|
||
|
[JSON.dump({
|
||
|
'message' => 'Access forbidden. Check your access level.',
|
||
|
'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
|
||
|
})]
|
||
|
]
|
||
|
end
|
||
|
|
||
|
def render_lfs_sendfile(oid)
|
||
|
return render_not_found unless oid.present?
|
||
|
|
||
|
lfs_object = object_for_download(oid)
|
||
|
|
||
|
if lfs_object && lfs_object.file.exists?
|
||
|
[
|
||
|
200,
|
||
|
{
|
||
|
# GitLab-workhorse will forward Content-Type header
|
||
|
"Content-Type" => "application/octet-stream",
|
||
|
"X-Sendfile" => lfs_object.file.path
|
||
|
},
|
||
|
[]
|
||
|
]
|
||
|
else
|
||
|
render_not_found
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def render_lfs_download_hypermedia(oid)
|
||
|
return render_not_found unless oid.present?
|
||
|
|
||
|
lfs_object = object_for_download(oid)
|
||
|
if lfs_object
|
||
|
[
|
||
|
200,
|
||
|
{ "Content-Type" => "application/vnd.git-lfs+json" },
|
||
|
[JSON.dump(download_hypermedia(oid))]
|
||
|
]
|
||
|
else
|
||
|
render_not_found
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def render_lfs_upload_ok(oid, size, tmp_file)
|
||
|
if store_file(oid, size, tmp_file)
|
||
|
[
|
||
|
200,
|
||
|
{
|
||
|
'Content-Type' => 'text/plain',
|
||
|
'Content-Length' => 0
|
||
|
},
|
||
|
[]
|
||
|
]
|
||
|
else
|
||
|
[
|
||
|
422,
|
||
|
{ 'Content-Type' => 'text/plain' },
|
||
|
["Unprocessable entity"]
|
||
|
]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def render_response_to_download
|
||
|
return render_not_enabled unless Gitlab.config.lfs.enabled
|
||
|
|
||
|
unless @project.public?
|
||
|
return render_unauthorized unless @user
|
||
|
return render_forbidden unless user_can_fetch?
|
||
|
end
|
||
|
|
||
|
yield
|
||
|
end
|
||
|
|
||
|
def render_response_to_push
|
||
|
return render_not_enabled unless Gitlab.config.lfs.enabled
|
||
|
return render_unauthorized unless @user
|
||
|
return render_forbidden unless user_can_push?
|
||
|
|
||
|
yield
|
||
|
end
|
||
|
|
||
|
def check_download_sendfile_header?
|
||
|
@env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
|
||
|
end
|
||
|
|
||
|
def check_download_accept_header?
|
||
|
@env['HTTP_ACCEPT'].to_s == "application/vnd.git-lfs+json; charset=utf-8"
|
||
|
end
|
||
|
|
||
|
def user_can_fetch?
|
||
|
# Check user access against the project they used to initiate the pull
|
||
|
@user.can?(:download_code, @origin_project)
|
||
|
end
|
||
|
|
||
|
def user_can_push?
|
||
|
# Check user access against the project they used to initiate the push
|
||
|
@user.can?(:push_code, @origin_project)
|
||
|
end
|
||
|
|
||
|
def storage_project(project)
|
||
|
if project.forked?
|
||
|
project.forked_from_project
|
||
|
else
|
||
|
project
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def store_file(oid, size, tmp_file)
|
||
|
tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
|
||
|
|
||
|
object = LfsObject.find_or_create_by(oid: oid, size: size)
|
||
|
if object.file.exists?
|
||
|
success = true
|
||
|
else
|
||
|
success = move_tmp_file_to_storage(object, tmp_file_path)
|
||
|
end
|
||
|
|
||
|
if success
|
||
|
success = link_to_project(object)
|
||
|
end
|
||
|
|
||
|
success
|
||
|
ensure
|
||
|
# Ensure that the tmp file is removed
|
||
|
FileUtils.rm_f(tmp_file_path)
|
||
|
end
|
||
|
|
||
|
def object_for_download(oid)
|
||
|
@project.lfs_objects.find_by(oid: oid)
|
||
|
end
|
||
|
|
||
|
def move_tmp_file_to_storage(object, path)
|
||
|
File.open(path) do |f|
|
||
|
object.file = f
|
||
|
end
|
||
|
|
||
|
object.file.store!
|
||
|
object.save
|
||
|
end
|
||
|
|
||
|
def link_to_project(object)
|
||
|
if object && !object.projects.exists?(@project)
|
||
|
object.projects << @project
|
||
|
object.save
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def select_existing_objects(objects)
|
||
|
objects_oids = objects.map { |o| o['oid'] }
|
||
|
@project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
|
||
|
end
|
||
|
|
||
|
def build_response(objects)
|
||
|
selected_objects = select_existing_objects(objects)
|
||
|
|
||
|
upload_hypermedia(objects, selected_objects)
|
||
|
end
|
||
|
|
||
|
def download_hypermedia(oid)
|
||
|
{
|
||
|
'_links' => {
|
||
|
'download' =>
|
||
|
{
|
||
|
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{oid}",
|
||
|
'header' => {
|
||
|
'Accept' => "application/vnd.git-lfs+json; charset=utf-8",
|
||
|
'Authorization' => @env['HTTP_AUTHORIZATION']
|
||
|
}.compact
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def upload_hypermedia(all_objects, existing_objects)
|
||
|
all_objects.each do |object|
|
||
|
object['_links'] = hypermedia_links(object) unless existing_objects.include?(object['oid'])
|
||
|
end
|
||
|
|
||
|
{ 'objects' => all_objects }
|
||
|
end
|
||
|
|
||
|
def hypermedia_links(object)
|
||
|
{
|
||
|
"upload" => {
|
||
|
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
|
||
|
'header' => { 'Authorization' => @env['HTTP_AUTHORIZATION'] }
|
||
|
}.compact
|
||
|
}
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|