最近使用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不太一样,没有知识储备能够解释这块的东西,有大佬的话可以解释一下。