diff --git a/src/linkahead/__init__.py b/src/linkahead/__init__.py index 3a8c5ba39c88deaa5dc945135e3828945fd39d58..4462974cdcc8e1e9e8bf7108076da7c352170e46 100644 --- a/src/linkahead/__init__.py +++ b/src/linkahead/__init__.py @@ -42,7 +42,7 @@ from .common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, LIST, REFERENCE, TEXT) # Import of the basic API classes: from .common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED, - SUGGESTED, Container, DropOffBox, Entity, File, + SUGGESTED, Container, Entity, File, Info, Message, Permissions, Property, Query, QueryTemplate, Record, RecordType, delete, execute_query, get_global_acl, diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index 38c1349067fce68dc3dc0311dc621bd0e383d4b0..23389f039f3f802028394890b0830fd3e2541842 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -202,6 +202,10 @@ class Entity: return self._wrapped_entity.size + @size.setter + def size(self, new_size): + self._size = new_size if new_size is None else int(new_size) + @property def id(self): if self.__id is not None: @@ -252,7 +256,17 @@ class Entity: @property def checksum(self): - return self._checksum + if self._checksum is not None: + return self._checksum + + if self._wrapped_entity is None: + return None + + return self._wrapped_entity._checksum + + @checksum.setter + def checksum(self, new_checksum): + self._checksum = new_checksum @description.setter def description(self, new_description): @@ -293,14 +307,16 @@ class Entity: @property def thumbnail(self): - if self.__thumbnail is not None or self._wrapped_entity is None: - return self.__thumbnail - - return self._wrapped_entity.thumbnail + warn(DeprecationWarning( + "The thumbnail feature has been removed from the CaosDB server " + "API")) + return None @thumbnail.setter def thumbnail(self, new_thumbnail): - self.__thumbnail = new_thumbnail + warn(DeprecationWarning( + "The thumbnail feature has been removed from the CaosDB server " + "API")) @property def file(self): @@ -315,14 +331,16 @@ class Entity: @property def pickup(self): - if self.__pickup is not None or self._wrapped_entity is None: - return self.__pickup - - return self._wrapped_entity.pickup + warn(DeprecationWarning( + "The drop-off/pickup feature has been removed from the CaosDB " + "server API")) + return None @pickup.setter def pickup(self, new_pickup): - self.__pickup = new_pickup + warn(DeprecationWarning( + "The drop-off/pickup feature has been removed from the CaosDB " + "server API")) def grant(self, realm=None, username=None, role=None, permission=None, priority=False, revoke_denial=True): @@ -1213,8 +1231,8 @@ out: List[Entity] if self.file is not None and local_serialization: xml.set("file", self.file) - if self._checksum is not None: - xml.set("checksum", self._checksum) + if self.checksum is not None: + xml.set("checksum", self.checksum) if self.size is not None: xml.set("size", str(self.size)) @@ -1992,35 +2010,35 @@ class File(Record): """This class represents LinkAhead's file entities. - For inserting a new file to the server, `path` gives the new location, and - (exactly?) one of `file` and `pickup` should (must?) be given to specify the - source of the file. - - Symlinking from the "extroot" file system is not supported by this API yet, - it can be done manually using the `InsertFilesInDir` flag. For sample code, - look at `test_files.py` in the Python integration tests of the - `load_files.py` script in the advanced user tools. - - @param name: A name for this file record (That's an entity name - not to be - confused with the last segment of the files path). - @param id: An ID. - @param description: A description for this file record. - @param path: The complete path, including the file name, of the file in the - server's "caosroot" file system. - @param file: A local path or python file object. The file designated by - this argument will be uploaded to the server via HTTP. - @param pickup: A file/folder in the DropOffBox (the server will move that - file into its "caosroot" file system). - @param thumbnail: (Local) filename to a thumbnail for this file. - @param properties: A list of properties for this file record. @todo is this - implemented? - @param from_location: Deprecated, use `pickup` instead. + For inserting a new file to the server or updating an existent one. + `path` gives the new location. `file` specifies the (local) file which is + to be uploaded. It is mandatory for insertions, but optionally for updates. + Parameters + ---------- + name : str + A name for this file record (That's an entity name - not to be confused + with the last segment of the files path). + id : int + An ID. + description : str + A description for this file record. + path : str + The complete path, including the file name, of the file in the server's + virtual file system. + file : str or readable + A local path or python file object. The file designated by this + argument will be uploaded to the server via HTTP. + pickup : str + deprecated. + thumbnail : str + deprecated. """ - def __init__(self, name=None, id=None, description=None, # @ReservedAssignment - path=None, file=None, pickup=None, # @ReservedAssignment - thumbnail=None, from_location=None): + def __init__(self, name=None, id=None, description=None, + path=None, file=None, pickup=None, + thumbnail=None): + Record.__init__(self, id=id, name=name, description=description) self.role = "File" self.datatype = None @@ -2030,16 +2048,15 @@ class File(Record): # local file path or pointer to local file self.file = file - self.thumbnail = thumbnail - - self.pickup = pickup - - if from_location is not None: + if thumbnail is not None: warn(DeprecationWarning( - "Param `from_location` is deprecated, use `pickup instead`.")) + "The thumbnail feature has been removed from the CaosDB " + "server API")) - if self.pickup is None: - self.pickup = from_location + if pickup is not None: + warn(DeprecationWarning( + "The drop-off/pickup feature has been removed from the CaosDB " + "server API")) def to_xml(self, xml=None, add_properties=ALL, local_serialization=False): """Convert this file to an xml element. @@ -2053,7 +2070,7 @@ class File(Record): return Entity.to_xml(self, xml=xml, add_properties=add_properties, local_serialization=local_serialization) - def download(self, target=None): + def download(self, target=None, check_hash=True): """Download this file-entity's actual file from the file server. It will be stored to the target or will be hold as a temporary file. @@ -2066,16 +2083,16 @@ class File(Record): file_ = open(target, 'wb') else: file_ = NamedTemporaryFile(mode='wb', delete=False) - checksum = File.download_from_path(file_, self.path) + checksum = File._download_from_path(file_, self.path) - if self._checksum is not None and self._checksum.lower() != checksum.hexdigest().lower(): + if check_hash and self.checksum is not None and self.checksum.lower() != checksum: raise ConsistencyError( "The downloaded file had an invalid checksum. Maybe the download did not finish?") return file_.name @staticmethod - def download_from_path(target_file, path): + def _download_from_path(target_file, path): _log_request("GET (download): " + path) response = get_connection().download_file(path) @@ -2089,46 +2106,27 @@ class File(Record): data = response.read(8000) target_file.close() - return checksum + return checksum.hexdigest().lower() @staticmethod - def _get_checksum(files): - import locale - - if hasattr(files, "name"): - return File._get_checksum_single_file(files.name) + def _get_checksum(file): + if hasattr(file, "name"): + file_name = file.name else: - if isdir(files): - checksumappend = "" - - for child in sorted(listdir(files), - key=cmp_to_key(locale.strcoll)): - - if isdir(files + '/' + child): - checksumappend += child - checksumappend += File._get_checksum(files + "/" + child) - checksum = sha512() - checksum.update(checksumappend.encode('utf-8')) + file_name = file - return checksum.hexdigest() - else: - return File._get_checksum_single_file(files) - - @staticmethod - def _get_checksum_single_file(single_file): - _file = open(single_file, 'rb') - data = _file.read(1000) checksum = sha512() - - while data: - checksum.update(data) + with open(file_name, 'rb') as _file: data = _file.read(1000) - _file.close() - return checksum.hexdigest() + while data: + checksum.update(data) + data = _file.read(1000) + + return checksum.hexdigest().lower() def add_property(self, property=None, id=None, name=None, description=None, datatype=None, - value=None, unit=None, importance=FIX, inheritance=FIX): # @ReservedAssignment + value=None, unit=None, importance=FIX, inheritance=FIX): """See ``Entity.add_property``.""" return super().add_property( @@ -3618,8 +3616,6 @@ class Container(list): if hasattr(entity, '_upload') and entity._upload is not None: entity_xml.set("upload", entity._upload) - elif hasattr(entity, 'pickup') and entity.pickup is not None: - entity_xml.set("pickup", entity.pickup) insert_xml.append(entity_xml) @@ -3690,20 +3686,18 @@ class Container(list): part.filename = entity._upload http_parts.append(part) - if entity.thumbnail is not None: - part = MultipartParam.from_file(paramname=hex( - randint(0, sys.maxsize)), filename=entity.thumbnail) - part.filename = entity._upload + ".thumbnail" - http_parts.append(part) + elif isinstance(entity, File) and hasattr(entity, "file") and entity.file is None: + entity._upload = None else: + entity._upload = None entity._checksum = None def insert(self, strict=False, raise_exception_on_error=True, unique=True, sync=True, flags=None): - """Insert this file entity into LinkAhead. A successful insertion will - generate a new persistent ID for this entity. This entity can be - identified, retrieved, updated, and deleted via this ID until it has - been deleted. + """Insert these entities into CaosDB. + + A successful insertion will generate a persistent ID for the new entities. These entities can be + identified, retrieved, updated, and deleted via their ID until they have been deleted. If the insertion fails, a LinkAheadException will be raised. The server will have returned at least one error-message describing the reason why it failed in that case (call @@ -3768,8 +3762,6 @@ class Container(list): if hasattr(entity, '_upload') and entity._upload is not None: entity_xml.set("upload", entity._upload) - elif hasattr(entity, 'pickup') and entity.pickup is not None: - entity_xml.set("pickup", entity.pickup) insert_xml.append(entity_xml) if len(self) > 0 and len(insert_xml) < 1: @@ -4547,43 +4539,6 @@ def execute_query(q, unique=False, raise_exception_on_error=True, cache=True, cache=cache, page_length=page_length) -class DropOffBox(list): - def __init__(self, *args, **kwargs): - warn(DeprecationWarning( - "The DropOffBox is deprecated and will be removed in future.")) - super().__init__(*args, **kwargs) - - path = None - - def sync(self): - c = get_connection() - _log_request("GET: Info") - http_response = c.retrieve(["Info"]) - body = http_response.read() - _log_response(body) - - xml = etree.fromstring(body) - - for child in xml: - if child.tag.lower() == "stats": - infoelem = child - - break - - for child in infoelem: - if child.tag.lower() == "dropoffbox": - dropoffboxelem = child - - break - del self[:] - self.path = dropoffboxelem.get('path') - - for f in dropoffboxelem: - self.append(f.get('path')) - - return self - - class UserInfo(): def __init__(self, xml):