metropolis: use new OS image format for install

This switches the USB and SSH installation methods to the new OS image
format based on OCI artifacts.

When stored on disk, the new format consists of a directory containing
an OCI layout, instead of a single file. This means that all steps which
copy or upload an image now need to handle a tree of files.

Change-Id: I526d32f5c50bd74f513f785118768a56b2655fa0
Reviewed-on: https://review.monogon.dev/c/monogon/+/4090
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/osbase/net/sshtakeover/sshtakeover.go b/osbase/net/sshtakeover/sshtakeover.go
index fdb63f3..abf82ea 100644
--- a/osbase/net/sshtakeover/sshtakeover.go
+++ b/osbase/net/sshtakeover/sshtakeover.go
@@ -9,12 +9,16 @@
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"net"
+	"os"
 
 	"github.com/pkg/sftp"
 	"golang.org/x/crypto/ssh"
+
+	"source.monogon.dev/osbase/structfs"
 )
 
 type Client struct {
@@ -125,6 +129,37 @@
 	return nil
 }
 
+func (p *Client) UploadTree(ctx context.Context, targetPath string, tree structfs.Tree) error {
+	if err := p.sc.RemoveAll(targetPath); err != nil && !errors.Is(err, os.ErrNotExist) {
+		return fmt.Errorf("RemoveAll: %w", err)
+	}
+	if err := p.sc.Mkdir(targetPath); err != nil {
+		return err
+	}
+	for nodePath, node := range tree.Walk() {
+		fullPath := targetPath + "/" + nodePath
+		switch {
+		case node.Mode.IsDir():
+			if err := p.sc.Mkdir(fullPath); err != nil {
+				return fmt.Errorf("sftp mkdir %q: %w", fullPath, err)
+			}
+		case node.Mode.IsRegular():
+			reader, err := node.Content.Open()
+			if err != nil {
+				return fmt.Errorf("upload %q: %w", nodePath, err)
+			}
+			if err := p.Upload(ctx, fullPath, reader); err != nil {
+				reader.Close()
+				return fmt.Errorf("upload %q: %w", fullPath, err)
+			}
+			reader.Close()
+		default:
+			return fmt.Errorf("upload %q: unsupported file type %s", nodePath, node.Mode.Type().String())
+		}
+	}
+	return nil
+}
+
 // SetProgress sets a callback which will be called repeatedly during uploads
 // with a number of bytes that have been read.
 func (p *Client) SetProgress(callback func(int64)) {