mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-12 15:28:21 +01:00
243 lines
7.3 KiB
Python
243 lines
7.3 KiB
Python
import os
|
|
import uuid
|
|
import json
|
|
import shutil
|
|
|
|
from dvc.system import System
|
|
from dvc.remote.base import RemoteBase
|
|
from dvc.state import State
|
|
from dvc.logger import Logger
|
|
from dvc.utils import remove, move
|
|
from dvc.output.base import OutputAlreadyTrackedError
|
|
|
|
|
|
class RemoteLOCAL(RemoteBase):
|
|
PARAM_MD5 = State.PARAM_MD5
|
|
|
|
CACHE_DIR = 'cache'
|
|
CACHE_TYPES = ['reflink', 'hardlink', 'symlink', 'copy']
|
|
CACHE_TYPE_MAP = {
|
|
'copy': shutil.copyfile,
|
|
'symlink': System.symlink,
|
|
'hardlink': System.hardlink,
|
|
'reflink': System.reflink,
|
|
}
|
|
|
|
def __init__(self, project, config):
|
|
#FIXME
|
|
from dvc.config import Config
|
|
|
|
self.project = project
|
|
link_state = project.link_state
|
|
cache_dir = config.get(Config.SECTION_REMOTE_URL, self.CACHE_DIR)
|
|
cache_type = config.get(Config.SECTION_CACHE_TYPE, None)
|
|
|
|
self.cache_type = cache_type
|
|
|
|
cache_dir = cache_dir if cache_dir else self.CACHE_DIR
|
|
if os.path.isabs(cache_dir):
|
|
self.cache_dir = cache_dir
|
|
else:
|
|
self.cache_dir = os.path.abspath(os.path.realpath(os.path.join(project.dvc_dir, cache_dir)))
|
|
|
|
if not os.path.exists(self.cache_dir):
|
|
os.mkdir(self.cache_dir)
|
|
|
|
self.state = State(self.cache_dir)
|
|
self.link_state = link_state
|
|
|
|
def all(self):
|
|
clist = []
|
|
for entry in os.listdir(self.cache_dir):
|
|
subdir = os.path.join(self.cache_dir, entry)
|
|
if not os.path.isdir(subdir):
|
|
continue
|
|
|
|
for cache in os.listdir(subdir):
|
|
path = os.path.join(subdir, cache)
|
|
clist.append(path)
|
|
|
|
return clist
|
|
|
|
def get(self, md5):
|
|
if not md5:
|
|
return None
|
|
|
|
return os.path.join(self.cache_dir, md5[0:2], md5[2:])
|
|
|
|
def path_to_md5(self, path):
|
|
relpath = os.path.relpath(path, self.cache_dir)
|
|
return os.path.dirname(relpath) + os.path.basename(relpath)
|
|
|
|
def changed(self, md5):
|
|
cache = self.get(md5)
|
|
if self.state.changed(cache, md5=md5):
|
|
if os.path.exists(cache):
|
|
Logger.warn('Corrupted cache file {}'.format(os.path.relpath(cache)))
|
|
remove(cache)
|
|
return True
|
|
|
|
return False
|
|
|
|
def link(self, src, link):
|
|
dname = os.path.dirname(link)
|
|
if not os.path.exists(dname):
|
|
os.makedirs(dname)
|
|
|
|
if self.cache_type != None:
|
|
types = [self.cache_type]
|
|
else:
|
|
types = self.CACHE_TYPES
|
|
|
|
for typ in types:
|
|
try:
|
|
msg = u'Checking out \'{}\' with cache \'{}\''
|
|
Logger.debug(msg.format(os.path.relpath(src), os.path.relpath(link)))
|
|
self.CACHE_TYPE_MAP[typ](src, link)
|
|
self.link_state.update(link)
|
|
return
|
|
except Exception as exc:
|
|
msg = 'Cache type \'{}\' is not supported'.format(typ)
|
|
Logger.debug(msg)
|
|
if typ == types[-1]:
|
|
raise DvcException(msg, cause=exc)
|
|
|
|
@staticmethod
|
|
def load_dir_cache(path):
|
|
if os.path.isabs(path):
|
|
relpath = os.path.relpath(path)
|
|
else:
|
|
relpath = path
|
|
|
|
try:
|
|
with open(path, 'r') as fd:
|
|
d = json.load(fd)
|
|
except Exception as exc:
|
|
msg = u'Failed to load dir cache \'{}\''
|
|
Logger.error(msg.format(relpath), exc)
|
|
return []
|
|
|
|
if not isinstance(d, list):
|
|
msg = u'Dir cache file format error \'{}\': skipping the file'
|
|
Logger.error(msg.format(relpath))
|
|
return []
|
|
|
|
return d
|
|
|
|
@classmethod
|
|
def get_dir_cache(cls, path):
|
|
res = {}
|
|
|
|
d = cls.load_dir_cache(path)
|
|
|
|
for entry in d:
|
|
res[entry[State.PARAM_RELPATH]] = entry[State.PARAM_MD5]
|
|
|
|
return res
|
|
|
|
def dir_cache(self, cache):
|
|
res = {}
|
|
dir_cache = self.get_dir_cache(cache)
|
|
|
|
for relpath, md5 in dir_cache.items():
|
|
res[relpath] = self.get(md5)
|
|
|
|
return res
|
|
|
|
@staticmethod
|
|
def is_dir_cache(cache):
|
|
return cache.endswith(State.MD5_DIR_SUFFIX)
|
|
|
|
def checkout(self, path_info, checksum_info):
|
|
path = path_info['path']
|
|
md5 = checksum_info.get(self.PARAM_MD5, None)
|
|
cache = self.get(md5)
|
|
|
|
if not cache or not os.path.exists(cache) or self.changed(md5):
|
|
if cache:
|
|
Logger.warn(u'\'{}({})\': cache file not found'.format(os.path.relpath(cache),
|
|
os.path.relpath(path)))
|
|
remove(path)
|
|
return
|
|
|
|
if os.path.exists(path):
|
|
msg = u'Data \'{}\' exists. Removing before checkout'
|
|
Logger.debug(msg.format(os.path.relpath(path)))
|
|
remove(path)
|
|
|
|
if not self.is_dir_cache(cache):
|
|
self.link(cache, path)
|
|
return
|
|
|
|
msg = u'Checking out directory \'{}\' with cache \'{}\''
|
|
Logger.debug(msg.format(os.path.relpath(path), os.path.relpath(cache)))
|
|
|
|
dir_cache = self.dir_cache(cache)
|
|
for relpath, c in dir_cache.items():
|
|
p = os.path.join(path, relpath)
|
|
self.link(c, p)
|
|
|
|
def _move(self, inp, outp):
|
|
# moving in two stages to make last the move atomic in
|
|
# case inp and outp are in different filesystems
|
|
tmp = '{}.{}'.format(outp, str(uuid.uuid4()))
|
|
move(inp, tmp)
|
|
move(tmp, outp)
|
|
|
|
def _save_file(self, path_info):
|
|
path = path_info['path']
|
|
md5 = self.state.update(path)
|
|
cache = self.get(md5)
|
|
if self.changed(md5):
|
|
Logger.debug(u'Saving \'{}\' to \'{}\''.format(os.path.relpath(path),
|
|
os.path.relpath(cache)))
|
|
self._move(path, cache)
|
|
self.state.update(cache)
|
|
self.checkout(path_info, {self.PARAM_MD5: md5})
|
|
|
|
return {self.PARAM_MD5: md5}
|
|
|
|
def _save_dir(self, path_info):
|
|
path = path_info['path']
|
|
md5 = self.state.update(path)
|
|
cache = self.get(md5)
|
|
dname = os.path.dirname(cache)
|
|
dir_info = self.state.collect_dir(path)
|
|
|
|
for entry in dir_info:
|
|
relpath = entry[State.PARAM_RELPATH]
|
|
p = os.path.join(path, relpath)
|
|
|
|
self._save_file({'scheme': 'local', 'path': p})
|
|
|
|
if not os.path.isdir(dname):
|
|
os.makedirs(dname)
|
|
|
|
Logger.debug(u'Saving directory \'{}\' to \'{}\''.format(os.path.relpath(path),
|
|
os.path.relpath(cache)))
|
|
|
|
with open(cache, 'w+') as fd:
|
|
json.dump(dir_info, fd, sort_keys=True)
|
|
|
|
return {self.PARAM_MD5: md5}
|
|
|
|
def save(self, path_info):
|
|
if path_info['scheme'] != 'local':
|
|
raise NotImplementedError
|
|
|
|
path = path_info['path']
|
|
|
|
if self.project.scm.is_tracked(path):
|
|
raise OutputAlreadyTrackedError(os.path.relpath(path))
|
|
|
|
if os.path.isdir(path):
|
|
return self._save_dir(path_info)
|
|
else:
|
|
return self._save_file(path_info)
|
|
|
|
def save_info(self, path_info):
|
|
if path_info['scheme'] != 'local':
|
|
raise NotImplementedError
|
|
|
|
return {self.PARAM_MD5: self.state.update(path_info['path'])}
|