basestore.py
226 lines
| 8.2 KiB
| text/x-python
|
PythonLexer
various
|
r15168 | # Copyright 2009-2010 Gregory P. Ward | ||
# Copyright 2009-2010 Intelerad Medical Systems Incorporated | ||||
# Copyright 2010-2011 Fog Creek Software | ||||
# Copyright 2010-2011 Unity Technologies | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Greg Ward
|
r15252 | '''base class for store implementations and store-related utility code''' | ||
various
|
r15168 | |||
import re | ||||
Pierre-Yves David
|
r26587 | from mercurial import util, node, hg, error | ||
various
|
r15168 | from mercurial.i18n import _ | ||
import lfutil | ||||
class StoreError(Exception): | ||||
'''Raised when there is a problem getting files from or putting | ||||
files to a central store.''' | ||||
def __init__(self, filename, hash, url, detail): | ||||
self.filename = filename | ||||
self.hash = hash | ||||
self.url = url | ||||
self.detail = detail | ||||
def longmessage(self): | ||||
Wagner Bruna
|
r18461 | return (_("error getting id %s from url %s for file %s: %s\n") % | ||
Mads Kiilerich
|
r19950 | (self.hash, util.hidepassword(self.url), self.filename, | ||
self.detail)) | ||||
various
|
r15168 | |||
def __str__(self): | ||||
Mads Kiilerich
|
r19950 | return "%s: %s" % (util.hidepassword(self.url), self.detail) | ||
various
|
r15168 | |||
class basestore(object): | ||||
def __init__(self, ui, repo, url): | ||||
self.ui = ui | ||||
self.repo = repo | ||||
self.url = url | ||||
def put(self, source, hash): | ||||
Mads Kiilerich
|
r19007 | '''Put source file into the store so it can be retrieved by hash.''' | ||
various
|
r15168 | raise NotImplementedError('abstract method') | ||
Na'Tosha Bard
|
r17127 | def exists(self, hashes): | ||
Mads Kiilerich
|
r18573 | '''Check to see if the store contains the given hashes. Given an | ||
iterable of hashes it returns a mapping from hash to bool.''' | ||||
various
|
r15168 | raise NotImplementedError('abstract method') | ||
def get(self, files): | ||||
'''Get the specified largefiles from the store and write to local | ||||
files under repo.root. files is a list of (filename, hash) | ||||
Mads Kiilerich
|
r17424 | tuples. Return (success, missing), lists of files successfully | ||
various
|
r15168 | downloaded and those not found in the store. success is a list | ||
of (filename, hash) tuples; missing is a list of filenames that | ||||
we could not get. (The detailed error message will already have | ||||
been presented to the user, so missing is just supplied as a | ||||
summary.)''' | ||||
success = [] | ||||
missing = [] | ||||
ui = self.ui | ||||
at = 0 | ||||
Mads Kiilerich
|
r19008 | available = self.exists(set(hash for (_filename, hash) in files)) | ||
various
|
r15168 | for filename, hash in files: | ||
r28463 | ui.progress(_('getting largefiles'), at, unit=_('files'), | |||
various
|
r15168 | total=len(files)) | ||
at += 1 | ||||
ui.note(_('getting %s:%s\n') % (filename, hash)) | ||||
Mads Kiilerich
|
r19008 | if not available.get(hash): | ||
ui.warn(_('%s: largefile %s not available from %s\n') | ||||
Mads Kiilerich
|
r19950 | % (filename, hash, util.hidepassword(self.url))) | ||
Mads Kiilerich
|
r19008 | missing.append(filename) | ||
continue | ||||
Mads Kiilerich
|
r19918 | if self._gethash(filename, hash): | ||
success.append((filename, hash)) | ||||
else: | ||||
various
|
r15168 | missing.append(filename) | ||
ui.progress(_('getting largefiles'), None) | ||||
return (success, missing) | ||||
Mads Kiilerich
|
r19918 | def _gethash(self, filename, hash): | ||
"""Get file with the provided hash and store it in the local repo's | ||||
store and in the usercache. | ||||
filename is for informational messages only. | ||||
""" | ||||
util.makedirs(lfutil.storepath(self.repo, '')) | ||||
storefilename = lfutil.storepath(self.repo, hash) | ||||
tmpname = storefilename + '.tmp' | ||||
tmpfile = util.atomictempfile(tmpname, | ||||
createmode=self.repo.store.createmode) | ||||
try: | ||||
gothash = self._getfile(tmpfile, filename, hash) | ||||
Gregory Szorc
|
r25660 | except StoreError as err: | ||
Mads Kiilerich
|
r19918 | self.ui.warn(err.longmessage()) | ||
gothash = "" | ||||
tmpfile.close() | ||||
if gothash != hash: | ||||
if gothash != "": | ||||
self.ui.warn(_('%s: data corruption (expected %s, got %s)\n') | ||||
% (filename, hash, gothash)) | ||||
util.unlink(tmpname) | ||||
return False | ||||
util.rename(tmpname, storefilename) | ||||
lfutil.linktousercache(self.repo, hash) | ||||
return True | ||||
various
|
r15168 | def verify(self, revs, contents=False): | ||
'''Verify the existence (and, optionally, contents) of every big | ||||
file revision referenced by every changeset in revs. | ||||
Return 0 if all is well, non-zero on any errors.''' | ||||
Mads Kiilerich
|
r18546 | self.ui.status(_('searching %d changesets for largefiles\n') % | ||
len(revs)) | ||||
various
|
r15168 | verified = set() # set of (filename, filenode) tuples | ||
liscju
|
r29067 | filestocheck = [] # list of (cset, filename, expectedhash) | ||
various
|
r15168 | for rev in revs: | ||
cctx = self.repo[rev] | ||||
cset = "%d:%s" % (cctx.rev(), node.short(cctx.node())) | ||||
Mads Kiilerich
|
r18486 | for standin in cctx: | ||
liscju
|
r29067 | filename = lfutil.splitstandin(standin) | ||
if filename: | ||||
fctx = cctx[standin] | ||||
key = (filename, fctx.filenode()) | ||||
if key not in verified: | ||||
verified.add(key) | ||||
expectedhash = fctx.data()[0:40] | ||||
filestocheck.append((cset, filename, expectedhash)) | ||||
failed = self._verifyfiles(contents, filestocheck) | ||||
various
|
r15168 | |||
Na'Tosha Bard
|
r16247 | numrevs = len(verified) | ||
numlfiles = len(set([fname for (fname, fnode) in verified])) | ||||
various
|
r15168 | if contents: | ||
Mads Kiilerich
|
r18546 | self.ui.status( | ||
_('verified contents of %d revisions of %d largefiles\n') | ||||
% (numrevs, numlfiles)) | ||||
various
|
r15168 | else: | ||
Mads Kiilerich
|
r18546 | self.ui.status( | ||
_('verified existence of %d revisions of %d largefiles\n') | ||||
% (numrevs, numlfiles)) | ||||
various
|
r15168 | return int(failed) | ||
def _getfile(self, tmpfile, filename, hash): | ||||
'''Fetch one revision of one file from the store and write it | ||||
to tmpfile. Compute the hash of the file on-the-fly as it | ||||
Mads Kiilerich
|
r18999 | downloads and return the hash. Close tmpfile. Raise | ||
various
|
r15168 | StoreError if unable to download the file (e.g. it does not | ||
exist in the store).''' | ||||
raise NotImplementedError('abstract method') | ||||
liscju
|
r29067 | def _verifyfiles(self, contents, filestocheck): | ||
'''Perform the actual verification of files in the store. | ||||
Mads Kiilerich
|
r18574 | 'contents' controls verification of content hash. | ||
liscju
|
r29067 | 'filestocheck' is list of files to check. | ||
Returns _true_ if any problems are found! | ||||
various
|
r15168 | ''' | ||
raise NotImplementedError('abstract method') | ||||
import localstore, wirestore | ||||
_storeprovider = { | ||||
'file': [localstore.localstore], | ||||
'http': [wirestore.wirestore], | ||||
'https': [wirestore.wirestore], | ||||
'ssh': [wirestore.wirestore], | ||||
} | ||||
_scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://') | ||||
# During clone this function is passed the src's ui object | ||||
# but it needs the dest's ui object so it can read out of | ||||
# the config file. Use repo.ui instead. | ||||
def _openstore(repo, remote=None, put=False): | ||||
ui = repo.ui | ||||
if not remote: | ||||
Na'Tosha Bard
|
r15943 | lfpullsource = getattr(repo, 'lfpullsource', None) | ||
if lfpullsource: | ||||
path = ui.expandpath(lfpullsource) | ||||
Mads Kiilerich
|
r23941 | elif put: | ||
path = ui.expandpath('default-push', 'default') | ||||
Na'Tosha Bard
|
r15943 | else: | ||
Mads Kiilerich
|
r23941 | path = ui.expandpath('default') | ||
Greg Ward
|
r15252 | |||
# ui.expandpath() leaves 'default-push' and 'default' alone if | ||||
# they cannot be expanded: fallback to the empty string, | ||||
# meaning the current directory. | ||||
various
|
r15168 | if path == 'default-push' or path == 'default': | ||
path = '' | ||||
remote = repo | ||||
else: | ||||
Mads Kiilerich
|
r18489 | path, _branches = hg.parseurl(path) | ||
various
|
r15168 | remote = hg.peer(repo, {}, path) | ||
# The path could be a scheme so use Mercurial's normal functionality | ||||
# to resolve the scheme to a repository and use its path | ||||
Matt Mackall
|
r15169 | path = util.safehasattr(remote, 'url') and remote.url() or remote.path | ||
various
|
r15168 | |||
match = _scheme_re.match(path) | ||||
if not match: # regular filesystem path | ||||
scheme = 'file' | ||||
else: | ||||
scheme = match.group(1) | ||||
try: | ||||
storeproviders = _storeprovider[scheme] | ||||
except KeyError: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('unsupported URL scheme %r') % scheme) | ||
various
|
r15168 | |||
Na'Tosha Bard
|
r16247 | for classobj in storeproviders: | ||
various
|
r15168 | try: | ||
Na'Tosha Bard
|
r16247 | return classobj(ui, repo, remote) | ||
various
|
r15168 | except lfutil.storeprotonotcapable: | ||
pass | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('%s does not appear to be a largefile store') % | ||
Mads Kiilerich
|
r19950 | util.hidepassword(path)) | ||