You've already forked docker-volume-backup
mirror of
https://github.com/offen/docker-volume-backup.git
synced 2025-06-14 22:25:03 +02:00
SSH Backup Storage Support (#107)
* SSH Client implemented * Private key auth implemented Code refactoring * Refactoring * Passphrase renamed to IdentityPassphrase Default private key location changed to .ssh/id
This commit is contained in:
@ -7,8 +7,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@ -39,6 +42,8 @@ type script struct {
|
||||
cli *client.Client
|
||||
minioClient *minio.Client
|
||||
webdavClient *gowebdav.Client
|
||||
sshClient *ssh.Client
|
||||
sftpClient *sftp.Client
|
||||
logger *logrus.Logger
|
||||
sender *router.ServiceRouter
|
||||
template *template.Template
|
||||
@ -159,6 +164,57 @@ func newScript() (*script, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if s.c.SSHHostName != "" {
|
||||
var authMethods []ssh.AuthMethod
|
||||
|
||||
if s.c.SSHPassword != "" {
|
||||
authMethods = append(authMethods, ssh.Password(s.c.SSHPassword))
|
||||
}
|
||||
|
||||
if _, err := os.Stat(s.c.SSHIdentityFile); err == nil {
|
||||
key, err := ioutil.ReadFile(s.c.SSHIdentityFile)
|
||||
if err != nil {
|
||||
return nil, errors.New("newScript: error reading the private key")
|
||||
}
|
||||
|
||||
var signer ssh.Signer
|
||||
if s.c.SSHIdentityPassphrase != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(s.c.SSHIdentityPassphrase))
|
||||
if err != nil {
|
||||
return nil, errors.New("newScript: error parsing the encrypted private key")
|
||||
}
|
||||
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, errors.New("newScript: error parsing the private key")
|
||||
}
|
||||
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
||||
}
|
||||
}
|
||||
|
||||
sshClientConfig := &ssh.ClientConfig{
|
||||
User: s.c.SSHUser,
|
||||
Auth: authMethods,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
sshClient, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", s.c.SSHHostName, s.c.SSHPort), sshClientConfig)
|
||||
s.sshClient = sshClient
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newScript: error creating ssh client: %w", err)
|
||||
}
|
||||
_, _, err = s.sshClient.SendRequest("keepalive", false, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sftpClient, err := sftp.NewClient(sshClient)
|
||||
s.sftpClient = sftpClient
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newScript: error creating sftp client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.c.EmailNotificationRecipient != "" {
|
||||
emailURL := fmt.Sprintf(
|
||||
"smtp://%s:%s@%s:%d/?from=%s&to=%s",
|
||||
@ -512,6 +568,52 @@ func (s *script) copyBackup() error {
|
||||
s.logger.Infof("Uploaded a copy of backup `%s` to WebDAV-URL '%s' at path '%s'.", s.file, s.c.WebdavUrl, s.c.WebdavPath)
|
||||
}
|
||||
|
||||
if s.sshClient != nil {
|
||||
source, err := os.Open(s.file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyBackup: error reading the file to be uploaded: %w", err)
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
destination, err := s.sftpClient.Create(filepath.Join(s.c.SSHRemotePath, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyBackup: error creating file on SSH storage: %w", err)
|
||||
}
|
||||
defer destination.Close()
|
||||
|
||||
chunk := make([]byte, 1000000)
|
||||
for {
|
||||
num, err := source.Read(chunk)
|
||||
if err == io.EOF {
|
||||
tot, err := destination.Write(chunk[:num])
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyBackup: error uploading the file to SSH storage: %w", err)
|
||||
}
|
||||
|
||||
if tot != len(chunk[:num]) {
|
||||
return fmt.Errorf("sshClient: failed to write stream")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyBackup: error uploading the file to SSH storage: %w", err)
|
||||
}
|
||||
|
||||
tot, err := destination.Write(chunk[:num])
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyBackup: error uploading the file to SSH storage: %w", err)
|
||||
}
|
||||
|
||||
if tot != len(chunk[:num]) {
|
||||
return fmt.Errorf("sshClient: failed to write stream")
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Infof("Uploaded a copy of backup `%s` to SSH storage '%s' at path '%s'.", s.file, s.c.SSHHostName, s.c.SSHRemotePath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
||||
if err := copyFile(s.file, path.Join(s.c.BackupArchive, name)); err != nil {
|
||||
return fmt.Errorf("copyBackup: error copying file to local archive: %w", err)
|
||||
@ -645,6 +747,37 @@ func (s *script) pruneBackups() error {
|
||||
})
|
||||
}
|
||||
|
||||
if s.sshClient != nil {
|
||||
candidates, err := s.sftpClient.ReadDir(s.c.SSHRemotePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pruneBackups: error reading directory from SSH storage: %w", err)
|
||||
}
|
||||
|
||||
var matches []string
|
||||
for _, candidate := range candidates {
|
||||
if !strings.HasPrefix(candidate.Name(), s.c.BackupPruningPrefix) {
|
||||
continue
|
||||
}
|
||||
if candidate.ModTime().Before(deadline) {
|
||||
matches = append(matches, candidate.Name())
|
||||
}
|
||||
}
|
||||
|
||||
s.stats.Storages.SSH = StorageStats{
|
||||
Total: uint(len(candidates)),
|
||||
Pruned: uint(len(matches)),
|
||||
}
|
||||
|
||||
doPrune(len(matches), len(candidates), "SSH backup(s)", func() error {
|
||||
for _, match := range matches {
|
||||
if err := s.sftpClient.Remove(filepath.Join(s.c.SSHRemotePath, match)); err != nil {
|
||||
return fmt.Errorf("pruneBackups: error removing file from SSH storage: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
||||
globPattern := path.Join(
|
||||
s.c.BackupArchive,
|
||||
|
Reference in New Issue
Block a user