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