# frozen_string_literal: true # This service manages the whole worflow of discovering the Lfs files in a # repository, linking them to the project and downloading (and linking) the non # existent ones. module Projects module LfsPointers class LfsObjectDownloadListService < BaseService include Gitlab::Utils::StrongMemoize HEAD_REV = 'HEAD' LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch' LfsObjectDownloadListError = Class.new(StandardError) def execute return [] unless project&.lfs_enabled? if external_lfs_endpoint? # If the endpoint host is different from the import_url it means # that the repo is using a third party service for storing the LFS files. # In this case, we have to disable lfs in the project disable_lfs! return [] end # Getting all Lfs pointers already in the database and linking them to the project linked_oids = LfsLinkService.new(project).execute(lfs_pointers_in_repository.keys) # Retrieving those oids not present in the database which we need to download missing_oids = lfs_pointers_in_repository.except(*linked_oids) # Downloading the required information and gathering it inside a LfsDownloadObject for each oid LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(missing_oids) rescue LfsDownloadLinkListService::DownloadLinksError => e raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}" end private def external_lfs_endpoint? lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host end def disable_lfs! unless project.update(lfs_enabled: false) raise LfsDownloadLinkListService::DownloadLinksError, "Invalid project state" end end # Retrieves all lfs pointers in the repository def lfs_pointers_in_repository @lfs_pointers_in_repository ||= LfsListService.new(project).execute end def lfsconfig_endpoint_uri strong_memoize(:lfsconfig_endpoint_uri) do # Retrieveing the blob data from the .lfsconfig file data = project.repository.lfsconfig_for(HEAD_REV) # Parsing the data to retrieve the url parsed_data = data&.match(LFS_ENDPOINT_PATTERN) if parsed_data URI.parse(parsed_data[1]).tap do |endpoint| endpoint.user ||= import_uri.user endpoint.password ||= import_uri.password end end end rescue URI::InvalidURIError raise LfsObjectDownloadListError, 'Invalid URL in .lfsconfig file' end def import_uri @import_uri ||= URI.parse(project.import_url) rescue URI::InvalidURIError raise LfsObjectDownloadListError, 'Invalid project import URL' end def current_endpoint_uri (lfsconfig_endpoint_uri || default_endpoint_uri) end # The import url must end with '.git' here we ensure it is def default_endpoint_uri @default_endpoint_uri ||= begin import_uri.dup.tap do |uri| path = uri.path.gsub(%r(/$), '') path += '.git' unless path.ends_with?('.git') uri.path = path + LFS_BATCH_API_ENDPOINT end end end end end end