def render( self, request: HttpRequest, editing_context: SourceEditContext, extra_context: typing.Optional[dict] = None, ) -> HttpResponse: render_context = { "project": self.project, "has_edit_permission": self.has_permission(ProjectPermissionType.EDIT), "file_name": utf8_basename(editing_context.path), "file_directory": utf8_dirname(editing_context.path), "file_path": editing_context.path, "file_extension": editing_context.extension, "file_content": editing_context.content, "file_editable": editing_context.editable, "source": editing_context.source, "supports_commit_message": editing_context.supports_commit_message, } render_context.update(extra_context or {}) return render( request, "projects/source_open.html", self.get_render_context(render_context), )
def sources_in_directory( directory: typing.Optional[str], sources: typing.Iterable[Source], authentication: LinkedSourceAuthentication, ) -> typing.Iterable[DirectoryListEntry]: """Yield a `DirectoryListEntry` for each `Source` in `sources` if the `Source` is inside the `directory`.""" directory = directory or "" seen_directories: typing.Set[str] = set() for source in sources: if isinstance(source, GithubSource): facade = GitHubFacade(source.repo, authentication.github_token) try: for entry in iterate_github_source(directory, source, facade): if (entry.type == DirectoryEntryType.DIRECTORY or entry.type == DirectoryEntryType.LINKED_SOURCE): if entry.name in seen_directories: continue seen_directories.add(entry.name) yield entry continue except IncorrectDirectoryException: pass if utf8_dirname(source.path) != directory: continue entry = make_directory_entry(directory, source) if (entry.type == DirectoryEntryType.DIRECTORY or entry.type == DirectoryEntryType.LINKED_SOURCE): if entry.name in seen_directories: continue seen_directories.add(entry.name) yield entry
def get_source(user: User, project: Project, path: str) -> typing.Union[Source, DiskSource]: """ Locate a `Source` that contains the file at `path`. Iterate up through the directory tree of the path until Source(s) mapped to that path are found. Then check if that file exists in the source. Fall back to DiskSource if no file is found in any linked Sources. """ # the paths won't match if the incoming path has a trailing slash as the paths on the source doesn't (shouldn't) path = path.rstrip("/") original_path = path gh_facade: typing.Optional[GitHubFacade] = None while True: sources = Source.objects.filter(path=path, project=project) if sources: if path == original_path: # This source should be one without sub files return sources[0] else: # These may be Github, etc, sources mapped into the same directory. Find one that has a file at the path for source in sources: if isinstance(source, GithubSource): source = typing.cast(GithubSource, source) if gh_facade is None: gh_token = user_github_token(user) gh_facade = GitHubFacade(source.repo, gh_token) relative_path = utf8_path_join( source.subpath, strip_directory(original_path, source.path)) if gh_facade.path_exists(relative_path): return source # this source has the file so it must be this one? # if not, continue on, keep going up the tree to find the root source else: raise RuntimeError( "Don't know how to examine the contents of {}". format(type(source))) if path == ".": break path = utf8_dirname(path) if path == "/" or path == "": path = "." # Fall Back to DiskSource return DiskSource()
def post(self, request: HttpRequest, account_name: str, project_name: str) -> HttpResponse: # type: ignore project = self.get_project(request.user, account_name, project_name) body = json.loads(request.body) source_id = body["source_id"] source_path = body["source_path"] target_id = body["target_type"] target_name = body["target_name"] if "/" in target_name: raise ValueError("Target name can not contain /") if source_id: source = get_object_or_404(Source, project=project, pk=source_id) else: source = DiskSource() scf = make_source_content_facade(request.user, source_path, source, project) target_path = utf8_path_join(utf8_dirname(source_path), target_name) target_type = ConversionFormatId.from_id(target_id) try: self.source_convert(request, project, scf, target_path, target_name, target_type) except oauth2client.client.Error: return JsonResponse({ "success": False, "error": "Could not authenticate with your Google account." "Please try logging into Stencila Hub with " "your Google account to refresh the token.", }) except RuntimeError: return JsonResponse({ "success": False, "error": "Conversion of your document failed. Please check the Project Activity page for more " "information.", }) for message in scf.message_iterator(): messages.add_message(request, message.level, message.message) messages.success( request, "{} was converted.".format(utf8_basename(source_path))) return JsonResponse({"success": True})
def create_file(self, relative_path: str) -> None: full_path = self.full_file_path(relative_path) if utf8_path_exists(full_path): raise OSError( "Can not create project file at {} as it already exists".format( full_path ) ) utf8_makedirs(utf8_dirname(full_path), exist_ok=True) with open(full_path, "a"): pass
def convert_to_google_docs( self, request: HttpRequest, project: Project, scf: SourceContentFacade, target_name: str, target_path: str, ) -> None: """ Convert a document to Google Docs. If the document is already in DOCX or HTML format it will just be uploaded, otherwise it is first converted to DOCX. The document is uploaded in DOCX/HTML and Google takes care of converting to Google Docs format. """ if scf.source_type not in (ConversionFormatId.html, ConversionFormatId.docx): output_content, output_mime_type = self.convert_source_for_google_docs( project, request.user, scf) else: output_mime_type = (mimetype_from_path(scf.file_path) or "application/octet-stream") output_content = scf.get_binary_content() gdf = scf.google_docs_facade if gdf is None: raise TypeError( "Google Docs Facade was not set up. Check that app tokens are good." ) new_doc_id = gdf.create_document(target_name, output_content, output_mime_type) existing_source = GoogleDocsSource.objects.filter( project=project, path=target_path).first() new_source = gdf.create_source_from_document(project, utf8_dirname(target_path), new_doc_id) if existing_source is not None: gdf.trash_document(existing_source.doc_id) messages.info( request, 'Existing Google Docs file "{}" was moved to the Trash.'. format(target_name), ) existing_source.doc_id = new_source.doc_id existing_source.save() else: new_source.save()
def move_file(self, current_relative_path: str, new_relative_path: str) -> None: current_path = self.full_file_path(current_relative_path) new_path = self.full_file_path(new_relative_path) if utf8_isdir(new_path): # path moving to is a directory so actually move inside the path filename = utf8_basename(current_path) new_path = utf8_path_join(new_path, filename) if utf8_path_exists(new_path): raise OSError( "Can not move {} to {} as target file exists.".format( current_relative_path, new_relative_path ) ) utf8_makedirs(utf8_dirname(new_path), exist_ok=True) utf8_rename(current_path, new_path)
def process_get( self, account_name: str, project_name: str, path: str, content_facade: SourceContentFacade, ) -> HttpResponse: # TODO: see if this can return a handle for streaming response file_content = content_facade.get_binary_content() if content_facade.error_exists: content_facade.add_messages_to_request(self.request) return project_files_redirect(account_name, project_name, utf8_dirname(path)) response = HttpResponse(file_content, content_type="application/octet-stream") response["Content-Disposition"] = "attachment; filename={}".format( content_facade.get_name()) return response
def process_get( self, request: HttpRequest, account_name: str, project_name: str, path: str, content_facade: SourceContentFacade, ) -> HttpResponse: edit_context = content_facade.get_edit_context() if edit_context is None: content_facade.add_messages_to_request(request) return project_files_redirect(account_name, project_name, utf8_dirname(path)) return self.render( request, edit_context, { "default_commit_message": self.get_default_commit_message(request) }, )
def get( # type: ignore self, request: HttpRequest, account_name: str, project_name: str, path: str, ) -> HttpResponse: project = self.get_project(request.user, account_name, project_name) source = get_source(request.user, project, path) scf = make_source_content_facade(request.user, path, source, project) # the isinstance check is because Source might be a DiskSource which is not a subclass of Source pi, created = PublishedItem.objects.get_or_create( project=project, source_path=path, source=source if isinstance(source, Source) else None, snapshot=None, ) if created or scf.source_modification_time > pi.updated or not pi.path: try: self.convert_and_publish(request.user, project, pi, created, source, path, scf) except (NonFileError, UnknownMimeTypeError, RuntimeError) as e: filename = utf8_basename(path) directory = utf8_dirname(path) # by default, no message since the user might have just entered a bad URL message_format: typing.Optional[str] = None if isinstance(e, RuntimeError): message_format = ( "Unable to preview <em>{}</em> as it could not be converted to HTML. Please " "check the Project Activity page for more information." ) elif isinstance(e, UnknownMimeTypeError): message_format = "Unable to preview <em>{}</em> as its file type could not be determined." messages.error( request, "Unable to preview <em>{}</em> as its file type could not be determined." .format(escape(utf8_basename(path))), extra_tags="safe", ) if message_format: messages.error( request, message_format.format(escape(filename)), extra_tags="safe", ) if directory: return redirect("project_files_path", account_name, project, directory) return redirect("project_files", account_name, project_name) return published_item_render( request, pi, reverse( "file_source_download", args=(project.account.name, project.name, pi.source_path), ), "HTML Preview of {}".format(pi.source_path), )
def media_path(self, path: str) -> str: media_path = "{}.html.media/{}".format(self.pk, path) return relative_path_join(utf8_dirname(self.path), media_path)
def perform_post( self, request: HttpRequest, account_name: str, project_name: str, path: str, content_facade: SourceContentFacade, ) -> HttpResponse: commit_message = request.POST.get( "commit_message") or self.get_default_commit_message(request) storage_limit = account_resource_limit(self.project.account, QuotaName.STORAGE_LIMIT) update_success = None content_override = None if storage_limit != -1 and isinstance(content_facade.source, DiskSource): old_size = content_facade.get_size() new_size = len(request.POST["file_content"]) if new_size > old_size: storage_used = (content_facade.disk_file_facade. get_project_directory_size()) is_account_admin = user_is_account_admin( self.request.user, self.project.account) subscription_upgrade_text = get_subscription_upgrade_text( is_account_admin, self.project.account) if (new_size - old_size) + storage_used > storage_limit: message = ( "The file content could not be saved as it would exceed the storage limit for the " "account <em>{}</em>. {}".format( escape(self.project.account), subscription_upgrade_text)) messages.error(request, message, extra_tags="safe") update_success = False content_override = request.POST["file_content"] if update_success is None: update_success = content_facade.update_content( request.POST["file_content"], commit_message) error_exists = content_facade.error_exists content_facade.add_messages_to_request(request) dirname = utf8_dirname(path) if error_exists or not update_success: edit_context = content_facade.get_edit_context(content_override) if edit_context is None or content_facade.error_exists: return project_files_redirect(account_name, project_name, dirname) return self.render( request, edit_context, { "commit_message": commit_message, "default_commit_message": self.get_default_commit_message(request), }, ) messages.success( request, "Content of {} updated.".format(os.path.basename(path))) return project_files_redirect(account_name, project_name, dirname)
def get( # type: ignore self, request: HttpRequest, account_name: str, project_name: str, version: int, path: str, ) -> HttpResponse: snapshot, file_path = self.get_snapshot_and_path( request, account_name, project_name, version, path) if file_path is None: raise TypeError( "Can't work with None path. But this won't happen unless path being passed is None." ) pi, created = PublishedItem.objects.get_or_create(project=self.project, snapshot=snapshot, source_path=path) published_path = utf8_path_join( generate_snapshot_publish_directory(settings.STORAGE_DIR, snapshot), "{}.html".format(pi.pk), ) if created or not pi.path: # don't bother checking modification time since snapshots shouldn't change try: source_type = conversion_format_from_path(file_path) self.do_conversion( snapshot.project, request.user, source_type, file_path, ConversionFormatId.html, published_path, False, ) pi.path = published_path pi.save() except Exception as e: if created: pi.delete() if not isinstance(e, (UnknownMimeTypeError, RuntimeError)): raise filename = escape(utf8_basename(path)) if isinstance(e, UnknownMimeTypeError): error_format = "Unable to preview <em>{}</em> as its file type could not be determined." else: error_format = ( "Unable to preview <em>{}</em> as it could not be converted to HTML. Please " "check the Project Activity page for more information." ) messages.error(request, error_format.format(filename), extra_tags="safe") dirname = utf8_dirname(path) redirect_args = [ snapshot.project.account.name, snapshot.project.name, snapshot.version_number, ] if dirname: redirect_args.append(dirname) redirect_view = "snapshot_files_path" else: redirect_view = "snapshot_files" return redirect(redirect_view, *redirect_args) project = self.project return published_item_render( request, pi, reverse( "api-snapshots-retrieve-file", args=( project.id, snapshot.number, pi.source_path, ), ), "HTML Preview of {}".format(pi.source_path), )