forked from treeverse/dvc
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstate.py
More file actions
237 lines (172 loc) 路 7.12 KB
/
Copy pathstate.py
File metadata and controls
237 lines (172 loc) 路 7.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
"""Manages state database used for checksum caching."""
import logging
import os
import re
from abc import ABC, abstractmethod
from urllib.parse import urlencode, urlunparse
from dvc.exceptions import DvcException
from dvc.fs.local import LocalFileSystem
from dvc.hash_info import HashInfo
from dvc.utils import relpath
from dvc.utils.fs import get_inode, get_mtime_and_size, remove
SQLITE_MAX_VARIABLES_NUMBER = 999
logger = logging.getLogger(__name__)
class StateVersionTooNewError(DvcException):
"""Thrown when DVC version is older than the state database version."""
def __init__(self, dvc_version, expected, actual):
super().__init__(
"you are using an old version '{dvc_version}' of DVC that is "
"using state file version '{expected}', which is not compatible "
"with the state file version '{actual}', that is used in this "
"repo. Please upgrade right now!".format(
dvc_version=dvc_version, expected=expected, actual=actual
)
)
class StateBase(ABC):
@abstractmethod
def close(self):
pass
@abstractmethod
def save(self, path_info, fs, hash_info):
pass
@abstractmethod
def get(self, path_info, fs):
pass
@abstractmethod
def save_link(self, path_info, fs):
pass
class StateNoop(StateBase):
def close(self):
pass
def save(self, path_info, fs, hash_info):
pass
def get(self, path_info, fs): # pylint: disable=unused-argument
return None
def save_link(self, path_info, fs):
pass
class State(StateBase): # pylint: disable=too-many-instance-attributes
def __init__(self, root_dir=None, tmp_dir=None, dvcignore=None):
from diskcache import Cache
super().__init__()
self.tmp_dir = tmp_dir
self.root_dir = root_dir
self.dvcignore = dvcignore
self.fs = LocalFileSystem()
if not tmp_dir:
return
config = {
"eviction_policy": "least-recently-used",
"disk_pickle_protocol": 4,
}
self.links = Cache(directory=os.path.join(tmp_dir, "links"), **config)
self.md5s = Cache(directory=os.path.join(tmp_dir, "md5s"), **config)
def close(self):
self.md5s.close()
self.links.close()
def save(self, path_info, fs, hash_info):
"""Save hash for the specified path info.
Args:
path_info (dict): path_info to save hash for.
hash_info (HashInfo): hash to save.
"""
if not isinstance(fs, LocalFileSystem):
return
assert isinstance(path_info, str) or path_info.scheme == "local"
assert hash_info
assert isinstance(hash_info, HashInfo)
assert os.path.exists(path_info)
mtime, size = get_mtime_and_size(path_info, self.fs, self.dvcignore)
inode = get_inode(path_info)
logger.debug(
"state save (%s, %s, %s) %s", inode, mtime, size, hash_info.value
)
self.md5s[inode] = (mtime, size, hash_info.value)
def get(self, path_info, fs):
"""Gets the hash for the specified path info. Hash will be
retrieved from the state database if available.
Args:
path_info (dict): path info to get the hash for.
Returns:
HashInfo or None: hash for the specified path info or None if it
doesn't exist in the state database.
"""
if not isinstance(fs, LocalFileSystem):
return None
assert isinstance(path_info, str) or path_info.scheme == "local"
path = os.fspath(path_info)
# NOTE: use os.path.exists instead of LocalFileSystem.exists
# because it uses lexists() and will return True for broken
# symlinks that we cannot stat() in get_mtime_and_size
if not os.path.exists(path):
return None
mtime, size = get_mtime_and_size(path, self.fs, self.dvcignore)
inode = get_inode(path)
value = self.md5s.get(inode)
if not value or value[0] != mtime or value[1] != size:
return None
return HashInfo("md5", value[2], size=int(size))
def save_link(self, path_info, fs):
"""Adds the specified path to the list of links created by dvc. This
list is later used on `dvc checkout` to cleanup old links.
Args:
path_info (dict): path info to add to the list of links.
"""
if not isinstance(fs, LocalFileSystem):
return
assert isinstance(path_info, str) or path_info.scheme == "local"
if not self.fs.exists(path_info):
return
mtime, _ = get_mtime_and_size(path_info, self.fs, self.dvcignore)
inode = get_inode(path_info)
relative_path = relpath(path_info, self.root_dir)
with self.links as ref:
ref[relative_path] = (inode, mtime)
def get_unused_links(self, used, fs):
"""Removes all saved links except the ones that are used.
Args:
used (list): list of used links that should not be removed.
"""
if not isinstance(fs, LocalFileSystem):
return
unused = []
with self.links as ref:
for relative_path in ref:
path = os.path.join(self.root_dir, relative_path)
if path in used or not self.fs.exists(path):
continue
inode = get_inode(path)
mtime, _ = get_mtime_and_size(path, self.fs, self.dvcignore)
if ref[relative_path] == (inode, mtime):
logger.debug("Removing '%s' as unused link.", path)
unused.append(relative_path)
return unused
def remove_links(self, unused, fs):
if not isinstance(fs, LocalFileSystem):
return
for path in unused:
remove(os.path.join(self.root_dir, path))
with self.links as ref:
for path in unused:
del ref[path]
def _connect_sqlite(filename, options):
# Connect by URI was added in Python 3.4 and sqlite 3.7.7,
# we ignore options, which should be fine unless repo is on old NFS/CIFS
import sqlite3
if sqlite3.sqlite_version_info < (3, 7, 7):
return sqlite3.connect(filename)
uri = _build_sqlite_uri(filename, options)
return sqlite3.connect(uri, uri=True)
def _build_sqlite_uri(filename, options):
# In the doc mentioned below we only need to replace ? -> %3f and
# # -> %23, but, if present, we also need to replace % -> %25 first
# (happens when we are on a weird FS that shows urlencoded filenames
# instead of proper ones) to not confuse sqlite.
uri_path = filename.replace("%", "%25")
# Convert filename to uri according to https://www.sqlite.org/uri.html, 3.1
uri_path = uri_path.replace("?", "%3f").replace("#", "%23")
if os.name == "nt":
uri_path = uri_path.replace("\\", "/")
uri_path = re.sub(r"^([a-z]:)", "/\\1", uri_path, flags=re.I)
uri_path = re.sub(r"/+", "/", uri_path)
# Empty netloc, params and fragment
return urlunparse(("file", "", uri_path, "", urlencode(options), ""))