added image pull feature to pull from dockerhub
This commit is contained in:
@@ -6,9 +6,37 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ——— bridge ———
|
||||
|
||||
const bridgeName = "mdbr0"
|
||||
const bridgeIP = "10.100.0.1"
|
||||
const bridgeSubnet = "10.100.0.0/24"
|
||||
|
||||
func ensureBridge() {
|
||||
if _, err := os.Stat("/sys/class/net/" + bridgeName); err == nil {
|
||||
return
|
||||
}
|
||||
runCmd("ip", "link", "add", bridgeName, "type", "bridge")
|
||||
runCmd("ip", "addr", "add", bridgeIP+"/24", "dev", bridgeName)
|
||||
runCmd("ip", "link", "set", bridgeName, "up")
|
||||
fmt.Printf("[bridge] %s created at %s\n", bridgeName, bridgeIP)
|
||||
}
|
||||
|
||||
func allocateIP() string {
|
||||
data, _ := os.ReadFile("/tmp/mdbr0-ip-last")
|
||||
last := 1
|
||||
if len(data) > 0 {
|
||||
last, _ = strconv.Atoi(string(data))
|
||||
}
|
||||
next := last + 1
|
||||
os.WriteFile("/tmp/mdbr0-ip-last", []byte(strconv.Itoa(next)), 0644)
|
||||
return fmt.Sprintf("10.100.0.%d", next)
|
||||
}
|
||||
|
||||
// 这个 demo 没有加 CLONE_NEWUSER(user namespace)。
|
||||
// 加了之后容器里的"root"会映射成宿主机上一个无权限的普通用户,
|
||||
// mount/mknod 这类系统调用会被内核直接拒绝——从根上堵死设备逃逸这条路。
|
||||
@@ -34,11 +62,43 @@ func main() {
|
||||
func run() {
|
||||
args := os.Args[2:]
|
||||
if len(args) == 0 {
|
||||
args = []string{"/bin/sh"}
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s run <image>[:tag] [command...]\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解析 "alpine" 或 "python:3.11"
|
||||
image := args[0]
|
||||
tag := "latest"
|
||||
if idx := strings.Index(image, ":"); idx != -1 {
|
||||
tag = image[idx+1:]
|
||||
image = image[:idx]
|
||||
}
|
||||
cmdArgs := args[1:]
|
||||
|
||||
// --no-entry: 跳过镜像的 entrypoint,直接跑用户命令
|
||||
skipEntrypoint := false
|
||||
if len(cmdArgs) > 0 && cmdArgs[0] == "--no-entry" {
|
||||
skipEntrypoint = true
|
||||
cmdArgs = cmdArgs[1:]
|
||||
}
|
||||
|
||||
result, err := pullImage(image, tag)
|
||||
must(err)
|
||||
|
||||
// 用户没指定命令,用镜像默认的
|
||||
if len(cmdArgs) == 0 {
|
||||
cmdArgs = result.Cmd
|
||||
if len(cmdArgs) == 0 {
|
||||
cmdArgs = []string{"/bin/sh"}
|
||||
}
|
||||
}
|
||||
// 镜像有 entrypoint 且不跳过,就排在命令前面
|
||||
if !skipEntrypoint {
|
||||
cmdArgs = append(result.Entrypoint, cmdArgs...)
|
||||
}
|
||||
|
||||
hostPID := os.Getpid()
|
||||
fmt.Printf("[run] Running %v as PID %d\n", args, hostPID)
|
||||
fmt.Printf("[run] %s:%s → %v as PID %d\n", image, tag, cmdArgs, hostPID)
|
||||
|
||||
overlayBase := fmt.Sprintf("/tmp/maingo-%d", hostPID)
|
||||
upperDir := overlayBase + "/upper"
|
||||
@@ -48,50 +108,52 @@ func run() {
|
||||
vethCtr := fmt.Sprintf("veth1-%d", hostPID)
|
||||
cgroupName := fmt.Sprintf("maingo-%d", hostPID)
|
||||
|
||||
ipSuffix := hostPID % 253
|
||||
hostIP := fmt.Sprintf("10.0.%d.1", ipSuffix)
|
||||
ctrIP := fmt.Sprintf("10.0.%d.2", ipSuffix)
|
||||
subnet := fmt.Sprintf("10.0.%d.0/24", ipSuffix)
|
||||
ctrIP := allocateIP()
|
||||
|
||||
must(os.MkdirAll(upperDir, 0755))
|
||||
must(os.MkdirAll(workDir, 0755))
|
||||
must(os.MkdirAll(mergedDir, 0755))
|
||||
|
||||
// 用 defer 而不是"cmd.Wait() 之后顺序执行":
|
||||
// must(cmd.Wait()) 在子进程非 0 退出时会 panic,
|
||||
// 之前的写法一旦这里 panic,后面的清理代码一行都不会跑,
|
||||
// 会在宿主机上留下孤儿 veth / iptables 规则 / cgroup / 临时目录。
|
||||
// defer 保证不管中间从哪里 panic,清理都会按 LIFO 顺序跑完。
|
||||
defer safeRemoveAll(overlayBase)
|
||||
defer cleanupCgroup(cgroupName)
|
||||
defer cleanupVeth(vethHost)
|
||||
defer cleanupNAT(subnet)
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
must(err)
|
||||
|
||||
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, args...)...)
|
||||
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, cmdArgs...)...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.ExtraFiles = []*os.File{r}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWUTS |
|
||||
Cloneflags: syscall.CLONE_NEWUSER |
|
||||
syscall.CLONE_NEWUTS |
|
||||
syscall.CLONE_NEWPID |
|
||||
syscall.CLONE_NEWNS |
|
||||
syscall.CLONE_NEWNET,
|
||||
}
|
||||
cmd.Env = append(os.Environ(),
|
||||
cmd.SysProcAttr.UidMappings = []syscall.SysProcIDMap{
|
||||
{ContainerID: 0, HostID: 0, Size: 65536},
|
||||
}
|
||||
cmd.SysProcAttr.GidMappings = []syscall.SysProcIDMap{
|
||||
{ContainerID: 0, HostID: 0, Size: 65536},
|
||||
}
|
||||
// 先加镜像的环境变量,再拼我们自己的——镜像的 Env 可能覆盖宿主机的
|
||||
cmd.Env = append(os.Environ(), result.Env...)
|
||||
cmd.Env = append(cmd.Env,
|
||||
"MAINGO_MERGED="+mergedDir,
|
||||
"MAINGO_UPPER="+upperDir,
|
||||
"MAINGO_WORK="+workDir,
|
||||
"MAINGO_LOWER="+result.LowerDirs,
|
||||
"MAINGO_CGROUP="+cgroupName)
|
||||
|
||||
must(cmd.Start())
|
||||
r.Close()
|
||||
|
||||
setupVeth(cmd.Process.Pid, vethHost, vethCtr, hostIP, ctrIP, subnet)
|
||||
setupNAT(cmd.Process.Pid, ctrIP, subnet)
|
||||
ensureBridge()
|
||||
setupVeth(cmd.Process.Pid, vethHost, vethCtr, ctrIP)
|
||||
setupNAT()
|
||||
|
||||
w.Write([]byte{1})
|
||||
w.Close()
|
||||
@@ -120,7 +182,7 @@ func child() {
|
||||
upperDir := os.Getenv("MAINGO_UPPER")
|
||||
workDir := os.Getenv("MAINGO_WORK")
|
||||
|
||||
lowerDir := "/home/mingdax/alpine-rootfs"
|
||||
lowerDir := os.Getenv("MAINGO_LOWER")
|
||||
opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", lowerDir, upperDir, workDir)
|
||||
must(syscall.Mount("overlay", mergedDir, "overlay", 0, opts))
|
||||
fmt.Printf("[overlay] mounted\n")
|
||||
@@ -140,7 +202,7 @@ func child() {
|
||||
must(syscall.Unmount("proc", 0))
|
||||
}
|
||||
|
||||
func setupVeth(childPID int, vethHost, vethCtr, hostIP, ctrIP, subnet string) {
|
||||
func setupVeth(childPID int, vethHost, vethCtr, ctrIP string) {
|
||||
pid := strconv.Itoa(childPID)
|
||||
|
||||
runCmdSilent("ip", "link", "del", vethHost)
|
||||
@@ -148,37 +210,35 @@ func setupVeth(childPID int, vethHost, vethCtr, hostIP, ctrIP, subnet string) {
|
||||
runCmd("ip", "link", "add", vethHost, "type", "veth", "peer", "name", vethCtr)
|
||||
runCmd("ip", "link", "set", vethCtr, "netns", pid)
|
||||
|
||||
runCmd("ip", "addr", "add", hostIP+"/24", "dev", vethHost)
|
||||
// 宿主端插进网桥,不配 IP——bridge 自己有一个 IP 当网关
|
||||
runCmd("ip", "link", "set", vethHost, "master", bridgeName)
|
||||
runCmd("ip", "link", "set", vethHost, "up")
|
||||
|
||||
// 容器端:配 IP、设默认网关、启 lo
|
||||
runCmd("nsenter", "-t", pid, "-n", "ip", "addr", "add", ctrIP+"/24", "dev", vethCtr)
|
||||
runCmd("nsenter", "-t", pid, "-n", "ip", "link", "set", vethCtr, "up")
|
||||
runCmd("nsenter", "-t", pid, "-n", "ip", "link", "set", "lo", "up")
|
||||
runCmd("nsenter", "-t", pid, "-n", "ip", "route", "add", "default", "via", bridgeIP)
|
||||
|
||||
fmt.Printf("[veth] pair ready: host=%s, container=%s\n", hostIP, ctrIP)
|
||||
fmt.Printf("[veth] %s → %s via %s\n", ctrIP, bridgeName, bridgeIP)
|
||||
}
|
||||
|
||||
func setupNAT(childPID int, ctrIP, subnet string) {
|
||||
pid := strconv.Itoa(childPID)
|
||||
|
||||
// setupNAT 确保共享子网能出公网。规则只加一次,多个容器复用。
|
||||
func setupNAT() {
|
||||
must(os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644))
|
||||
runCmd("iptables", "-t", "nat", "-A", "POSTROUTING", "-s", subnet, "-j", "MASQUERADE")
|
||||
runCmd("nsenter", "-t", pid, "-n", "ip", "route", "add", "default", "via",
|
||||
ctrIP[:len(ctrIP)-1]+"1")
|
||||
|
||||
fmt.Printf("[nat] NAT enabled: %s → internet\n", subnet)
|
||||
if _, err := os.Stat("/tmp/mdbr0-nat-ready"); err == nil {
|
||||
return
|
||||
}
|
||||
runCmd("iptables", "-t", "nat", "-A", "POSTROUTING", "-s", bridgeSubnet, "-j", "MASQUERADE")
|
||||
os.WriteFile("/tmp/mdbr0-nat-ready", []byte("1"), 0644)
|
||||
fmt.Printf("[nat] MASQUERADE %s → internet\n", bridgeSubnet)
|
||||
}
|
||||
|
||||
func cleanupVeth(vethHost string) {
|
||||
runCmdSilent("ip", "link", "del", vethHost)
|
||||
}
|
||||
|
||||
func cleanupNAT(subnet string) {
|
||||
runCmdSilent("iptables", "-t", "nat", "-D", "POSTROUTING", "-s", subnet, "-j",
|
||||
"MASQUERADE")
|
||||
fmt.Printf("[nat] cleaned up\n")
|
||||
}
|
||||
|
||||
func waitForNetwork() {
|
||||
f := os.NewFile(3, "syncpipe")
|
||||
buf := make([]byte, 1)
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ——— 1. 数据结构:把 curl 看到的 JSON 翻译成 Go struct ———
|
||||
//
|
||||
// curl /manifests/latest 返回的 JSON:
|
||||
//
|
||||
// { "manifests": [ { "digest": "sha256:xxx", "platform": {"architecture":"amd64","os":"linux"} }, ... ] }
|
||||
//
|
||||
// 这个叫 manifest list(多架构索引)。
|
||||
type ManifestList struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
Manifests []ManifestEntry `json:"manifests"`
|
||||
}
|
||||
|
||||
type ManifestEntry struct {
|
||||
Digest string `json:"digest"` // 指向真正 manifest 的 sha256
|
||||
Platform Platform `json:"platform"` // 比如 amd64/linux
|
||||
}
|
||||
|
||||
type Platform struct {
|
||||
Architecture string `json:"architecture"`
|
||||
OS string `json:"os"`
|
||||
}
|
||||
|
||||
// curl /manifests/sha256:79ff19... 返回的 JSON:
|
||||
//
|
||||
// { "config": {"digest":"sha256:xxx"}, "layers": [{"digest":"sha256:yyy"}] }
|
||||
//
|
||||
// 这个是单架构的 manifest——里面有 config 和 layers。
|
||||
type ImageManifest struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
Config BlobRef `json:"config"` // 指向 config blob
|
||||
Layers []BlobRef `json:"layers"` // 文件系统层,索引 0 是最底层
|
||||
}
|
||||
|
||||
// BlobRef 是个"指针"——用 sha256 digest 指向 registry 上的一个文件。
|
||||
// config blob 和每一层 layer blob 都用这个表示。
|
||||
type BlobRef struct {
|
||||
Digest string `json:"digest"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// curl /blobs/sha256:d529dd0c...(config blob)返回的 JSON:
|
||||
//
|
||||
// { "config": { "Env": ["PATH=..."], "Cmd": ["/bin/sh"] } }
|
||||
//
|
||||
// 只有 Env 和 Cmd 是我们需要的。
|
||||
// ImageConfig 是 config blob 的 Go 表示。
|
||||
// Docker 里实际执行的命令 = Entrypoint(如果有)+ Cmd
|
||||
// 比如 redis: Entrypoint=["docker-entrypoint.sh"], Cmd=["redis-server"]
|
||||
//
|
||||
// → 实际执行: docker-entrypoint.sh redis-server
|
||||
type ImageConfig struct {
|
||||
Config struct {
|
||||
Env []string `json:"Env"`
|
||||
Cmd []string `json:"Cmd"`
|
||||
Entrypoint []string `json:"Entrypoint"`
|
||||
} `json:"config"`
|
||||
}
|
||||
|
||||
// PullResult 是 pullImage() 的返回值——run() 只需要这三样
|
||||
type PullResult struct {
|
||||
LowerDirs string // 冒号分隔:/tmp/layers/.../sha1:/tmp/layers/.../sha2
|
||||
Env []string // PATH, PYTHON_VERSION... 直接拼进 cmd.Env
|
||||
Cmd []string // 镜像默认命令,用户没指定时用
|
||||
Entrypoint []string // 镜像入口脚本,如果有就排在 Cmd 前面
|
||||
}
|
||||
|
||||
// ——— 2. 注册表 HTTP 客户端 ———
|
||||
|
||||
// getToken 向 Docker Hub 要一个临时 token。
|
||||
// 免费公开镜像不需要登录——传 repo 名就行,返回一个 Bearer token。
|
||||
// 所有后续请求都要带 Authorization: Bearer <token>
|
||||
func getToken(repo string) (string, error) {
|
||||
url := fmt.Sprintf(
|
||||
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull",
|
||||
repo,
|
||||
)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("token request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var out struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return "", fmt.Errorf("token decode: %w", err)
|
||||
}
|
||||
return out.Token, nil
|
||||
}
|
||||
|
||||
// doRequest 封装了"带 token 和 Accept 头发一个 GET,把 JSON 解析到 target"。
|
||||
// manifest list、manifest、config blob 共用这个函数——它们都是 JSON。
|
||||
// layer blob 不走这个(layer 是 tar.gz 二进制)。
|
||||
func doRequest(url, token, accept string, target interface{}) error {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
if accept != "" {
|
||||
req.Header.Set("Accept", accept)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%s: status %d", url, resp.StatusCode)
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(target)
|
||||
}
|
||||
|
||||
// ——— 3. pullImage ——
|
||||
// 输入 "alpine" + "latest" → 输出 PullResult{LowerDirs, Env, Cmd}
|
||||
//
|
||||
// 流程:
|
||||
// 1. 解析镜像名("alpine" → "library/alpine")
|
||||
// 2. 拿 token
|
||||
// 3. GET /manifests/latest → ManifestList → 挑 amd64/linux
|
||||
// 4. GET /manifests/sha256:xxx → ImageManifest → config.digest + layers[].digest
|
||||
// 5. GET /blobs/config.digest → ImageConfig → Env + Cmd
|
||||
// 6. 遍历 layers,每个 GET /blobs/digest → tar.gz → 解压到 /tmp/layers/
|
||||
// 7. 返回
|
||||
func pullImage(image, tag string) (*PullResult, error) {
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
repo := image
|
||||
if !strings.Contains(image, "/") {
|
||||
repo = "library/" + image // "alpine" → "library/alpine"
|
||||
}
|
||||
|
||||
token, err := getToken(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseURL := fmt.Sprintf("https://registry-1.docker.io/v2/%s", repo)
|
||||
|
||||
// 1. manifest list → 挑 amd64
|
||||
var ml ManifestList
|
||||
if err := doRequest(
|
||||
fmt.Sprintf("%s/manifests/%s", baseURL, tag),
|
||||
token,
|
||||
"application/vnd.docker.distribution.manifest.v2+json",
|
||||
&ml,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("manifest list: %w", err)
|
||||
}
|
||||
|
||||
var targetDigest string
|
||||
for _, e := range ml.Manifests {
|
||||
if e.Platform.Architecture == "amd64" && e.Platform.OS == "linux" {
|
||||
targetDigest = e.Digest
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetDigest == "" {
|
||||
return nil, fmt.Errorf("no amd64/linux manifest for %s:%s", image, tag)
|
||||
}
|
||||
|
||||
// 2. 真正的 manifest → config.digest + layers
|
||||
var im ImageManifest
|
||||
if err := doRequest(
|
||||
fmt.Sprintf("%s/manifests/%s", baseURL, targetDigest),
|
||||
token,
|
||||
"application/vnd.oci.image.manifest.v1+json",
|
||||
&im,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("image manifest: %w", err)
|
||||
}
|
||||
|
||||
// 3. config blob → Env + Cmd
|
||||
var cfg ImageConfig
|
||||
if err := doRequest(
|
||||
fmt.Sprintf("%s/blobs/%s", baseURL, im.Config.Digest),
|
||||
token,
|
||||
"",
|
||||
&cfg,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("config blob: %w", err)
|
||||
}
|
||||
|
||||
// 4. 逐层下载
|
||||
layersBase := fmt.Sprintf("/tmp/layers/%s/%s", strings.ReplaceAll(repo, "/", "_"), tag)
|
||||
var lowerDirs []string
|
||||
for _, layer := range im.Layers {
|
||||
safeName := strings.Replace(layer.Digest, ":", "_", 1)
|
||||
layerDir := filepath.Join(layersBase, safeName)
|
||||
|
||||
// 目录已存在 = 之前下载过,跳过
|
||||
if _, err := os.Stat(layerDir); os.IsNotExist(err) {
|
||||
fmt.Printf("[pull] downloading %s (%d MB)...\n",
|
||||
layer.Digest[:19], layer.Size/1024/1024)
|
||||
if err := extractLayer(baseURL, layer.Digest, token, layerDir); err != nil {
|
||||
fmt.Printf("[pull] WARNING: layer %s unavailable (%v), skipping\n", layer.Digest[:19], err)
|
||||
continue // 404 或其他 registry 错误——跳过这一层
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[pull] %s (cached)\n", layer.Digest[:19])
|
||||
}
|
||||
// OCI layers[0] = 基底层,overlay lowerdir 第一个 = 最上层
|
||||
// 所以要反转——最新的层放在最前面
|
||||
lowerDirs = append([]string{layerDir}, lowerDirs...)
|
||||
}
|
||||
|
||||
return &PullResult{
|
||||
LowerDirs: strings.Join(lowerDirs, ":"),
|
||||
Env: cfg.Config.Env,
|
||||
Cmd: cfg.Config.Cmd,
|
||||
Entrypoint: cfg.Config.Entrypoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractLayer 下载一个 blob(tar.gz 格式)并解压到 destDir。
|
||||
// 处理普通文件、目录、符号链接。不处理硬链接(容器镜像很少用)。
|
||||
func extractLayer(baseURL, digest, token, destDir string) error {
|
||||
url := fmt.Sprintf("%s/blobs/%s", baseURL, digest)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 第一层:gzip 解压
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gzip: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
// 第二层:tar 遍历
|
||||
tr := tar.NewReader(gz)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("tar: %w", err)
|
||||
}
|
||||
|
||||
target := filepath.Join(destDir, hdr.Name)
|
||||
|
||||
switch hdr.Typeflag {
|
||||
case tar.TypeDir:
|
||||
os.MkdirAll(target, 0755)
|
||||
os.Chmod(target, os.FileMode(hdr.Mode))
|
||||
case tar.TypeReg:
|
||||
os.MkdirAll(filepath.Dir(target), 0755)
|
||||
f, err := os.Create(target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create %s: %w", target, err)
|
||||
}
|
||||
if _, err := io.Copy(f, tr); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
os.Chmod(target, os.FileMode(hdr.Mode))
|
||||
case tar.TypeSymlink:
|
||||
os.Symlink(hdr.Linkname, target)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user