Get started¶
We will push a 256 MiB file to a registry running on this machine, pull it back to a second path, and check that the two copies are byte for byte the same. Everything runs locally, so none of it needs a registry account.
You need Go 1.26.5 or newer, Docker, and a POSIX shell — macOS or Linux. Every command below is meant to be run in order, from the same directory.
Start a registry¶
Make a directory to work in:
Write a configuration file for the registry:
cat > zot-config.json <<'EOF'
{
"storage": {"rootDirectory": "/var/lib/registry"},
"http": {"address": "0.0.0.0", "port": "5000"},
"log": {"level": "error"}
}
EOF
This is the configuration bigoci's own end-to-end tests run against: the smallest one that serves the registry API.
Start zot, the registry:
docker run -d --name bigoci-zot -p 5001:5000 \
-v "$PWD/zot-config.json:/etc/zot/config.json:ro" \
ghcr.io/project-zot/zot:v2.1.20
The first run downloads the image, then prints the container id, which is different every time. The registry serves port 5000 inside the container; we publish it on 5001 because macOS gives port 5000 to its AirPlay receiver.
Wait for it to answer. zot takes a second or two to start, so ask until it does:
/v2/ is the base endpoint of the registry API. An answer from it means the
registry is speaking the protocol, not merely listening. If the loop is still
running after a few seconds, stop it with Ctrl-C and read
docker logs bigoci-zot.
Make a file to move¶
macOS pads the count with leading spaces; the number is what matters.
256 MiB is small for bigoci, which is built for files of 5 GB and up. It is enough to split into several parts, and it copies in seconds.
Set up the Go module¶
go: creating new go.mod: module bigoci-tutorial
go: added github.com/imgoci/bigoci v0.1.1
go: added github.com/distribution/reference v0.6.0
go: added github.com/opencontainers/go-digest v1.0.0
go: added github.com/opencontainers/image-spec v1.1.1
go: added golang.org/x/sync v0.22.0
go: added oras.land/oras-go/v2 v2.6.2
The go: downloading lines that appear between these are omitted, and the
versions move as dependencies are updated.
Write the program¶
Create main.go next to model.bin, with this content:
package main
import (
"context"
"fmt"
"os"
"github.com/imgoci/bigoci"
)
// repo is the registry and the repository on it that we push to and pull from.
const repo = "localhost:5001/tutorial/model"
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func run() error {
ctx := context.Background()
// WithPlainHTTP talks http:// instead of https://. Local registries only.
client, err := bigoci.New(bigoci.WithPlainHTTP())
if err != nil {
return err
}
desc, err := client.Push(ctx, repo+":v1",
bigoci.FromFile("model.bin"),
bigoci.WithPartSize(64<<20), // 64 MiB parts, so 256 MiB is four of them
)
if err != nil {
return err
}
fmt.Println("pushed", desc.Digest)
// The digest names exactly what the push wrote, whatever the tag does later.
ref := bigoci.Reference(repo + "@" + desc.Digest.String())
if err := client.Pull(ctx, ref, bigoci.ToFile("model-pulled.bin")); err != nil {
return err
}
fmt.Println("pulled model-pulled.bin")
return nil
}
One client serves both directions and holds no state from either, so a program that moves many files builds it once.
Push and pull¶
pushed sha256:1a518141bdc272fea807a141400dda47865ddbaa6b447f672970551e26eaadef
pulled model-pulled.bin
Your digest differs from this one. It describes the bytes of the file, and
/dev/urandom gave you different bytes.
The push split model.bin into four 64 MiB parts, uploaded them all in
parallel, and wrote a manifest listing them in order. The pull
read that manifest, fetched the parts in parallel, checked each one against the
digest the manifest gives it, and renamed the finished file into place only once
every part passed. Design covers why it works that
way.
Check the two copies match¶
cmp prints nothing and exits 0 when two files are byte for byte the same.
The registry now holds the artifact under the tag we pushed it to:
Clean up¶
Where to go next¶
- Push and pull a file — part size, worker count, resuming an interrupted pull, and the errors worth branching on.
- API reference — every exported name, including
WithProgress, which reports how far a transfer has got. - Authenticate to a registry — for a registry that asks for a credential.
- Format — the manifest and the parts we just wrote.