Skip to content

feat(storage): add copy command - #887

Draft
natalie-o-perret wants to merge 5 commits into
masterfrom
feat/storage-copy
Draft

feat(storage): add copy command#887
natalie-o-perret wants to merge 5 commits into
masterfrom
feat/storage-copy

Conversation

@natalie-o-perret

@natalie-o-perret natalie-o-perret commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Description

Depends on #885.

Adds exo storage copy (cp) for server-side copies of objects and recursive prefixes within or across buckets. exo storage move now reuses the copy path before deleting sources and gains the mv alias; unsafe identical or overlapping same-bucket paths are rejected. Closes #883.

Checklist

(For exoscale contributors)

  • Changelog updated (under Unreleased block, and add the Pull Request #number for each bit you add to the CHANGELOG.md)
  • Testing

Testing

Local validation:

  • go fmt ./... && git diff --check: passed with no formatting diff
  • make lint: passed
  • golangci-lint run --timeout 4m: passed with 0 issues
  • go vet ./...: passed
  • make build: passed
  • make test-verbose: passed with the race detector enabled
  • (cd tests/e2e && go test -v): passed all no-API scenarios
  • govulncheck ./...: reported no vulnerabilities in called code

Live SOS smoke test using two temporary private buckets in ch-gva-2 and a 42-byte fixture. The account selector is omitted and bucket names are represented by $SRC and $DST.

Live command transcript

Version

$ ./bin/exo version
exo dev 1eee4bab (egoscale v3.1.42)

Single-object copy

$ ./bin/exo storage copy -v sos://$SRC/source/original.txt sos://$SRC/copies/copied.txt
copying: sos://$SRC/source/original.txt -> sos://$SRC/copies/copied.txt
copied: source/original.txt -> https://sos-ch-gva-2.exo.io/$SRC/copies/copied.txt (42 bytes, <timestamp>)

$ ./bin/exo storage ls -r sos://$SRC/
<timestamp>   42 B  copies/copied.txt
<timestamp>   42 B  source/original.txt

The source remained after the copy.

cp alias across buckets

$ ./bin/exo storage cp -v sos://$SRC/source/original.txt sos://$DST/alias/copied-by-cp.txt
copying: sos://$SRC/source/original.txt -> sos://$DST/alias/copied-by-cp.txt
copied: source/original.txt -> https://sos-ch-gva-2.exo.io/$DST/alias/copied-by-cp.txt (42 bytes, <timestamp>)

$ ./bin/exo storage ls -r sos://$DST/
<timestamp>   42 B  alias/copied-by-cp.txt

Recursive prefix copy across buckets

$ ./bin/exo storage copy -f -r -v sos://$SRC/source/ sos://$DST/prefix-copy/
copying: sos://$SRC/source/nested/second.txt -> sos://$DST/prefix-copy/nested/second.txt
copied: sos://$SRC/source/nested/second.txt -> sos://$DST/prefix-copy/nested/second.txt
copying: sos://$SRC/source/original.txt -> sos://$DST/prefix-copy/original.txt
copied: sos://$SRC/source/original.txt -> sos://$DST/prefix-copy/original.txt
copied 2 objects

$ ./bin/exo storage ls -r sos://$SRC/source/
<timestamp>   42 B  source/nested/second.txt
<timestamp>   42 B  source/original.txt

$ ./bin/exo storage ls -r sos://$DST/prefix-copy/
<timestamp>   42 B  prefix-copy/nested/second.txt
<timestamp>   42 B  prefix-copy/original.txt

Both source objects remained after the recursive copy.

Content verification

$ sha256sum fixture.txt downloaded-copy.txt
aa46f08b3e9557beec1e8ed16c65173c792c6f86bf917681a1231829bf0d5b8b  fixture.txt
aa46f08b3e9557beec1e8ed16c65173c792c6f86bf917681a1231829bf0d5b8b  downloaded-copy.txt

$ cmp fixture.txt downloaded-copy.txt

cmp exited successfully without output.

Dry-run and overlap guard

$ ./bin/exo storage copy -n sos://$SRC/source/original.txt sos://$SRC/dry-run/not-created.txt
[DRY-RUN]
copy sos://$SRC/source/original.txt -> sos://$SRC/dry-run/not-created.txt

$ ./bin/exo storage ls -r sos://$SRC/dry-run/

$ ./bin/exo storage copy -n -r sos://$SRC/source/ sos://$SRC/source/backup/
error: source and destination prefixes must not overlap

The dry-run destination was not created.

Existing move command

$ ./bin/exo storage move -v sos://$SRC/copies/copied.txt sos://$SRC/moves/moved-by-move.txt
copying: sos://$SRC/copies/copied.txt -> sos://$SRC/moves/moved-by-move.txt
deleting: sos://$SRC/copies/copied.txt
moved: copies/copied.txt -> https://sos-ch-gva-2.exo.io/$SRC/moves/moved-by-move.txt (42 bytes, <timestamp>)

$ ./bin/exo storage ls -r sos://$SRC/
<timestamp>   42 B  moves/moved-by-move.txt
<timestamp>   42 B  source/nested/second.txt
<timestamp>   42 B  source/original.txt

The old copies/copied.txt key was removed and the source fixtures were unchanged.

mv alias across buckets

$ ./bin/exo storage mv -v sos://$DST/alias/copied-by-cp.txt sos://$SRC/moves/moved-by-mv.txt
copying: sos://$DST/alias/copied-by-cp.txt -> sos://$SRC/moves/moved-by-mv.txt
deleting: sos://$DST/alias/copied-by-cp.txt
moved: alias/copied-by-cp.txt -> https://sos-ch-gva-2.exo.io/$SRC/moves/moved-by-mv.txt (42 bytes, <timestamp>)

$ ./bin/exo storage ls -r sos://$SRC/moves/
<timestamp>   42 B  moves/moved-by-move.txt
<timestamp>   42 B  moves/moved-by-mv.txt

The old $DST/alias/copied-by-cp.txt key was removed.

Recursive move regression

$ ./bin/exo storage move -f -r -v sos://$DST/prefix-copy/ sos://$DST/prefix-moved/
copying: sos://$DST/prefix-copy/nested/second.txt -> sos://$DST/prefix-moved/nested/second.txt
deleting: sos://$DST/prefix-copy/nested/second.txt
moved: sos://$DST/prefix-copy/nested/second.txt -> sos://$DST/prefix-moved/nested/second.txt
copying: sos://$DST/prefix-copy/original.txt -> sos://$DST/prefix-moved/original.txt
deleting: sos://$DST/prefix-copy/original.txt
moved: sos://$DST/prefix-copy/original.txt -> sos://$DST/prefix-moved/original.txt
moved 2 objects

$ ./bin/exo storage ls -r sos://$DST/prefix-copy/

$ ./bin/exo storage ls -r sos://$DST/prefix-moved/
<timestamp>   42 B  prefix-moved/nested/second.txt
<timestamp>   42 B  prefix-moved/original.txt

Cleanup

$ ./bin/exo storage rb -f -r sos://$SRC
Bucket sos://$SRC deleted successfully

$ ./bin/exo storage rb -f -r sos://$DST
Bucket sos://$DST deleted successfully

$ ./bin/exo storage show sos://$SRC
error: unable to initialize storage client: option error: bucket not found

$ ./bin/exo storage show sos://$DST
error: unable to initialize storage client: option error: bucket not found

The storage API CI job passes. The unrelated compute API job fails during NLB setup because the shared test account has exceeded its network load balancer quota.


Note

AI assistance: PR description, test scaffolding.

@natalie-o-perret
natalie-o-perret changed the base branch from master to build/update-golang-x-deps August 4, 2026 16:57
@natalie-o-perret
natalie-o-perret marked this pull request as ready for review August 4, 2026 17:13
@natalie-o-perret
natalie-o-perret changed the base branch from build/update-golang-x-deps to fix/unlisted-template-ids August 4, 2026 17:24
@natalie-o-perret
natalie-o-perret marked this pull request as draft August 4, 2026 17:30
@natalie-o-perret
natalie-o-perret changed the base branch from fix/unlisted-template-ids to build/update-golang-x-deps August 4, 2026 19:46
@natalie-o-perret

Copy link
Copy Markdown
Contributor Author

[SC-192277]

@natalie-o-perret
natalie-o-perret force-pushed the build/update-golang-x-deps branch from 338b079 to 36370ab Compare August 6, 2026 08:29
Base automatically changed from build/update-golang-x-deps to master August 6, 2026 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: Make it possible to copy objects

1 participant