Skip to content

Commit

Permalink
feat: support for copying packages from one channel to another (#680)
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfv authored Jan 31, 2024
1 parent fd3003a commit ce3aeb7
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 0 deletions.
89 changes: 89 additions & 0 deletions quetz/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,95 @@ def post_package(
dao.create_package(channel.name, new_package, user_id, authorization.OWNER)


@api_router.post(
"/channels/{channel_name}/packages/copy",
status_code=201,
tags=["packages"],
)
def copy_package(
source_channel: str,
source_package: str,
subdir: str,
filename: str,
background_tasks: BackgroundTasks,
channel: db_models.Channel = Depends(
ChannelChecker(allow_proxy=False, allow_mirror=False),
),
auth: authorization.Rules = Depends(get_rules),
dao: Dao = Depends(get_dao),
):
user_id = auth.assert_owner()

# get channel as object
source_channel_obj = dao.get_channel(source_channel)
if not source_channel_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Channel {source_channel} not found",
)
auth.assert_channel_read(source_channel_obj)

package_version = dao.get_package_version_by_filename(
source_channel, source_package, filename, subdir
)

if not package_version:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Package {source_channel}/{source_package} not found",
)

# this should always be true
if package_version.size:
# make sure that we are not over the size limit
dao.assert_size_limits(channel.name, package_version.size)

# assert that user can create a package in the target channel
auth.assert_create_package(channel.name)
if not dao.get_package(channel.name, package_version.package.name):
package_model = rest_models.Package(
name=package_version.package.name,
summary=package_version.package.summary,
description=package_version.package.description,
url=package_version.package.url,
)

dao.create_package(channel.name, package_model, user_id, authorization.OWNER)

try:
version = dao.create_version(
channel_name=channel.name,
package_name=package_version.package.name,
package_format=package_version.package_format,
platform=package_version.platform,
version=package_version.version,
build_number=package_version.build_number,
build_string=package_version.build_string,
size=package_version.size,
filename=package_version.filename,
info=package_version.info,
uploader_id=user_id,
upsert=False,
)
except IntegrityError:
logger.error(
f"duplicate package '{package_version.package.name}' "
f"in channel '{channel.name}'"
)
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Duplicate")

target_name = os.path.join(subdir, filename)
pkgstore.copy_file(source_channel, target_name, channel.name, target_name)
file_object = pkgstore.serve_path(channel.name, target_name)

condainfo = CondaInfo(file_object, filename, lazy=True)
pm.hook.post_add_package_version(version=version, condainfo=condainfo)

wrapped_bg_task = background_task_wrapper(indexing.update_indexes, logger)
# Background task to update indexes
background_tasks.add_task(wrapped_bg_task, dao, pkgstore, channel.name)


@api_router.get(
"/channels/{channel_name}/members",
response_model=List[rest_models.Member],
Expand Down
49 changes: 49 additions & 0 deletions quetz/pkgstores.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ def delete_file(self, channel: str, destination: str):
def move_file(self, channel: str, source: str, destination: str):
"""move file from source to destination in package store"""

@abc.abstractmethod
def copy_file(
self, source_channel: str, source: str, target_channel: str, destination: str
):
"""move file from source to destination in package store"""

@abc.abstractmethod
def file_exists(self, channel: str, destination: str):
"""Return True if the file exists"""
Expand Down Expand Up @@ -206,6 +212,15 @@ def move_file(self, channel: str, source: str, destination: str):
path.join(self.channels_dir, channel, destination),
)

def copy_file(
self, source_channel: str, source: str, target_channel: str, destination: str
):
with self._atomic_open(target_channel, destination) as f:
package = self.fs.open(
path.join(self.channels_dir, source_channel, source), "rb"
)
shutil.copyfileobj(package, f)

def file_exists(self, channel: str, destination: str):
return self.fs.exists(path.join(self.channels_dir, channel, destination))

Expand Down Expand Up @@ -405,6 +420,17 @@ def move_file(self, channel: str, source: str, destination: str):
path.join(channel_bucket, destination),
)

def copy_file(
self, source_channel: str, source: str, target_channel: str, destination: str
):
source_channel_bucket = self._bucket_map(source_channel)
target_channel_bucket = self._bucket_map(target_channel)
with self._get_fs() as fs:
fs.copy(
path.join(source_channel_bucket, source),
path.join(target_channel_bucket, destination),
)

def file_exists(self, channel: str, destination: str):
channel_bucket = self._bucket_map(channel)
with self._get_fs() as fs:
Expand Down Expand Up @@ -559,6 +585,17 @@ def move_file(self, channel: str, source: str, destination: str):
path.join(channel_container, destination),
)

def copy_file(
self, source_channel: str, source: str, target_channel: str, destination: str
):
source_channel_container = self._container_map(source_channel)
target_channel_container = self._container_map(target_channel)
with self._get_fs() as fs:
fs.copy(
path.join(source_channel_container, source),
path.join(target_channel_container, destination),
)

def file_exists(self, channel: str, destination: str):
channel_container = self._container_map(channel)
with self._get_fs() as fs:
Expand Down Expand Up @@ -728,6 +765,18 @@ def move_file(self, channel: str, source: str, destination: str):
path.join(channel_container, destination),
)

def copy_file(
self, source_channel: str, source: str, target_channel: str, destination: str
):
source_channel_container = self._bucket_map(source_channel)
target_channel_container = self._bucket_map(target_channel)

with self._get_fs() as fs:
fs.copy(
path.join(source_channel_container, source),
path.join(target_channel_container, destination),
)

def file_exists(self, channel: str, destination: str):
channel_container = self._bucket_map(channel)
with self._get_fs() as fs:
Expand Down
9 changes: 9 additions & 0 deletions quetz/tests/test_pkg_stores.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ def test_move_file(any_store, channel, channel_name):
assert_files(pkg_store, channel_name, ['test_2.txt'])


def test_copy_file(any_store, channel, channel_name):
pkg_store = any_store

pkg_store.add_file("content", channel_name, "test.txt")
pkg_store.copy_file(channel_name, "test.txt", channel_name, "test_2.txt")

assert_files(pkg_store, channel_name, ['test.txt', 'test_2.txt'])


@pytest.mark.parametrize("redirect_enabled", [False, True])
@pytest.mark.parametrize("redirect_endpoint", ["/files", "/static"])
def test_local_store_url(redirect_enabled, redirect_endpoint):
Expand Down

0 comments on commit ce3aeb7

Please sign in to comment.