Revision 4d15c94e
b/snf-pithos-app/pithos/api/functions.py | ||
---|---|---|
788 | 788 |
copy_from = request.META.get('HTTP_X_COPY_FROM') |
789 | 789 |
move_from = request.META.get('HTTP_X_MOVE_FROM') |
790 | 790 |
if copy_from or move_from: |
791 |
delimiter = request.GET.get('delimiter') |
|
791 | 792 |
content_length = get_content_length(request) # Required by the API. |
792 | 793 |
|
793 | 794 |
src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT') |
... | ... | |
799 | 800 |
except ValueError: |
800 | 801 |
raise BadRequest('Invalid X-Move-From header') |
801 | 802 |
version_id = copy_or_move_object(request, src_account, src_container, src_name, |
802 |
v_account, v_container, v_object, move=True) |
|
803 |
v_account, v_container, v_object, move=True, delimiter=delimiter)
|
|
803 | 804 |
else: |
804 | 805 |
try: |
805 | 806 |
src_container, src_name = split_container_object_string(copy_from) |
806 | 807 |
except ValueError: |
807 | 808 |
raise BadRequest('Invalid X-Copy-From header') |
808 | 809 |
version_id = copy_or_move_object(request, src_account, src_container, src_name, |
809 |
v_account, v_container, v_object, move=False) |
|
810 |
v_account, v_container, v_object, move=False, delimiter=delimiter)
|
|
810 | 811 |
response = HttpResponse(status=201) |
811 | 812 |
response['X-Object-Version'] = version_id |
812 | 813 |
return response |
... | ... | |
967 | 968 |
raise ItemNotFound('Container or object does not exist') |
968 | 969 |
validate_matching_preconditions(request, meta) |
969 | 970 |
|
971 |
delimiter = request.GET.get('delimiter') |
|
972 |
|
|
970 | 973 |
version_id = copy_or_move_object(request, v_account, v_container, v_object, |
971 |
dest_account, dest_container, dest_name, move=False) |
|
974 |
dest_account, dest_container, dest_name, move=False, delimiter=delimiter)
|
|
972 | 975 |
response = HttpResponse(status=201) |
973 | 976 |
response['X-Object-Version'] = version_id |
974 | 977 |
return response |
... | ... | |
1003 | 1006 |
raise ItemNotFound('Container or object does not exist') |
1004 | 1007 |
validate_matching_preconditions(request, meta) |
1005 | 1008 |
|
1009 |
delimiter = request.GET.get('delimiter') |
|
1010 |
|
|
1006 | 1011 |
version_id = copy_or_move_object(request, v_account, v_container, v_object, |
1007 |
dest_account, dest_container, dest_name, move=True) |
|
1012 |
dest_account, dest_container, dest_name, move=True, delimiter=delimiter)
|
|
1008 | 1013 |
response = HttpResponse(status=201) |
1009 | 1014 |
response['X-Object-Version'] = version_id |
1010 | 1015 |
return response |
... | ... | |
1221 | 1226 |
# badRequest (400) |
1222 | 1227 |
|
1223 | 1228 |
until = get_int_parameter(request.GET.get('until')) |
1229 |
delimiter = request.GET.get('delimiter') |
|
1230 |
|
|
1224 | 1231 |
try: |
1225 | 1232 |
request.backend.delete_object(request.user_uniq, v_account, v_container, |
1226 |
v_object, until) |
|
1233 |
v_object, until, delimiter=delimiter)
|
|
1227 | 1234 |
except NotAllowedError: |
1228 | 1235 |
raise Forbidden('Not allowed') |
1229 | 1236 |
except NameError: |
b/snf-pithos-app/pithos/api/util.py | ||
---|---|---|
320 | 320 |
raise ValueError |
321 | 321 |
return s[:pos], s[(pos + 1):] |
322 | 322 |
|
323 |
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False): |
|
323 |
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False, delimiter=None):
|
|
324 | 324 |
"""Copy or move an object.""" |
325 | 325 |
|
326 | 326 |
if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META: |
... | ... | |
331 | 331 |
if move: |
332 | 332 |
version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name, |
333 | 333 |
dest_account, dest_container, dest_name, |
334 |
content_type, 'pithos', meta, False, permissions) |
|
334 |
content_type, 'pithos', meta, False, permissions, delimiter)
|
|
335 | 335 |
else: |
336 | 336 |
version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name, |
337 | 337 |
dest_account, dest_container, dest_name, |
338 |
content_type, 'pithos', meta, False, permissions, src_version) |
|
338 |
content_type, 'pithos', meta, False, permissions, src_version, delimiter)
|
|
339 | 339 |
except NotAllowedError: |
340 | 340 |
raise Forbidden('Not allowed') |
341 | 341 |
except (NameError, IndexError): |
b/snf-pithos-backend/pithos/backends/base.py | ||
---|---|---|
169 | 169 |
""" |
170 | 170 |
return |
171 | 171 |
|
172 |
def list_containers(self, user, account, marker=None, limit=10000, shared=False, until=None): |
|
172 |
def list_containers(self, user, account, marker=None, limit=10000, shared=False, until=None, public=False):
|
|
173 | 173 |
"""Return a list of container names existing under an account. |
174 | 174 |
|
175 | 175 |
Parameters: |
... | ... | |
179 | 179 |
|
180 | 180 |
'shared': Only list containers with permissions set |
181 | 181 |
|
182 |
'public': Only list containers containing public objects |
|
183 |
|
|
182 | 184 |
|
183 | 185 |
Raises: |
184 | 186 |
NotAllowedError: Operation not permitted |
... | ... | |
284 | 286 |
""" |
285 | 287 |
return |
286 | 288 |
|
287 |
def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, domain=None, keys=[], shared=False, until=None, size_range=None): |
|
289 |
def list_objects(self, user, account, container, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, domain=None, keys=[], shared=False, until=None, size_range=None, public=False):
|
|
288 | 290 |
"""Return a list of object (name, version_id) tuples existing under a container. |
289 | 291 |
|
290 | 292 |
Parameters: |
... | ... | |
312 | 314 |
|
313 | 315 |
'size_range': Include objects with byte size in (from, to). |
314 | 316 |
Use None to specify unlimited |
317 |
|
|
318 |
'public': Only list public objects |
|
319 |
|
|
315 | 320 |
|
316 | 321 |
Raises: |
317 | 322 |
NotAllowedError: Operation not permitted |
... | ... | |
489 | 494 |
"""Update an object's checksum.""" |
490 | 495 |
return |
491 | 496 |
|
492 |
def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None): |
|
497 |
def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None, delimiter=None):
|
|
493 | 498 |
"""Copy an object's data and metadata and return the new version. |
494 | 499 |
|
495 | 500 |
Parameters: |
... | ... | |
502 | 507 |
'permissions': New object permissions |
503 | 508 |
|
504 | 509 |
'src_version': Copy from the version provided |
510 |
|
|
511 |
'delimiter': Copy objects whose path starts with src_name + delimiter |
|
505 | 512 |
|
506 | 513 |
Raises: |
507 | 514 |
NotAllowedError: Operation not permitted |
... | ... | |
516 | 523 |
""" |
517 | 524 |
return '' |
518 | 525 |
|
519 |
def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None): |
|
526 |
def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, delimiter=None):
|
|
520 | 527 |
"""Move an object's data and metadata and return the new version. |
521 | 528 |
|
522 | 529 |
Parameters: |
... | ... | |
527 | 534 |
'replace_meta': Replace metadata instead of update |
528 | 535 |
|
529 | 536 |
'permissions': New object permissions |
537 |
|
|
538 |
'delimiter': Move objects whose path starts with src_name + delimiter |
|
530 | 539 |
|
531 | 540 |
Raises: |
532 | 541 |
NotAllowedError: Operation not permitted |
... | ... | |
539 | 548 |
""" |
540 | 549 |
return '' |
541 | 550 |
|
542 |
def delete_object(self, user, account, container, name, until=None): |
|
551 |
def delete_object(self, user, account, container, name, until=None, delimiter=None):
|
|
543 | 552 |
"""Delete/purge an object. |
544 | 553 |
|
554 |
Parameters: |
|
555 |
'delimiter': Delete objects whose path starting with name + delimiter |
|
556 |
|
|
545 | 557 |
Raises: |
546 | 558 |
NotAllowedError: Operation not permitted |
547 | 559 |
|
b/snf-pithos-backend/pithos/backends/lib/sqlite/node.py | ||
---|---|---|
201 | 201 |
return r[0] |
202 | 202 |
return None |
203 | 203 |
|
204 |
def node_lookup_bulk(self, paths): |
|
205 |
"""Lookup the current nodes for the given paths. |
|
206 |
Return () if the path is not found. |
|
207 |
""" |
|
208 |
|
|
209 |
placeholders = ','.join('?' for path in paths) |
|
210 |
q = "select path, node from nodes where path in (%s)" % placeholders |
|
211 |
self.execute(q, paths) |
|
212 |
return self.fetchall() |
|
213 |
|
|
204 | 214 |
def node_get_properties(self, node): |
205 | 215 |
"""Return the node's (parent, path). |
206 | 216 |
Return None if the node is not found. |
... | ... | |
500 | 510 |
if props is not None: |
501 | 511 |
return props |
502 | 512 |
return None |
513 |
|
|
514 |
def version_lookup_bulk(self, nodes, before=inf, cluster=0): |
|
515 |
"""Lookup the current versions of the given nodes. |
|
516 |
Return a list with their properties: |
|
517 |
(serial, node, hash, size, type, source, mtime, muser, uuid, checksum, cluster). |
|
518 |
""" |
|
519 |
|
|
520 |
placeholders = ','.join('?' for node in nodes) |
|
521 |
q = ("select serial, node, hash, size, type, source, mtime, muser, uuid, checksum, cluster " |
|
522 |
"from versions " |
|
523 |
"where serial in (select max(serial) " |
|
524 |
"from versions " |
|
525 |
"where node in (%s) and mtime < ? group by node) " |
|
526 |
"and cluster = ?" % placeholders) |
|
527 |
args = nodes |
|
528 |
args.extend((before, cluster)) |
|
529 |
self.execute(q, args) |
|
530 |
return self.fetchall() |
|
503 | 531 |
|
504 | 532 |
def version_get_properties(self, serial, keys=(), propnames=_propnames): |
505 | 533 |
"""Return a sequence of values for the properties of |
... | ... | |
656 | 684 |
|
657 | 685 |
subqlist = [] |
658 | 686 |
args = [] |
659 |
print pathq |
|
660 | 687 |
for path, match in pathq: |
661 | 688 |
if match == MATCH_PREFIX: |
662 | 689 |
subqlist.append("n.path like ? escape '\\'") |
b/snf-pithos-backend/pithos/backends/lib/sqlite/permissions.py | ||
---|---|---|
112 | 112 |
self.xfeature_destroy(path) |
113 | 113 |
self.public_unset(path) |
114 | 114 |
|
115 |
def access_clear_bulk(self, paths): |
|
116 |
"""Revoke access to path (both permissions and public).""" |
|
117 |
|
|
118 |
self.xfeature_destroy_bulk(paths) |
|
119 |
self.public_unset_bulk(paths) |
|
120 |
|
|
115 | 121 |
def access_check(self, path, access, member): |
116 | 122 |
"""Return true if the member has this access to the path.""" |
117 | 123 |
|
b/snf-pithos-backend/pithos/backends/lib/sqlite/public.py | ||
---|---|---|
58 | 58 |
q = "update public set active = 0 where path = ?" |
59 | 59 |
self.execute(q, (path,)) |
60 | 60 |
|
61 |
def public_unset_bulk(self, paths): |
|
62 |
placeholders = ','.join('?' for path in paths) |
|
63 |
q = "update public set active = 0 where path in (%s)" % placeholders |
|
64 |
self.execute(q, paths) |
|
65 |
|
|
61 | 66 |
def public_get(self, path): |
62 | 67 |
q = "select public_id from public where path = ? and active = 1" |
63 | 68 |
self.execute(q, (path,)) |
b/snf-pithos-backend/pithos/backends/lib/sqlite/xfeatures.py | ||
---|---|---|
99 | 99 |
q = "delete from xfeatures where path = ?" |
100 | 100 |
self.execute(q, (path,)) |
101 | 101 |
|
102 |
def xfeature_destroy_bulk(self, paths): |
|
103 |
"""Destroy features and all their key, value pairs.""" |
|
104 |
|
|
105 |
placeholders = ','.join('?' for path in paths) |
|
106 |
q = "delete from xfeatures where path in (%s)" % placeholders |
|
107 |
self.execute(q, paths) |
|
108 |
|
|
102 | 109 |
def feature_dict(self, feature): |
103 | 110 |
"""Return a dict mapping keys to list of values for feature.""" |
104 | 111 |
|
b/snf-pithos-backend/pithos/backends/modular.py | ||
---|---|---|
476 | 476 |
allowed = self._get_formatted_paths(allowed) |
477 | 477 |
return self._list_object_properties(node, path, prefix, delimiter, marker, limit, virtual, domain, keys, until, size_range, allowed, all_props) |
478 | 478 |
|
479 |
def _list_objects_no_limit(self, user, account, container, prefix, delimiter, virtual, domain, keys, shared, until, size_range, all_props, public): |
|
480 |
objects = [] |
|
481 |
while True: |
|
482 |
marker = objects[-1] if objects else None |
|
483 |
limit = 10000 |
|
484 |
l = self._list_objects(user, account, container, prefix, delimiter, marker, limit, virtual, domain, keys, shared, until, size_range, all_props, public) |
|
485 |
objects.extend(l) |
|
486 |
if not l or len(l) < limit: |
|
487 |
break |
|
488 |
return objects |
|
489 |
|
|
479 | 490 |
def _list_object_permissions(self, user, account, container, prefix, shared, public): |
480 | 491 |
allowed = [] |
481 | 492 |
path = '/'.join((account, container, prefix)).rstrip('/') |
... | ... | |
724 | 735 |
if x[self.SERIAL] >= int(version) and x[self.HASH] == props[self.HASH] and x[self.SIZE] == props[self.SIZE]: |
725 | 736 |
self.node.version_put_property(x[self.SERIAL], 'checksum', checksum) |
726 | 737 |
|
727 |
def _copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, dest_domain=None, dest_meta={}, replace_meta=False, permissions=None, src_version=None, is_move=False): |
|
738 |
def _copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, dest_domain=None, dest_meta={}, replace_meta=False, permissions=None, src_version=None, is_move=False, delimiter=None): |
|
739 |
dest_version_ids = [] |
|
728 | 740 |
self._can_read(user, src_account, src_container, src_name) |
729 | 741 |
path, node = self._lookup_object(src_account, src_container, src_name) |
730 | 742 |
# TODO: Will do another fetch of the properties in duplicate version... |
... | ... | |
732 | 744 |
src_version_id = props[self.SERIAL] |
733 | 745 |
hash = props[self.HASH] |
734 | 746 |
size = props[self.SIZE] |
735 |
|
|
736 | 747 |
is_copy = not is_move and (src_account, src_container, src_name) != (dest_account, dest_container, dest_name) # New uuid. |
737 |
dest_version_id = self._update_object_hash(user, dest_account, dest_container, dest_name, size, type, hash, None, dest_domain, dest_meta, replace_meta, permissions, src_node=node, src_version_id=src_version_id, is_copy=is_copy) |
|
738 |
return dest_version_id |
|
748 |
dest_version_ids.append(self._update_object_hash(user, dest_account, dest_container, dest_name, size, type, hash, None, dest_domain, dest_meta, replace_meta, permissions, src_node=node, src_version_id=src_version_id, is_copy=is_copy)) |
|
749 |
if is_move: |
|
750 |
self._delete_object(user, src_account, src_container, src_name) |
|
751 |
|
|
752 |
if delimiter: |
|
753 |
prefix = src_name + delimiter if not src_name.endswith(delimiter) else src_name |
|
754 |
src_names = self._list_objects_no_limit(user, src_account, src_container, prefix, delimiter=None, virtual=True, domain=None, keys=[], shared=False, until=None, size_range=None, all_props=True, public=False) |
|
755 |
paths = [elem[0] for elem in src_names] |
|
756 |
nodes = [elem[2] for elem in src_names] |
|
757 |
# TODO: Will do another fetch of the properties in duplicate version... |
|
758 |
props = self._get_versions(nodes) # Check to see if source exists. |
|
759 |
|
|
760 |
for prop, path, node in zip(props, paths, nodes): |
|
761 |
src_version_id = prop[self.SERIAL] |
|
762 |
hash = prop[self.HASH] |
|
763 |
vtype = prop[self.TYPE] |
|
764 |
dest_prefix = dest_name + delimiter if not dest_name.endswith(delimiter) else dest_name |
|
765 |
vdest_name = path.replace(prefix, dest_prefix, 1) |
|
766 |
dest_version_ids.append(self._update_object_hash(user, dest_account, dest_container, vdest_name, size, vtype, hash, None, dest_domain, dest_meta, replace_meta, permissions, src_node=node, src_version_id=src_version_id, is_copy=is_copy)) |
|
767 |
if is_move: |
|
768 |
self._delete_object(user, src_account, src_container, path) |
|
769 |
return dest_version_ids[0] if len(dest_version_ids) == 1 else dest_version_ids |
|
739 | 770 |
|
740 | 771 |
@backend_method |
741 |
def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None): |
|
772 |
def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None, delimiter=None):
|
|
742 | 773 |
"""Copy an object's data and metadata.""" |
743 | 774 |
|
744 |
logger.debug("copy_object: %s %s %s %s %s %s %s %s %s %s %s %s %s", user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version)
|
|
745 |
dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version, False) |
|
775 |
logger.debug("copy_object: %s %s %s %s %s %s %s %s %s %s %s %s %s %s", user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version, delimiter)
|
|
776 |
dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, src_version, False, delimiter)
|
|
746 | 777 |
return dest_version_id |
747 | 778 |
|
748 | 779 |
@backend_method |
749 |
def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None): |
|
780 |
def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, delimiter=None):
|
|
750 | 781 |
"""Move an object's data and metadata.""" |
751 | 782 |
|
752 |
logger.debug("move_object: %s %s %s %s %s %s %s %s %s %s %s %s", user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions)
|
|
783 |
logger.debug("move_object: %s %s %s %s %s %s %s %s %s %s %s %s %s", user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, delimiter)
|
|
753 | 784 |
if user != src_account: |
754 | 785 |
raise NotAllowedError |
755 |
dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, None, True) |
|
756 |
if (src_account, src_container, src_name) != (dest_account, dest_container, dest_name): |
|
757 |
self._delete_object(user, src_account, src_container, src_name) |
|
786 |
dest_version_id = self._copy_object(user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta, replace_meta, permissions, None, True, delimiter) |
|
758 | 787 |
return dest_version_id |
759 | 788 |
|
760 |
def _delete_object(self, user, account, container, name, until=None, prefix='', delimiter=None):
|
|
789 |
def _delete_object(self, user, account, container, name, until=None, delimiter=None): |
|
761 | 790 |
if user != account: |
762 | 791 |
raise NotAllowedError |
763 | 792 |
|
... | ... | |
791 | 820 |
self._report_size_change(user, account, -del_size, {'action': 'object delete'}) |
792 | 821 |
self._report_object_change(user, account, path, details={'action': 'object delete'}) |
793 | 822 |
self.permissions.access_clear(path) |
823 |
|
|
824 |
if delimiter: |
|
825 |
prefix = name + delimiter if not name.endswith(delimiter) else name |
|
826 |
src_names = self._list_objects_no_limit(user, account, container, prefix, delimiter=None, virtual=True, domain=None, keys=[], shared=False, until=None, size_range=None, all_props=True, public=False) |
|
827 |
paths = [] |
|
828 |
for t in src_names: |
|
829 |
path = '/'.join((account, container, t[0])) |
|
830 |
node = t[2] |
|
831 |
src_version_id, dest_version_id = self._put_version_duplicate(user, node, size=0, type='', hash=None, checksum='', cluster=CLUSTER_DELETED) |
|
832 |
del_size = self._apply_versioning(account, container, src_version_id) |
|
833 |
if del_size: |
|
834 |
self._report_size_change(user, account, -del_size, {'action': 'object delete'}) |
|
835 |
self._report_object_change(user, account, path, details={'action': 'object delete'}) |
|
836 |
paths.append(path) |
|
837 |
self.permissions.access_clear_bulk(paths) |
|
794 | 838 |
|
795 | 839 |
@backend_method |
796 |
def (self, user, account, container, name, until=None, prefix='', delimiter=None): |
|
840 |
def delete_object(self, user, account, container, name, until=None, prefix='', delimiter=None):
|
|
797 | 841 |
"""Delete/purge an object.""" |
798 | 842 |
|
799 | 843 |
logger.debug("delete_object: %s %s %s %s %s %s %s", user, account, container, name, until, prefix, delimiter) |
800 |
self._delete_object(user, account, container, name, until) |
|
844 |
self._delete_object(user, account, container, name, until, delimiter)
|
|
801 | 845 |
|
802 | 846 |
@backend_method |
803 | 847 |
def list_versions(self, user, account, container, name): |
... | ... | |
936 | 980 |
if props is None or props[self.CLUSTER] == CLUSTER_DELETED: |
937 | 981 |
raise IndexError('Version does not exist') |
938 | 982 |
return props |
983 |
|
|
984 |
def _get_versions(self, nodes, version=None): |
|
985 |
if version is None: |
|
986 |
props = self.node.version_lookup_bulk(nodes, inf, CLUSTER_NORMAL) |
|
987 |
if not props: |
|
988 |
raise NameError('Object does not exist') |
|
989 |
else: |
|
990 |
try: |
|
991 |
version = int(version) |
|
992 |
except ValueError: |
|
993 |
raise IndexError('Version does not exist') |
|
994 |
props = self.node.version_get_properties(version) |
|
995 |
if props is None or props[self.CLUSTER] == CLUSTER_DELETED: |
|
996 |
raise IndexError('Version does not exist') |
|
997 |
return props |
|
939 | 998 |
|
940 | 999 |
def _put_version_duplicate(self, user, node, src_node=None, size=None, type=None, hash=None, checksum=None, cluster=CLUSTER_NORMAL, is_copy=False): |
941 | 1000 |
"""Create a new version of the node.""" |
Also available in: Unified diff