GoでSFTPに接続する方法

プログラミング言語の Go を使って、SFTPへの接続、ファイルのリストアップ、アップロード、ダウンロードを行う方法についてご説明します。

SFTP は、ファイルやデータを安全に転送するための標準的で安全なプロトコルとして広く知られています。実際にSFTPサーバーに接続するのは、特にドキュメントが不十分な Go 言語を使う場合、非常に面倒なことになります。そこで、本記事にたどり着きました。この記事を読み終わる頃には、いとも簡単にSFTPを活用して接続できるようになるでしょう。

必要事項

いつものように、まずは準備から始めましょう。

接続するにはSFTPサーバが必要です。お持ちでない場合は、30秒もあればSFTP To GoでSFTPエンドポイントをセットアップすることができます。

SFTP サーバに接続し、対話するには、 github.com/pkg/sftpと  golang.org/x/crypto/というライブラリが必要になります。それをインストールする準備ができたら、以下を手動で実行します:

$ go get github.com/pkg/sftp
$ go get golang.org/x/crypto/ssh

またはgo.modファイルを作成し、その中で依存関係宣言をします。Go.mod ファイルは Go モジュールを確定するもので、特に他の Go モジュールへの依存性の追加のために使われます。そして以下をgo.modとして保存しましょう:

module sftptogo.com/examples/go

go 1.13

require (
	github.com/pkg/sftp v1.12.0
	golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
)
mod.go

SFTPへの接続と切断

今回は、次のように、SFTP サーバーに接続するのに必要な情報をすべて URI 形式で格納した SFTPTOGO_URL という環境変数を使います: sftp://user:password@host.

HerokuのアドオンとしてSFTP To Goを使う場合、この変数はアプリ内に自動的に作成され、必要な情報がすべて含まれています。以下のコードでは、この変数を解析してURI部分を抽出し、リモートサーバーのホストキーをknown_hosts ファイルから取得して、リモートホストを識別しています。

接続が確立されると、SFTP クライアントオブジェクトが変数 scに代入されます:

package main

import (
    "fmt"
    "io"
    "os"
    "net"
    "net/url"
    "bufio"
    "strings"
    "path/filepath"

    "golang.org/x/crypto/ssh"
    "golang.org/x/crypto/ssh/agent"

    "github.com/pkg/sftp"
)

func main() {
    // Get SFTP To Go URL from environment
    rawurl := os.Getenv("SFTPTOGO_URL")

    parsedUrl, err := url.Parse(rawurl)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to parse SFTP To Go URL: %s\n", err)
        os.Exit(1)
    }

    // Get user name and pass
    user := parsedUrl.User.Username()
    pass, _ := parsedUrl.User.Password()

    // Parse Host and Port
    host := parsedUrl.Host
    // Default SFTP port
    port := 22

    hostKey := getHostKey(host)

    fmt.Fprintf(os.Stdout, "Connecting to %s ...\n", host)

    var auths []ssh.AuthMethod

    // Try to use $SSH_AUTH_SOCK which contains the path of the unix file socket that the sshd agent uses 
    // for communication with other processes.
    if aconn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
        auths = append(auths, ssh.PublicKeysCallback(agent.NewClient(aconn).Signers))
    }

    // Use password authentication if provided
    if pass != "" {
        auths = append(auths, ssh.Password(pass))
    }
    
    // Initialize client configuration
    config := ssh.ClientConfig{
        User: user,
        Auth: auths,
        // Uncomment to ignore host key check
        //HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        HostKeyCallback: ssh.FixedHostKey(hostKey),
    }

    addr := fmt.Sprintf("%s:%d", host, port)

    // Connect to server
    conn, err := ssh.Dial("tcp", addr, &config)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to connecto to [%s]: %v\n", addr, err)
        os.Exit(1)
    }

    defer conn.Close()

    // Create new SFTP client
    sc, err := sftp.NewClient(conn)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to start SFTP subsystem: %v\n", err)
        os.Exit(1)
    }
    defer sc.Close()
}

