feat: implement copy#240
Conversation
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
|
Thanks @jiwangCHEN I can give a review when I am back at keyboard. To start, please follow existing conventions in the project. For example, a type like descriptor should go into types.py. Ensure you integrate your addition maximally with existing structure. Thanks! |
|
And another question. Copy was added to oras-go as a replacement for conventional push and pull - each is just a directional copy. If we add it here those functions need to be adapted to use it so we are not doing the same thing two different ways. |
The Descriptor type is now defined alongside container_type in oras/types.py, the project's established home for cross-cutting type aliases. oras/copy/descriptor.py re-exports it for backward compatibility, so existing imports continue to work. Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
|
Thanks. I have address the first comment:
|
|
Thanks. There is still the issue that copy (in the Go library) was a replacement for push/pull - the push pull interfaces would use it under the hood. With your changes here, we have both. We need have one or the other. |
|
Understood. Layout.push_to_registry() and Layout.copy_to_registry() both push layout content to a registry — I'm working on a replacement for those. For Layout.pull_from_registry(), since it's a manual blob-download with no copy engine equivalent, it requires implementing Target (not just ReadOnlyTarget); I'll handle that as well. |
|
More specifically, the provider.py interfaces need to use copy under the hood. https://github.com/oras-project/oras-py/blob/main/oras/provider.py |
|
Hi @vsoch, The provider and examples have been updated to support the copy engine. |
|
@jiwangCHEN push and pull interfaces should not go away. A push or pull is a directional copy. There should be no breaking changes in changing the underlying structure to use copy. |
|
Thanks. Let me know if this change plan looks solid to you: Rewrite
|
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
refactor: copy dependency refactor
|
Please report the model name and version you used to generate this code, along with the prompts and how you, the person, have reviewed it. Please describe to me, in your own words (not using the LLM) the changes and the tests you have implemented. |
I was using Claude Opus 4.8, following a prompt workflow aligned with the development cycle: planning, implementation, PEP 8 style check, and test case validation. In the initial implementation, I focused on a specific prompt and refined the output through multiple iterations Copy OverviewConcurrencyDuring reconsolidating the classes, I used following prompts. Would have Copilot to review the work before proceeding to the next iteration including address the comments above. |
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Signed-off-by: jiwangCHEN <hyper1char@gmail.com>
Summary
Implements a complete OCI copy engine for oras-py (a Python port of oras-go's
copy engine) and rewires
Registry.push/Registry.pullto run on top of it.Copies a content DAG between any two targets — registries, OCI layouts, or
in-memory stores — with concurrent traversal, per-node deduplication,
existence checks, optional cross-repo blob mounting, foreign-layer filtering,
and digest/size verification.
Design
Traversal: post-order concurrent DAG walk.
A node's children must be fully committed to the destination before the node itself is pushed — a registry will reject a manifest whose layers don't yet exist, so this is a correctness invariant, not an ordering preference. To keep this deadlock-free at any concurrency level (including 1), a worker must release its semaphore slot before recursing into its children (region.end()) and reacquire it before copying itself (region.start()).
Deduplication: a first-class primitive.
Every node passes through a StatusTracker before any other work. When N workers reach the same descriptor (e.g., a blob shared by multiple manifests in an index), exactly one becomes the doer and the other N−1 become waiters; without this, shared blobs would be fetched and pushed N times concurrently and race on dst.push().
Caching: decouple resolution from traversal.
Wrap the source in a CacheProxy that absorbs manifest and index bytes on first fetch (during root resolution and successor discovery), so that when the traversal later copies the root it reads from memory instead of re-hitting the network. Provide a stop_caching flag: while map_root runs, it may fetch different content (e.g., platform selection), and that content must not pollute the cache.
Extensibility: hooks, not subclassing.
Expose pre_copy, post_copy, on_copy_skipped, and on_mounted as injected callables. The tagging mechanism — guaranteeing dst_ref ends up pointing at the root — must be implemented entirely through these hooks, with no special-case branch in the traversal. A SkipNode exception raised from pre_copy must short-circuit the node's copy; this is the mechanism by which a ReferencePusher atomically pushes and tags the root in a single PUT.
Limitation identified
Public API
Registry.copy(src, dst, opts=...)Registry.as_target(container)oras.copy.copyLayout.copy_to_registry/copy_from_registryLayout.as_target()oras.copy.copyTesting
oras/tests/test_copy.py— descriptor utils, memory storage, cache proxy,status tracker, graph traversal, dedup, mounting, error origins, digest/size
verification, path-traversal rejection, and
LimitedRegiondeadlockprevention (incl.
concurrency=1).oras/tests/test_provider.py— mock-basedcopy/push/pullplus alive-registry registry-to-registry round-trip.