Skip to content

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
m-alisafaee committed Sep 28, 2023
1 parent 918fbd3 commit 0d96985
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 41 deletions.
4 changes: 2 additions & 2 deletions renku/command/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
"""Project management."""

from renku.command.command_builder.command import Command
from renku.core.constant import DATABASE_METADATA_PATH
from renku.core.constant import PROJECT_METADATA_PATH
from renku.core.project import edit_project, show_project


def edit_project_command():
"""Command for editing project metadata."""
command = Command().command(edit_project).lock_project().with_database(write=True)
return command.require_migration().with_commit(commit_only=DATABASE_METADATA_PATH)
return command.require_migration().with_commit(commit_only=PROJECT_METADATA_PATH)


def show_project_command():
Expand Down
9 changes: 8 additions & 1 deletion renku/core/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from enum import IntEnum
from pathlib import Path

FILESYSTEM_ROOT = os.path.abspath(os.sep)
"""Path to the root of the filesystem."""

APP_NAME = "Renku"
"""Application name for storing configuration."""

Expand Down Expand Up @@ -82,10 +85,14 @@
Path(RENKU_HOME) / DATABASE_PATH,
]

PROJECT_METADATA_PATH = [
Path(RENKU_HOME) / DATABASE_PATH,
Path(RENKU_HOME) / IMAGES,
]

DATASET_METADATA_PATHS = [
Path(RENKU_HOME) / DATABASE_PATH,
Path(RENKU_HOME) / DATASET_IMAGES,
Path(RENKU_HOME) / IMAGES,
Path(RENKU_HOME) / POINTERS,
Path(RENKU_HOME) / REFS,
".gitattributes",
Expand Down
39 changes: 39 additions & 0 deletions renku/core/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from urllib.request import urlretrieve

from renku.core import errors
from renku.core.util.os import is_subpath
from renku.core.util.urls import is_remote
from renku.domain_model.image import ImageObject
from renku.domain_model.project_context import project_context

Expand Down Expand Up @@ -105,3 +107,40 @@ def to_image_object(self, image_folder: Path, owner_id: str) -> ImageObject:
position=self.position,
id=ImageObject.generate_id(owner_id=owner_id, position=self.position),
)

def download_image(self, owner_id: str) -> ImageObject:
"""Download the image and save it to a temporary file."""

# def to_image_object(self, image_folder: Path, owner_id: str) -> ImageObject:
"""Convert request model to ``ImageObject``."""
self.safe_image_paths.append(project_context.path)

if is_remote(self.content_url):
if not self.mirror_locally:
return ImageObject(
content_url=self.content_url,
position=self.position,
id=ImageObject.generate_id(owner_id=owner_id, position=self.position),
)

# NOTE: Download the image
try:
path, _ = urlretrieve(self.content_url)
except urllib.error.URLError as e:
raise errors.ImageError(f"Cannot download image with url {self.content_url}: {e}") from e

path = Path(path)
self.safe_image_paths.append(Path(path).parent)
else:
path = Path(self.content_url).resolve()

if not os.path.exists(path):
raise errors.ImageError(f"Image with local path '{self.content_url}' not found")
elif not any(is_subpath(path, base=p) for p in self.safe_image_paths):
raise errors.ImageError(f"File with local path '{self.content_url}' cannot be used as project image")

