前言

最近使用Go写了一个小工具,过程使用OS库操作文件的时候,遇到了一些一个跨系统的问题,在Windows可以正常运行的代码,打包到Linux就无法运行了,在此记录一下。除此之外还有通过go-scp包进行远程拷贝的方法记录。

正文

SCP使用实例

安装scp用到的库

程序中有个需求,是要从远端拷贝文件或拷贝文件到远端。看了网上的ssh+sftp的例子,虽然也可以实现这个需求,但是感觉要麻烦点,git上面看到一个go-scp的三方库,使用了一下,很好用,首先是安装包:

go get github.com/bramvdbogaerde/go-scp
go get golang.org/x/crypto/ssh

百度的大部分例子从装包这里就不太对。总之这样get目前试是没问题的,然后在代码里import进来:

import (
	scp "github.com/bramvdbogaerde/go-scp"
	"golang.org/x/crypto/ssh"
)

建立ssh客户端连接

为了方便理解,创建一个结构体,用来存储ssh客户端信息:

type sshConfig struct {
	IP       string // ssh服务器
	PORT     string // ssh服务器端口
	USER     string // ssh用户
	PASSWORD string //ssh密码
}

然后就是建立ssh连接的函数,该函数会返回有个ssh.Client指针,等会用于建立SCP连接:

func sshconnect(s *sshConfig) (*ssh.Client, error) {
	var (
		auth         []ssh.AuthMethod
		addr         string
		clientConfig *ssh.ClientConfig
		sshClient    *ssh.Client
		err          error
	)
	// 认证配置
	auth = make([]ssh.AuthMethod, 0)
	auth = append(auth, ssh.Password(s.PASSWORD))
	clientConfig = &ssh.ClientConfig{
		User:    s.USER,
		Auth:    auth,
		Timeout: 10 * time.Second,
		// 必须添加HostKeyCallback回调函数,否则会报错
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			return nil
		},
	}
	// 拼接ip和端口,如192.168.1.1:22
	addr = fmt.Sprintf("%s:%s", s.IP, s.PORT)
	if sshClient, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
		return nil, err
	}
	return sshClient, nil
}

SCP上传和下载

上传
使用上面创建的ssh连接,通过NewClientBySSH创建scp的连接,对于要进行上传的文件打开文件流;

func CopyToRemote(local string, remote string, cfg *ssh.Client) {
	client, err := scp.NewClientBySSH(cfg)
	ErrCheck(err, "SCP Client create failed.")
	defer client.Close()
	srcFile, err := os.Open(local)
	ErrCheck(err, "Local file open failed.Path: "+local)
	defer srcFile.Close()
	err = client.CopyFromFile(context.Background(), *srcFile, remote, "0644")
	ErrCheck(err, "Scp local files failed.Remote: "+remote+" Local: "+local)
	log.Printf("Copy file to remote server finished! local: %s | remote: %s\n", local, remote)
}

下载
下载整体思路和上传区别不大,只不过因为要往本地文件写,需要使用OpenFile的方式创建文件流,然后进行文件的写入:

func CopyToLocal(local string, remote string, cfg *ssh.Client) {
	client, err := scp.NewClientBySSH(cfg)
	ErrCheck(err, "SCP Client create failed.")
	defer client.Close()
	srcFile, err := os.OpenFile(local, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
	ErrCheck(err, "Local file open failed.Path: "+local)
	defer srcFile.Close()
	err = client.CopyFromRemote(context.Background(), srcFile, remote)
	ErrCheck(err, "Scp remote files failed.Remote: "+remote+" Local: "+local)
	log.Printf("Copy file from remote server to local finished! remote: %s|local: %s \n", remote, local)
}

调用实例

s := sshConfig{
	IP:       ip,
	PORT:     *sshPort,
	USER:     *sshUser,
	PASSWORD: *sshPassword,
}
remoteFile := "/etc/hosts"
hostFileName := ip + "_hosts"
cfg, err := sshconnect(&s)
if cfg == nil || err != nil {
	log.Printf("The ssh connect create failed.IP: %s\n", ip)
	_, err = d.Write([]byte(fmt.Sprintf("%s: %s\n", ip, "connect failed.")))
	continue
}
defer cfg.Close()
CopyToLocal(hostFileName, remoteFile, cfg)

文件写入问题

主要是os.OpenFile对文件进行写操作时,在Windows上单独使用时一切正常,迁移到Linux系统以后就发现对文件的写操作存在问题,其一是覆写代码:

func CopyToLocal(local string, remote string, cfg *ssh.Client) {
	client, err := scp.NewClientBySSH(cfg)
	ErrCheck(err, "SCP Client create failed.")
	defer client.Close()
	// 向本地文件写入,不存在则创建
	srcFile, err := os.OpenFile(local, os.O_CREATE, 0644)
	ErrCheck(err, "Local file open failed.Path: "+local)
	defer srcFile.Close()
	err = client.CopyFromRemote(context.Background(), srcFile, remote)
	ErrCheck(err, "Scp remote files failed.Remote: "+remote+" Local: "+local)
	log.Printf("Copy file from remote server to local finished! remote: %s|local: %s \n", remote, local)
}

这段覆写代码在Windows可以正常使用,若文件存在会被覆写,但是到了Linux必须改成如下代码才可以:

srcFile, err := os.OpenFile(local, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)

如果没有O_WRONLY(或者读写都给)的参数,就会发现处理后的文件大小为0;同样的问题也出现在了追加方法中:

func RepairHostFile(localhostFile string, hostname string) {
	// 在本地修改hosts文件
	f, err := os.OpenFile(localhostFile, os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		log.Printf("Append hostname to %s failed.\n", localhostFile)
	}
	addr, err := net.ResolveIPAddr("ip", hostname)
	if err != nil {
		log.Printf("Hostname resolve faild.%s\n", hostname)
		return
	}
	hostname = addr.IP.String() + " " + hostname + "\n"
	f.WriteString(hostname)
	defer f.Close()
}

这里的OpenFile在Windows中也是只要加O_APPEND参数即可,但是在Linux至少要加上WRONLY参数,否则也是文件写不进去,猜测还是因为Linux内核或者系统调用和Windows不太一样,没有知识储备能够解释这块的东西,有大佬的话可以解释一下。