// Get host key from local known hosts
func getHostKey(host string) ssh.PublicKey {
    // parse OpenSSH known_hosts file
    // ssh or use ssh-keyscan to get initial key
    file, err := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to read known_hosts file: %v\n", err)
        os.Exit(1)
    }
    defer file.Close()
 
    scanner := bufio.NewScanner(file)
    var hostKey ssh.PublicKey
    for scanner.Scan() {
        fields := strings.Split(scanner.Text(), " ")
        if len(fields) != 3 {
            continue
        }
        if strings.Contains(fields[0], host) {
            var err error
            hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
            if err != nil {
                fmt.Fprintf(os.Stderr, "Error parsing %q: %v\n", fields[2], err)
                os.Exit(1)
            }
            break
        }
    }
 
    if hostKey == nil {
        fmt.Fprintf(os.Stderr, "No hostkey found for %s", host)
        os.Exit(1)
    }
 
    return hostKey
}
connect.go

ファイルのリストアップ

接続ができたので、それを使ってリモート SFTP サーバー上のファイル一覧が取得できます。これは、listFiles関数にSFTP クライアント (sc) とリモートのディレクトリパスを引数として渡すことで行われます。呼び出しの例は、  listFiles(*sc, ".")です。この関数は、SFTPサーバーにあるファイルの名前、変更タイムスタンプ、サイズを出力します。

func listFiles(sc sftp.Client, remoteDir string) (err error) {
    fmt.Fprintf(os.Stdout, "Listing [%s] ...\n\n", remoteDir)
    
    files, err := sc.ReadDir(remoteDir)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to list remote dir: %v\n", err)
        return
    }

    for _, f := range files {
        var name, modTime, size string

        name = f.Name()
        modTime = f.ModTime().Format("2006-01-02 15:04:05")
        size = fmt.Sprintf("%12d", f.Size())

        if f.IsDir() {
            name = name + "/"
            modTime = ""
            size = "PRE"
        }
        // Output each file name and size in bytes
        fmt.Fprintf(os.Stdout, "%19s %12s %s\n", modTime, size, name)
    }

    return
}
listfiles.go

ファイルのアップロード

次のステップは、ファイルのアップロードです。 uploadFile関数に『FTP クライアント』、『ローカル ファイルへのパス』、および『リモート パス (アップロード後にファイルが置かれる場所)』を引数として渡します。関数の呼び出しは次のようになります:  uploadFile(*sc, "./local.txt", "./remote.txt")

// Upload file to sftp server
func uploadFile(sc sftp.Client, localFile, remoteFile string) (err error) {
    fmt.Fprintf(os.Stdout, "Uploading [%s] to [%s] ...\n", localFile, remoteFile)

    srcFile, err := os.Open(localFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open local file: %v\n", err)
        return
    }
    defer srcFile.Close()

    // Make remote directories recursion
    parent := filepath.Dir(remoteFile)
    path := string(filepath.Separator)
    dirs := strings.Split(parent, path)
    for _, dir := range dirs {
        path = filepath.Join(path, dir)
        sc.Mkdir(path)
    }

    // Note: SFTP To Go doesn't support O_RDWR mode
    dstFile, err := sc.OpenFile(remoteFile, (os.O_WRONLY|os.O_CREATE|os.O_TRUNC))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open remote file: %v\n", err)
        return
    }
    defer dstFile.Close()

    bytes, err := io.Copy(dstFile, srcFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to upload local file: %v\n", err)
        os.Exit(1)
    }
    fmt.Fprintf(os.Stdout, "%d bytes copied\n", bytes)
    
    return
}
uploadfile.go

ファイルのダウンロード

