Skip to content

optimize image copy on windows #217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 8 additions & 65 deletions pkg/imagepullers/noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,10 @@ import (
"fmt"
"io"
"os"
"path/filepath"

"github.com/containers/podman/v5/pkg/machine/define"

"github.com/containers/podman/v5/pkg/machine/env"

"github.com/lima-vm/go-qcow2reader"
"github.com/lima-vm/go-qcow2reader/convert"
"github.com/lima-vm/go-qcow2reader/image"
"github.com/lima-vm/go-qcow2reader/image/qcow2"
"github.com/lima-vm/go-qcow2reader/image/raw"
)

type NoopImagePuller struct {
Expand All @@ -36,21 +29,6 @@ func (puller *NoopImagePuller) SetSourceURI(sourcePath string) {
puller.sourceURI = sourcePath
}

func imageExtension(vmType define.VMType, sourceURI string) string {
switch vmType {
case define.WSLVirt:
ext := filepath.Ext(sourceURI)
if ext == ".wsl" {
return ".wsl"
}
return ".tar.gz"
case define.QemuVirt, define.HyperVVirt:
return filepath.Ext(sourceURI)
default:
return "." + vmType.ImageFormat().Kind()
}
}

func (puller *NoopImagePuller) LocalPath() (*define.VMFile, error) {
// if localPath has already been calculated returns it
if puller.localPath != nil {
Expand Down Expand Up @@ -81,61 +59,26 @@ func (puller *NoopImagePuller) Download() error {
if err != nil {
return err
}
return doCopyFile(puller.sourceURI, localPath.Path, puller.vmType)
}

func doCopyFile(src, dest string, vmType define.VMType) error {
srcF, err := os.Open(src)
src, err := os.Open(puller.sourceURI)
if err != nil {
return err
}
defer srcF.Close()

destF, err := os.Create(dest)
if err != nil {
return err
}
defer destF.Close()
defer src.Close()

switch vmType {
case define.AppleHvVirt, define.LibKrun:
return copyFileMac(srcF, destF)
default:
return copyFile(srcF, destF)
}
return doCopyFile(src, localPath.Path)
}

func copyFileMac(src, dest *os.File) error {
srcImg, err := qcow2reader.Open(src)
func copyFile(src *os.File, dest string) error {
destF, err := os.Create(dest)
if err != nil {
return err
}
defer srcImg.Close()

switch srcImg.Type() {
case raw.Type:
// if the image is raw it performs a simple copy
return copyFile(src, dest)
case qcow2.Type:
// if the image is qcow2 it performs a conversion to raw
return convertToRaw(srcImg, dest)
default:
return fmt.Errorf("%s format not supported for conversion to raw", srcImg.Type())
}
}

func convertToRaw(srcImg image.Image, dest *os.File) error {
if err := srcImg.Readable(); err != nil {
return fmt.Errorf("source image is not readable: %w", err)
}

return convert.Convert(dest, srcImg, convert.Options{})
}
defer destF.Close()

func copyFile(src, dst *os.File) error {
bufferedWriter := bufio.NewWriter(dst)
bufferedWriter := bufio.NewWriter(destF)
defer bufferedWriter.Flush()

_, err := io.Copy(bufferedWriter, src)
_, err = io.Copy(bufferedWriter, src)
return err
}
51 changes: 51 additions & 0 deletions pkg/imagepullers/noop_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//go:build darwin

package imagepullers

import (
"fmt"
"os"

"github.com/containers/podman/v5/pkg/machine/define"

"github.com/lima-vm/go-qcow2reader"
"github.com/lima-vm/go-qcow2reader/convert"
"github.com/lima-vm/go-qcow2reader/image"
"github.com/lima-vm/go-qcow2reader/image/qcow2"
"github.com/lima-vm/go-qcow2reader/image/raw"
)

func imageExtension(vmType define.VMType, _ string) string {
return "." + vmType.ImageFormat().Kind()
}

func doCopyFile(src *os.File, dest string) error {
srcImg, err := qcow2reader.Open(src)
if err != nil {
return err
}
defer srcImg.Close()

switch srcImg.Type() {
case raw.Type:
return copyFile(src, dest)
case qcow2.Type:
return convertToRaw(srcImg, dest)
default:
return fmt.Errorf("%s format not supported for conversion to raw", srcImg.Type())
}
}

func convertToRaw(srcImg image.Image, dest string) error {
destF, err := os.Create(dest)
if err != nil {
return err
}
defer destF.Close()

if err := srcImg.Readable(); err != nil {
return fmt.Errorf("source image is not readable: %w", err)
}

return convert.Convert(destF, srcImg, convert.Options{})
}
18 changes: 18 additions & 0 deletions pkg/imagepullers/noop_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build !windows && !darwin

package imagepullers

import (
"os"
"path/filepath"

"github.com/containers/podman/v5/pkg/machine/define"
)

func imageExtension(_ define.VMType, sourceURI string) string {
return filepath.Ext(sourceURI)
}

func doCopyFile(src *os.File, dest string) error {
return copyFile(src, dest)
}
81 changes: 81 additions & 0 deletions pkg/imagepullers/noop_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//go:build windows

package imagepullers

import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"

"github.com/containers/podman/v5/pkg/machine/define"
"github.com/sirupsen/logrus"
)

func imageExtension(vmType define.VMType, sourceURI string) string {
switch vmType {
case define.WSLVirt:
ext := filepath.Ext(sourceURI)
if ext == ".wsl" {
return ".wsl"
}
return ".tar.gz"
default:
return filepath.Ext(sourceURI)
}
}

func doCopyFile(src *os.File, dest string) error {
binary, err := exec.LookPath("robocopy")
if err != nil {
logrus.Debugf("warning: failed to get robocopy binary: %v. Falling back to default file copy between %s and %s\n", err, src.Name(), dest)
return copyFile(src, dest)
}

srcDir, srcFile := filepath.Split(src.Name())
destDir := filepath.Dir(dest)

// Executes the robocopy command with options optimized for a fast, single-file copy.
// /J: Copies using unbuffered I/O (better for large files).
// /MT: Enables multi-threaded copying for improved performance.
// /R:0: Sets retries on failed copies to 0 to avoid long waits.
// /IS: Includes Same files, which forces an overwrite even if the destination
// file appears identical to the source.
cmd := exec.Command(binary, "/J", "/MT", "/R:0", "/IS", srcDir, destDir, srcFile)
if logrus.IsLevelEnabled(logrus.DebugLevel) {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
}

err = cmd.Run()
if err != nil {
// robocopy does not use classic exit codes like linux commands, so we need to check for specific errors
// Only exit code 8 is considered an error, all other exit codes are considered successful with exceptions
// see https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
exitCode := exitErr.ExitCode()
if exitCode >= 8 {
return fmt.Errorf("failed to run robocopy: %w", err)
}
} else {
return fmt.Errorf("failed to run robocopy: %w", err)
}
}

if err := os.Remove(dest); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to remove existing destination file: %w", err)
}

err = os.Rename(filepath.Join(destDir, srcFile), dest)
if err != nil {
return fmt.Errorf("failed to rename file: %w", err)
}

return nil
}
Loading