Download object store directory keys such as zarr stores - #124
Merged
Merged
Conversation
A zarr store on the object store is a set of objects sharing a key prefix, not a single object, so 'isd_s3_cli go -k <prefix>' fails with a 404 on HeadObject. object_copy_local() now hands such a key to the new object_copy_local_directory(), which lists the prefix and downloads every member into its place under the target directory. The single object download is factored out into object_get_local(), which takes the expected size as an argument so the member sizes returned by object_glob() can be reused; rechecking each member key on its own would misread chunk names such as 'time/1' as directories, since listing is a prefix match that also returns 'time/10'. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved download-path issues can cause failures or leave the legacy API without the fix.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds support for downloading object-store prefixes, such as Zarr stores, as local directory trees.
Changes:
- Downloads nested prefix members recursively.
- Reuses listed object sizes for validation.
- Updates the package version to 3.0.16.
File summaries
| File | Summary |
|---|---|
src/rda_python_common/pg_file.py |
Adds prefix downloads, but legacy API handling, directory markers, and exact-key classification still require fixes. |
src/rda_python_common/__init__.py |
Updates the package version. |
README.md |
Updates the documented version. |
pyproject.toml |
Updates the project version. |
Review details
Suppressed comments (5)
src/rda_python_common/pg_file.py:870
- The relative key suffix is concatenated into a local path without validation. A valid object key such as
prefix/../../outsideproducestodir/../../outside, so the download escapestodirand can overwrite files outside the requested destination. Reject absolute paths and..components, or verify the normalized destination remains undertodir, before downloading.
tofile = "{}/{}".format(todir, key[plen:])
if not self.object_get_local(tofile, key, flist[key]['data_size'], bucket, logact): return self.FAILURE
src/rda_python_common/pg_file.py:870
- These keys come from
object_glob()and are passed intoobject_get_local(), which interpolates them into theisd_s3_cli go -k ...command. A store member containing shell metacharacters (for example;) can therefore turn a listed object name into command injection whenpgsystem()uses a shell. Pass argv arguments to the subprocess or shell-quote every command argument before downloading listed keys.
if not self.object_get_local(tofile, key, flist[key]['data_size'], bucket, logact): return self.FAILURE
src/rda_python_common/pg_file.py:825
- The downloaded file is
fromnamein the current target directory, but this call passes the remote object keyfromfiletoset_local_mode(). When the desired mode differs, it attempts to chmod a path such asprefix/nested/memberrelative to the target directory, so the downloaded member keeps the wrong permissions (and the ignored return value hides the failure). Use the localfromnamehere.
if info['data_size'] == fsize:
self.set_local_mode(fromfile, info['isfile'], 0, info['mode'], info['logname'], logact)
src/rda_python_common/pg_file.py:864
- An object-store directory marker such as
fromdir + '/'(or any nested key ending in/) also satisfies this filter. Its suffix is empty/trailing, so the next line buildstodir/andobject_get_local()uses an empty basename;check_local_file('')then fails and aborts the whole directory download. Exclude marker keys or handle them as directories.
keys = [key for key in flist if key.startswith(prefix)]
src/rda_python_common/pg_file.py:789
- This routes every
check_object_file()result withisfile == 0into the directory path, butcheck_object_file()also marks an exact object as non-file when prefix listing returns similarly named keys (for exampletime/1alongsidetime/10).object_copy_local_directory()then filters fortime/1/, finds no members, and reports the valid exact object as missing. Make the object-vs-prefix classification delimiter-aware or handle an exact-key match before treating the result as a directory.
if not finfo['isfile']: return self.object_copy_local_directory(tofile, fromfile, bucket, logact)
return self.object_get_local(tofile, fromfile, finfo['data_size'], bucket, logact)
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if not finfo: | ||
| if finfo != None: return ret | ||
| return self.lmsg(fromfile, "{}-{} to copy to {}".format(self.OHOST, self.PGLOG['MISSFILE'], tofile), logact) | ||
| if not finfo['isfile']: return self.object_copy_local_directory(tofile, fromfile, bucket, logact) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
object_copy_local()now recognises a key that is a prefix over many objects (a zarr store) and hands it to the newobject_copy_local_directory(), which lists the prefix and downloads every member into its place under the target directory.object_get_local(), taking the expected size as an argument so the member sizes returned byobject_glob()are reused.Why
dsquasar
-A 3failed ond694517withisd_s3_cli go -k .../....zarr -b gdex-datareturning a 404 on HeadObject: the key is a prefix, not an object. The upload side already handled this incheck_object_file(); the download side did not.Note
Rechecking each member key on its own would misread chunk names: listing is a prefix match, so
time/1also returnstime/10andtime/11, whichcheck_object_file()would classify as a directory. The sizes fromobject_glob()are used instead, which also halves the round trips.Test plan
py_compilecleanobject_glob/object_get_localrun: nested member paths preserved, sibling prefixx.zarr2/...excluded, trailing/on the prefix stripped-A 3on d694517 against Boreas