あともう少しで完成です!残りはファイルをダウンロードするだけです。downloadFile関数に、SFTPクライアント、リモートファイルへのパス、ダウンロードしたファイルを格納するローカルパスを引数として渡します。関数の呼び出しは次のようになります: downloadFile(*sc, "./remote.txt", "./download.txt")

// Download file from sftp server
func downloadFile(sc sftp.Client, remoteFile, localFile string) (err error) {

    fmt.Fprintf(os.Stdout, "Downloading [%s] to [%s] ...\n", remoteFile, localFile)
    // Note: SFTP To Go doesn't support O_RDWR mode
    srcFile, err := sc.OpenFile(remoteFile, (os.O_RDONLY))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open remote file: %v\n", err)
        return
    }
    defer srcFile.Close()

    dstFile, err := os.Create(localFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open local file: %v\n", err)
        return
    }
    defer dstFile.Close()

    bytes, err := io.Copy(dstFile, srcFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to download remote file: %v\n", err)
        os.Exit(1)
    }
    fmt.Fprintf(os.Stdout, "%d bytes copied\n", bytes)
    
    return
}
downloadfile.go

まとめ

というわけで、最後までたどり着きました! もし、このプログラムを最初から最後まで通して実行したい場合は、次のコードをコピーして、 main.goという名前で保存してください:

package main

import (
    "fmt"
    "io"
    "os"
    "net"
    "net/url"
    "bufio"
    "strings"
    "path/filepath"

    "golang.org/x/crypto/ssh"
    "golang.org/x/crypto/ssh/agent"

    "github.com/pkg/sftp"
)

func main() {
    // Get SFTP To Go URL from environment
    rawurl := os.Getenv("SFTPTOGO_URL")

    parsedUrl, err := url.Parse(rawurl)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to parse SFTP To Go URL: %s\n", err)
        os.Exit(1)
    }

    // Get user name and pass
    user := parsedUrl.User.Username()
    pass, _ := parsedUrl.User.Password()

    // Parse Host and Port
    host := parsedUrl.Host
    // Default SFTP port
    port := 22

    hostKey := getHostKey(host)

    fmt.Fprintf(os.Stdout, "Connecting to %s ...\n", host)

    var auths []ssh.AuthMethod

    // Try to use $SSH_AUTH_SOCK which contains the path of the unix file socket that the sshd agent uses 
    // for communication with other processes.
    if aconn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
        auths = append(auths, ssh.PublicKeysCallback(agent.NewClient(aconn).Signers))
    }

    // Use password authentication if provided
    if pass != "" {
        auths = append(auths, ssh.Password(pass))
    }
    
    // Initialize client configuration
    config := ssh.ClientConfig{
        User: user,
        Auth: auths,
        // Uncomment to ignore host key check
        //HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        HostKeyCallback: ssh.FixedHostKey(hostKey),
    }

    addr := fmt.Sprintf("%s:%d", host, port)

    // Connect to server
    conn, err := ssh.Dial("tcp", addr, &config)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to connecto to [%s]: %v\n", addr, err)
        os.Exit(1)
    }

    defer conn.Close()

    // Create new SFTP client
    sc, err := sftp.NewClient(conn)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to start SFTP subsystem: %v\n", err)
        os.Exit(1)
    }
    defer sc.Close()

    //*
    //* List working directory files
    //*
    listFiles(*sc, ".")
    fmt.Fprintf(os.Stdout, "\n")

    //*
    //* Upload local file to remote file
    //*
    uploadFile(*sc, "./local.txt", "./remote.txt")
    fmt.Fprintf(os.Stdout, "\n")

    //*
    //* Download remote file to local file
    //*
    downloadFile(*sc, "./remote.txt", "./download.txt")
    fmt.Fprintf(os.Stdout, "\n")
}

