diff --git a/cmd/oci-image-tool/cas-get.go b/cmd/oci-image-tool/cas-get.go new file mode 100644 index 0000000..42cc0b7 --- /dev/null +++ b/cmd/oci-image-tool/cas-get.go @@ -0,0 +1,80 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-tools/image/cas/layout" + "github.com/urfave/cli" + "golang.org/x/net/context" +) + +type casGetCmd struct { + path string + digest digest.Digest +} + +var casGetCommand = cli.Command{ + Name: "get", + Usage: "Retrieve a blob from the store and write it to stdout.", + ArgsUsage: "PATH DIGEST", + Action: casGetHandle, +} + +func casGetHandle(ctx *cli.Context) (err error) { + if ctx.NArg() != 2 { + return fmt.Errorf("both PATH and DIGEST must be provided") + } + + state := &casGetCmd{} + state.path = ctx.Args().Get(0) + state.digest, err = digest.Parse(ctx.Args().Get(1)) + if err != nil { + return err + } + + engineContext := context.Background() + + engine, err := layout.NewEngine(engineContext, state.path) + if err != nil { + return err + } + defer engine.Close() + + reader, err := engine.Get(engineContext, state.digest) + if err != nil { + return err + } + defer reader.Close() + + bytes, err := ioutil.ReadAll(reader) + if err != nil { + return err + } + + n, err := os.Stdout.Write(bytes) + if err != nil { + return err + } + if n < len(bytes) { + return fmt.Errorf("wrote %d of %d bytes", n, len(bytes)) + } + + return nil +} diff --git a/cmd/oci-image-tool/cas.go b/cmd/oci-image-tool/cas.go new file mode 100644 index 0000000..ac07bb5 --- /dev/null +++ b/cmd/oci-image-tool/cas.go @@ -0,0 +1,32 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/urfave/cli" +) + +func casHandle(context *cli.Context) error { + return cli.ShowCommandHelp(context, context.Command.Name) +} + +var casCommand = cli.Command{ + Name: "cas", + Usage: "Content-addressable storage manipulation", + Action: casHandle, + Subcommands: []cli.Command{ + casGetCommand, + }, +} diff --git a/cmd/oci-image-tool/main.go b/cmd/oci-image-tool/main.go index e91d59a..b4cdb48 100644 --- a/cmd/oci-image-tool/main.go +++ b/cmd/oci-image-tool/main.go @@ -52,6 +52,7 @@ func main() { validateCommand, unpackCommand, createCommand, + casCommand, } if err := app.Run(os.Args); err != nil { diff --git a/image/cas/interface.go b/image/cas/interface.go new file mode 100644 index 0000000..6a91eb6 --- /dev/null +++ b/image/cas/interface.go @@ -0,0 +1,34 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package cas implements generic content-addressable storage. +package cas + +import ( + "io" + + "github.com/opencontainers/go-digest" + "golang.org/x/net/context" +) + +// Engine represents a content-addressable storage engine. +type Engine interface { + // Get returns a reader for retrieving a blob from the store. + // Returns os.ErrNotExist if the digest is not found. + Get(ctx context.Context, digest digest.Digest) (reader io.ReadCloser, err error) + + // Close releases resources held by the engine. Subsequent engine + // method calls will fail. + Close() (err error) +} diff --git a/image/cas/layout/interface.go b/image/cas/layout/interface.go new file mode 100644 index 0000000..dda72d6 --- /dev/null +++ b/image/cas/layout/interface.go @@ -0,0 +1,25 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "io" +) + +// ReadWriteSeekCloser wraps the Read, Write, Seek, and Close methods. +type ReadWriteSeekCloser interface { + io.ReadWriteSeeker + io.Closer +} diff --git a/image/cas/layout/main.go b/image/cas/layout/main.go new file mode 100644 index 0000000..eaf578a --- /dev/null +++ b/image/cas/layout/main.go @@ -0,0 +1,51 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package layout implements the cas interface using the image-spec's +// image-layout [1]. +// +// [1]: https://github.com/opencontainers/image-spec/blob/master/image-layout.md +package layout + +import ( + "os" + "strings" + + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-tools/image/cas" + "golang.org/x/net/context" +) + +// NewEngine instantiates an engine with the appropriate backend (tar, +// HTTP, ...). +func NewEngine(ctx context.Context, path string) (engine cas.Engine, err error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + + return NewTarEngine(ctx, file) +} + +// blobPath returns the PATH to the DIGEST blob. SEPARATOR selects +// the path separator used between components. +func blobPath(digest digest.Digest, separator string) (path string, err error) { + err = digest.Validate() + if err != nil { + return "", err + } + algorithm := digest.Algorithm().String() + components := []string{".", "blobs", algorithm, digest.Hex()} + return strings.Join(components, separator), nil +} diff --git a/image/cas/layout/tar.go b/image/cas/layout/tar.go new file mode 100644 index 0000000..1bf353e --- /dev/null +++ b/image/cas/layout/tar.go @@ -0,0 +1,78 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "archive/tar" + "io" + "io/ioutil" + "os" + + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-tools/image/cas" + "golang.org/x/net/context" +) + +// TarEngine is a cas.Engine backed by a tar file. +type TarEngine struct { + file ReadWriteSeekCloser +} + +// NewTarEngine returns a new TarEngine. +func NewTarEngine(ctx context.Context, file ReadWriteSeekCloser) (engine cas.Engine, err error) { + engine = &TarEngine{ + file: file, + } + + return engine, nil +} + +// Get returns a reader for retrieving a blob from the store. +func (engine *TarEngine) Get(ctx context.Context, digest digest.Digest) (reader io.ReadCloser, err error) { + targetName, err := blobPath(digest, "/") + if err != nil { + return nil, err + } + + _, err = engine.file.Seek(0, io.SeekStart) + if err != nil { + return nil, err + } + + tarReader := tar.NewReader(engine.file) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + header, err := tarReader.Next() + if err == io.EOF { + return nil, os.ErrNotExist + } else if err != nil { + return nil, err + } + + if header.Name == targetName { + return ioutil.NopCloser(tarReader), nil + } + } +} + +// Close releases resources held by the engine. +func (engine *TarEngine) Close() (err error) { + return engine.file.Close() +}