Skip to content

Commit ce3aeb7

Browse files
authored
feat: support for copying packages from one channel to another (#680)
1 parent fd3003a commit ce3aeb7

File tree

3 files changed

+147
-0
lines changed

3 files changed

+147
-0
lines changed

quetz/main.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,95 @@ def post_package(
940940
dao.create_package(channel.name, new_package, user_id, authorization.OWNER)
941941

942942

943+
@api_router.post(
944+
"/channels/{channel_name}/packages/copy",
945+
status_code=201,
946+
tags=["packages"],
947+
)
948+
def copy_package(
949+
source_channel: str,
950+
source_package: str,
951+
subdir: str,
952+
filename: str,
953+
background_tasks: BackgroundTasks,
954+
channel: db_models.Channel = Depends(
955+
ChannelChecker(allow_proxy=False, allow_mirror=False),
956+
),
957+
auth: authorization.Rules = Depends(get_rules),
958+
dao: Dao = Depends(get_dao),
959+
):
960+
user_id = auth.assert_owner()
961+
962+
# get channel as object
963+
source_channel_obj = dao.get_channel(source_channel)
964+
if not source_channel_obj:
965+
raise HTTPException(
966+
status_code=status.HTTP_404_NOT_FOUND,
967+
detail=f"Channel {source_channel} not found",
968+
)
969+
auth.assert_channel_read(source_channel_obj)
970+
971+
package_version = dao.get_package_version_by_filename(
972+
source_channel, source_package, filename, subdir
973+
)
974+
975+
if not package_version:
976+
raise HTTPException(
977+
status_code=status.HTTP_404_NOT_FOUND,
978+
detail=f"Package {source_channel}/{source_package} not found",
979+
)
980+
981+
# this should always be true
982+
if package_version.size:
983+
# make sure that we are not over the size limit
984+
dao.assert_size_limits(channel.name, package_version.size)
985+
986+
# assert that user can create a package in the target channel
987+
auth.assert_create_package(channel.name)
988+
if not dao.get_package(channel.name, package_version.package.name):
989+
package_model = rest_models.Package(
990+
name=package_version.package.name,
991+
summary=package_version.package.summary,
992+
description=package_version.package.description,
993+
url=package_version.package.url,
994+
)
995+
996+
dao.create_package(channel.name, package_model, user_id, authorization.OWNER)
997+
998+
try:
999+
version = dao.create_version(
1000+
channel_name=channel.name,
1001+
package_name=package_version.package.name,
1002+
package_format=package_version.package_format,
1003+
platform=package_version.platform,
1004+
version=package_version.version,
1005+
build_number=package_version.build_number,
1006+
build_string=package_version.build_string,
1007+
size=package_version.size,
1008+
filename=package_version.filename,
1009+
info=package_version.info,
1010+
uploader_id=user_id,
1011+
upsert=False,
1012+
)
1013+
except IntegrityError:
1014+
logger.error(
1015+
f"duplicate package '{package_version.package.name}' "
1016+
f"in channel '{channel.name}'"
1017+
)
1018+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Duplicate")
1019+
1020+
target_name = os.path.join(subdir, filename)
1021+
pkgstore.copy_file(source_channel, target_name, channel.name, target_name)
1022+
file_object = pkgstore.serve_path(channel.name, target_name)
1023+
1024+
condainfo = CondaInfo(file_object, filename, lazy=True)
1025+
pm.hook.post_add_package_version(version=version, condainfo=condainfo)
1026+
1027+
wrapped_bg_task = background_task_wrapper(indexing.update_indexes, logger)
1028+
# Background task to update indexes
1029+
background_tasks.add_task(wrapped_bg_task, dao, pkgstore, channel.name)
1030+
1031+
9431032
@api_router.get(
9441033
"/channels/{channel_name}/members",
9451034
response_model=List[rest_models.Member],

quetz/pkgstores.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ def delete_file(self, channel: str, destination: str):
108108
def move_file(self, channel: str, source: str, destination: str):
109109
"""move file from source to destination in package store"""
110110

111+
@abc.abstractmethod
112+
def copy_file(
113+
self, source_channel: str, source: str, target_channel: str, destination: str
114+
):
115+
"""move file from source to destination in package store"""
116+
111117
@abc.abstractmethod
112118
def file_exists(self, channel: str, destination: str):
113119
"""Return True if the file exists"""
@@ -206,6 +212,15 @@ def move_file(self, channel: str, source: str, destination: str):
206212
path.join(self.channels_dir, channel, destination),
207213
)
208214

215+
def copy_file(
216+
self, source_channel: str, source: str, target_channel: str, destination: str
217+
):
218+
with self._atomic_open(target_channel, destination) as f:
219+
package = self.fs.open(
220+
path.join(self.channels_dir, source_channel, source), "rb"
221+
)
222+
shutil.copyfileobj(package, f)
223+
209224
def file_exists(self, channel: str, destination: str):
210225
return self.fs.exists(path.join(self.channels_dir, channel, destination))
211226

@@ -405,6 +420,17 @@ def move_file(self, channel: str, source: str, destination: str):
405420
path.join(channel_bucket, destination),
406421
)
407422

423+
def copy_file(
424+
self, source_channel: str, source: str, target_channel: str, destination: str
425+
):
426+
source_channel_bucket = self._bucket_map(source_channel)
427+
target_channel_bucket = self._bucket_map(target_channel)
428+
with self._get_fs() as fs:
429+
fs.copy(
430+
path.join(source_channel_bucket, source),
431+
path.join(target_channel_bucket, destination),
432+
)
433+
408434
def file_exists(self, channel: str, destination: str):
409435
channel_bucket = self._bucket_map(channel)
410436
with self._get_fs() as fs:
@@ -559,6 +585,17 @@ def move_file(self, channel: str, source: str, destination: str):
559585
path.join(channel_container, destination),
560586
)
561587

