def create_file(self, context, request: TracimRequest, hapic_data=None): """ Create a file .This will create 2 new revision. """ # INFO - G.M - 2019-09-03 - check validation of file here, because marshmallow # required doesn't work correctly with cgi_fieldstorage. # check is done with None because cgi_fieldstorage cannot be converted to bool if hapic_data.files.files is None: raise NoFileValidationError( 'No file "files" given at input, validation failed.') app_config = request.registry.settings["CFG"] # type: CFG api = ContentApi( show_archived=True, show_deleted=True, current_user=request.current_user, session=request.dbsession, config=app_config, ) api.check_upload_size(request.content_length, request.current_workspace) _file = hapic_data.files.files parent_id = hapic_data.forms.parent_id parent = None # type: typing.Optional['Content'] if parent_id: try: parent = api.get_one(content_id=parent_id, content_type=content_type_list.Any_SLUG) except ContentNotFound as exc: raise ParentNotFound( "Parent with content_id {} not found".format( parent_id)) from exc content = api.create( filename=_file.filename, content_type_slug=FILE_TYPE, workspace=request.current_workspace, parent=parent, ) api.save(content, ActionDescription.CREATION) with new_revision(session=request.dbsession, tm=transaction.manager, content=content): api.update_file_data( content, new_filename=_file.filename, new_mimetype=_file.type, new_content=_file.file, ) api.execute_created_content_actions(content) return api.get_content_in_context(content)
def upload_file(self, context, request: TracimRequest, hapic_data=None): """ Upload a new version of raw file of content. This will create a new revision. Good pratice for filename is filename is `{label}{file_extension}` or `{filename}`. Default filename value is 'raw' (without file extension) or nothing. """ # INFO - G.M - 2019-09-03 - check validation of file here, because marshmallow # required doesn't work correctly with cgi_fieldstorage. # check is done with None because cgi_fieldstorage cannot be converted to bool if hapic_data.files.files is None: raise NoFileValidationError( 'No file "files" given at input, validation failed.') app_config = request.registry.settings["CFG"] # type: CFG api = ContentApi( show_archived=True, show_deleted=True, current_user=request.current_user, session=request.dbsession, config=app_config, ) content = api.get_one(hapic_data.path.content_id, content_type=content_type_list.Any_SLUG) api.check_upload_size(request.content_length, content.workspace) _file = hapic_data.files.files with new_revision(session=request.dbsession, tm=transaction.manager, content=content): api.update_file_data( content, new_filename=_file.filename, new_mimetype=_file.type, new_content=_file.file, ) api.save(content) api.execute_update_content_actions(content) return
class FileResource(DAVNonCollection): """ FileResource resource corresponding to tracim's files """ def __init__(self, path: str, environ: dict, content: Content, tracim_context: "WebdavTracimContext") -> None: super(FileResource, self).__init__(path, environ) self.tracim_context = tracim_context self.content = content self.user = tracim_context.current_user self.session = tracim_context.dbsession self.content_api = ContentApi( current_user=self.user, config=tracim_context.app_config, session=self.session, namespaces_filter=[self.content.content_namespace], ) # this is the property that windows client except to check if the file is read-write or read-only, # but i wasn't able to set this property so you'll have to look into it >.> # self.setPropertyValue('Win32FileAttributes', '00000021') def __repr__(self) -> str: return "<DAVNonCollection: FileResource (%d)>" % self.content.cached_revision_id @webdav_check_right(is_reader) def getContentLength(self) -> int: return self.content.depot_file.file.content_length @webdav_check_right(is_reader) def getContentType(self) -> str: return self.content.file_mimetype @webdav_check_right(is_reader) def getCreationDate(self) -> float: return mktime(self.content.created.timetuple()) @webdav_check_right(is_reader) def getDisplayName(self) -> str: return webdav_convert_file_name_to_display(self.content.file_name) @webdav_check_right(is_reader) def getDisplayInfo(self): return {"type": self.content.type.capitalize()} def getLastModified(self) -> float: return mktime(self.content.updated.timetuple()) @webdav_check_right(is_reader) def getContent(self) -> typing.BinaryIO: return self.content.depot_file.file @webdav_check_right(is_contributor) def beginWrite(self, contentType: str = None) -> FakeFileStream: try: self.content_api.check_upload_size( int(self.environ["CONTENT_LENGTH"]), self.content.workspace) except ( FileSizeOverMaxLimitation, FileSizeOverWorkspaceEmptySpace, FileSizeOverOwnerEmptySpace, ) as exc: raise DAVError(HTTP_REQUEST_ENTITY_TOO_LARGE, contextinfo=str(exc)) return FakeFileStream( content=self.content, content_api=self.content_api, file_name=self.content.file_name, workspace=self.content.workspace, path=self.path, session=self.session, ) def moveRecursive(self, destpath): """As we support recursive move, copymovesingle won't be called, though with copy it'll be called but i have to check if the client ever call that function...""" destpath = normpath(destpath) self.tracim_context.set_destpath(destpath) if normpath(dirname(destpath)) == normpath(dirname(self.path)): # INFO - G.M - 2018-12-12 - renaming case checker = is_contributor else: # INFO - G.M - 2018-12-12 - move case checker = can_move_content try: checker.check(self.tracim_context) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) invalid_path = False invalid_path = dirname( destpath) == self.environ["http_authenticator.realm"] if not invalid_path: self.move_file(destpath) if invalid_path: raise DAVError(HTTP_FORBIDDEN) def move_file(self, destpath: str) -> None: """ Move file mean changing the path to access to a file. This can mean simple renaming(1), moving file from a directory to one another(2) but also renaming + moving file from a directory to one another at the same time (3). (1): move /dir1/file1 -> /dir1/file2 (2): move /dir1/file1 -> /dir2/file1 (3): move /dir1/file1 -> /dir2/file2 :param destpath: destination path of webdav move :return: nothing """ workspace = self.content.workspace parent = self.content.parent destpath = normpath(destpath) self.tracim_context.set_destpath(destpath) if normpath(dirname(destpath)) == normpath(dirname(self.path)): # INFO - G.M - 2018-12-12 - renaming case checker = is_contributor else: # INFO - G.M - 2018-12-12 - move case checker = can_move_content try: checker.check(self.tracim_context) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) try: with new_revision(content=self.content, tm=transaction.manager, session=self.session): # INFO - G.M - 2018-03-09 - First, renaming file if needed if basename(destpath) != self.getDisplayName(): new_filename = webdav_convert_file_name_to_bdd( basename(destpath)) regex_file_extension = re.compile("(?P<label>.*){}".format( re.escape(self.content.file_extension))) same_extension = regex_file_extension.match(new_filename) if same_extension: new_label = same_extension.group("label") new_file_extension = self.content.file_extension else: new_label, new_file_extension = os.path.splitext( new_filename) self.content_api.update_content(self.content, new_label=new_label) self.content.file_extension = new_file_extension self.content_api.save(self.content) # INFO - G.M - 2018-03-09 - Moving file if needed destination_workspace = self.tracim_context.candidate_workspace try: destination_parent = self.tracim_context.candidate_parent_content except ContentNotFound: destination_parent = None if destination_parent != parent or destination_workspace != workspace: # INFO - G.M - 12-03-2018 - Avoid moving the file "at the same place" # if the request does not result in a real move. self.content_api.move( item=self.content, new_parent=destination_parent, must_stay_in_same_workspace=False, new_workspace=destination_workspace, ) self.content_api.execute_update_content_actions(self.content) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) from exc transaction.commit() def copyMoveSingle(self, destpath, isMove): if isMove: # INFO - G.M - 12-03-2018 - This case should not happen # As far as moveRecursive method exist, all move should not go # through this method. If such case appear, try replace this to : #### # self.move_file(destpath) # return #### raise NotImplementedError("Feature not available") destpath = normpath(destpath) self.tracim_context.set_destpath(destpath) try: can_move_content.check(self.tracim_context) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) content_in_context = self.content_api.get_content_in_context( self.content) try: self.content_api.check_upload_size(content_in_context.size or 0, self.content.workspace) except ( FileSizeOverMaxLimitation, FileSizeOverWorkspaceEmptySpace, FileSizeOverOwnerEmptySpace, ) as exc: raise DAVError(HTTP_REQUEST_ENTITY_TOO_LARGE, contextinfo=str(exc)) new_filename = webdav_convert_file_name_to_bdd(basename(destpath)) regex_file_extension = re.compile("(?P<label>.*){}".format( re.escape(self.content.file_extension))) same_extension = regex_file_extension.match(new_filename) if same_extension: new_label = same_extension.group("label") new_file_extension = self.content.file_extension else: new_label, new_file_extension = os.path.splitext(new_filename) self.tracim_context.set_destpath(destpath) destination_workspace = self.tracim_context.candidate_workspace try: destination_parent = self.tracim_context.candidate_parent_content except ContentNotFound: destination_parent = None try: new_content = self.content_api.copy( item=self.content, new_label=new_label, new_file_extension=new_file_extension, new_parent=destination_parent, new_workspace=destination_workspace, ) self.content_api.execute_created_content_actions(new_content) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) from exc transaction.commit() def supportRecursiveMove(self, destpath): return True @webdav_check_right(is_content_manager) def delete(self): try: with new_revision(session=self.session, tm=transaction.manager, content=self.content): self.content_api.delete(self.content) self.content_api.execute_update_content_actions(self.content) self.content_api.save(self.content) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) from exc transaction.commit()
class ContentOnlyContainer(WebdavContainer): """ Container that can get children content """ def __init__( self, path: str, environ: dict, label: str, content: Content, provider: "TracimDavProvider", workspace: Workspace, tracim_context: "WebdavTracimContext", ) -> None: """ Some rules: - if content given is None, return workspace root contents - if the given content is correct, return the subcontent of this content and user-known workspaces without any user-known parent to the list. - in case of content collision, only the first named content (sorted by content_id from lower to higher) will be returned. """ self.path = path self.environ = environ self.workspace = workspace self.content = content self.tracim_context = tracim_context self.user = tracim_context.current_user self.session = tracim_context.dbsession self.label = label self.provider = provider self.content_api = ContentApi( current_user=self.user, session=tracim_context.dbsession, config=tracim_context.app_config, show_temporary=True, namespaces_filter=[ContentNamespaces.CONTENT], ) # Internal methods def _get_members( self, already_existing_names: typing.Optional[typing.List[str]] = None ) -> typing.List[Content]: members_names = [] members = [] if self.content: parent_id = self.content.content_id children = self.content_api.get_all( content_type=content_type_list.Any_SLUG, workspace=self.workspace, parent_ids=[parent_id], order_by_properties=["content_id"], ) else: children = self.content_api.get_all( content_type=content_type_list.Any_SLUG, workspace=self.workspace, parent_ids=[0], order_by_properties=["content_id"], ) for child in children: if child.file_name in members_names: continue else: members_names.append(child.file_name) members.append(child) return members def _generate_child_content_resource( self, parent_path: str, child_content: Content) -> _DAVResource: content_path = "%s/%s" % ( self.path, webdav_convert_file_name_to_display(child_content.file_name), ) return get_content_resource( path=content_path, environ=self.environ, workspace=self.workspace, content=child_content, tracim_context=self.tracim_context, ) # Container methods def createEmptyResource(self, file_name: str): """ Create a new file on the current workspace/folder. """ content = None fixed_file_name = webdav_convert_file_name_to_display(file_name) path = os.path.join(self.path, file_name) resource = self.provider.getResourceInst(path, self.environ) if resource: content = resource.content try: self.content_api.check_upload_size( int(self.environ["CONTENT_LENGTH"]), self.workspace) except ( FileSizeOverMaxLimitation, FileSizeOverWorkspaceEmptySpace, FileSizeOverOwnerEmptySpace, ) as exc: raise DAVError(HTTP_REQUEST_ENTITY_TOO_LARGE, contextinfo=str(exc)) # return item return FakeFileStream( session=self.session, file_name=fixed_file_name, content_api=self.content_api, workspace=self.workspace, content=content, parent=self.content, path=self.path + "/" + fixed_file_name, ) def createCollection(self, label: str) -> "FolderResource": """ Create a new folder for the current workspace/folder. As it's not possible for the user to choose which types of content are allowed in this folder, we allow allow all of them. This method return the DAVCollection created. """ folder_label = webdav_convert_file_name_to_bdd(label) try: folder = self.content_api.create( content_type_slug=content_type_list.Folder.slug, workspace=self.workspace, label=folder_label, parent=self.content, ) self.content_api.execute_created_content_actions(folder) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) from exc self.content_api.save(folder) transaction.commit() # fixed_path folder_path = "%s/%s" % (self.path, webdav_convert_file_name_to_display(label)) # return item return FolderResource( folder_path, self.environ, content=folder, tracim_context=self.tracim_context, workspace=self.workspace, ) def getMemberNames(self) -> [str]: """ Access to the list of content names for current workspace/folder """ # INFO - G.M - 2020-14-10 - Unclear if this method is really used by wsgidav retlist = [] for content in self._get_members(): retlist.append( webdav_convert_file_name_to_display(content.file_name)) return retlist def getMember(self, label: str) -> _DAVResource: """ Access to a specific members """ return self.provider.getResourceInst( "%s/%s" % (self.path, webdav_convert_file_name_to_display(label)), self.environ) def getMemberList(self) -> [_DAVResource]: """ Access to the list of content of current workspace/folder """ members = [] for content in self._get_members(): members.append( self._generate_child_content_resource(parent_path=self.path, child_content=content)) return members
class WorkspaceResource(DAVCollection): """ Workspace resource corresponding to tracim's workspaces. Direct children can only be folders, though files might come later on and are supported """ def __init__( self, label: str, path: str, environ: dict, workspace: Workspace, tracim_context: "WebdavTracimContext", ) -> None: super(WorkspaceResource, self).__init__(path, environ) self.workspace = workspace self.content = None self.tracim_context = tracim_context self.user = tracim_context.current_user self.session = tracim_context.dbsession self.label = label self.content_api = ContentApi( current_user=self.user, session=tracim_context.dbsession, config=tracim_context.app_config, show_temporary=True, namespaces_filter=[ContentNamespaces.CONTENT], ) self._file_count = 0 def __repr__(self) -> str: return "<DAVCollection: Workspace (%d)>" % self.workspace.workspace_id def getPreferredPath(self): return self.path def getCreationDate(self) -> float: return mktime(self.workspace.created.timetuple()) def getDisplayName(self) -> str: return webdav_convert_file_name_to_display(self.label) def getDisplayInfo(self): return {"type": "workspace".capitalize()} def getLastModified(self) -> float: return mktime(self.workspace.updated.timetuple()) def getMemberNames(self) -> [str]: retlist = [] children = self.content_api.get_all( parent_ids=[self.content.id] if self.content is not None else None, workspace=self.workspace, ) for content in children: # the purpose is to display .history only if there's at least one content's type that has a history if content.type != content_type_list.Folder.slug: self._file_count += 1 retlist.append( webdav_convert_file_name_to_display(content.file_name)) return retlist def getMember(self, content_label: str) -> _DAVResource: return self.provider.getResourceInst( "%s/%s" % (self.path, webdav_convert_file_name_to_display(content_label)), self.environ) @webdav_check_right(is_contributor) def createEmptyResource(self, file_name: str): """ [For now] we don't allow to create files right under workspaces. Though if we come to allow it, deleting the error's raise will make it possible. """ # TODO : remove commentary here raise DAVError(HTTP_FORBIDDEN) if "/.deleted/" in self.path or "/.archived/" in self.path: raise DAVError(HTTP_FORBIDDEN) content = None # Note: To prevent bugs, check here again if resource already exist # fixed path fixed_file_name = webdav_convert_file_name_to_display(file_name) path = os.path.join(self.path, file_name) resource = self.provider.getResourceInst(path, self.environ) if resource: content = resource.content try: self.content_api.check_upload_size( int(self.environ["CONTENT_LENGTH"]), self.workspace) except ( FileSizeOverMaxLimitation, FileSizeOverWorkspaceEmptySpace, FileSizeOverOwnerEmptySpace, ) as exc: raise DAVError(HTTP_REQUEST_ENTITY_TOO_LARGE, contextinfo=str(exc)) # return item return FakeFileStream( session=self.session, file_name=fixed_file_name, content_api=self.content_api, workspace=self.workspace, content=content, parent=self.content, path=self.path + "/" + fixed_file_name, ) @webdav_check_right(is_content_manager) def createCollection(self, label: str) -> "FolderResource": """ Create a new folder for the current workspace. As it's not possible for the user to choose which types of content are allowed in this folder, we allow allow all of them. This method return the DAVCollection created. """ if "/.deleted/" in self.path or "/.archived/" in self.path: raise DAVError(HTTP_FORBIDDEN) folder_label = webdav_convert_file_name_to_bdd(label) try: folder = self.content_api.create( content_type_slug=content_type_list.Folder.slug, workspace=self.workspace, label=folder_label, parent=self.content, ) self.content_api.execute_created_content_actions(folder) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) from exc self.content_api.save(folder) transaction.commit() # fixed_path folder_path = "%s/%s" % (self.path, webdav_convert_file_name_to_display(label)) # return item return FolderResource( folder_path, self.environ, content=folder, tracim_context=self.tracim_context, workspace=self.workspace, ) def delete(self): """For now, it is not possible to delete a workspace through the webdav client.""" # FIXME - G.M - 2018-12-11 - For an unknown reason current_workspace # of tracim_context is here invalid. self.tracim_context._current_workspace = self.workspace try: can_delete_workspace.check(self.tracim_context) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) raise DAVError(HTTP_FORBIDDEN, "Workspace deletion is not allowed through webdav") def supportRecursiveMove(self, destpath): return True def moveRecursive(self, destpath): # INFO - G.M - 2018-12-11 - We only allow renaming if dirname(normpath( destpath)) == self.environ["http_authenticator.realm"]: # FIXME - G.M - 2018-12-11 - For an unknown reason current_workspace # of tracim_context is here invalid. self.tracim_context._current_workspace = self.workspace try: can_modify_workspace.check(self.tracim_context) except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) try: workspace_api = WorkspaceApi(current_user=self.user, session=self.session, config=self.provider.app_config) workspace_api.update_workspace( workspace=self.workspace, label=webdav_convert_file_name_to_bdd( basename(normpath(destpath))), description=self.workspace.description, ) self.session.add(self.workspace) self.session.flush() workspace_api.execute_update_workspace_actions(self.workspace) transaction.commit() except TracimException as exc: raise DAVError(HTTP_FORBIDDEN, contextinfo=str(exc)) def getMemberList(self) -> [_DAVResource]: members = [] children = self.content_api.get_all(False, content_type_list.Any_SLUG, self.workspace) for content in children: content_path = "%s/%s" % ( self.path, webdav_convert_file_name_to_display(content.file_name), ) if content.type == content_type_list.Folder.slug: members.append( FolderResource( path=content_path, environ=self.environ, workspace=self.workspace, content=content, tracim_context=self.tracim_context, )) elif content.type == content_type_list.File.slug: self._file_count += 1 members.append( FileResource( path=content_path, environ=self.environ, content=content, tracim_context=self.tracim_context, )) else: self._file_count += 1 members.append( OtherFileResource(content_path, self.environ, content, tracim_context=self.tracim_context)) return members