Show More
@@ -1,461 +1,461 b'' | |||||
1 | from __future__ import absolute_import |
|
1 | from __future__ import absolute_import | |
2 |
|
2 | |||
3 | import errno |
|
3 | import errno | |
4 | import os |
|
4 | import os | |
5 | import shutil |
|
5 | import shutil | |
6 | import stat |
|
6 | import stat | |
7 | import time |
|
7 | import time | |
8 |
|
8 | |||
9 | from mercurial.i18n import _ |
|
9 | from mercurial.i18n import _ | |
10 | from mercurial.node import bin, hex |
|
10 | from mercurial.node import bin, hex | |
11 | from mercurial.pycompat import open |
|
11 | from mercurial.pycompat import open | |
12 | from mercurial import ( |
|
12 | from mercurial import ( | |
13 | error, |
|
13 | error, | |
14 | pycompat, |
|
14 | pycompat, | |
15 | util, |
|
15 | util, | |
16 | ) |
|
16 | ) | |
17 | from mercurial.utils import hashutil |
|
17 | from mercurial.utils import hashutil | |
18 | from . import ( |
|
18 | from . import ( | |
19 | constants, |
|
19 | constants, | |
20 | shallowutil, |
|
20 | shallowutil, | |
21 | ) |
|
21 | ) | |
22 |
|
22 | |||
23 |
|
23 | |||
24 | class basestore(object): |
|
24 | class basestore(object): | |
25 | def __init__(self, repo, path, reponame, shared=False): |
|
25 | def __init__(self, repo, path, reponame, shared=False): | |
26 | """Creates a remotefilelog store object for the given repo name. |
|
26 | """Creates a remotefilelog store object for the given repo name. | |
27 |
|
27 | |||
28 | `path` - The file path where this store keeps its data |
|
28 | `path` - The file path where this store keeps its data | |
29 | `reponame` - The name of the repo. This is used to partition data from |
|
29 | `reponame` - The name of the repo. This is used to partition data from | |
30 | many repos. |
|
30 | many repos. | |
31 | `shared` - True if this store is a shared cache of data from the central |
|
31 | `shared` - True if this store is a shared cache of data from the central | |
32 | server, for many repos on this machine. False means this store is for |
|
32 | server, for many repos on this machine. False means this store is for | |
33 | the local data for one repo. |
|
33 | the local data for one repo. | |
34 | """ |
|
34 | """ | |
35 | self.repo = repo |
|
35 | self.repo = repo | |
36 | self.ui = repo.ui |
|
36 | self.ui = repo.ui | |
37 | self._path = path |
|
37 | self._path = path | |
38 | self._reponame = reponame |
|
38 | self._reponame = reponame | |
39 | self._shared = shared |
|
39 | self._shared = shared | |
40 | self._uid = os.getuid() if not pycompat.iswindows else None |
|
40 | self._uid = os.getuid() if not pycompat.iswindows else None | |
41 |
|
41 | |||
42 | self._validatecachelog = self.ui.config( |
|
42 | self._validatecachelog = self.ui.config( | |
43 | b"remotefilelog", b"validatecachelog" |
|
43 | b"remotefilelog", b"validatecachelog" | |
44 | ) |
|
44 | ) | |
45 | self._validatecache = self.ui.config( |
|
45 | self._validatecache = self.ui.config( | |
46 | b"remotefilelog", b"validatecache", b'on' |
|
46 | b"remotefilelog", b"validatecache", b'on' | |
47 | ) |
|
47 | ) | |
48 | if self._validatecache not in (b'on', b'strict', b'off'): |
|
48 | if self._validatecache not in (b'on', b'strict', b'off'): | |
49 | self._validatecache = b'on' |
|
49 | self._validatecache = b'on' | |
50 | if self._validatecache == b'off': |
|
50 | if self._validatecache == b'off': | |
51 | self._validatecache = False |
|
51 | self._validatecache = False | |
52 |
|
52 | |||
53 | if shared: |
|
53 | if shared: | |
54 | shallowutil.mkstickygroupdir(self.ui, path) |
|
54 | shallowutil.mkstickygroupdir(self.ui, path) | |
55 |
|
55 | |||
56 | def getmissing(self, keys): |
|
56 | def getmissing(self, keys): | |
57 | missing = [] |
|
57 | missing = [] | |
58 | for name, node in keys: |
|
58 | for name, node in keys: | |
59 | filepath = self._getfilepath(name, node) |
|
59 | filepath = self._getfilepath(name, node) | |
60 | exists = os.path.exists(filepath) |
|
60 | exists = os.path.exists(filepath) | |
61 | if ( |
|
61 | if ( | |
62 | exists |
|
62 | exists | |
63 | and self._validatecache == b'strict' |
|
63 | and self._validatecache == b'strict' | |
64 | and not self._validatekey(filepath, b'contains') |
|
64 | and not self._validatekey(filepath, b'contains') | |
65 | ): |
|
65 | ): | |
66 | exists = False |
|
66 | exists = False | |
67 | if not exists: |
|
67 | if not exists: | |
68 | missing.append((name, node)) |
|
68 | missing.append((name, node)) | |
69 |
|
69 | |||
70 | return missing |
|
70 | return missing | |
71 |
|
71 | |||
72 | # BELOW THIS ARE IMPLEMENTATIONS OF REPACK SOURCE |
|
72 | # BELOW THIS ARE IMPLEMENTATIONS OF REPACK SOURCE | |
73 |
|
73 | |||
74 | def markledger(self, ledger, options=None): |
|
74 | def markledger(self, ledger, options=None): | |
75 | if options and options.get(constants.OPTION_PACKSONLY): |
|
75 | if options and options.get(constants.OPTION_PACKSONLY): | |
76 | return |
|
76 | return | |
77 | if self._shared: |
|
77 | if self._shared: | |
78 | for filename, nodes in self._getfiles(): |
|
78 | for filename, nodes in self._getfiles(): | |
79 | for node in nodes: |
|
79 | for node in nodes: | |
80 | ledger.markdataentry(self, filename, node) |
|
80 | ledger.markdataentry(self, filename, node) | |
81 | ledger.markhistoryentry(self, filename, node) |
|
81 | ledger.markhistoryentry(self, filename, node) | |
82 |
|
82 | |||
83 | def cleanup(self, ledger): |
|
83 | def cleanup(self, ledger): | |
84 | ui = self.ui |
|
84 | ui = self.ui | |
85 | entries = ledger.sources.get(self, []) |
|
85 | entries = ledger.sources.get(self, []) | |
86 | count = 0 |
|
86 | count = 0 | |
87 | progress = ui.makeprogress( |
|
87 | progress = ui.makeprogress( | |
88 | _(b"cleaning up"), unit=b"files", total=len(entries) |
|
88 | _(b"cleaning up"), unit=b"files", total=len(entries) | |
89 | ) |
|
89 | ) | |
90 | for entry in entries: |
|
90 | for entry in entries: | |
91 | if entry.gced or (entry.datarepacked and entry.historyrepacked): |
|
91 | if entry.gced or (entry.datarepacked and entry.historyrepacked): | |
92 | progress.update(count) |
|
92 | progress.update(count) | |
93 | path = self._getfilepath(entry.filename, entry.node) |
|
93 | path = self._getfilepath(entry.filename, entry.node) | |
94 | util.tryunlink(path) |
|
94 | util.tryunlink(path) | |
95 | count += 1 |
|
95 | count += 1 | |
96 | progress.complete() |
|
96 | progress.complete() | |
97 |
|
97 | |||
98 | # Clean up the repo cache directory. |
|
98 | # Clean up the repo cache directory. | |
99 | self._cleanupdirectory(self._getrepocachepath()) |
|
99 | self._cleanupdirectory(self._getrepocachepath()) | |
100 |
|
100 | |||
101 | # BELOW THIS ARE NON-STANDARD APIS |
|
101 | # BELOW THIS ARE NON-STANDARD APIS | |
102 |
|
102 | |||
103 | def _cleanupdirectory(self, rootdir): |
|
103 | def _cleanupdirectory(self, rootdir): | |
104 | """Removes the empty directories and unnecessary files within the root |
|
104 | """Removes the empty directories and unnecessary files within the root | |
105 | directory recursively. Note that this method does not remove the root |
|
105 | directory recursively. Note that this method does not remove the root | |
106 | directory itself. """ |
|
106 | directory itself. """ | |
107 |
|
107 | |||
108 | oldfiles = set() |
|
108 | oldfiles = set() | |
109 | otherfiles = set() |
|
109 | otherfiles = set() | |
110 | # osutil.listdir returns stat information which saves some rmdir/listdir |
|
110 | # osutil.listdir returns stat information which saves some rmdir/listdir | |
111 | # syscalls. |
|
111 | # syscalls. | |
112 | for name, mode in util.osutil.listdir(rootdir): |
|
112 | for name, mode in util.osutil.listdir(rootdir): | |
113 | if stat.S_ISDIR(mode): |
|
113 | if stat.S_ISDIR(mode): | |
114 | dirpath = os.path.join(rootdir, name) |
|
114 | dirpath = os.path.join(rootdir, name) | |
115 | self._cleanupdirectory(dirpath) |
|
115 | self._cleanupdirectory(dirpath) | |
116 |
|
116 | |||
117 | # Now that the directory specified by dirpath is potentially |
|
117 | # Now that the directory specified by dirpath is potentially | |
118 | # empty, try and remove it. |
|
118 | # empty, try and remove it. | |
119 | try: |
|
119 | try: | |
120 | os.rmdir(dirpath) |
|
120 | os.rmdir(dirpath) | |
121 | except OSError: |
|
121 | except OSError: | |
122 | pass |
|
122 | pass | |
123 |
|
123 | |||
124 | elif stat.S_ISREG(mode): |
|
124 | elif stat.S_ISREG(mode): | |
125 | if name.endswith(b'_old'): |
|
125 | if name.endswith(b'_old'): | |
126 | oldfiles.add(name[:-4]) |
|
126 | oldfiles.add(name[:-4]) | |
127 | else: |
|
127 | else: | |
128 | otherfiles.add(name) |
|
128 | otherfiles.add(name) | |
129 |
|
129 | |||
130 | # Remove the files which end with suffix '_old' and have no |
|
130 | # Remove the files which end with suffix '_old' and have no | |
131 | # corresponding file without the suffix '_old'. See addremotefilelognode |
|
131 | # corresponding file without the suffix '_old'. See addremotefilelognode | |
132 | # method for the generation/purpose of files with '_old' suffix. |
|
132 | # method for the generation/purpose of files with '_old' suffix. | |
133 | for filename in oldfiles - otherfiles: |
|
133 | for filename in oldfiles - otherfiles: | |
134 | filepath = os.path.join(rootdir, filename + b'_old') |
|
134 | filepath = os.path.join(rootdir, filename + b'_old') | |
135 | util.tryunlink(filepath) |
|
135 | util.tryunlink(filepath) | |
136 |
|
136 | |||
137 | def _getfiles(self): |
|
137 | def _getfiles(self): | |
138 | """Return a list of (filename, [node,...]) for all the revisions that |
|
138 | """Return a list of (filename, [node,...]) for all the revisions that | |
139 | exist in the store. |
|
139 | exist in the store. | |
140 |
|
140 | |||
141 | This is useful for obtaining a list of all the contents of the store |
|
141 | This is useful for obtaining a list of all the contents of the store | |
142 | when performing a repack to another store, since the store API requires |
|
142 | when performing a repack to another store, since the store API requires | |
143 | name+node keys and not namehash+node keys. |
|
143 | name+node keys and not namehash+node keys. | |
144 | """ |
|
144 | """ | |
145 | existing = {} |
|
145 | existing = {} | |
146 | for filenamehash, node in self._listkeys(): |
|
146 | for filenamehash, node in self._listkeys(): | |
147 | existing.setdefault(filenamehash, []).append(node) |
|
147 | existing.setdefault(filenamehash, []).append(node) | |
148 |
|
148 | |||
149 | filenamemap = self._resolvefilenames(existing.keys()) |
|
149 | filenamemap = self._resolvefilenames(existing.keys()) | |
150 |
|
150 | |||
151 | for filename, sha in pycompat.iteritems(filenamemap): |
|
151 | for filename, sha in pycompat.iteritems(filenamemap): | |
152 | yield (filename, existing[sha]) |
|
152 | yield (filename, existing[sha]) | |
153 |
|
153 | |||
154 | def _resolvefilenames(self, hashes): |
|
154 | def _resolvefilenames(self, hashes): | |
155 | """Given a list of filename hashes that are present in the |
|
155 | """Given a list of filename hashes that are present in the | |
156 | remotefilelog store, return a mapping from filename->hash. |
|
156 | remotefilelog store, return a mapping from filename->hash. | |
157 |
|
157 | |||
158 | This is useful when converting remotefilelog blobs into other storage |
|
158 | This is useful when converting remotefilelog blobs into other storage | |
159 | formats. |
|
159 | formats. | |
160 | """ |
|
160 | """ | |
161 | if not hashes: |
|
161 | if not hashes: | |
162 | return {} |
|
162 | return {} | |
163 |
|
163 | |||
164 | filenames = {} |
|
164 | filenames = {} | |
165 | missingfilename = set(hashes) |
|
165 | missingfilename = set(hashes) | |
166 |
|
166 | |||
167 | # Start with a full manifest, since it'll cover the majority of files |
|
167 | # Start with a full manifest, since it'll cover the majority of files | |
168 | for filename in self.repo[b'tip'].manifest(): |
|
168 | for filename in self.repo[b'tip'].manifest(): | |
169 | sha = hashutil.sha1(filename).digest() |
|
169 | sha = hashutil.sha1(filename).digest() | |
170 | if sha in missingfilename: |
|
170 | if sha in missingfilename: | |
171 | filenames[filename] = sha |
|
171 | filenames[filename] = sha | |
172 | missingfilename.discard(sha) |
|
172 | missingfilename.discard(sha) | |
173 |
|
173 | |||
174 | # Scan the changelog until we've found every file name |
|
174 | # Scan the changelog until we've found every file name | |
175 | cl = self.repo.unfiltered().changelog |
|
175 | cl = self.repo.unfiltered().changelog | |
176 | for rev in pycompat.xrange(len(cl) - 1, -1, -1): |
|
176 | for rev in pycompat.xrange(len(cl) - 1, -1, -1): | |
177 | if not missingfilename: |
|
177 | if not missingfilename: | |
178 | break |
|
178 | break | |
179 | files = cl.readfiles(cl.node(rev)) |
|
179 | files = cl.readfiles(cl.node(rev)) | |
180 | for filename in files: |
|
180 | for filename in files: | |
181 | sha = hashutil.sha1(filename).digest() |
|
181 | sha = hashutil.sha1(filename).digest() | |
182 | if sha in missingfilename: |
|
182 | if sha in missingfilename: | |
183 | filenames[filename] = sha |
|
183 | filenames[filename] = sha | |
184 | missingfilename.discard(sha) |
|
184 | missingfilename.discard(sha) | |
185 |
|
185 | |||
186 | return filenames |
|
186 | return filenames | |
187 |
|
187 | |||
188 | def _getrepocachepath(self): |
|
188 | def _getrepocachepath(self): | |
189 | return ( |
|
189 | return ( | |
190 | os.path.join(self._path, self._reponame) |
|
190 | os.path.join(self._path, self._reponame) | |
191 | if self._shared |
|
191 | if self._shared | |
192 | else self._path |
|
192 | else self._path | |
193 | ) |
|
193 | ) | |
194 |
|
194 | |||
195 | def _listkeys(self): |
|
195 | def _listkeys(self): | |
196 | """List all the remotefilelog keys that exist in the store. |
|
196 | """List all the remotefilelog keys that exist in the store. | |
197 |
|
197 | |||
198 | Returns a iterator of (filename hash, filecontent hash) tuples. |
|
198 | Returns a iterator of (filename hash, filecontent hash) tuples. | |
199 | """ |
|
199 | """ | |
200 |
|
200 | |||
201 | for root, dirs, files in os.walk(self._getrepocachepath()): |
|
201 | for root, dirs, files in os.walk(self._getrepocachepath()): | |
202 | for filename in files: |
|
202 | for filename in files: | |
203 | if len(filename) != 40: |
|
203 | if len(filename) != 40: | |
204 | continue |
|
204 | continue | |
205 | node = filename |
|
205 | node = filename | |
206 | if self._shared: |
|
206 | if self._shared: | |
207 | # .../1a/85ffda..be21 |
|
207 | # .../1a/85ffda..be21 | |
208 | filenamehash = root[-41:-39] + root[-38:] |
|
208 | filenamehash = root[-41:-39] + root[-38:] | |
209 | else: |
|
209 | else: | |
210 | filenamehash = root[-40:] |
|
210 | filenamehash = root[-40:] | |
211 | yield (bin(filenamehash), bin(node)) |
|
211 | yield (bin(filenamehash), bin(node)) | |
212 |
|
212 | |||
213 | def _getfilepath(self, name, node): |
|
213 | def _getfilepath(self, name, node): | |
214 | node = hex(node) |
|
214 | node = hex(node) | |
215 | if self._shared: |
|
215 | if self._shared: | |
216 | key = shallowutil.getcachekey(self._reponame, name, node) |
|
216 | key = shallowutil.getcachekey(self._reponame, name, node) | |
217 | else: |
|
217 | else: | |
218 | key = shallowutil.getlocalkey(name, node) |
|
218 | key = shallowutil.getlocalkey(name, node) | |
219 |
|
219 | |||
220 | return os.path.join(self._path, key) |
|
220 | return os.path.join(self._path, key) | |
221 |
|
221 | |||
222 | def _getdata(self, name, node): |
|
222 | def _getdata(self, name, node): | |
223 | filepath = self._getfilepath(name, node) |
|
223 | filepath = self._getfilepath(name, node) | |
224 | try: |
|
224 | try: | |
225 | data = shallowutil.readfile(filepath) |
|
225 | data = shallowutil.readfile(filepath) | |
226 | if self._validatecache and not self._validatedata(data, filepath): |
|
226 | if self._validatecache and not self._validatedata(data, filepath): | |
227 | if self._validatecachelog: |
|
227 | if self._validatecachelog: | |
228 | with open(self._validatecachelog, b'a+') as f: |
|
228 | with open(self._validatecachelog, b'ab+') as f: | |
229 | f.write(b"corrupt %s during read\n" % filepath) |
|
229 | f.write(b"corrupt %s during read\n" % filepath) | |
230 | os.rename(filepath, filepath + b".corrupt") |
|
230 | os.rename(filepath, filepath + b".corrupt") | |
231 | raise KeyError(b"corrupt local cache file %s" % filepath) |
|
231 | raise KeyError(b"corrupt local cache file %s" % filepath) | |
232 | except IOError: |
|
232 | except IOError: | |
233 | raise KeyError( |
|
233 | raise KeyError( | |
234 | b"no file found at %s for %s:%s" % (filepath, name, hex(node)) |
|
234 | b"no file found at %s for %s:%s" % (filepath, name, hex(node)) | |
235 | ) |
|
235 | ) | |
236 |
|
236 | |||
237 | return data |
|
237 | return data | |
238 |
|
238 | |||
239 | def addremotefilelognode(self, name, node, data): |
|
239 | def addremotefilelognode(self, name, node, data): | |
240 | filepath = self._getfilepath(name, node) |
|
240 | filepath = self._getfilepath(name, node) | |
241 |
|
241 | |||
242 | oldumask = os.umask(0o002) |
|
242 | oldumask = os.umask(0o002) | |
243 | try: |
|
243 | try: | |
244 | # if this node already exists, save the old version for |
|
244 | # if this node already exists, save the old version for | |
245 | # recovery/debugging purposes. |
|
245 | # recovery/debugging purposes. | |
246 | if os.path.exists(filepath): |
|
246 | if os.path.exists(filepath): | |
247 | newfilename = filepath + b'_old' |
|
247 | newfilename = filepath + b'_old' | |
248 | # newfilename can be read-only and shutil.copy will fail. |
|
248 | # newfilename can be read-only and shutil.copy will fail. | |
249 | # Delete newfilename to avoid it |
|
249 | # Delete newfilename to avoid it | |
250 | if os.path.exists(newfilename): |
|
250 | if os.path.exists(newfilename): | |
251 | shallowutil.unlinkfile(newfilename) |
|
251 | shallowutil.unlinkfile(newfilename) | |
252 | shutil.copy(filepath, newfilename) |
|
252 | shutil.copy(filepath, newfilename) | |
253 |
|
253 | |||
254 | shallowutil.mkstickygroupdir(self.ui, os.path.dirname(filepath)) |
|
254 | shallowutil.mkstickygroupdir(self.ui, os.path.dirname(filepath)) | |
255 | shallowutil.writefile(filepath, data, readonly=True) |
|
255 | shallowutil.writefile(filepath, data, readonly=True) | |
256 |
|
256 | |||
257 | if self._validatecache: |
|
257 | if self._validatecache: | |
258 | if not self._validatekey(filepath, b'write'): |
|
258 | if not self._validatekey(filepath, b'write'): | |
259 | raise error.Abort( |
|
259 | raise error.Abort( | |
260 | _(b"local cache write was corrupted %s") % filepath |
|
260 | _(b"local cache write was corrupted %s") % filepath | |
261 | ) |
|
261 | ) | |
262 | finally: |
|
262 | finally: | |
263 | os.umask(oldumask) |
|
263 | os.umask(oldumask) | |
264 |
|
264 | |||
265 | def markrepo(self, path): |
|
265 | def markrepo(self, path): | |
266 | """Call this to add the given repo path to the store's list of |
|
266 | """Call this to add the given repo path to the store's list of | |
267 | repositories that are using it. This is useful later when doing garbage |
|
267 | repositories that are using it. This is useful later when doing garbage | |
268 | collection, since it allows us to insecpt the repos to see what nodes |
|
268 | collection, since it allows us to insecpt the repos to see what nodes | |
269 | they want to be kept alive in the store. |
|
269 | they want to be kept alive in the store. | |
270 | """ |
|
270 | """ | |
271 | repospath = os.path.join(self._path, b"repos") |
|
271 | repospath = os.path.join(self._path, b"repos") | |
272 | with open(repospath, b'ab') as reposfile: |
|
272 | with open(repospath, b'ab') as reposfile: | |
273 | reposfile.write(os.path.dirname(path) + b"\n") |
|
273 | reposfile.write(os.path.dirname(path) + b"\n") | |
274 |
|
274 | |||
275 | repospathstat = os.stat(repospath) |
|
275 | repospathstat = os.stat(repospath) | |
276 | if repospathstat.st_uid == self._uid: |
|
276 | if repospathstat.st_uid == self._uid: | |
277 | os.chmod(repospath, 0o0664) |
|
277 | os.chmod(repospath, 0o0664) | |
278 |
|
278 | |||
279 | def _validatekey(self, path, action): |
|
279 | def _validatekey(self, path, action): | |
280 | with open(path, b'rb') as f: |
|
280 | with open(path, b'rb') as f: | |
281 | data = f.read() |
|
281 | data = f.read() | |
282 |
|
282 | |||
283 | if self._validatedata(data, path): |
|
283 | if self._validatedata(data, path): | |
284 | return True |
|
284 | return True | |
285 |
|
285 | |||
286 | if self._validatecachelog: |
|
286 | if self._validatecachelog: | |
287 | with open(self._validatecachelog, b'ab+') as f: |
|
287 | with open(self._validatecachelog, b'ab+') as f: | |
288 | f.write(b"corrupt %s during %s\n" % (path, action)) |
|
288 | f.write(b"corrupt %s during %s\n" % (path, action)) | |
289 |
|
289 | |||
290 | os.rename(path, path + b".corrupt") |
|
290 | os.rename(path, path + b".corrupt") | |
291 | return False |
|
291 | return False | |
292 |
|
292 | |||
293 | def _validatedata(self, data, path): |
|
293 | def _validatedata(self, data, path): | |
294 | try: |
|
294 | try: | |
295 | if len(data) > 0: |
|
295 | if len(data) > 0: | |
296 | # see remotefilelogserver.createfileblob for the format |
|
296 | # see remotefilelogserver.createfileblob for the format | |
297 | offset, size, flags = shallowutil.parsesizeflags(data) |
|
297 | offset, size, flags = shallowutil.parsesizeflags(data) | |
298 | if len(data) <= size: |
|
298 | if len(data) <= size: | |
299 | # it is truncated |
|
299 | # it is truncated | |
300 | return False |
|
300 | return False | |
301 |
|
301 | |||
302 | # extract the node from the metadata |
|
302 | # extract the node from the metadata | |
303 | offset += size |
|
303 | offset += size | |
304 | datanode = data[offset : offset + 20] |
|
304 | datanode = data[offset : offset + 20] | |
305 |
|
305 | |||
306 | # and compare against the path |
|
306 | # and compare against the path | |
307 | if os.path.basename(path) == hex(datanode): |
|
307 | if os.path.basename(path) == hex(datanode): | |
308 | # Content matches the intended path |
|
308 | # Content matches the intended path | |
309 | return True |
|
309 | return True | |
310 | return False |
|
310 | return False | |
311 | except (ValueError, RuntimeError): |
|
311 | except (ValueError, RuntimeError): | |
312 | pass |
|
312 | pass | |
313 |
|
313 | |||
314 | return False |
|
314 | return False | |
315 |
|
315 | |||
316 | def gc(self, keepkeys): |
|
316 | def gc(self, keepkeys): | |
317 | ui = self.ui |
|
317 | ui = self.ui | |
318 | cachepath = self._path |
|
318 | cachepath = self._path | |
319 |
|
319 | |||
320 | # prune cache |
|
320 | # prune cache | |
321 | queue = pycompat.queue.PriorityQueue() |
|
321 | queue = pycompat.queue.PriorityQueue() | |
322 | originalsize = 0 |
|
322 | originalsize = 0 | |
323 | size = 0 |
|
323 | size = 0 | |
324 | count = 0 |
|
324 | count = 0 | |
325 | removed = 0 |
|
325 | removed = 0 | |
326 |
|
326 | |||
327 | # keep files newer than a day even if they aren't needed |
|
327 | # keep files newer than a day even if they aren't needed | |
328 | limit = time.time() - (60 * 60 * 24) |
|
328 | limit = time.time() - (60 * 60 * 24) | |
329 |
|
329 | |||
330 | progress = ui.makeprogress( |
|
330 | progress = ui.makeprogress( | |
331 | _(b"removing unnecessary files"), unit=b"files" |
|
331 | _(b"removing unnecessary files"), unit=b"files" | |
332 | ) |
|
332 | ) | |
333 | progress.update(0) |
|
333 | progress.update(0) | |
334 | for root, dirs, files in os.walk(cachepath): |
|
334 | for root, dirs, files in os.walk(cachepath): | |
335 | for file in files: |
|
335 | for file in files: | |
336 | if file == b'repos': |
|
336 | if file == b'repos': | |
337 | continue |
|
337 | continue | |
338 |
|
338 | |||
339 | # Don't delete pack files |
|
339 | # Don't delete pack files | |
340 | if b'/packs/' in root: |
|
340 | if b'/packs/' in root: | |
341 | continue |
|
341 | continue | |
342 |
|
342 | |||
343 | progress.update(count) |
|
343 | progress.update(count) | |
344 | path = os.path.join(root, file) |
|
344 | path = os.path.join(root, file) | |
345 | key = os.path.relpath(path, cachepath) |
|
345 | key = os.path.relpath(path, cachepath) | |
346 | count += 1 |
|
346 | count += 1 | |
347 | try: |
|
347 | try: | |
348 | pathstat = os.stat(path) |
|
348 | pathstat = os.stat(path) | |
349 | except OSError as e: |
|
349 | except OSError as e: | |
350 | # errno.ENOENT = no such file or directory |
|
350 | # errno.ENOENT = no such file or directory | |
351 | if e.errno != errno.ENOENT: |
|
351 | if e.errno != errno.ENOENT: | |
352 | raise |
|
352 | raise | |
353 | msg = _( |
|
353 | msg = _( | |
354 | b"warning: file %s was removed by another process\n" |
|
354 | b"warning: file %s was removed by another process\n" | |
355 | ) |
|
355 | ) | |
356 | ui.warn(msg % path) |
|
356 | ui.warn(msg % path) | |
357 | continue |
|
357 | continue | |
358 |
|
358 | |||
359 | originalsize += pathstat.st_size |
|
359 | originalsize += pathstat.st_size | |
360 |
|
360 | |||
361 | if key in keepkeys or pathstat.st_atime > limit: |
|
361 | if key in keepkeys or pathstat.st_atime > limit: | |
362 | queue.put((pathstat.st_atime, path, pathstat)) |
|
362 | queue.put((pathstat.st_atime, path, pathstat)) | |
363 | size += pathstat.st_size |
|
363 | size += pathstat.st_size | |
364 | else: |
|
364 | else: | |
365 | try: |
|
365 | try: | |
366 | shallowutil.unlinkfile(path) |
|
366 | shallowutil.unlinkfile(path) | |
367 | except OSError as e: |
|
367 | except OSError as e: | |
368 | # errno.ENOENT = no such file or directory |
|
368 | # errno.ENOENT = no such file or directory | |
369 | if e.errno != errno.ENOENT: |
|
369 | if e.errno != errno.ENOENT: | |
370 | raise |
|
370 | raise | |
371 | msg = _( |
|
371 | msg = _( | |
372 | b"warning: file %s was removed by another " |
|
372 | b"warning: file %s was removed by another " | |
373 | b"process\n" |
|
373 | b"process\n" | |
374 | ) |
|
374 | ) | |
375 | ui.warn(msg % path) |
|
375 | ui.warn(msg % path) | |
376 | continue |
|
376 | continue | |
377 | removed += 1 |
|
377 | removed += 1 | |
378 | progress.complete() |
|
378 | progress.complete() | |
379 |
|
379 | |||
380 | # remove oldest files until under limit |
|
380 | # remove oldest files until under limit | |
381 | limit = ui.configbytes(b"remotefilelog", b"cachelimit") |
|
381 | limit = ui.configbytes(b"remotefilelog", b"cachelimit") | |
382 | if size > limit: |
|
382 | if size > limit: | |
383 | excess = size - limit |
|
383 | excess = size - limit | |
384 | progress = ui.makeprogress( |
|
384 | progress = ui.makeprogress( | |
385 | _(b"enforcing cache limit"), unit=b"bytes", total=excess |
|
385 | _(b"enforcing cache limit"), unit=b"bytes", total=excess | |
386 | ) |
|
386 | ) | |
387 | removedexcess = 0 |
|
387 | removedexcess = 0 | |
388 | while queue and size > limit and size > 0: |
|
388 | while queue and size > limit and size > 0: | |
389 | progress.update(removedexcess) |
|
389 | progress.update(removedexcess) | |
390 | atime, oldpath, oldpathstat = queue.get() |
|
390 | atime, oldpath, oldpathstat = queue.get() | |
391 | try: |
|
391 | try: | |
392 | shallowutil.unlinkfile(oldpath) |
|
392 | shallowutil.unlinkfile(oldpath) | |
393 | except OSError as e: |
|
393 | except OSError as e: | |
394 | # errno.ENOENT = no such file or directory |
|
394 | # errno.ENOENT = no such file or directory | |
395 | if e.errno != errno.ENOENT: |
|
395 | if e.errno != errno.ENOENT: | |
396 | raise |
|
396 | raise | |
397 | msg = _( |
|
397 | msg = _( | |
398 | b"warning: file %s was removed by another process\n" |
|
398 | b"warning: file %s was removed by another process\n" | |
399 | ) |
|
399 | ) | |
400 | ui.warn(msg % oldpath) |
|
400 | ui.warn(msg % oldpath) | |
401 | size -= oldpathstat.st_size |
|
401 | size -= oldpathstat.st_size | |
402 | removed += 1 |
|
402 | removed += 1 | |
403 | removedexcess += oldpathstat.st_size |
|
403 | removedexcess += oldpathstat.st_size | |
404 | progress.complete() |
|
404 | progress.complete() | |
405 |
|
405 | |||
406 | ui.status( |
|
406 | ui.status( | |
407 | _(b"finished: removed %d of %d files (%0.2f GB to %0.2f GB)\n") |
|
407 | _(b"finished: removed %d of %d files (%0.2f GB to %0.2f GB)\n") | |
408 | % ( |
|
408 | % ( | |
409 | removed, |
|
409 | removed, | |
410 | count, |
|
410 | count, | |
411 | float(originalsize) / 1024.0 / 1024.0 / 1024.0, |
|
411 | float(originalsize) / 1024.0 / 1024.0 / 1024.0, | |
412 | float(size) / 1024.0 / 1024.0 / 1024.0, |
|
412 | float(size) / 1024.0 / 1024.0 / 1024.0, | |
413 | ) |
|
413 | ) | |
414 | ) |
|
414 | ) | |
415 |
|
415 | |||
416 |
|
416 | |||
417 | class baseunionstore(object): |
|
417 | class baseunionstore(object): | |
418 | def __init__(self, *args, **kwargs): |
|
418 | def __init__(self, *args, **kwargs): | |
419 | # If one of the functions that iterates all of the stores is about to |
|
419 | # If one of the functions that iterates all of the stores is about to | |
420 | # throw a KeyError, try this many times with a full refresh between |
|
420 | # throw a KeyError, try this many times with a full refresh between | |
421 | # attempts. A repack operation may have moved data from one store to |
|
421 | # attempts. A repack operation may have moved data from one store to | |
422 | # another while we were running. |
|
422 | # another while we were running. | |
423 | self.numattempts = kwargs.get('numretries', 0) + 1 |
|
423 | self.numattempts = kwargs.get('numretries', 0) + 1 | |
424 | # If not-None, call this function on every retry and if the attempts are |
|
424 | # If not-None, call this function on every retry and if the attempts are | |
425 | # exhausted. |
|
425 | # exhausted. | |
426 | self.retrylog = kwargs.get('retrylog', None) |
|
426 | self.retrylog = kwargs.get('retrylog', None) | |
427 |
|
427 | |||
428 | def markforrefresh(self): |
|
428 | def markforrefresh(self): | |
429 | for store in self.stores: |
|
429 | for store in self.stores: | |
430 | if util.safehasattr(store, b'markforrefresh'): |
|
430 | if util.safehasattr(store, b'markforrefresh'): | |
431 | store.markforrefresh() |
|
431 | store.markforrefresh() | |
432 |
|
432 | |||
433 | @staticmethod |
|
433 | @staticmethod | |
434 | def retriable(fn): |
|
434 | def retriable(fn): | |
435 | def noop(*args): |
|
435 | def noop(*args): | |
436 | pass |
|
436 | pass | |
437 |
|
437 | |||
438 | def wrapped(self, *args, **kwargs): |
|
438 | def wrapped(self, *args, **kwargs): | |
439 | retrylog = self.retrylog or noop |
|
439 | retrylog = self.retrylog or noop | |
440 | funcname = fn.__name__ |
|
440 | funcname = fn.__name__ | |
441 | i = 0 |
|
441 | i = 0 | |
442 | while i < self.numattempts: |
|
442 | while i < self.numattempts: | |
443 | if i > 0: |
|
443 | if i > 0: | |
444 | retrylog( |
|
444 | retrylog( | |
445 | b're-attempting (n=%d) %s\n' |
|
445 | b're-attempting (n=%d) %s\n' | |
446 | % (i, pycompat.sysbytes(funcname)) |
|
446 | % (i, pycompat.sysbytes(funcname)) | |
447 | ) |
|
447 | ) | |
448 | self.markforrefresh() |
|
448 | self.markforrefresh() | |
449 | i += 1 |
|
449 | i += 1 | |
450 | try: |
|
450 | try: | |
451 | return fn(self, *args, **kwargs) |
|
451 | return fn(self, *args, **kwargs) | |
452 | except KeyError: |
|
452 | except KeyError: | |
453 | if i == self.numattempts: |
|
453 | if i == self.numattempts: | |
454 | # retries exhausted |
|
454 | # retries exhausted | |
455 | retrylog( |
|
455 | retrylog( | |
456 | b'retries exhausted in %s, raising KeyError\n' |
|
456 | b'retries exhausted in %s, raising KeyError\n' | |
457 | % pycompat.sysbytes(funcname) |
|
457 | % pycompat.sysbytes(funcname) | |
458 | ) |
|
458 | ) | |
459 | raise |
|
459 | raise | |
460 |
|
460 | |||
461 | return wrapped |
|
461 | return wrapped |
General Comments 0
You need to be logged in to leave comments.
Login now