588+
def copy_file(
589+
self, source_channel: str, source: str, target_channel: str, destination: str
590+
):
591+
source_channel_container = self._container_map(source_channel)
592+
target_channel_container = self._container_map(target_channel)
593+
with self._get_fs() as fs:
594+
fs.copy(
595+
path.join(source_channel_container, source),
596+
path.join(target_channel_container, destination),
597+
)
598+
562599
def file_exists(self, channel: str, destination: str):
563600
channel_container = self._container_map(channel)
564601
with self._get_fs() as fs:
@@ -728,6 +765,18 @@ def move_file(self, channel: str, source: str, destination: str):
728765
path.join(channel_container, destination),
729766
)
730767

768+
def copy_file(
769+
self, source_channel: str, source: str, target_channel: str, destination: str
770+
):
771+
source_channel_container = self._bucket_map(source_channel)
772+
target_channel_container = self._bucket_map(target_channel)
773+
774+
with self._get_fs() as fs:
775+
fs.copy(
776+
path.join(source_channel_container, source),
777+
path.join(target_channel_container, destination),
778+
)
779+
731780
def file_exists(self, channel: str, destination: str):
732781
channel_container = self._bucket_map(channel)
733782
with self._get_fs() as fs:

quetz/tests/test_pkg_stores.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ def test_move_file(any_store, channel, channel_name):
254254
assert_files(pkg_store, channel_name, ['test_2.txt'])
255255

256256

257+
def test_copy_file(any_store, channel, channel_name):
258+
pkg_store = any_store
259+
260+
pkg_store.add_file("content", channel_name, "test.txt")
261+
pkg_store.copy_file(channel_name, "test.txt", channel_name, "test_2.txt")
262+
263+
assert_files(pkg_store, channel_name, ['test.txt', 'test_2.txt'])
264+
265+
257266
@pytest.mark.parametrize("redirect_enabled", [False, True])
258267
@pytest.mark.parametrize("redirect_endpoint", ["/files", "/static"])
259268
def test_local_store_url(redirect_enabled, redirect_endpoint):

0 commit comments

Comments
 (0)