##// END OF EJS Templates
py3: use pycompat.bytestr() on extra values because it can be int...
py3: use pycompat.bytestr() on extra values because it can be int One such example is {'close': 1} which marks a branch as closed. This makes test-convert-mtn.t pass on Python 3. Differential Revision: https://phab.mercurial-scm.org/D5864

File last commit:

r41485:1bc01490 default
r41715:b436059c default
Show More
blobstore.py
659 lines | 24.8 KiB | text/x-python | PythonLexer
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 # blobstore.py - local and remote (speaking Git-LFS protocol) blob storages
#
# Copyright 2017 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 import contextlib
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 import errno
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 import hashlib
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 import json
import os
import re
Matt Harbison
lfs: narrow the exceptions that trigger a transfer retry...
r35491 import socket
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: quiesce check-module-import warnings...
r35098 from mercurial.i18n import _
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 from mercurial import (
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 encoding,
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 error,
Pulkit Goyal
py3: use node.hex(m.digest()) instead of m.hexdigest()...
r40711 node,
Matt Harbison
lfs: override walk() in lfsvfs...
r35363 pathutil,
Augie Fackler
lfs: add some bytestring wrappers in blobstore.py...
r36619 pycompat,
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 url as urlmod,
util,
vfs as vfsmod,
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 worker,
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 )
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 from mercurial.utils import (
stringutil,
)
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281 from ..largefiles import lfutil
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 # 64 bytes for SHA256
Pulkit Goyal
py3: make sure regexes are bytes...
r36473 _lfsre = re.compile(br'\A[a-f0-9]{64}\Z')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
class lfsvfs(vfsmod.vfs):
def join(self, path):
"""split the path at first two characters, like: XX/XXXXX..."""
if not _lfsre.match(path):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise error.ProgrammingError(b'unexpected lfs path: %s' % path)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 return super(lfsvfs, self).join(path[0:2], path[2:])
Matt Harbison
lfs: override walk() in lfsvfs...
r35363 def walk(self, path=None, onerror=None):
Matt Harbison
lfs: correct the directory list value returned by lfsvfs.walk()...
r35397 """Yield (dirpath, [], oids) tuple for blobs under path
Matt Harbison
lfs: override walk() in lfsvfs...
r35363
Oids only exist in the root of this vfs, so dirpath is always ''.
"""
root = os.path.normpath(self.base)
# when dirpath == root, dirpath[prefixlen:] becomes empty
# because len(dirpath) < prefixlen.
prefixlen = len(pathutil.normasprefix(root))
oids = []
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 for dirpath, dirs, files in os.walk(self.reljoin(self.base, path
or b''),
Matt Harbison
lfs: override walk() in lfsvfs...
r35363 onerror=onerror):
dirpath = dirpath[prefixlen:]
# Silently skip unexpected files and directories
if len(dirpath) == 2:
oids.extend([dirpath + f for f in files
if _lfsre.match(dirpath + f)])
Matt Harbison
lfs: correct the directory list value returned by lfsvfs.walk()...
r35397 yield ('', [], oids)
Matt Harbison
lfs: override walk() in lfsvfs...
r35363
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 class nullvfs(lfsvfs):
def __init__(self):
pass
def exists(self, oid):
return False
def read(self, oid):
# store.read() calls into here if the blob doesn't exist in its
# self.vfs. Raise the same error as a normal vfs when asked to read a
# file that doesn't exist. The only difference is the full file path
# isn't available in the error.
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise IOError(errno.ENOENT,
pycompat.sysstr(b'%s: No such file or directory' % oid))
Matt Harbison
lfs: add the ability to disable the usercache...
r37535
def walk(self, path=None, onerror=None):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 return (b'', [], [])
Matt Harbison
lfs: add the ability to disable the usercache...
r37535
def write(self, oid, data):
pass
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 class filewithprogress(object):
"""a file-like object that supports __len__ and read.
Useful to provide progress information for how many bytes are read.
"""
def __init__(self, fp, callback):
self._fp = fp
self._callback = callback # func(readsize)
fp.seek(0, os.SEEK_END)
self._len = fp.tell()
fp.seek(0)
def __len__(self):
return self._len
def read(self, size):
if self._fp is None:
return b''
data = self._fp.read(size)
if data:
if self._callback:
self._callback(len(data))
else:
self._fp.close()
self._fp = None
return data
class local(object):
"""Local blobstore for large file contents.
This blobstore is used both as a cache and as a staging area for large blobs
to be uploaded to the remote blobstore.
"""
def __init__(self, repo):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 fullpath = repo.svfs.join(b'lfs/objects')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 self.vfs = lfsvfs(fullpath)
Matt Harbison
lfs: special case the null:// usercache instead of treating it as a url...
r37580
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if repo.ui.configbool(b'experimental', b'lfs.disableusercache'):
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 self.cachevfs = nullvfs()
else:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 usercache = lfutil._usercachedir(repo.ui, b'lfs')
Matt Harbison
lfs: special case the null:// usercache instead of treating it as a url...
r37580 self.cachevfs = lfsvfs(usercache)
Matt Harbison
lfs: add note messages indicating what store holds the lfs blob...
r35489 self.ui = repo.ui
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: add a local store method for opening a blob...
r35543 def open(self, oid):
"""Open a read-only file descriptor to the named blob, in either the
usercache or the local store."""
Matt Harbison
lfs: add a comment to describe subtle local blobstore open() behavior
r35555 # The usercache is the most likely place to hold the file. Commit will
# write to both it and the local store, as will anything that downloads
# the blobs. However, things like clone without an update won't
# populate the local store. For an init + push of a local clone,
# the usercache is the only place it _could_ be. If not present, the
# missing file msg here will indicate the local repo, not the usercache.
Matt Harbison
lfs: add a local store method for opening a blob...
r35543 if self.cachevfs.exists(oid):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 return self.cachevfs(oid, b'rb')
Matt Harbison
lfs: add a local store method for opening a blob...
r35543
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 return self.vfs(oid, b'rb')
Matt Harbison
lfs: add a local store method for opening a blob...
r35543
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565 def download(self, oid, src):
"""Read the blob from the remote source in chunks, verify the content,
and write to this local blobstore."""
sha256 = hashlib.sha256()
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 with self.vfs(oid, b'wb', atomictemp=True) as fp:
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565 for chunk in util.filechunkiter(src, size=1048576):
fp.write(chunk)
sha256.update(chunk)
Pulkit Goyal
py3: use node.hex(m.digest()) instead of m.hexdigest()...
r40711 realoid = node.hex(sha256.digest())
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565 if realoid != oid:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise LfsCorruptionError(_(b'corrupt remote lfs object: %s')
Matt Harbison
lfs: gracefully handle aborts on the server when corrupt blobs are detected...
r37710 % oid)
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 self._linktousercache(oid)
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565
Matt Harbison
lfs: remove the verification option when writing to the local store...
r35567 def write(self, oid, data):
"""Write blob to local blobstore.
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492
Matt Harbison
lfs: remove the verification option when writing to the local store...
r35567 This should only be called from the filelog during a commit or similar.
As such, there is no need to verify the data. Imports from a remote
store must use ``download()`` instead."""
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 with self.vfs(oid, b'wb', atomictemp=True) as fp:
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 fp.write(data)
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 self._linktousercache(oid)
Matt Harbison
lfs: ensure the blob is linked to the remote store on skipped uploads...
r39491 def linkfromusercache(self, oid):
"""Link blobs found in the user cache into this store.
The server module needs to do this when it lets the client know not to
upload the blob, to ensure it is always available in this store.
Normally this is done implicitly when the client reads or writes the
blob, but that doesn't happen when the server tells the client that it
already has the blob.
"""
if (not isinstance(self.cachevfs, nullvfs)
and not self.vfs.exists(oid)):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: found %s in the usercache\n') % oid)
Matt Harbison
lfs: ensure the blob is linked to the remote store on skipped uploads...
r39491 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 def _linktousercache(self, oid):
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281 # XXX: should we verify the content of the cache, and hardlink back to
# the local store on success, but truncate, write and link on failure?
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 if (not self.cachevfs.exists(oid)
and not isinstance(self.cachevfs, nullvfs)):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: adding %s to the usercache\n') % oid)
Matt Harbison
lfs: remove the verification option when writing to the local store...
r35567 lfutil.link(self.vfs.join(oid), self.cachevfs.join(oid))
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 def read(self, oid, verify=True):
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 """Read blob from local blobstore."""
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281 if not self.vfs.exists(oid):
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 blob = self._read(self.cachevfs, oid, verify)
Matt Harbison
lfs: only hardlink between the usercache and local store if the blob verifies...
r35493
# Even if revlog will verify the content, it needs to be verified
# now before making the hardlink to avoid propagating corrupt blobs.
# Don't abort if corruption is detected, because `hg verify` will
# give more useful info about the corruption- simply don't add the
# hardlink.
Pulkit Goyal
py3: use node.hex(m.digest()) instead of m.hexdigest()...
r40711 if verify or node.hex(hashlib.sha256(blob).digest()) == oid:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: found %s in the usercache\n') % oid)
Matt Harbison
lfs: only hardlink between the usercache and local store if the blob verifies...
r35493 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
Matt Harbison
lfs: add note messages indicating what store holds the lfs blob...
r35489 else:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: found %s in the local lfs store\n') % oid)
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 blob = self._read(self.vfs, oid, verify)
return blob
def _read(self, vfs, oid, verify):
"""Read blob (after verifying) from the given store"""
blob = vfs.read(oid)
if verify:
_verify(oid, blob)
return blob
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: add a blob verification method to the local store...
r37163 def verify(self, oid):
"""Indicate whether or not the hash of the underlying file matches its
name."""
sha256 = hashlib.sha256()
with self.open(oid) as fp:
for chunk in util.filechunkiter(fp, size=1048576):
sha256.update(chunk)
Pulkit Goyal
py3: use node.hex(m.digest()) instead of m.hexdigest()...
r40711 return oid == node.hex(sha256.digest())
Matt Harbison
lfs: add a blob verification method to the local store...
r37163
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 def has(self, oid):
"""Returns True if the local blobstore contains the requested blob,
False otherwise."""
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281 return self.cachevfs.exists(oid) or self.vfs.exists(oid)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 def _urlerrorreason(urlerror):
'''Create a friendly message for the given URLError to be used in an
LfsRemoteError message.
'''
inst = urlerror
if isinstance(urlerror.reason, Exception):
inst = urlerror.reason
if util.safehasattr(inst, 'reason'):
try: # usually it is in the form (errno, strerror)
reason = inst.reason.args[1]
except (AttributeError, IndexError):
# it might be anything, for example a string
reason = inst.reason
if isinstance(reason, pycompat.unicode):
# SSLError of Python 2.7.9 contains a unicode
reason = encoding.unitolocal(reason)
return reason
elif getattr(inst, "strerror", None):
return encoding.strtolocal(inst.strerror)
else:
return stringutil.forcebytestr(urlerror)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 class _gitlfsremote(object):
def __init__(self, repo, url):
ui = repo.ui
self.ui = ui
baseurl, authinfo = url.authinfo()
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.baseurl = baseurl.rstrip(b'/')
useragent = repo.ui.config(b'experimental', b'lfs.user-agent')
Matt Harbison
lfs: add an experimental config to override User-Agent for the blob transfer...
r35456 if not useragent:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 useragent = b'git-lfs/2.3.4 (Mercurial %s)' % util.version()
Matt Harbison
lfs: add git to the User-Agent header for blob transfers...
r35455 self.urlopener = urlmod.opener(ui, authinfo, useragent)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.retry = ui.configint(b'lfs', b'retry')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def writebatch(self, pointers, fromstore):
"""Batch upload from local to remote blobstore."""
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self._batch(_deduplicate(pointers), fromstore, b'upload')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def readbatch(self, pointers, tostore):
"""Batch download from remote to local blostore."""
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self._batch(_deduplicate(pointers), tostore, b'download')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def _batchrequest(self, pointers, action):
"""Get metadata about objects pointed by pointers for given action
Return decoded JSON object like {'objects': [{'oid': '', 'size': 1}]}
See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
"""
Matt Harbison
py3: raw stringify various JSON and HTTP headers in the LFS blobstore module...
r41472 objects = [{r'oid': pycompat.strurl(p.oid()),
r'size': p.size()} for p in pointers]
requestdata = pycompat.bytesurl(json.dumps({
r'objects': objects,
r'operation': pycompat.strurl(action),
}))
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 url = b'%s/objects/batch' % self.baseurl
batchreq = util.urlreq.request(pycompat.strurl(url), data=requestdata)
Matt Harbison
py3: raw stringify various JSON and HTTP headers in the LFS blobstore module...
r41472 batchreq.add_header(r'Accept', r'application/vnd.git-lfs+json')
batchreq.add_header(r'Content-Type', r'application/vnd.git-lfs+json')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 try:
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 with contextlib.closing(self.urlopener.open(batchreq)) as rsp:
rawjson = rsp.read()
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 except util.urlerr.httperror as ex:
Matt Harbison
lfs: improve the hints for common errors in the Batch API...
r40696 hints = {
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 400: _(b'check that lfs serving is enabled on %s and "%s" is '
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 b'supported') % (self.baseurl, action),
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 404: _(b'the "lfs.url" config may be used to override %s')
Matt Harbison
lfs: improve the hints for common errors in the Batch API...
r40696 % self.baseurl,
}
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 hint = hints.get(ex.code, _(b'api=%s, action=%s') % (url, action))
raise LfsRemoteError(
_(b'LFS HTTP error: %s') % stringutil.forcebytestr(ex),
hint=hint)
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 except util.urlerr.urlerror as ex:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 hint = (_(b'the "lfs.url" config may be used to override %s')
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 % self.baseurl)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise LfsRemoteError(_(b'LFS error: %s') % _urlerrorreason(ex),
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 hint=hint)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 try:
response = json.loads(rawjson)
except ValueError:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise LfsRemoteError(_(b'LFS server returns invalid JSON: %s')
% rawjson.encode("utf-8"))
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944
if self.ui.debugflag:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.debug(b'Status: %d\n' % rsp.status)
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944 # lfs-test-server and hg serve return headers in different order
Matt Harbison
lfs: strip the response headers from the Batch API before printing...
r41476 headers = pycompat.bytestr(rsp.info()).strip()
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.debug(b'%s\n'
% b'\n'.join(sorted(headers.splitlines())))
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944
Matt Harbison
py3: raw stringify various JSON and HTTP headers in the LFS blobstore module...
r41472 if r'objects' in response:
response[r'objects'] = sorted(response[r'objects'],
key=lambda p: p[r'oid'])
self.ui.debug(b'%s\n'
% pycompat.bytesurl(
json.dumps(response, indent=2,
separators=(r'', r': '),
sort_keys=True)))
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 def encodestr(x):
if isinstance(x, pycompat.unicode):
return x.encode(u'utf-8')
return x
return pycompat.rapply(encodestr, response)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Jun Wu
lfs: remove internal url in test...
r35684 def _checkforservererror(self, pointers, responses, action):
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 """Scans errors from objects
Matt Harbison
lfs: correct documentation typo
r35712 Raises LfsRemoteError if any objects have an error"""
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 for response in responses:
Jun Wu
lfs: remove internal url in test...
r35684 # The server should return 404 when objects cannot be found. Some
# server implementation (ex. lfs-test-server) does not set "error"
# but just removes "download" from "actions". Treat that case
# as the same as 404 error.
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if b'error' not in response:
if (action == b'download'
and action not in response.get(b'actions', [])):
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 code = 404
Matt Harbison
lfs: raise an error if the server sends an unsolicited oid...
r35713 else:
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 continue
else:
# An error dict without a code doesn't make much sense, so
# treat as a server error.
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 code = response.get(b'error').get(b'code', 500)
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259
ptrmap = {p.oid(): p for p in pointers}
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 p = ptrmap.get(response[b'oid'], None)
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 if p:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 filename = getattr(p, 'filename', b'unknown')
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 errors = {
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 404: b'The object does not exist',
410: b'The object was removed by the owner',
422: b'Validation error',
500: b'Internal server error',
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 }
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 msg = errors.get(code, b'status code %d' % code)
raise LfsRemoteError(_(b'LFS server error for "%s": %s')
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 % (filename, msg))
else:
raise LfsRemoteError(
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 _(b'LFS server error. Unsolicited response for oid %s')
% response[b'oid'])
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def _extractobjects(self, response, pointers, action):
"""extract objects from response of the batch API
response: parsed JSON object returned by batch API
return response['objects'] filtered by action
raise if any object has an error
"""
# Scan errors from objects - fail early
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 objects = response.get(b'objects', [])
Jun Wu
lfs: remove internal url in test...
r35684 self._checkforservererror(pointers, objects, action)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
# Filter objects with given action. Practically, this skips uploading
# objects which exist in the server.
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 filteredobjects = [o for o in objects
if action in o.get(b'actions', [])]
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
return filteredobjects
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 def _basictransfer(self, obj, action, localstore):
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 """Download or upload a single object using basic transfer protocol
obj: dict, an object description returned by batch API
action: string, one of ['upload', 'download']
localstore: blobstore.local
See https://github.com/git-lfs/git-lfs/blob/master/docs/api/\
basic-transfers.md
"""
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 oid = obj[b'oid']
href = obj[b'actions'][action].get(b'href')
headers = obj[b'actions'][action].get(b'header', {}).items()
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 request = util.urlreq.request(pycompat.strurl(href))
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action == b'upload':
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 # If uploading blobs, read data from local blobstore.
Matt Harbison
lfs: drop a duplicate blob verification method
r37234 if not localstore.verify(oid):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise error.Abort(_(b'detected corrupt lfs object: %s') % oid,
hint=_(b'run hg verify'))
Matt Harbison
lfs: use the local store method for opening a blob...
r35544 request.data = filewithprogress(localstore.open(oid), None)
Matt Harbison
py3: raw stringify various JSON and HTTP headers in the LFS blobstore module...
r41472 request.get_method = lambda: r'PUT'
request.add_header(r'Content-Type', r'application/octet-stream')
Matt Harbison
lfs: explicitly add the Content-Length header when uploading blobs, for py3...
r41485 request.add_header(r'Content-Length', len(request.data))
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
for k, v in headers:
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 request.add_header(pycompat.strurl(k), pycompat.strurl(v))
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
response = b''
try:
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 with contextlib.closing(self.urlopener.open(request)) as req:
ui = self.ui # Shorten debug lines
if self.ui.debugflag:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 ui.debug(b'Status: %d\n' % req.status)
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 # lfs-test-server and hg serve return headers in different
# order
Matt Harbison
lfs: strip the response headers from the Batch API before printing...
r41476 headers = pycompat.bytestr(req.info()).strip()
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 ui.debug(b'%s\n'
% b'\n'.join(sorted(headers.splitlines())))
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action == b'download':
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 # If downloading blobs, store downloaded data to local
# blobstore
localstore.download(oid, req)
else:
while True:
data = req.read(1048576)
if not data:
break
response += data
if response:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 ui.debug(b'lfs %s response: %s' % (action, response))
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 except util.urlerr.httperror as ex:
Matt Harbison
lfs: dump the full response on httperror in debug mode...
r35752 if self.ui.debugflag:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.debug(b'%s: %s\n' % (oid, ex.read())) # XXX: also bytes?
raise LfsRemoteError(_(b'LFS HTTP error: %s (oid=%s, action=%s)')
% (stringutil.forcebytestr(ex), oid, action))
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 except util.urlerr.urlerror as ex:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 hint = (_(b'attempted connection to %s')
% pycompat.bytesurl(util.urllibcompat.getfullurl(request)))
raise LfsRemoteError(_(b'LFS error: %s') % _urlerrorreason(ex),
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 hint=hint)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def _batch(self, pointers, localstore, action):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action not in [b'upload', b'download']:
raise error.ProgrammingError(b'invalid Git-LFS action: %s' % action)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
response = self._batchrequest(pointers, action)
objects = self._extractobjects(response, pointers, action)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 total = sum(x.get(b'size', 0) for x in objects)
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 sizes = {}
for obj in objects:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 sizes[obj.get(b'oid')] = obj.get(b'size', 0)
topic = {b'upload': _(b'lfs uploading'),
b'download': _(b'lfs downloading')}[action]
Matt Harbison
lfs: use ui.note() and ui.debug() instead of ui.write() and their flags...
r35494 if len(objects) > 1:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: need to transfer %d objects (%s)\n')
Matt Harbison
lfs: use ui.note() and ui.debug() instead of ui.write() and their flags...
r35494 % (len(objects), util.bytecount(total)))
Matt Harbison
lfs: use a context manager to control the progress bar lifetime
r39426
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 def transfer(chunk):
for obj in chunk:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 objsize = obj.get(b'size', 0)
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 if self.ui.verbose:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action == b'download':
msg = _(b'lfs: downloading %s (%s)\n')
elif action == b'upload':
msg = _(b'lfs: uploading %s (%s)\n')
self.ui.note(msg % (obj.get(b'oid'),
Matt Harbison
lfs: use ui.note() and ui.debug() instead of ui.write() and their flags...
r35494 util.bytecount(objsize)))
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 retry = self.retry
while True:
try:
self._basictransfer(obj, action, localstore)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 yield 1, obj.get(b'oid')
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 break
Matt Harbison
lfs: narrow the exceptions that trigger a transfer retry...
r35491 except socket.error as ex:
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 if retry > 0:
Matt Harbison
lfs: use ui.note() and ui.debug() instead of ui.write() and their flags...
r35494 self.ui.note(
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 _(b'lfs: failed: %r (remaining retry %d)\n')
% (stringutil.forcebytestr(ex), retry))
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 retry -= 1
continue
raise
Matt Harbison
lfs: default to not using workers for upload/download...
r35750 # Until https multiplexing gets sorted out
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if self.ui.configbool(b'experimental', b'lfs.worker-enable'):
Matt Harbison
lfs: default to not using workers for upload/download...
r35750 oids = worker.worker(self.ui, 0.1, transfer, (),
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 sorted(objects, key=lambda o: o.get(b'oid')))
Matt Harbison
lfs: default to not using workers for upload/download...
r35750 else:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 oids = transfer(sorted(objects, key=lambda o: o.get(b'oid')))
Matt Harbison
lfs: default to not using workers for upload/download...
r35750
Matt Harbison
lfs: use a context manager to control the progress bar lifetime
r39426 with self.ui.makeprogress(topic, total=total) as progress:
progress.update(0)
processed = 0
blobs = 0
for _one, oid in oids:
processed += sizes[oid]
blobs += 1
progress.update(processed)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: processed: %s\n') % oid)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: emit a status message to indicate how many blobs were uploaded...
r35899 if blobs > 0:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action == b'upload':
self.ui.status(_(b'lfs: uploaded %d files (%s)\n')
Matt Harbison
lfs: emit a status message to indicate how many blobs were uploaded...
r35899 % (blobs, util.bytecount(processed)))
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 elif action == b'download':
self.ui.status(_(b'lfs: downloaded %d files (%s)\n')
Matt Harbison
lfs: enable the final download count status message...
r37783 % (blobs, util.bytecount(processed)))
Matt Harbison
lfs: emit a status message to indicate how many blobs were uploaded...
r35899
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 def __del__(self):
# copied from mercurial/httppeer.py
urlopener = getattr(self, 'urlopener', None)
if urlopener:
for h in urlopener.handlers:
h.close()
getattr(h, "close_all", lambda : None)()
class _dummyremote(object):
"""Dummy store storing blobs to temp directory."""
def __init__(self, repo, url):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 fullpath = repo.vfs.join(b'lfs', url.path)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 self.vfs = lfsvfs(fullpath)
def writebatch(self, pointers, fromstore):
Matt Harbison
lfs: deduplicate oids in the transfer...
r35945 for p in _deduplicate(pointers):
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 content = fromstore.read(p.oid(), verify=True)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 with self.vfs(p.oid(), b'wb', atomictemp=True) as fp:
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 fp.write(content)
def readbatch(self, pointers, tostore):
Matt Harbison
lfs: deduplicate oids in the transfer...
r35945 for p in _deduplicate(pointers):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 with self.vfs(p.oid(), b'rb') as fp:
Matt Harbison
lfs: use the localstore download method to transfer from remote stores...
r35566 tostore.download(p.oid(), fp)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
class _nullremote(object):
"""Null store storing blobs to /dev/null."""
def __init__(self, repo, url):
pass
def writebatch(self, pointers, fromstore):
pass
def readbatch(self, pointers, tostore):
pass
class _promptremote(object):
"""Prompt user to set lfs.url when accessed."""
def __init__(self, repo, url):
pass
def writebatch(self, pointers, fromstore, ui=None):
self._prompt()
def readbatch(self, pointers, tostore, ui=None):
self._prompt()
def _prompt(self):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise error.Abort(_(b'lfs.url needs to be configured'))
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
_storemap = {
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 b'https': _gitlfsremote,
b'http': _gitlfsremote,
b'file': _dummyremote,
b'null': _nullremote,
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 None: _promptremote,
}
Matt Harbison
lfs: deduplicate oids in the transfer...
r35945 def _deduplicate(pointers):
"""Remove any duplicate oids that exist in the list"""
reduced = util.sortdict()
for p in pointers:
reduced[p.oid()] = p
return reduced.values()
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 def _verify(oid, content):
Pulkit Goyal
py3: use node.hex(m.digest()) instead of m.hexdigest()...
r40711 realoid = node.hex(hashlib.sha256(content).digest())
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 if realoid != oid:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise LfsCorruptionError(_(b'detected corrupt lfs object: %s') % oid,
hint=_(b'run hg verify'))
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492
Matt Harbison
lfs: infer the blob store URL from an explicit push dest or default-push...
r37582 def remote(repo, remote=None):
Matt Harbison
lfs: infer the blob store URL from paths.default...
r37536 """remotestore factory. return a store in _storemap depending on config
If ``lfs.url`` is specified, use that remote endpoint. Otherwise, try to
infer the endpoint, based on the remote repository using the same path
adjustments as git. As an extension, 'http' is supported as well so that
``hg serve`` works out of the box.
https://github.com/git-lfs/git-lfs/blob/master/docs/api/server-discovery.md
"""
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 lfsurl = repo.ui.config(b'lfs', b'url')
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583 url = util.url(lfsurl or '')
if lfsurl is None:
Matt Harbison
lfs: infer the blob store URL from an explicit push dest or default-push...
r37582 if remote:
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583 path = remote
Matt Harbison
lfs: infer the blob store URL from an explicit push dest or default-push...
r37582 elif util.safehasattr(repo, '_subtoppath'):
Matt Harbison
lfs: infer the blob store URL from an explicit pull source...
r37581 # The pull command sets this during the optional update phase, which
# tells exactly where the pull originated, whether 'paths.default'
# or explicit.
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583 path = repo._subtoppath
Matt Harbison
lfs: infer the blob store URL from an explicit pull source...
r37581 else:
# TODO: investigate 'paths.remote:lfsurl' style path customization,
# and fall back to inferring from 'paths.remote' if unspecified.
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 path = repo.ui.config(b'paths', b'default') or b''
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583
defaulturl = util.url(path)
Matt Harbison
lfs: infer the blob store URL from paths.default...
r37536
# TODO: support local paths as well.
# TODO: consider the ssh -> https transformation that git applies
if defaulturl.scheme in (b'http', b'https'):
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583 if defaulturl.path and defaulturl.path[:-1] != b'/':
defaulturl.path += b'/'
Matt Harbison
lfs: fix the inferred remote store path when using a --prefix...
r37709 defaulturl.path = (defaulturl.path or b'') + b'.git/info/lfs'
Matt Harbison
lfs: infer the blob store URL from paths.default...
r37536
url = util.url(bytes(defaulturl))
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 repo.ui.note(_(b'lfs: assuming remote store: %s\n') % url)
Matt Harbison
lfs: infer the blob store URL from paths.default...
r37536
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 scheme = url.scheme
if scheme not in _storemap:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise error.Abort(_(b'lfs: unknown url scheme: %s') % scheme)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 return _storemap[scheme](repo, url)
Gregory Szorc
global: replace most uses of RevlogError with StorageError (API)...
r39813 class LfsRemoteError(error.StorageError):
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 pass
Matt Harbison
lfs: gracefully handle aborts on the server when corrupt blobs are detected...
r37710
class LfsCorruptionError(error.Abort):
"""Raised when a corrupt blob is detected, aborting an operation
It exists to allow specialized handling on the server side."""