# -*- coding: utf-8 -*- import transaction from os.path import normpath as base_normpath from sqlalchemy.orm import Session from wsgidav import util from wsgidav import compat from tracim.lib.core.content import ContentApi from tracim.models.data import Workspace, Content, ContentType, \ ActionDescription from tracim.models.revision_protection import new_revision def transform_to_display(string: str) -> str: """ As characters that Windows does not support may have been inserted through Tracim in names, before displaying information we update path so that all these forbidden characters are replaced with similar shape character that are allowed so that the user isn't trouble and isn't limited in his naming choice """ _TO_DISPLAY = { '/': '⧸', '\\': '⧹', ':': '∶', '*': '∗', '?': 'ʔ', '"': 'ʺ', '<': '❮', '>': '❯', '|': '∣' } for key, value in _TO_DISPLAY.items(): string = string.replace(key, value) return string def transform_to_bdd(string: str) -> str: """ Called before sending request to the database to recover the right names """ _TO_BDD = { '⧸': '/', '⧹': '\\', '∶': ':', '∗': '*', 'ʔ': '?', 'ʺ': '"', '❮': '<', '❯': '>', '∣': '|' } for key, value in _TO_BDD.items(): string = string.replace(key, value) return string def normpath(path): if path == b'': path = b'/' elif path == '': path = '/' return base_normpath(path) class HistoryType(object): Deleted = 'deleted' Archived = 'archived' Standard = 'standard' All = 'all' class SpecialFolderExtension(object): Deleted = '/.deleted' Archived = '/.archived' History = '/.history' class FakeFileStream(object): """ Fake a FileStream that we're giving to wsgidav to receive data and create files / new revisions There's two scenarios : - when a new file is created, wsgidav will call the method createEmptyResource and except to get a _DAVResource which should have both 'beginWrite' and 'endWrite' method implemented - when a file which already exists is updated, he's going to call the 'beginWrite' function of the _DAVResource to get a filestream and write content in it In the first case scenario, the transfer takes two part : it first create the resource (createEmptyResource) then add its content (beginWrite, write, close..). If we went without this class, we would create two revision of the file upon creating a new file, which is not what we want. """ def __init__( self, session: Session, content_api: ContentApi, workspace: Workspace, path: str, file_name: str='', content: Content=None, parent: Content=None ): """ :param content_api: :param workspace: :param path: :param file_name: :param content: :param parent: """ self._file_stream = compat.BytesIO() self._session = session self._file_name = file_name if file_name != '' else self._content.file_name self._content = content self._api = content_api self._workspace = workspace self._parent = parent self._path = path def getRefUrl(self) -> str: """ As wsgidav expect to receive a _DAVResource upon creating a new resource, this method's result is used by Windows client to establish both file's path and file's name """ return self._path def beginWrite(self, contentType) -> 'FakeFileStream': """ Called by wsgidav, it expect a filestream which possess both 'write' and 'close' operation to write the file content. """ return self def endWrite(self, withErrors: bool): """ Called by request_server when finished writing everything. As we call operation to create new content or revision in the close operation, called before endWrite, there is nothing to do here. """ pass def write(self, s: str): """ Called by request_server when writing content to files, we put it inside a filestream """ self._file_stream.write(s) def close(self): """ Called by request_server when the file content has been written. We either add a new content or create a new revision """ self._file_stream.seek(0) if self._content is None: self.create_file() else: self.update_file() transaction.commit() def create_file(self): """ Called when this is a new file; will create a new Content initialized with the correct content """ is_temporary = self._file_name.startswith('.~') or self._file_name.startswith('~') file = self._api.create( filename=self._file_name, content_type=ContentType.File, workspace=self._workspace, parent=self._parent, is_temporary=is_temporary ) self._api.update_file_data( file, self._file_name, util.guessMimeType(self._file_name), self._file_stream.read() ) self._api.save(file, ActionDescription.CREATION) def update_file(self): """ Called when we're updating an existing content; we create a new revision and update the file content """ with new_revision( session=self._session, content=self._content, tm=transaction.manager, ): self._api.update_file_data( self._content, self._file_name, util.guessMimeType(self._content.file_name), self._file_stream.read() ) self._api.save(self._content, ActionDescription.REVISION)