##// END OF EJS Templates
largefiles: added fix for downloading largefiles without an extension in name.
milka -
r4666:23c030e1 default
parent child Browse files
Show More
@@ -1,875 +1,876 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2020 RhodeCode GmbH
3 # Copyright (C) 2014-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Module holding everything related to vcs nodes, with vcs2 architecture.
22 Module holding everything related to vcs nodes, with vcs2 architecture.
23 """
23 """
24
24
25 import os
25 import os
26 import stat
26 import stat
27
27
28 from zope.cachedescriptors.property import Lazy as LazyProperty
28 from zope.cachedescriptors.property import Lazy as LazyProperty
29
29
30 import rhodecode
30 import rhodecode
31 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
32 from rhodecode.lib.utils import safe_unicode, safe_str
32 from rhodecode.lib.utils import safe_unicode, safe_str
33 from rhodecode.lib.utils2 import md5
33 from rhodecode.lib.utils2 import md5
34 from rhodecode.lib.vcs import path as vcspath
34 from rhodecode.lib.vcs import path as vcspath
35 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
37 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
38
38
39 LARGEFILE_PREFIX = '.hglf'
39 LARGEFILE_PREFIX = '.hglf'
40
40
41
41
42 class NodeKind:
42 class NodeKind:
43 SUBMODULE = -1
43 SUBMODULE = -1
44 DIR = 1
44 DIR = 1
45 FILE = 2
45 FILE = 2
46 LARGEFILE = 3
46 LARGEFILE = 3
47
47
48
48
49 class NodeState:
49 class NodeState:
50 ADDED = u'added'
50 ADDED = u'added'
51 CHANGED = u'changed'
51 CHANGED = u'changed'
52 NOT_CHANGED = u'not changed'
52 NOT_CHANGED = u'not changed'
53 REMOVED = u'removed'
53 REMOVED = u'removed'
54
54
55
55
56 class NodeGeneratorBase(object):
56 class NodeGeneratorBase(object):
57 """
57 """
58 Base class for removed added and changed filenodes, it's a lazy generator
58 Base class for removed added and changed filenodes, it's a lazy generator
59 class that will create filenodes only on iteration or call
59 class that will create filenodes only on iteration or call
60
60
61 The len method doesn't need to create filenodes at all
61 The len method doesn't need to create filenodes at all
62 """
62 """
63
63
64 def __init__(self, current_paths, cs):
64 def __init__(self, current_paths, cs):
65 self.cs = cs
65 self.cs = cs
66 self.current_paths = current_paths
66 self.current_paths = current_paths
67
67
68 def __call__(self):
68 def __call__(self):
69 return [n for n in self]
69 return [n for n in self]
70
70
71 def __getslice__(self, i, j):
71 def __getslice__(self, i, j):
72 for p in self.current_paths[i:j]:
72 for p in self.current_paths[i:j]:
73 yield self.cs.get_node(p)
73 yield self.cs.get_node(p)
74
74
75 def __len__(self):
75 def __len__(self):
76 return len(self.current_paths)
76 return len(self.current_paths)
77
77
78 def __iter__(self):
78 def __iter__(self):
79 for p in self.current_paths:
79 for p in self.current_paths:
80 yield self.cs.get_node(p)
80 yield self.cs.get_node(p)
81
81
82
82
83 class AddedFileNodesGenerator(NodeGeneratorBase):
83 class AddedFileNodesGenerator(NodeGeneratorBase):
84 """
84 """
85 Class holding added files for current commit
85 Class holding added files for current commit
86 """
86 """
87
87
88
88
89 class ChangedFileNodesGenerator(NodeGeneratorBase):
89 class ChangedFileNodesGenerator(NodeGeneratorBase):
90 """
90 """
91 Class holding changed files for current commit
91 Class holding changed files for current commit
92 """
92 """
93
93
94
94
95 class RemovedFileNodesGenerator(NodeGeneratorBase):
95 class RemovedFileNodesGenerator(NodeGeneratorBase):
96 """
96 """
97 Class holding removed files for current commit
97 Class holding removed files for current commit
98 """
98 """
99 def __iter__(self):
99 def __iter__(self):
100 for p in self.current_paths:
100 for p in self.current_paths:
101 yield RemovedFileNode(path=p)
101 yield RemovedFileNode(path=p)
102
102
103 def __getslice__(self, i, j):
103 def __getslice__(self, i, j):
104 for p in self.current_paths[i:j]:
104 for p in self.current_paths[i:j]:
105 yield RemovedFileNode(path=p)
105 yield RemovedFileNode(path=p)
106
106
107
107
108 class Node(object):
108 class Node(object):
109 """
109 """
110 Simplest class representing file or directory on repository. SCM backends
110 Simplest class representing file or directory on repository. SCM backends
111 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
111 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
112 directly.
112 directly.
113
113
114 Node's ``path`` cannot start with slash as we operate on *relative* paths
114 Node's ``path`` cannot start with slash as we operate on *relative* paths
115 only. Moreover, every single node is identified by the ``path`` attribute,
115 only. Moreover, every single node is identified by the ``path`` attribute,
116 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
116 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
117 """
117 """
118 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
118 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
119 # security attacks could be used with this
119 # security attacks could be used with this
120 commit = None
120 commit = None
121
121
122 def __init__(self, path, kind):
122 def __init__(self, path, kind):
123 self._validate_path(path) # can throw exception if path is invalid
123 self._validate_path(path) # can throw exception if path is invalid
124 self.path = safe_str(path.rstrip('/')) # we store paths as str
124 self.path = safe_str(path.rstrip('/')) # we store paths as str
125 if path == '' and kind != NodeKind.DIR:
125 if path == '' and kind != NodeKind.DIR:
126 raise NodeError("Only DirNode and its subclasses may be "
126 raise NodeError("Only DirNode and its subclasses may be "
127 "initialized with empty path")
127 "initialized with empty path")
128 self.kind = kind
128 self.kind = kind
129
129
130 if self.is_root() and not self.is_dir():
130 if self.is_root() and not self.is_dir():
131 raise NodeError("Root node cannot be FILE kind")
131 raise NodeError("Root node cannot be FILE kind")
132
132
133 def _validate_path(self, path):
133 def _validate_path(self, path):
134 if path.startswith('/'):
134 if path.startswith('/'):
135 raise NodeError(
135 raise NodeError(
136 "Cannot initialize Node objects with slash at "
136 "Cannot initialize Node objects with slash at "
137 "the beginning as only relative paths are supported. "
137 "the beginning as only relative paths are supported. "
138 "Got %s" % (path,))
138 "Got %s" % (path,))
139
139
140 @LazyProperty
140 @LazyProperty
141 def parent(self):
141 def parent(self):
142 parent_path = self.get_parent_path()
142 parent_path = self.get_parent_path()
143 if parent_path:
143 if parent_path:
144 if self.commit:
144 if self.commit:
145 return self.commit.get_node(parent_path)
145 return self.commit.get_node(parent_path)
146 return DirNode(parent_path)
146 return DirNode(parent_path)
147 return None
147 return None
148
148
149 @LazyProperty
149 @LazyProperty
150 def unicode_path(self):
150 def unicode_path(self):
151 return safe_unicode(self.path)
151 return safe_unicode(self.path)
152
152
153 @LazyProperty
153 @LazyProperty
154 def has_rtlo(self):
154 def has_rtlo(self):
155 """Detects if a path has right-to-left-override marker"""
155 """Detects if a path has right-to-left-override marker"""
156 return self.RTLO_MARKER in self.unicode_path
156 return self.RTLO_MARKER in self.unicode_path
157
157
158 @LazyProperty
158 @LazyProperty
159 def unicode_path_safe(self):
159 def unicode_path_safe(self):
160 """
160 """
161 Special SAFE representation of path without the right-to-left-override.
161 Special SAFE representation of path without the right-to-left-override.
162 This should be only used for "showing" the file, cannot be used for any
162 This should be only used for "showing" the file, cannot be used for any
163 urls etc.
163 urls etc.
164 """
164 """
165 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
165 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
166
166
167 @LazyProperty
167 @LazyProperty
168 def dir_path(self):
168 def dir_path(self):
169 """
169 """
170 Returns name of the directory from full path of this vcs node. Empty
170 Returns name of the directory from full path of this vcs node. Empty
171 string is returned if there's no directory in the path
171 string is returned if there's no directory in the path
172 """
172 """
173 _parts = self.path.rstrip('/').rsplit('/', 1)
173 _parts = self.path.rstrip('/').rsplit('/', 1)
174 if len(_parts) == 2:
174 if len(_parts) == 2:
175 return safe_unicode(_parts[0])
175 return safe_unicode(_parts[0])
176 return u''
176 return u''
177
177
178 @LazyProperty
178 @LazyProperty
179 def name(self):
179 def name(self):
180 """
180 """
181 Returns name of the node so if its path
181 Returns name of the node so if its path
182 then only last part is returned.
182 then only last part is returned.
183 """
183 """
184 return safe_unicode(self.path.rstrip('/').split('/')[-1])
184 return safe_unicode(self.path.rstrip('/').split('/')[-1])
185
185
186 @property
186 @property
187 def kind(self):
187 def kind(self):
188 return self._kind
188 return self._kind
189
189
190 @kind.setter
190 @kind.setter
191 def kind(self, kind):
191 def kind(self, kind):
192 if hasattr(self, '_kind'):
192 if hasattr(self, '_kind'):
193 raise NodeError("Cannot change node's kind")
193 raise NodeError("Cannot change node's kind")
194 else:
194 else:
195 self._kind = kind
195 self._kind = kind
196 # Post setter check (path's trailing slash)
196 # Post setter check (path's trailing slash)
197 if self.path.endswith('/'):
197 if self.path.endswith('/'):
198 raise NodeError("Node's path cannot end with slash")
198 raise NodeError("Node's path cannot end with slash")
199
199
200 def __cmp__(self, other):
200 def __cmp__(self, other):
201 """
201 """
202 Comparator using name of the node, needed for quick list sorting.
202 Comparator using name of the node, needed for quick list sorting.
203 """
203 """
204
204
205 kind_cmp = cmp(self.kind, other.kind)
205 kind_cmp = cmp(self.kind, other.kind)
206 if kind_cmp:
206 if kind_cmp:
207 if isinstance(self, SubModuleNode):
207 if isinstance(self, SubModuleNode):
208 # we make submodules equal to dirnode for "sorting" purposes
208 # we make submodules equal to dirnode for "sorting" purposes
209 return NodeKind.DIR
209 return NodeKind.DIR
210 return kind_cmp
210 return kind_cmp
211 return cmp(self.name, other.name)
211 return cmp(self.name, other.name)
212
212
213 def __eq__(self, other):
213 def __eq__(self, other):
214 for attr in ['name', 'path', 'kind']:
214 for attr in ['name', 'path', 'kind']:
215 if getattr(self, attr) != getattr(other, attr):
215 if getattr(self, attr) != getattr(other, attr):
216 return False
216 return False
217 if self.is_file():
217 if self.is_file():
218 if self.content != other.content:
218 if self.content != other.content:
219 return False
219 return False
220 else:
220 else:
221 # For DirNode's check without entering each dir
221 # For DirNode's check without entering each dir
222 self_nodes_paths = list(sorted(n.path for n in self.nodes))
222 self_nodes_paths = list(sorted(n.path for n in self.nodes))
223 other_nodes_paths = list(sorted(n.path for n in self.nodes))
223 other_nodes_paths = list(sorted(n.path for n in self.nodes))
224 if self_nodes_paths != other_nodes_paths:
224 if self_nodes_paths != other_nodes_paths:
225 return False
225 return False
226 return True
226 return True
227
227
228 def __ne__(self, other):
228 def __ne__(self, other):
229 return not self.__eq__(other)
229 return not self.__eq__(other)
230
230
231 def __repr__(self):
231 def __repr__(self):
232 return '<%s %r>' % (self.__class__.__name__, self.path)
232 return '<%s %r>' % (self.__class__.__name__, self.path)
233
233
234 def __str__(self):
234 def __str__(self):
235 return self.__repr__()
235 return self.__repr__()
236
236
237 def __unicode__(self):
237 def __unicode__(self):
238 return self.name
238 return self.name
239
239
240 def get_parent_path(self):
240 def get_parent_path(self):
241 """
241 """
242 Returns node's parent path or empty string if node is root.
242 Returns node's parent path or empty string if node is root.
243 """
243 """
244 if self.is_root():
244 if self.is_root():
245 return ''
245 return ''
246 return vcspath.dirname(self.path.rstrip('/')) + '/'
246 return vcspath.dirname(self.path.rstrip('/')) + '/'
247
247
248 def is_file(self):
248 def is_file(self):
249 """
249 """
250 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
250 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
251 otherwise.
251 otherwise.
252 """
252 """
253 return self.kind == NodeKind.FILE
253 return self.kind == NodeKind.FILE
254
254
255 def is_dir(self):
255 def is_dir(self):
256 """
256 """
257 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
257 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
258 otherwise.
258 otherwise.
259 """
259 """
260 return self.kind == NodeKind.DIR
260 return self.kind == NodeKind.DIR
261
261
262 def is_root(self):
262 def is_root(self):
263 """
263 """
264 Returns ``True`` if node is a root node and ``False`` otherwise.
264 Returns ``True`` if node is a root node and ``False`` otherwise.
265 """
265 """
266 return self.kind == NodeKind.DIR and self.path == ''
266 return self.kind == NodeKind.DIR and self.path == ''
267
267
268 def is_submodule(self):
268 def is_submodule(self):
269 """
269 """
270 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
270 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
271 otherwise.
271 otherwise.
272 """
272 """
273 return self.kind == NodeKind.SUBMODULE
273 return self.kind == NodeKind.SUBMODULE
274
274
275 def is_largefile(self):
275 def is_largefile(self):
276 """
276 """
277 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
277 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
278 otherwise
278 otherwise
279 """
279 """
280 return self.kind == NodeKind.LARGEFILE
280 return self.kind == NodeKind.LARGEFILE
281
281
282 def is_link(self):
282 def is_link(self):
283 if self.commit:
283 if self.commit:
284 return self.commit.is_link(self.path)
284 return self.commit.is_link(self.path)
285 return False
285 return False
286
286
287 @LazyProperty
287 @LazyProperty
288 def added(self):
288 def added(self):
289 return self.state is NodeState.ADDED
289 return self.state is NodeState.ADDED
290
290
291 @LazyProperty
291 @LazyProperty
292 def changed(self):
292 def changed(self):
293 return self.state is NodeState.CHANGED
293 return self.state is NodeState.CHANGED
294
294
295 @LazyProperty
295 @LazyProperty
296 def not_changed(self):
296 def not_changed(self):
297 return self.state is NodeState.NOT_CHANGED
297 return self.state is NodeState.NOT_CHANGED
298
298
299 @LazyProperty
299 @LazyProperty
300 def removed(self):
300 def removed(self):
301 return self.state is NodeState.REMOVED
301 return self.state is NodeState.REMOVED
302
302
303
303
304 class FileNode(Node):
304 class FileNode(Node):
305 """
305 """
306 Class representing file nodes.
306 Class representing file nodes.
307
307
308 :attribute: path: path to the node, relative to repository's root
308 :attribute: path: path to the node, relative to repository's root
309 :attribute: content: if given arbitrary sets content of the file
309 :attribute: content: if given arbitrary sets content of the file
310 :attribute: commit: if given, first time content is accessed, callback
310 :attribute: commit: if given, first time content is accessed, callback
311 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
311 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
312 """
312 """
313 _filter_pre_load = []
313 _filter_pre_load = []
314
314
315 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
315 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
316 """
316 """
317 Only one of ``content`` and ``commit`` may be given. Passing both
317 Only one of ``content`` and ``commit`` may be given. Passing both
318 would raise ``NodeError`` exception.
318 would raise ``NodeError`` exception.
319
319
320 :param path: relative path to the node
320 :param path: relative path to the node
321 :param content: content may be passed to constructor
321 :param content: content may be passed to constructor
322 :param commit: if given, will use it to lazily fetch content
322 :param commit: if given, will use it to lazily fetch content
323 :param mode: ST_MODE (i.e. 0100644)
323 :param mode: ST_MODE (i.e. 0100644)
324 """
324 """
325 if content and commit:
325 if content and commit:
326 raise NodeError("Cannot use both content and commit")
326 raise NodeError("Cannot use both content and commit")
327 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
327 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
328 self.commit = commit
328 self.commit = commit
329 self._content = content
329 self._content = content
330 self._mode = mode or FILEMODE_DEFAULT
330 self._mode = mode or FILEMODE_DEFAULT
331
331
332 self._set_bulk_properties(pre_load)
332 self._set_bulk_properties(pre_load)
333
333
334 def _set_bulk_properties(self, pre_load):
334 def _set_bulk_properties(self, pre_load):
335 if not pre_load:
335 if not pre_load:
336 return
336 return
337 pre_load = [entry for entry in pre_load
337 pre_load = [entry for entry in pre_load
338 if entry not in self._filter_pre_load]
338 if entry not in self._filter_pre_load]
339 if not pre_load:
339 if not pre_load:
340 return
340 return
341
341
342 for attr_name in pre_load:
342 for attr_name in pre_load:
343 result = getattr(self, attr_name)
343 result = getattr(self, attr_name)
344 if callable(result):
344 if callable(result):
345 result = result()
345 result = result()
346 self.__dict__[attr_name] = result
346 self.__dict__[attr_name] = result
347
347
348 @LazyProperty
348 @LazyProperty
349 def mode(self):
349 def mode(self):
350 """
350 """
351 Returns lazily mode of the FileNode. If `commit` is not set, would
351 Returns lazily mode of the FileNode. If `commit` is not set, would
352 use value given at initialization or `FILEMODE_DEFAULT` (default).
352 use value given at initialization or `FILEMODE_DEFAULT` (default).
353 """
353 """
354 if self.commit:
354 if self.commit:
355 mode = self.commit.get_file_mode(self.path)
355 mode = self.commit.get_file_mode(self.path)
356 else:
356 else:
357 mode = self._mode
357 mode = self._mode
358 return mode
358 return mode
359
359
360 @LazyProperty
360 @LazyProperty
361 def raw_bytes(self):
361 def raw_bytes(self):
362 """
362 """
363 Returns lazily the raw bytes of the FileNode.
363 Returns lazily the raw bytes of the FileNode.
364 """
364 """
365 if self.commit:
365 if self.commit:
366 if self._content is None:
366 if self._content is None:
367 self._content = self.commit.get_file_content(self.path)
367 self._content = self.commit.get_file_content(self.path)
368 content = self._content
368 content = self._content
369 else:
369 else:
370 content = self._content
370 content = self._content
371 return content
371 return content
372
372
373 def stream_bytes(self):
373 def stream_bytes(self):
374 """
374 """
375 Returns an iterator that will stream the content of the file directly from
375 Returns an iterator that will stream the content of the file directly from
376 vcsserver without loading it to memory.
376 vcsserver without loading it to memory.
377 """
377 """
378 if self.commit:
378 if self.commit:
379 return self.commit.get_file_content_streamed(self.path)
379 return self.commit.get_file_content_streamed(self.path)
380 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
380 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
381
381
382 @LazyProperty
382 @LazyProperty
383 def md5(self):
383 def md5(self):
384 """
384 """
385 Returns md5 of the file node.
385 Returns md5 of the file node.
386 """
386 """
387 return md5(self.raw_bytes)
387 return md5(self.raw_bytes)
388
388
389 def metadata_uncached(self):
389 def metadata_uncached(self):
390 """
390 """
391 Returns md5, binary flag of the file node, without any cache usage.
391 Returns md5, binary flag of the file node, without any cache usage.
392 """
392 """
393
393
394 content = self.content_uncached()
394 content = self.content_uncached()
395
395
396 is_binary = content and '\0' in content
396 is_binary = content and '\0' in content
397 size = 0
397 size = 0
398 if content:
398 if content:
399 size = len(content)
399 size = len(content)
400
400
401 return is_binary, md5(content), size, content
401 return is_binary, md5(content), size, content
402
402
403 def content_uncached(self):
403 def content_uncached(self):
404 """
404 """
405 Returns lazily content of the FileNode. If possible, would try to
405 Returns lazily content of the FileNode. If possible, would try to
406 decode content from UTF-8.
406 decode content from UTF-8.
407 """
407 """
408 if self.commit:
408 if self.commit:
409 content = self.commit.get_file_content(self.path)
409 content = self.commit.get_file_content(self.path)
410 else:
410 else:
411 content = self._content
411 content = self._content
412 return content
412 return content
413
413
414 @LazyProperty
414 @LazyProperty
415 def content(self):
415 def content(self):
416 """
416 """
417 Returns lazily content of the FileNode. If possible, would try to
417 Returns lazily content of the FileNode. If possible, would try to
418 decode content from UTF-8.
418 decode content from UTF-8.
419 """
419 """
420 content = self.raw_bytes
420 content = self.raw_bytes
421
421
422 if self.is_binary:
422 if self.is_binary:
423 return content
423 return content
424 return safe_unicode(content)
424 return safe_unicode(content)
425
425
426 @LazyProperty
426 @LazyProperty
427 def size(self):
427 def size(self):
428 if self.commit:
428 if self.commit:
429 return self.commit.get_file_size(self.path)
429 return self.commit.get_file_size(self.path)
430 raise NodeError(
430 raise NodeError(
431 "Cannot retrieve size of the file without related "
431 "Cannot retrieve size of the file without related "
432 "commit attribute")
432 "commit attribute")
433
433
434 @LazyProperty
434 @LazyProperty
435 def message(self):
435 def message(self):
436 if self.commit:
436 if self.commit:
437 return self.last_commit.message
437 return self.last_commit.message
438 raise NodeError(
438 raise NodeError(
439 "Cannot retrieve message of the file without related "
439 "Cannot retrieve message of the file without related "
440 "commit attribute")
440 "commit attribute")
441
441
442 @LazyProperty
442 @LazyProperty
443 def last_commit(self):
443 def last_commit(self):
444 if self.commit:
444 if self.commit:
445 pre_load = ["author", "date", "message", "parents"]
445 pre_load = ["author", "date", "message", "parents"]
446 return self.commit.get_path_commit(self.path, pre_load=pre_load)
446 return self.commit.get_path_commit(self.path, pre_load=pre_load)
447 raise NodeError(
447 raise NodeError(
448 "Cannot retrieve last commit of the file without "
448 "Cannot retrieve last commit of the file without "
449 "related commit attribute")
449 "related commit attribute")
450
450
451 def get_mimetype(self):
451 def get_mimetype(self):
452 """
452 """
453 Mimetype is calculated based on the file's content. If ``_mimetype``
453 Mimetype is calculated based on the file's content. If ``_mimetype``
454 attribute is available, it will be returned (backends which store
454 attribute is available, it will be returned (backends which store
455 mimetypes or can easily recognize them, should set this private
455 mimetypes or can easily recognize them, should set this private
456 attribute to indicate that type should *NOT* be calculated).
456 attribute to indicate that type should *NOT* be calculated).
457 """
457 """
458
458
459 if hasattr(self, '_mimetype'):
459 if hasattr(self, '_mimetype'):
460 if (isinstance(self._mimetype, (tuple, list,)) and
460 if (isinstance(self._mimetype, (tuple, list,)) and
461 len(self._mimetype) == 2):
461 len(self._mimetype) == 2):
462 return self._mimetype
462 return self._mimetype
463 else:
463 else:
464 raise NodeError('given _mimetype attribute must be an 2 '
464 raise NodeError('given _mimetype attribute must be an 2 '
465 'element list or tuple')
465 'element list or tuple')
466
466
467 db = get_mimetypes_db()
467 db = get_mimetypes_db()
468 mtype, encoding = db.guess_type(self.name)
468 mtype, encoding = db.guess_type(self.name)
469
469
470 if mtype is None:
470 if mtype is None:
471 if self.is_binary:
471 if not self.is_largefile() and self.is_binary:
472 mtype = 'application/octet-stream'
472 mtype = 'application/octet-stream'
473 encoding = None
473 encoding = None
474 else:
474 else:
475 mtype = 'text/plain'
475 mtype = 'text/plain'
476 encoding = None
476 encoding = None
477
477
478 # try with pygments
478 # try with pygments
479 try:
479 try:
480 from pygments.lexers import get_lexer_for_filename
480 from pygments.lexers import get_lexer_for_filename
481 mt = get_lexer_for_filename(self.name).mimetypes
481 mt = get_lexer_for_filename(self.name).mimetypes
482 except Exception:
482 except Exception:
483 mt = None
483 mt = None
484
484
485 if mt:
485 if mt:
486 mtype = mt[0]
486 mtype = mt[0]
487
487
488 return mtype, encoding
488 return mtype, encoding
489
489
490 @LazyProperty
490 @LazyProperty
491 def mimetype(self):
491 def mimetype(self):
492 """
492 """
493 Wrapper around full mimetype info. It returns only type of fetched
493 Wrapper around full mimetype info. It returns only type of fetched
494 mimetype without the encoding part. use get_mimetype function to fetch
494 mimetype without the encoding part. use get_mimetype function to fetch
495 full set of (type,encoding)
495 full set of (type,encoding)
496 """
496 """
497 return self.get_mimetype()[0]
497 return self.get_mimetype()[0]
498
498
499 @LazyProperty
499 @LazyProperty
500 def mimetype_main(self):
500 def mimetype_main(self):
501 return self.mimetype.split('/')[0]
501 return self.mimetype.split('/')[0]
502
502
503 @classmethod
503 @classmethod
504 def get_lexer(cls, filename, content=None):
504 def get_lexer(cls, filename, content=None):
505 from pygments import lexers
505 from pygments import lexers
506
506
507 extension = filename.split('.')[-1]
507 extension = filename.split('.')[-1]
508 lexer = None
508 lexer = None
509
509
510 try:
510 try:
511 lexer = lexers.guess_lexer_for_filename(
511 lexer = lexers.guess_lexer_for_filename(
512 filename, content, stripnl=False)
512 filename, content, stripnl=False)
513 except lexers.ClassNotFound:
513 except lexers.ClassNotFound:
514 lexer = None
514 lexer = None
515
515
516 # try our EXTENSION_MAP
516 # try our EXTENSION_MAP
517 if not lexer:
517 if not lexer:
518 try:
518 try:
519 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
519 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
520 if lexer_class:
520 if lexer_class:
521 lexer = lexers.get_lexer_by_name(lexer_class[0])
521 lexer = lexers.get_lexer_by_name(lexer_class[0])
522 except lexers.ClassNotFound:
522 except lexers.ClassNotFound:
523 lexer = None
523 lexer = None
524
524
525 if not lexer:
525 if not lexer:
526 lexer = lexers.TextLexer(stripnl=False)
526 lexer = lexers.TextLexer(stripnl=False)
527
527
528 return lexer
528 return lexer
529
529
530 @LazyProperty
530 @LazyProperty
531 def lexer(self):
531 def lexer(self):
532 """
532 """
533 Returns pygment's lexer class. Would try to guess lexer taking file's
533 Returns pygment's lexer class. Would try to guess lexer taking file's
534 content, name and mimetype.
534 content, name and mimetype.
535 """
535 """
536 return self.get_lexer(self.name, self.content)
536 return self.get_lexer(self.name, self.content)
537
537
538 @LazyProperty
538 @LazyProperty
539 def lexer_alias(self):
539 def lexer_alias(self):
540 """
540 """
541 Returns first alias of the lexer guessed for this file.
541 Returns first alias of the lexer guessed for this file.
542 """
542 """
543 return self.lexer.aliases[0]
543 return self.lexer.aliases[0]
544
544
545 @LazyProperty
545 @LazyProperty
546 def history(self):
546 def history(self):
547 """
547 """
548 Returns a list of commit for this file in which the file was changed
548 Returns a list of commit for this file in which the file was changed
549 """
549 """
550 if self.commit is None:
550 if self.commit is None:
551 raise NodeError('Unable to get commit for this FileNode')
551 raise NodeError('Unable to get commit for this FileNode')
552 return self.commit.get_path_history(self.path)
552 return self.commit.get_path_history(self.path)
553
553
554 @LazyProperty
554 @LazyProperty
555 def annotate(self):
555 def annotate(self):
556 """
556 """
557 Returns a list of three element tuples with lineno, commit and line
557 Returns a list of three element tuples with lineno, commit and line
558 """
558 """
559 if self.commit is None:
559 if self.commit is None:
560 raise NodeError('Unable to get commit for this FileNode')
560 raise NodeError('Unable to get commit for this FileNode')
561 pre_load = ["author", "date", "message", "parents"]
561 pre_load = ["author", "date", "message", "parents"]
562 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
562 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
563
563
564 @LazyProperty
564 @LazyProperty
565 def state(self):
565 def state(self):
566 if not self.commit:
566 if not self.commit:
567 raise NodeError(
567 raise NodeError(
568 "Cannot check state of the node if it's not "
568 "Cannot check state of the node if it's not "
569 "linked with commit")
569 "linked with commit")
570 elif self.path in (node.path for node in self.commit.added):
570 elif self.path in (node.path for node in self.commit.added):
571 return NodeState.ADDED
571 return NodeState.ADDED
572 elif self.path in (node.path for node in self.commit.changed):
572 elif self.path in (node.path for node in self.commit.changed):
573 return NodeState.CHANGED
573 return NodeState.CHANGED
574 else:
574 else:
575 return NodeState.NOT_CHANGED
575 return NodeState.NOT_CHANGED
576
576
577 @LazyProperty
577 @LazyProperty
578 def is_binary(self):
578 def is_binary(self):
579 """
579 """
580 Returns True if file has binary content.
580 Returns True if file has binary content.
581 """
581 """
582 if self.commit:
582 if self.commit:
583 return self.commit.is_node_binary(self.path)
583 return self.commit.is_node_binary(self.path)
584 else:
584 else:
585 raw_bytes = self._content
585 raw_bytes = self._content
586 return raw_bytes and '\0' in raw_bytes
586 return raw_bytes and '\0' in raw_bytes
587
587
588 @LazyProperty
588 @LazyProperty
589 def extension(self):
589 def extension(self):
590 """Returns filenode extension"""
590 """Returns filenode extension"""
591 return self.name.split('.')[-1]
591 return self.name.split('.')[-1]
592
592
593 @property
593 @property
594 def is_executable(self):
594 def is_executable(self):
595 """
595 """
596 Returns ``True`` if file has executable flag turned on.
596 Returns ``True`` if file has executable flag turned on.
597 """
597 """
598 return bool(self.mode & stat.S_IXUSR)
598 return bool(self.mode & stat.S_IXUSR)
599
599
600 def get_largefile_node(self):
600 def get_largefile_node(self):
601 """
601 """
602 Try to return a Mercurial FileNode from this node. It does internal
602 Try to return a Mercurial FileNode from this node. It does internal
603 checks inside largefile store, if that file exist there it will
603 checks inside largefile store, if that file exist there it will
604 create special instance of LargeFileNode which can get content from
604 create special instance of LargeFileNode which can get content from
605 LF store.
605 LF store.
606 """
606 """
607 if self.commit:
607 if self.commit:
608 return self.commit.get_largefile_node(self.path)
608 return self.commit.get_largefile_node(self.path)
609
609
610 def count_lines(self, content, count_empty=False):
610 def count_lines(self, content, count_empty=False):
611
611
612 if count_empty:
612 if count_empty:
613 all_lines = 0
613 all_lines = 0
614 empty_lines = 0
614 empty_lines = 0
615 for line in content.splitlines(True):
615 for line in content.splitlines(True):
616 if line == '\n':
616 if line == '\n':
617 empty_lines += 1
617 empty_lines += 1
618 all_lines += 1
618 all_lines += 1
619
619
620 return all_lines, all_lines - empty_lines
620 return all_lines, all_lines - empty_lines
621 else:
621 else:
622 # fast method
622 # fast method
623 empty_lines = all_lines = content.count('\n')
623 empty_lines = all_lines = content.count('\n')
624 if all_lines == 0 and content:
624 if all_lines == 0 and content:
625 # one-line without a newline
625 # one-line without a newline
626 empty_lines = all_lines = 1
626 empty_lines = all_lines = 1
627
627
628 return all_lines, empty_lines
628 return all_lines, empty_lines
629
629
630 def lines(self, count_empty=False):
630 def lines(self, count_empty=False):
631 all_lines, empty_lines = 0, 0
631 all_lines, empty_lines = 0, 0
632
632
633 if not self.is_binary:
633 if not self.is_binary:
634 content = self.content
634 content = self.content
635 all_lines, empty_lines = self.count_lines(content, count_empty=count_empty)
635 all_lines, empty_lines = self.count_lines(content, count_empty=count_empty)
636 return all_lines, empty_lines
636 return all_lines, empty_lines
637
637
638 def __repr__(self):
638 def __repr__(self):
639 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
639 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
640 getattr(self.commit, 'short_id', ''))
640 getattr(self.commit, 'short_id', ''))
641
641
642
642
643 class RemovedFileNode(FileNode):
643 class RemovedFileNode(FileNode):
644 """
644 """
645 Dummy FileNode class - trying to access any public attribute except path,
645 Dummy FileNode class - trying to access any public attribute except path,
646 name, kind or state (or methods/attributes checking those two) would raise
646 name, kind or state (or methods/attributes checking those two) would raise
647 RemovedFileNodeError.
647 RemovedFileNodeError.
648 """
648 """
649 ALLOWED_ATTRIBUTES = [
649 ALLOWED_ATTRIBUTES = [
650 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
650 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
651 'added', 'changed', 'not_changed', 'removed'
651 'added', 'changed', 'not_changed', 'removed'
652 ]
652 ]
653
653
654 def __init__(self, path):
654 def __init__(self, path):
655 """
655 """
656 :param path: relative path to the node
656 :param path: relative path to the node
657 """
657 """
658 super(RemovedFileNode, self).__init__(path=path)
658 super(RemovedFileNode, self).__init__(path=path)
659
659
660 def __getattribute__(self, attr):
660 def __getattribute__(self, attr):
661 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
661 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
662 return super(RemovedFileNode, self).__getattribute__(attr)
662 return super(RemovedFileNode, self).__getattribute__(attr)
663 raise RemovedFileNodeError(
663 raise RemovedFileNodeError(
664 "Cannot access attribute %s on RemovedFileNode" % attr)
664 "Cannot access attribute %s on RemovedFileNode" % attr)
665
665
666 @LazyProperty
666 @LazyProperty
667 def state(self):
667 def state(self):
668 return NodeState.REMOVED
668 return NodeState.REMOVED
669
669
670
670
671 class DirNode(Node):
671 class DirNode(Node):
672 """
672 """
673 DirNode stores list of files and directories within this node.
673 DirNode stores list of files and directories within this node.
674 Nodes may be used standalone but within repository context they
674 Nodes may be used standalone but within repository context they
675 lazily fetch data within same repository's commit.
675 lazily fetch data within same repository's commit.
676 """
676 """
677
677
678 def __init__(self, path, nodes=(), commit=None):
678 def __init__(self, path, nodes=(), commit=None):
679 """
679 """
680 Only one of ``nodes`` and ``commit`` may be given. Passing both
680 Only one of ``nodes`` and ``commit`` may be given. Passing both
681 would raise ``NodeError`` exception.
681 would raise ``NodeError`` exception.
682
682
683 :param path: relative path to the node
683 :param path: relative path to the node
684 :param nodes: content may be passed to constructor
684 :param nodes: content may be passed to constructor
685 :param commit: if given, will use it to lazily fetch content
685 :param commit: if given, will use it to lazily fetch content
686 """
686 """
687 if nodes and commit:
687 if nodes and commit:
688 raise NodeError("Cannot use both nodes and commit")
688 raise NodeError("Cannot use both nodes and commit")
689 super(DirNode, self).__init__(path, NodeKind.DIR)
689 super(DirNode, self).__init__(path, NodeKind.DIR)
690 self.commit = commit
690 self.commit = commit
691 self._nodes = nodes
691 self._nodes = nodes
692
692
693 @LazyProperty
693 @LazyProperty
694 def content(self):
694 def content(self):
695 raise NodeError(
695 raise NodeError(
696 "%s represents a dir and has no `content` attribute" % self)
696 "%s represents a dir and has no `content` attribute" % self)
697
697
698 @LazyProperty
698 @LazyProperty
699 def nodes(self):
699 def nodes(self):
700 if self.commit:
700 if self.commit:
701 nodes = self.commit.get_nodes(self.path)
701 nodes = self.commit.get_nodes(self.path)
702 else:
702 else:
703 nodes = self._nodes
703 nodes = self._nodes
704 self._nodes_dict = dict((node.path, node) for node in nodes)
704 self._nodes_dict = dict((node.path, node) for node in nodes)
705 return sorted(nodes)
705 return sorted(nodes)
706
706
707 @LazyProperty
707 @LazyProperty
708 def files(self):
708 def files(self):
709 return sorted((node for node in self.nodes if node.is_file()))
709 return sorted((node for node in self.nodes if node.is_file()))
710
710
711 @LazyProperty
711 @LazyProperty
712 def dirs(self):
712 def dirs(self):
713 return sorted((node for node in self.nodes if node.is_dir()))
713 return sorted((node for node in self.nodes if node.is_dir()))
714
714
715 def __iter__(self):
715 def __iter__(self):
716 for node in self.nodes:
716 for node in self.nodes:
717 yield node
717 yield node
718
718
719 def get_node(self, path):
719 def get_node(self, path):
720 """
720 """
721 Returns node from within this particular ``DirNode``, so it is now
721 Returns node from within this particular ``DirNode``, so it is now
722 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
722 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
723 'docs'. In order to access deeper nodes one must fetch nodes between
723 'docs'. In order to access deeper nodes one must fetch nodes between
724 them first - this would work::
724 them first - this would work::
725
725
726 docs = root.get_node('docs')
726 docs = root.get_node('docs')
727 docs.get_node('api').get_node('index.rst')
727 docs.get_node('api').get_node('index.rst')
728
728
729 :param: path - relative to the current node
729 :param: path - relative to the current node
730
730
731 .. note::
731 .. note::
732 To access lazily (as in example above) node have to be initialized
732 To access lazily (as in example above) node have to be initialized
733 with related commit object - without it node is out of
733 with related commit object - without it node is out of
734 context and may know nothing about anything else than nearest
734 context and may know nothing about anything else than nearest
735 (located at same level) nodes.
735 (located at same level) nodes.
736 """
736 """
737 try:
737 try:
738 path = path.rstrip('/')
738 path = path.rstrip('/')
739 if path == '':
739 if path == '':
740 raise NodeError("Cannot retrieve node without path")
740 raise NodeError("Cannot retrieve node without path")
741 self.nodes # access nodes first in order to set _nodes_dict
741 self.nodes # access nodes first in order to set _nodes_dict
742 paths = path.split('/')
742 paths = path.split('/')
743 if len(paths) == 1:
743 if len(paths) == 1:
744 if not self.is_root():
744 if not self.is_root():
745 path = '/'.join((self.path, paths[0]))
745 path = '/'.join((self.path, paths[0]))
746 else:
746 else:
747 path = paths[0]
747 path = paths[0]
748 return self._nodes_dict[path]
748 return self._nodes_dict[path]
749 elif len(paths) > 1:
749 elif len(paths) > 1:
750 if self.commit is None:
750 if self.commit is None:
751 raise NodeError("Cannot access deeper nodes without commit")
751 raise NodeError("Cannot access deeper nodes without commit")
752 else:
752 else:
753 path1, path2 = paths[0], '/'.join(paths[1:])
753 path1, path2 = paths[0], '/'.join(paths[1:])
754 return self.get_node(path1).get_node(path2)
754 return self.get_node(path1).get_node(path2)
755 else:
755 else:
756 raise KeyError
756 raise KeyError
757 except KeyError:
757 except KeyError:
758 raise NodeError("Node does not exist at %s" % path)
758 raise NodeError("Node does not exist at %s" % path)
759
759
760 @LazyProperty
760 @LazyProperty
761 def state(self):
761 def state(self):
762 raise NodeError("Cannot access state of DirNode")
762 raise NodeError("Cannot access state of DirNode")
763
763
764 @LazyProperty
764 @LazyProperty
765 def size(self):
765 def size(self):
766 size = 0
766 size = 0
767 for root, dirs, files in self.commit.walk(self.path):
767 for root, dirs, files in self.commit.walk(self.path):
768 for f in files:
768 for f in files:
769 size += f.size
769 size += f.size
770
770
771 return size
771 return size
772
772
773 @LazyProperty
773 @LazyProperty
774 def last_commit(self):
774 def last_commit(self):
775 if self.commit:
775 if self.commit:
776 pre_load = ["author", "date", "message", "parents"]
776 pre_load = ["author", "date", "message", "parents"]
777 return self.commit.get_path_commit(self.path, pre_load=pre_load)
777 return self.commit.get_path_commit(self.path, pre_load=pre_load)
778 raise NodeError(
778 raise NodeError(
779 "Cannot retrieve last commit of the file without "
779 "Cannot retrieve last commit of the file without "
780 "related commit attribute")
780 "related commit attribute")
781
781
782 def __repr__(self):
782 def __repr__(self):
783 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
783 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
784 getattr(self.commit, 'short_id', ''))
784 getattr(self.commit, 'short_id', ''))
785
785
786
786
787 class RootNode(DirNode):
787 class RootNode(DirNode):
788 """
788 """
789 DirNode being the root node of the repository.
789 DirNode being the root node of the repository.
790 """
790 """
791
791
792 def __init__(self, nodes=(), commit=None):
792 def __init__(self, nodes=(), commit=None):
793 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
793 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
794
794
795 def __repr__(self):
795 def __repr__(self):
796 return '<%s>' % self.__class__.__name__
796 return '<%s>' % self.__class__.__name__
797
797
798
798
799 class SubModuleNode(Node):
799 class SubModuleNode(Node):
800 """
800 """
801 represents a SubModule of Git or SubRepo of Mercurial
801 represents a SubModule of Git or SubRepo of Mercurial
802 """
802 """
803 is_binary = False
803 is_binary = False
804 size = 0
804 size = 0
805
805
806 def __init__(self, name, url=None, commit=None, alias=None):
806 def __init__(self, name, url=None, commit=None, alias=None):
807 self.path = name
807 self.path = name
808 self.kind = NodeKind.SUBMODULE
808 self.kind = NodeKind.SUBMODULE
809 self.alias = alias
809 self.alias = alias
810
810
811 # we have to use EmptyCommit here since this can point to svn/git/hg
811 # we have to use EmptyCommit here since this can point to svn/git/hg
812 # submodules we cannot get from repository
812 # submodules we cannot get from repository
813 self.commit = EmptyCommit(str(commit), alias=alias)
813 self.commit = EmptyCommit(str(commit), alias=alias)
814 self.url = url or self._extract_submodule_url()
814 self.url = url or self._extract_submodule_url()
815
815
816 def __repr__(self):
816 def __repr__(self):
817 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
817 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
818 getattr(self.commit, 'short_id', ''))
818 getattr(self.commit, 'short_id', ''))
819
819
820 def _extract_submodule_url(self):
820 def _extract_submodule_url(self):
821 # TODO: find a way to parse gits submodule file and extract the
821 # TODO: find a way to parse gits submodule file and extract the
822 # linking URL
822 # linking URL
823 return self.path
823 return self.path
824
824
825 @LazyProperty
825 @LazyProperty
826 def name(self):
826 def name(self):
827 """
827 """
828 Returns name of the node so if its path
828 Returns name of the node so if its path
829 then only last part is returned.
829 then only last part is returned.
830 """
830 """
831 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
831 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
832 return u'%s @ %s' % (org, self.commit.short_id)
832 return u'%s @ %s' % (org, self.commit.short_id)
833
833
834
834
835 class LargeFileNode(FileNode):
835 class LargeFileNode(FileNode):
836
836
837 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
837 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
838 self.path = path
838 self.path = path
839 self.org_path = org_path
839 self.org_path = org_path
840 self.kind = NodeKind.LARGEFILE
840 self.kind = NodeKind.LARGEFILE
841 self.alias = alias
841 self.alias = alias
842 self._content = ''
842
843
843 def _validate_path(self, path):
844 def _validate_path(self, path):
844 """
845 """
845 we override check since the LargeFileNode path is system absolute
846 we override check since the LargeFileNode path is system absolute
846 """
847 """
847 pass
848 pass
848
849
849 def __repr__(self):
850 def __repr__(self):
850 return '<%s %r>' % (self.__class__.__name__, self.path)
851 return '<%s %r>' % (self.__class__.__name__, self.path)
851
852
852 @LazyProperty
853 @LazyProperty
853 def size(self):
854 def size(self):
854 return os.stat(self.path).st_size
855 return os.stat(self.path).st_size
855
856
856 @LazyProperty
857 @LazyProperty
857 def raw_bytes(self):
858 def raw_bytes(self):
858 with open(self.path, 'rb') as f:
859 with open(self.path, 'rb') as f:
859 content = f.read()
860 content = f.read()
860 return content
861 return content
861
862
862 @LazyProperty
863 @LazyProperty
863 def name(self):
864 def name(self):
864 """
865 """
865 Overwrites name to be the org lf path
866 Overwrites name to be the org lf path
866 """
867 """
867 return self.org_path
868 return self.org_path
868
869
869 def stream_bytes(self):
870 def stream_bytes(self):
870 with open(self.path, 'rb') as stream:
871 with open(self.path, 'rb') as stream:
871 while True:
872 while True:
872 data = stream.read(16 * 1024)
873 data = stream.read(16 * 1024)
873 if not data:
874 if not data:
874 break
875 break
875 yield data
876 yield data
General Comments 0
You need to be logged in to leave comments. Login now