mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-09 23:10:24 +01:00
cache: use checksums instead of commit_id
From now on, we identify our cache files by their md5sum, so that we can avoid cache collission in branching scenario and also dedup same data at the same time. Fixes #250, #251 Signed-off-by: Ruslan Kuprieiev <[email protected]>
This commit is contained in:
@@ -107,9 +107,8 @@ class CmdImportFile(CmdBase):
|
||||
raise ImportFileError('Output file directory "{}" does not exists'.format(
|
||||
os.path.dirname(data_item.data.relative)))
|
||||
|
||||
cache_dir = os.path.dirname(data_item.cache.relative)
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
if not os.path.exists(data_item.cache_dir):
|
||||
os.makedirs(data_item.cache_dir)
|
||||
|
||||
data_targets.append((input, data_item))
|
||||
|
||||
|
||||
+4
-5
@@ -129,14 +129,13 @@ ProjectName =
|
||||
|
||||
def create_empty_file(self):
|
||||
empty_data_path = os.path.join(self.parsed_args.data_dir, self.EMPTY_FILE_NAME)
|
||||
cache_file_suffix = self.EMPTY_FILE_NAME + '_' + self.EMPTY_FILE_CHECKSUM
|
||||
empty_cache_path = os.path.join(Config.CONFIG_DIR, Config.CACHE_DIR, cache_file_suffix)
|
||||
|
||||
open(empty_cache_path, 'w').close()
|
||||
System.symlink(os.path.join('..', empty_cache_path), empty_data_path)
|
||||
data_item = self.settings.path_factory.data_item(empty_data_path)
|
||||
open(empty_data_path, 'w').close()
|
||||
data_item.move_data_to_cache()
|
||||
|
||||
StateFile(StateFile.COMMAND_EMPTY_FILE,
|
||||
self.settings.path_factory.data_item(empty_data_path),
|
||||
data_item,
|
||||
self.settings,
|
||||
input_files=[],
|
||||
output_files=[],
|
||||
|
||||
+2
-2
@@ -112,12 +112,12 @@ class CmdRun(CmdBase):
|
||||
input_set = set(data_items_from_args + input_data_items) - output_set
|
||||
input_files_dvc = [x.data.dvc for x in input_set]
|
||||
|
||||
code_dependencies_dvc = self.git.abs_paths_to_dvc(code_dependencies + not_data_items_from_args)
|
||||
code_dependencies_dvc = self.git.abs_paths_to_dvc(set(code_dependencies + not_data_items_from_args))
|
||||
|
||||
result = []
|
||||
for data_item in repo_change.changed_data_items:
|
||||
Logger.debug('Move output file "{}" to cache dir "{}" and create a symlink'.format(
|
||||
data_item.data.relative, data_item.cache.relative))
|
||||
data_item.data.relative, data_item.cache_dir))
|
||||
data_item.move_data_to_cache()
|
||||
|
||||
Logger.debug('Create state file "{}"'.format(data_item.state.relative))
|
||||
|
||||
+17
-34
@@ -154,7 +154,7 @@ class DataCloudBase(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _import(self, bucket, path, fname, item):
|
||||
def _import(self, bucket, path, item):
|
||||
"""
|
||||
Cloud-specific method for importing data file.
|
||||
"""
|
||||
@@ -168,14 +168,13 @@ class DataCloudBase(object):
|
||||
""" Generic method for pulling data from the cloud """
|
||||
fname = item.resolved_cache.dvc
|
||||
key_name = self.cache_file_key(fname)
|
||||
self.create_directory(item)
|
||||
return self._import(self.storage_bucket, key_name, fname, item)
|
||||
return self._import(self.storage_bucket, key_name, item)
|
||||
|
||||
def import_data(self, url, item):
|
||||
""" Generic method for importing data """
|
||||
parsed = urlparse(url)
|
||||
|
||||
return self._import(parsed.netloc, parsed.path, item.cache.relative, item)
|
||||
return self._import(parsed.netloc, parsed.path, item)
|
||||
|
||||
def sync(self, fname):
|
||||
"""
|
||||
@@ -189,23 +188,6 @@ class DataCloudBase(object):
|
||||
else:
|
||||
return self.pull(item)
|
||||
|
||||
def create_directory(self, item):
|
||||
""" Create local directories for data item cache """
|
||||
self._lock.acquire()
|
||||
try:
|
||||
directory = os.path.dirname(item.cache.relative)
|
||||
if not os.path.exists(directory):
|
||||
Logger.debug(u'Creating directory {}'.format(directory))
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
except OSError as ex:
|
||||
raise DataCloudError(u'Cannot create directory {}: {}'.format(directory, ex))
|
||||
elif not os.path.isdir(directory):
|
||||
msg = u'File {} cannot be synced because {} is not a directory'
|
||||
raise DataCloudError(msg.format(item.cache.relative, directory))
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def remove(self, item):
|
||||
"""
|
||||
Cloud-specific method for removing data item from the cloud.
|
||||
@@ -233,11 +215,11 @@ class DataCloudLOCAL(DataCloudBase):
|
||||
copyfile(item.resolved_cache.dvc, self.storage_path)
|
||||
return item
|
||||
|
||||
def _import(self, bucket, i, out, item):
|
||||
tmp_file = self.tmp_file(out)
|
||||
def _import(self, bucket, i, item):
|
||||
tmp_file = self.tmp_file(item.data.dvc)
|
||||
try:
|
||||
copyfile(i, tmp_file)
|
||||
os.rename(tmp_file, out)
|
||||
item.import_cache(tmp_file)
|
||||
except Exception as exc:
|
||||
Logger.error('Failed to copy "{}": {}'.format(i, exc))
|
||||
return None
|
||||
@@ -246,15 +228,15 @@ class DataCloudLOCAL(DataCloudBase):
|
||||
|
||||
def pull(self, item):
|
||||
Logger.debug('sync from cloud ' + self.storage_path + " " + item.resolved_cache.dvc)
|
||||
return self._import(None, self.storage_path, item.resolved_cache.dvc, item)
|
||||
return self._import(None, self.storage_path, item)
|
||||
|
||||
def remove(self, item):
|
||||
Logger.debug('rm from cloud ' + item.resolved_cache.dvc)
|
||||
os.remove(item.resolved_cache.dvc)
|
||||
|
||||
def import_data(self, path, item):
|
||||
Logger.debug('import from cloud ' + path + " " + item.cache.relative)
|
||||
return self._import(None, path, item.cache.relative, item)
|
||||
Logger.debug('import from cloud ' + path + " " + item.data.dvc)
|
||||
return self._import(None, path, item)
|
||||
|
||||
def _status(self, data_item):
|
||||
local = data_item.resolved_cache.relative
|
||||
@@ -366,8 +348,7 @@ class DataCloudHTTP(DataCloudBase):
|
||||
Download single file from url.
|
||||
"""
|
||||
|
||||
to_file = item.cache.relative
|
||||
tmp_file = self.tmp_file(to_file)
|
||||
tmp_file = self.tmp_file(item.data.dvc)
|
||||
|
||||
downloaded, header = self._downloaded_size(tmp_file)
|
||||
req = requests.get(url, stream=True, headers=header)
|
||||
@@ -382,7 +363,7 @@ class DataCloudHTTP(DataCloudBase):
|
||||
if not self._verify_md5(req, tmp_file):
|
||||
return None
|
||||
|
||||
os.rename(tmp_file, to_file)
|
||||
item.import_cache(tmp_file)
|
||||
|
||||
return item
|
||||
|
||||
@@ -471,10 +452,11 @@ class DataCloudAWS(DataCloudBase):
|
||||
"""
|
||||
return fname + '.download'
|
||||
|
||||
def _import(self, bucket_name, key_name, fname, data_item):
|
||||
def _import(self, bucket_name, key_name, data_item):
|
||||
|
||||
bucket = self._get_bucket_aws(bucket_name)
|
||||
|
||||
fname = data_item.data.dvc
|
||||
tmp_file = self.tmp_file(fname)
|
||||
name = os.path.basename(fname)
|
||||
key = bucket.get_key(key_name)
|
||||
@@ -494,7 +476,7 @@ class DataCloudAWS(DataCloudBase):
|
||||
num_retries=10)
|
||||
try:
|
||||
key.get_contents_to_filename(tmp_file, cb=create_cb(name), res_download_handler=res_h)
|
||||
os.rename(tmp_file, fname)
|
||||
data_item.import_cache(tmp_file)
|
||||
except Exception as exc:
|
||||
Logger.error('Failed to download "{}": {}'.format(key_name, exc))
|
||||
return None
|
||||
@@ -714,10 +696,11 @@ class DataCloudGCP(DataCloudBase):
|
||||
|
||||
return False
|
||||
|
||||
def _import(self, bucket_name, key, fname, data_item):
|
||||
def _import(self, bucket_name, key, data_item):
|
||||
|
||||
bucket = self._get_bucket_gc(bucket_name)
|
||||
|
||||
fname = data_item.data.dvc
|
||||
name = os.path.basename(fname)
|
||||
tmp_file = self.tmp_file(fname)
|
||||
|
||||
@@ -738,7 +721,7 @@ class DataCloudGCP(DataCloudBase):
|
||||
|
||||
try:
|
||||
blob.download_to_filename(tmp_file)
|
||||
os.rename(tmp_file, fname)
|
||||
data_item.import_cache(tmp_file)
|
||||
except Exception as exc:
|
||||
Logger.error('Failed to download "{}": {}'.format(key, exc))
|
||||
return None
|
||||
|
||||
+1
-1
@@ -58,5 +58,5 @@ class Executor(object):
|
||||
code, out, err = Executor.exec_cmd(cmd, stdout_file=stdout_file,
|
||||
stderr_file=stderr_file, cwd=cwd, shell=shell)
|
||||
if code != 0:
|
||||
raise ExecutorError('Git command error ({}):\n{}'.format(' '.join(cmd), err))
|
||||
raise ExecutorError('Git command error ({}):\n{}\n{}'.format(' '.join(cmd), out, err))
|
||||
return out
|
||||
|
||||
+6
-3
@@ -179,10 +179,13 @@ class GitWrapper(GitWrapperI):
|
||||
|
||||
Executor.exec_cmd_only_success(['git', 'add', '--all'])
|
||||
out_status = Executor.exec_cmd_only_success(['git', 'status', '--porcelain'])
|
||||
Executor.exec_cmd_only_success(['git', 'commit', '-m', message])
|
||||
Logger.debug('[dvc-git] Commit all changes. Success.')
|
||||
changes = GitWrapper.parse_porcelain_files(out_status)
|
||||
if len(changes) == 0:
|
||||
return []
|
||||
|
||||
return GitWrapper.parse_porcelain_files(out_status)
|
||||
Executor.exec_cmd_only_success(['git', 'commit', '-m', '\'{}\''.format(message)])
|
||||
Logger.debug('[dvc-git] Commit all changes. Success.')
|
||||
return changes
|
||||
|
||||
def commit_all_changes_and_log_status(self, message):
|
||||
statuses = self.commit_all_changes(message)
|
||||
|
||||
+13
-7
@@ -5,6 +5,7 @@ from dvc.path.path import Path
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.system import System
|
||||
from dvc.utils import cached_property
|
||||
from dvc.data_cloud import file_md5
|
||||
|
||||
|
||||
class DataItemError(DvcException):
|
||||
@@ -81,14 +82,19 @@ class DataItem(object):
|
||||
state_file = os.path.join(state_dir, self.data_dvc_short + self.STATE_FILE_SUFFIX)
|
||||
return Path(state_file, self._git)
|
||||
|
||||
@cached_property
|
||||
def cache_dir(self):
|
||||
return os.path.join(self._git.git_dir_abs, self._config.cache_dir)
|
||||
|
||||
@cached_property
|
||||
def cache(self):
|
||||
cache_dir = os.path.join(self._git.git_dir_abs, self._config.cache_dir)
|
||||
cache_dir = self.cache_dir
|
||||
|
||||
if self._cache_file:
|
||||
file_name = os.path.relpath(os.path.realpath(self._cache_file), cache_dir)
|
||||
else:
|
||||
file_name = self.data_dvc_short + self.CACHE_FILE_SEP + self._git.curr_commit
|
||||
from dvc.state_file import StateFile
|
||||
file_name = StateFile.load(self, self._git).md5
|
||||
|
||||
cache_file = os.path.join(cache_dir, file_name)
|
||||
return Path(cache_file, self._git)
|
||||
@@ -125,9 +131,9 @@ class DataItem(object):
|
||||
return os.path.relpath(self.cache.relative, data_file_dir)
|
||||
|
||||
def move_data_to_cache(self):
|
||||
cache_dir = os.path.dirname(self.cache.relative)
|
||||
if not os.path.isdir(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
|
||||
shutil.move(self.data.relative, self.cache.relative)
|
||||
self.import_cache(self.data.relative)
|
||||
System.symlink(self.symlink_file, self.data.relative)
|
||||
|
||||
def import_cache(self, fname):
|
||||
self._cache_file = os.path.join(self.cache_dir, file_md5(fname)[0])
|
||||
os.rename(fname, self._cache_file)
|
||||
|
||||
+11
-3
@@ -42,6 +42,7 @@ class StateFile(object):
|
||||
PARAM_SHELL = "Shell"
|
||||
PARAM_TARGET_METRICS = 'TargetMetrics'
|
||||
TARGET_METRICS_SINGLE_METRIC = 'SingleMetric'
|
||||
PARAM_MD5 = 'Md5'
|
||||
|
||||
def __init__(self,
|
||||
command,
|
||||
@@ -56,7 +57,8 @@ class StateFile(object):
|
||||
stderr=None,
|
||||
cwd=None,
|
||||
shell=False,
|
||||
target_metrics={}):
|
||||
target_metrics={},
|
||||
md5=None):
|
||||
self.data_item = data_item
|
||||
|
||||
self.settings = settings
|
||||
@@ -66,6 +68,10 @@ class StateFile(object):
|
||||
self.code_dependencies = code_dependencies
|
||||
self.shell = shell
|
||||
|
||||
self.md5 = md5
|
||||
if not md5 and data_item:
|
||||
self.md5 = os.path.basename(data_item.cache.relative)
|
||||
|
||||
if command not in self.ACCEPTED_COMMANDS:
|
||||
raise StateFileError('Args error: unknown command %s' % command)
|
||||
self.command = command
|
||||
@@ -182,7 +188,8 @@ class StateFile(object):
|
||||
StateFile.decode_path(json.get(StateFile.PARAM_STDERR)),
|
||||
StateFile.decode_path(json.get(StateFile.PARAM_CWD)),
|
||||
json.get(StateFile.PARAM_SHELL, False),
|
||||
json.get(StateFile.PARAM_TARGET_METRICS, {}))
|
||||
json.get(StateFile.PARAM_TARGET_METRICS, {}),
|
||||
json.get(StateFile.PARAM_MD5, ''))
|
||||
|
||||
@staticmethod
|
||||
def load(data_item, settings):
|
||||
@@ -213,7 +220,8 @@ class StateFile(object):
|
||||
self.PARAM_STDOUT: self.encode_path(self.stdout),
|
||||
self.PARAM_STDERR: self.encode_path(self.stderr),
|
||||
self.PARAM_SHELL: self.shell,
|
||||
self.PARAM_TARGET_METRICS: self.target_metrics
|
||||
self.PARAM_TARGET_METRICS: self.target_metrics,
|
||||
self.PARAM_MD5: self.md5
|
||||
}
|
||||
|
||||
if self.locked:
|
||||
|
||||
Reference in New Issue
Block a user