func listFiles(sc sftp.Client, remoteDir string) (err error) {
    fmt.Fprintf(os.Stdout, "Listing [%s] ...\n\n", remoteDir)
    
    files, err := sc.ReadDir(remoteDir)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to list remote dir: %v\n", err)
        return
    }

    for _, f := range files {
        var name, modTime, size string

        name = f.Name()
        modTime = f.ModTime().Format("2006-01-02 15:04:05")
        size = fmt.Sprintf("%12d", f.Size())

        if f.IsDir() {
            name = name + "/"
            modTime = ""
            size = "PRE"
        }
        // Output each file name and size in bytes
        fmt.Fprintf(os.Stdout, "%19s %12s %s\n", modTime, size, name)
    }

    return
}


// Upload file to sftp server
func uploadFile(sc sftp.Client, localFile, remoteFile string) (err error) {
    fmt.Fprintf(os.Stdout, "Uploading [%s] to [%s] ...\n", localFile, remoteFile)

    srcFile, err := os.Open(localFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open local file: %v\n", err)
        return
    }
    defer srcFile.Close()

    // Make remote directories recursion
    parent := filepath.Dir(remoteFile)
    path := string(filepath.Separator)
    dirs := strings.Split(parent, path)
    for _, dir := range dirs {
        path = filepath.Join(path, dir)
        sc.Mkdir(path)
    }

    // Note: SFTP To Go doesn't support O_RDWR mode
    dstFile, err := sc.OpenFile(remoteFile, (os.O_WRONLY|os.O_CREATE|os.O_TRUNC))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open remote file: %v\n", err)
        return
    }
    defer dstFile.Close()

    bytes, err := io.Copy(dstFile, srcFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to upload local file: %v\n", err)
        os.Exit(1)
    }
    fmt.Fprintf(os.Stdout, "%d bytes copied\n", bytes)
    
    return
}

// Download file from sftp server
func downloadFile(sc sftp.Client, remoteFile, localFile string) (err error) {

    fmt.Fprintf(os.Stdout, "Downloading [%s] to [%s] ...\n", remoteFile, localFile)
    // Note: SFTP To Go doesn't support O_RDWR mode
    srcFile, err := sc.OpenFile(remoteFile, (os.O_RDONLY))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open remote file: %v\n", err)
        return
    }
    defer srcFile.Close()

    dstFile, err := os.Create(localFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open local file: %v\n", err)
        return
    }
    defer dstFile.Close()

    bytes, err := io.Copy(dstFile, srcFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to download remote file: %v\n", err)
        os.Exit(1)
    }
    fmt.Fprintf(os.Stdout, "%d bytes copied\n", bytes)
    
    return
}

// Get host key from local known hosts
func getHostKey(host string) ssh.PublicKey {
    // parse OpenSSH known_hosts file
    // ssh or use ssh-keyscan to get initial key
    file, err := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to read known_hosts file: %v\n", err)
        os.Exit(1)
    }
    defer file.Close()
 
    scanner := bufio.NewScanner(file)
    var hostKey ssh.PublicKey
    for scanner.Scan() {
        fields := strings.Split(scanner.Text(), " ")
        if len(fields) != 3 {
            continue
        }
        if strings.Contains(fields[0], host) {
            var err error
            hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
            if err != nil {
                fmt.Fprintf(os.Stderr, "Error parsing %q: %v\n", fields[2], err)
                os.Exit(1)
            }
            break
        }
    }
 
    if hostKey == nil {
        fmt.Fprintf(os.Stderr, "No hostkey found for %s", host)
        os.Exit(1)
    }
 
    return hostKey
}

最後に、以下のコマンドで実行します:

go main.go

お疲れ様でした。これで無事、GoとSFTPとの接続が完了しました。

最大限のセキュリティと信頼性を備えたSFTP To Go
SFTP To Goは、管理されているSFTP/FTPS/S3をサービスとして提供します - 最高の信頼性、セキュリティ、可用性、1分のセットアップで。あらゆる規模の企業に最適です。
今すぐSFTP To Goをお試しに!

Githubにあるコードサンプルを使い、スキルをレベルアップしましょう。