2020-09-17 14:10:12 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
|
|
module Lfs
|
|
|
|
# Gitlab::Lfs::Client implements a simple LFS client, designed to talk to
|
|
|
|
# LFS servers as described in these documents:
|
|
|
|
# * https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
|
|
|
|
# * https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
|
|
|
|
class Client
|
2020-09-28 08:10:02 -04:00
|
|
|
GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs+json'
|
2020-10-01 08:10:14 -04:00
|
|
|
GIT_LFS_USER_AGENT = "GitLab #{Gitlab::VERSION} LFS client"
|
2020-09-28 08:10:02 -04:00
|
|
|
DEFAULT_HEADERS = {
|
|
|
|
'Accept' => GIT_LFS_CONTENT_TYPE,
|
2020-10-01 08:10:14 -04:00
|
|
|
'Content-Type' => GIT_LFS_CONTENT_TYPE,
|
|
|
|
'User-Agent' => GIT_LFS_USER_AGENT
|
2020-09-28 08:10:02 -04:00
|
|
|
}.freeze
|
|
|
|
|
2020-09-17 14:10:12 -04:00
|
|
|
attr_reader :base_url
|
|
|
|
|
|
|
|
def initialize(base_url, credentials:)
|
|
|
|
@base_url = base_url
|
|
|
|
@credentials = credentials
|
|
|
|
end
|
|
|
|
|
2020-09-28 08:10:02 -04:00
|
|
|
def batch!(operation, objects)
|
2020-09-17 14:10:12 -04:00
|
|
|
body = {
|
|
|
|
operation: operation,
|
|
|
|
transfers: ['basic'],
|
|
|
|
# We don't know `ref`, so can't send it
|
2020-09-28 08:10:02 -04:00
|
|
|
objects: objects.as_json(only: [:oid, :size])
|
2020-09-17 14:10:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
rsp = Gitlab::HTTP.post(
|
|
|
|
batch_url,
|
|
|
|
basic_auth: basic_auth,
|
|
|
|
body: body.to_json,
|
2020-09-28 08:10:02 -04:00
|
|
|
headers: build_request_headers
|
2020-09-17 14:10:12 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
raise BatchSubmitError unless rsp.success?
|
|
|
|
|
|
|
|
# HTTParty provides rsp.parsed_response, but it only kicks in for the
|
|
|
|
# application/json content type in the response, which we can't rely on
|
|
|
|
body = Gitlab::Json.parse(rsp.body)
|
|
|
|
transfer = body.fetch('transfer', 'basic')
|
|
|
|
|
2021-05-04 11:10:36 -04:00
|
|
|
raise UnsupportedTransferError, transfer.inspect unless transfer == 'basic'
|
2020-09-17 14:10:12 -04:00
|
|
|
|
|
|
|
body
|
|
|
|
end
|
|
|
|
|
2020-09-28 08:10:02 -04:00
|
|
|
def upload!(object, upload_action, authenticated:)
|
2020-09-17 14:10:12 -04:00
|
|
|
file = object.file.open
|
|
|
|
|
|
|
|
params = {
|
|
|
|
body_stream: file,
|
|
|
|
headers: {
|
|
|
|
'Content-Length' => object.size.to_s,
|
2020-10-01 08:10:14 -04:00
|
|
|
'Content-Type' => 'application/octet-stream',
|
|
|
|
'User-Agent' => GIT_LFS_USER_AGENT
|
2020-09-17 14:10:12 -04:00
|
|
|
}.merge(upload_action['header'] || {})
|
|
|
|
}
|
|
|
|
|
2020-10-05 14:08:51 -04:00
|
|
|
authenticated = true if params[:headers].key?('Authorization')
|
2020-09-17 14:10:12 -04:00
|
|
|
params[:basic_auth] = basic_auth unless authenticated
|
|
|
|
|
|
|
|
rsp = Gitlab::HTTP.put(upload_action['href'], params)
|
|
|
|
|
|
|
|
raise ObjectUploadError unless rsp.success?
|
|
|
|
ensure
|
|
|
|
file&.close
|
|
|
|
end
|
|
|
|
|
2020-09-28 08:10:02 -04:00
|
|
|
def verify!(object, verify_action, authenticated:)
|
|
|
|
params = {
|
|
|
|
body: object.to_json(only: [:oid, :size]),
|
|
|
|
headers: build_request_headers(verify_action['header'])
|
|
|
|
}
|
|
|
|
|
2020-10-05 14:08:51 -04:00
|
|
|
authenticated = true if params[:headers].key?('Authorization')
|
2020-09-28 08:10:02 -04:00
|
|
|
params[:basic_auth] = basic_auth unless authenticated
|
|
|
|
|
|
|
|
rsp = Gitlab::HTTP.post(verify_action['href'], params)
|
|
|
|
|
|
|
|
raise ObjectVerifyError unless rsp.success?
|
|
|
|
end
|
|
|
|
|
2020-09-17 14:10:12 -04:00
|
|
|
private
|
|
|
|
|
2020-09-28 08:10:02 -04:00
|
|
|
def build_request_headers(extra_headers = nil)
|
|
|
|
DEFAULT_HEADERS.merge(extra_headers || {})
|
|
|
|
end
|
|
|
|
|
2020-09-17 14:10:12 -04:00
|
|
|
attr_reader :credentials
|
|
|
|
|
|
|
|
def batch_url
|
|
|
|
base_url + '/info/lfs/objects/batch'
|
|
|
|
end
|
|
|
|
|
|
|
|
def basic_auth
|
2021-04-23 02:09:39 -04:00
|
|
|
# Some legacy credentials have a nil auth_method, which means password
|
|
|
|
# https://gitlab.com/gitlab-org/gitlab/-/issues/328674
|
|
|
|
return unless credentials.fetch(:auth_method, 'password') == 'password'
|
|
|
|
return if credentials.empty?
|
2020-09-17 14:10:12 -04:00
|
|
|
|
|
|
|
{ username: credentials[:user], password: credentials[:password] }
|
|
|
|
end
|
|
|
|
|
|
|
|
class BatchSubmitError < StandardError
|
|
|
|
def message
|
|
|
|
"Failed to submit batch"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class UnsupportedTransferError < StandardError
|
|
|
|
def initialize(transfer = nil)
|
|
|
|
super
|
|
|
|
@transfer = transfer
|
|
|
|
end
|
|
|
|
|
|
|
|
def message
|
|
|
|
"Unsupported transfer: #{@transfer}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class ObjectUploadError < StandardError
|
|
|
|
def message
|
|
|
|
"Failed to upload object"
|
|
|
|
end
|
|
|
|
end
|
2020-09-28 08:10:02 -04:00
|
|
|
|
|
|
|
class ObjectVerifyError < StandardError
|
|
|
|
def message
|
|
|
|
"Failed to verify object"
|
|
|
|
end
|
|
|
|
end
|
2020-09-17 14:10:12 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|