return ImageObject(
content_url=path.as_posix(),
position=self.position,
id=ImageObject.generate_id(owner_id=owner_id, position=self.position),
)
6 changes: 3 additions & 3 deletions renku/core/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from renku.core.image import ImageRequestModel
from renku.core.interface.database_gateway import IDatabaseGateway
from renku.core.migration.utils import OLD_METADATA_PATH
from renku.core.project import copy_project_image
from renku.core.project import set_project_image
from renku.core.storage import init_external_storage, storage_installed
from renku.core.template.template import (
FileAction,
Expand Down Expand Up @@ -321,8 +321,8 @@ def create_from_template(
) as project:
copy_template_to_project(rendered_template=rendered_template, project=project, actions=actions)

# NOTE: Copy image to project
copy_project_image(image=image)
# NOTE: Copy image to project
set_project_image(image=image)

if install_mergetool:
setup_mergetool()
Expand Down
58 changes: 36 additions & 22 deletions renku/core/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# limitations under the License.
"""Project business logic."""

import os
import shutil
from typing import Dict, List, Optional, Union, cast

from pydantic import validate_arguments
Expand All @@ -25,6 +27,7 @@
from renku.core.image import ImageRequestModel
from renku.core.interface.project_gateway import IProjectGateway
from renku.core.util.metadata import construct_creator
from renku.core.util.os import get_relative_path
from renku.domain_model.constant import NO_VALUE, NoValueType
from renku.domain_model.project_context import project_context
from renku.domain_model.provenance.agent import Person
Expand Down Expand Up @@ -69,20 +72,22 @@ def edit_project(
if creator is not NO_VALUE:
parsed_creator, no_email_warnings = construct_creator(cast(Union[Dict, str], creator), ignore_email=True)

if image is not NO_VALUE:
copy_project_image(image=image)
if image is None:
delete_project_image()
elif image is not NO_VALUE:
set_project_image(image=image)

updated = {k: v for k, v in possible_updates.items() if v is not NO_VALUE}

if updated:
project = project_gateway.get_project()
# NOTE: No need to pass ``image`` here since we already copied/deleted the file and updated the project
project.update_metadata(
creator=parsed_creator,
description=description,
keywords=keywords,
custom_metadata=custom_metadata,
custom_metadata_source=custom_metadata_source,
image=image,
)
project_gateway.update_project(project)

Expand All @@ -98,28 +103,37 @@ def show_project() -> ProjectViewModel:
return ProjectViewModel.from_project(project_context.project)


def copy_project_image(image: Optional[ImageRequestModel]) -> None:
"""Copy a project's images.
def set_project_image(image: Optional[ImageRequestModel]) -> None:
"""Download and set a project's images.
Args:
image(Optional[ImageRequestModel]): The image to set.
"""
image_folder = project_context.project_image_path
image_folder.mkdir(exist_ok=True, parents=True)
image_object = image.download_image(owner_id=project_context.project.id)

project_image = project_context.project_image_pathname

# NOTE: Do nothing if the new path is the same as the old one
if project_image.resolve() != image_object.content_url:
# NOTE: Always delete the old image
delete_project_image()

if not image_object.is_remote:
project_image.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(image_object.content_url, project_context.project_image_pathname)

image_object.content_url = get_relative_path(project_image, base=project_context.path)

project_context.project.image = image_object


def delete_project_image() -> None:
"""Delete project image in a project."""
try:
image_object = image.to_image_object(
image_folder=project_context.project_image_path, owner_id=project_context.project.id
)
except errors.ImageError:
raise # TODO: Do we need to raise a project error and have its handler!?

# # NOTE: Delete images if they were removed
# if previous_image.content_url in new_urls or urllib.parse.urlparse(previous_image.content_url).netloc:
# continue
#
# path = prev.content_url
# if not os.path.isabs(path):
# path = os.path.normpath(os.path.join(project_context.path, path))
#
# os.remove(path)
os.remove(project_context.project_image_pathname)
except FileNotFoundError:
pass
except OSError as e:
raise errors.ImageError(f"Cannot delete project image '{project_context.project_image_pathname}': {e}") from e
else:
project_context.project.image = None
9 changes: 7 additions & 2 deletions renku/core/util/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,16 @@ def with_project_metadata(
custom_metadata=custom_metadata,
)

yield project

if not read_only:
# NOTE: Set project so that ``project_context`` can be used inside the code
project_gateway.update_project(project)

yield project

database_gateway.commit()
else:
yield project



@contextlib.contextmanager
Expand Down
6 changes: 6 additions & 0 deletions renku/core/util/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,9 @@ def check_url(url: str) -> Tuple[bool, bool]:
is_git = is_remote and (u.path.lower().endswith(".git") or scheme in ("git+https", "git+ssh") or starts_with_git)

return is_remote, is_git


def is_remote(uri: str) -> bool:
"""Returns True if a given URI is remote."""
is_remote, _ = check_url(uri)
return is_remote
15 changes: 11 additions & 4 deletions renku/domain_model/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,26 @@
# limitations under the License.
"""Image model."""

from pathlib import Path
from typing import Union
from urllib.parse import urlparse

from renku.core.util.urls import get_path
from renku.core.util.urls import get_path, is_remote
from renku.infrastructure.immutable import Slots


class ImageObject(Slots):
"""Represents a schema.org `ImageObject`."""
"""Represents a schema.org ``ImageObject``."""

__slots__ = ("content_url", "id", "position")

id: str
content_url: str
position: int

def __init__(self, *, content_url: str, id: str, position: int):
def __init__(self, *, content_url: Union[str, Path], id: str, position: int):
id = get_path(id)
super().__init__(content_url=content_url, position=position, id=id)
super().__init__(content_url=str(content_url), position=position, id=id)

@staticmethod
def generate_id(owner_id: str, position: int) -> str:
Expand All @@ -43,3 +45,8 @@ def generate_id(owner_id: str, position: int) -> str:
def is_absolute(self):
"""Whether content_url is an absolute or relative url."""
return bool(urlparse(self.content_url).netloc)

@property
def is_remote(self) -> bool:
"""Return True if the URI isn't on the local filesystem."""
return is_remote(self.content_url)
2 changes: 1 addition & 1 deletion renku/domain_model/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def generate_id(namespace: str, name: str):

def update_metadata(self, custom_metadata=None, custom_metadata_source=None, **kwargs):
"""Updates metadata."""
editable_attributes = ["creator", "description", "keywords", "image"]
editable_attributes = ["creator", "description", "keywords"]
for name, value in kwargs.items():
if name not in editable_attributes:
raise errors.ParameterError(f"Cannot edit field: '{name}'")
Expand Down
6 changes: 3 additions & 3 deletions renku/domain_model/project_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ def dataset_images_path(self) -> Path:
return self.path / RENKU_HOME / DATASET_IMAGES

@property
def project_image_path(self) -> Path:
"""Return the path that contains project's image."""
return self.path / IMAGES / "project"
def project_image_pathname(self) -> Path:
"""Return the path to the project's image file."""
return self.path / RENKU_HOME / IMAGES / "project" / "0.png"

@property
def dockerfile_path(self) -> Path:
Expand Down
3 changes: 2 additions & 1 deletion renku/ui/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ def init(
):
"""Initialize a project in PATH. Default is the current path."""
from renku.command.init import init_project_command
from renku.core.constant import FILESYSTEM_ROOT
from renku.core.image import ImageRequestModel
from renku.core.util.git import check_global_git_user_is_configured
from renku.ui.cli.utils.callback import ClickCallback
Expand All @@ -310,7 +311,7 @@ def init(
name=name,
description=description,
keywords=keyword,
image=ImageRequestModel(content_url=image),
image=ImageRequestModel(content_url=image, safe_image_paths=[FILESYSTEM_ROOT]),
template_id=template_id,
template_source=template_source,
template_ref=template_ref,
Expand Down
10 changes: 8 additions & 2 deletions renku/ui/cli/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ def project():
def edit(description, keywords, creators, image, metadata, unset, metadata_source):
"""Edit project metadata."""
from renku.command.project import edit_project_command
from renku.core.constant import FILESYSTEM_ROOT
from renku.core.image import ImageRequestModel

if list(creators) == [NO_VALUE]:
creators = NO_VALUE
Expand All @@ -109,7 +111,11 @@ def edit(description, keywords, creators, image, metadata, unset, metadata_sourc
if "i" in unset or "image" in unset:
if image is not NO_VALUE:
raise click.UsageError("Cant use '--image' together with unsetting image")
image = None
image_request = None
elif image is not NO_VALUE:
image_request = ImageRequestModel(content_url=image, safe_image_paths=[FILESYSTEM_ROOT])
else:
image_request = NO_VALUE

if metadata_source is not NO_VALUE and metadata is NO_VALUE:
raise click.UsageError("The '--metadata-source' option can only be used with the '--metadata' flag")
Expand All @@ -133,7 +139,7 @@ def edit(description, keywords, creators, image, metadata, unset, metadata_sourc
description=description,
creator=creators,
keywords=keywords,
image=image,
image=image_request,
custom_metadata=custom_metadata,
custom_metadata_source=metadata_source,
)
Expand Down

0 comments on commit 0d96985

Please sign in to comment.