mirror of
https://github.com/gilbertchen/duplicacy
synced 2025-12-06 00:03:38 +00:00
Add a new Samba backend
The storage url is smb://user@server[:port]/share/path. The password can be set in the environment variable DUPLICACY_SMB_PASSWORD for default storage or DUPLICACY_<STORAGE_NAME>_SMB_PASSWORD. This backend is based on https://github.com/hirochachacha/go-smb2. The previous samba:// backend is just an alias for the disk-based backend with caching enabled.
This commit is contained in:
250
src/duplicacy_sambastorage.go
Normal file
250
src/duplicacy_sambastorage.go
Normal file
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) Acrosync LLC. All rights reserved.
|
||||
// Free for personal use and commercial trial
|
||||
// Commercial use requires per-user licenses available from https://duplicacy.com
|
||||
|
||||
package duplicacy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"fmt"
|
||||
"net"
|
||||
"path"
|
||||
"time"
|
||||
"strings"
|
||||
"syscall"
|
||||
"math/rand"
|
||||
|
||||
"github.com/hirochachacha/go-smb2"
|
||||
)
|
||||
|
||||
// SambaStorage is a local on-disk file storage implementing the Storage interface.
|
||||
type SambaStorage struct {
|
||||
StorageBase
|
||||
|
||||
share *smb2.Share
|
||||
storageDir string
|
||||
numberOfThreads int
|
||||
}
|
||||
|
||||
// CreateSambaStorage creates a file storage.
|
||||
func CreateSambaStorage(server string, port int, username string, password string, shareName string, storageDir string, threads int) (storage *SambaStorage, err error) {
|
||||
|
||||
connection, err := net.Dial("tcp", fmt.Sprintf("%s:%d", server, port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer := &smb2.Dialer{
|
||||
Initiator: &smb2.NTLMInitiator{
|
||||
User: username,
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
|
||||
client, err := dialer.Dial(connection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
share, err := client.Mount(shareName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Random number fo generating the temporary chunk file suffix.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
storage = &SambaStorage{
|
||||
share: share,
|
||||
numberOfThreads: threads,
|
||||
}
|
||||
|
||||
exist, isDir, _, err := storage.GetFileInfo(0, storageDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to check the storage path %s: %v", storageDir, err)
|
||||
}
|
||||
|
||||
if !exist {
|
||||
return nil, fmt.Errorf("The storage path %s does not exist", storageDir)
|
||||
}
|
||||
|
||||
if !isDir {
|
||||
return nil, fmt.Errorf("The storage path %s is not a directory", storageDir)
|
||||
}
|
||||
|
||||
storage.storageDir = storageDir
|
||||
storage.DerivedStorage = storage
|
||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively).
|
||||
func (storage *SambaStorage) ListFiles(threadIndex int, dir string) (files []string, sizes []int64, err error) {
|
||||
|
||||
fullPath := path.Join(storage.storageDir, dir)
|
||||
|
||||
list, err := storage.share.ReadDir(fullPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, f := range list {
|
||||
name := f.Name()
|
||||
if (f.IsDir() || f.Mode() & os.ModeSymlink != 0) && name[len(name)-1] != '/' {
|
||||
name += "/"
|
||||
}
|
||||
files = append(files, name)
|
||||
sizes = append(sizes, f.Size())
|
||||
}
|
||||
|
||||
return files, sizes, nil
|
||||
}
|
||||
|
||||
// DeleteFile deletes the file or directory at 'filePath'.
|
||||
func (storage *SambaStorage) DeleteFile(threadIndex int, filePath string) (err error) {
|
||||
err = storage.share.Remove(path.Join(storage.storageDir, filePath))
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// MoveFile renames the file.
|
||||
func (storage *SambaStorage) MoveFile(threadIndex int, from string, to string) (err error) {
|
||||
return storage.share.Rename(path.Join(storage.storageDir, from), path.Join(storage.storageDir, to))
|
||||
}
|
||||
|
||||
// CreateDirectory creates a new directory.
|
||||
func (storage *SambaStorage) CreateDirectory(threadIndex int, dir string) (err error) {
|
||||
fmt.Printf("Creating directory %s\n", dir)
|
||||
err = storage.share.Mkdir(path.Join(storage.storageDir, dir), 0744)
|
||||
if err != nil && os.IsExist(err) {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// GetFileInfo returns the information about the file or directory at 'filePath'.
|
||||
func (storage *SambaStorage) GetFileInfo(threadIndex int, filePath string) (exist bool, isDir bool, size int64, err error) {
|
||||
stat, err := storage.share.Stat(path.Join(storage.storageDir, filePath))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, false, 0, nil
|
||||
} else {
|
||||
return false, false, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, stat.IsDir(), stat.Size(), nil
|
||||
}
|
||||
|
||||
// DownloadFile reads the file at 'filePath' into the chunk.
|
||||
func (storage *SambaStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
|
||||
|
||||
file, err := storage.share.Open(path.Join(storage.storageDir, filePath))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
if _, err = RateLimitedCopy(chunk, file, storage.DownloadRateLimit/storage.numberOfThreads); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// UploadFile writes 'content' to the file at 'filePath'
|
||||
func (storage *SambaStorage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {
|
||||
|
||||
fullPath := path.Join(storage.storageDir, filePath)
|
||||
|
||||
if len(strings.Split(filePath, "/")) > 2 {
|
||||
dir := path.Dir(fullPath)
|
||||
stat, err := storage.share.Stat(dir)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
err = storage.share.MkdirAll(dir, 0744)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if !stat.IsDir() && stat.Mode() & os.ModeSymlink == 0 {
|
||||
return fmt.Errorf("The path %s is not a directory or symlink", dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
letters := "abcdefghijklmnopqrstuvwxyz"
|
||||
suffix := make([]byte, 8)
|
||||
for i := range suffix {
|
||||
suffix[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
|
||||
temporaryFile := fullPath + "." + string(suffix) + ".tmp"
|
||||
|
||||
file, err := storage.share.Create(temporaryFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := CreateRateLimitedReader(content, storage.UploadRateLimit/storage.numberOfThreads)
|
||||
_, err = io.Copy(file, reader)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = file.Sync(); err != nil {
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
isNotSupported := ok && pathErr.Op == "sync" && pathErr.Err == syscall.ENOTSUP
|
||||
if !isNotSupported {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = storage.share.Rename(temporaryFile, fullPath)
|
||||
if err != nil {
|
||||
|
||||
if _, e := storage.share.Stat(fullPath); e == nil {
|
||||
storage.share.Remove(temporaryFile)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a local snapshot cache is needed for the storage to avoid downloading/uploading chunks too often when
|
||||
// managing snapshots.
|
||||
func (storage *SambaStorage) IsCacheNeeded() bool { return true }
|
||||
|
||||
// If the 'MoveFile' method is implemented.
|
||||
func (storage *SambaStorage) IsMoveFileImplemented() bool { return true }
|
||||
|
||||
// If the storage can guarantee strong consistency.
|
||||
func (storage *SambaStorage) IsStrongConsistent() bool { return true }
|
||||
|
||||
// If the storage supports fast listing of files names.
|
||||
func (storage *SambaStorage) IsFastListing() bool { return false }
|
||||
|
||||
// Enable the test mode.
|
||||
func (storage *SambaStorage) EnableTestMode() {}
|
||||
@@ -757,6 +757,43 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
|
||||
return nil
|
||||
}
|
||||
return storjStorage
|
||||
} else if matched[1] == "smb" {
|
||||
server := matched[3]
|
||||
username := matched[2]
|
||||
if username == "" {
|
||||
LOG_ERROR("STORAGE_CREATE", "No username is provided to access the SAMBA storage")
|
||||
return nil
|
||||
}
|
||||
username = username[:len(username)-1]
|
||||
storageDir := matched[5]
|
||||
port := 445
|
||||
|
||||
if strings.Contains(server, ":") {
|
||||
index := strings.Index(server, ":")
|
||||
port, _ = strconv.Atoi(server[index+1:])
|
||||
server = server[:index]
|
||||
}
|
||||
|
||||
if !strings.Contains(storageDir, "/") {
|
||||
LOG_ERROR("STORAGE_CREATE", "No share name specified for the SAMBA storage")
|
||||
return nil
|
||||
}
|
||||
|
||||
index := strings.Index(storageDir, "/")
|
||||
shareName := storageDir[:index]
|
||||
storageDir = storageDir[index+1:]
|
||||
|
||||
prompt := fmt.Sprintf("Enter the SAMBA password:")
|
||||
password := GetPassword(preference, "smb_password", prompt, true, resetPassword)
|
||||
sambaStorage, err := CreateSambaStorage(server, port, username, password, shareName, storageDir, threads)
|
||||
if err != nil {
|
||||
LOG_ERROR("STORAGE_CREATE", "Failed to load the SAMBA storage at %s: %v", storageURL, err)
|
||||
return nil
|
||||
}
|
||||
SavePassword(preference, "smb_password", password)
|
||||
return sambaStorage
|
||||
|
||||
|
||||
} else {
|
||||
LOG_ERROR("STORAGE_CREATE", "The storage type '%s' is not supported", matched[1])
|
||||
return nil
|
||||
|
||||
@@ -136,15 +136,15 @@ func loadStorage(localStoragePath string, threads int) (Storage, error) {
|
||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||
return storage, err
|
||||
} else if *testStorageName == "one" {
|
||||
storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads)
|
||||
storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads, "", "", "")
|
||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||
return storage, err
|
||||
} else if *testStorageName == "odb" {
|
||||
storage, err := CreateOneDriveStorage(config["token_file"], true, config["storage_path"], threads)
|
||||
storage, err := CreateOneDriveStorage(config["token_file"], true, config["storage_path"], threads, "", "", "")
|
||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||
return storage, err
|
||||
} else if *testStorageName == "one" {
|
||||
storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads)
|
||||
storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads, "", "", "")
|
||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||
return storage, err
|
||||
} else if *testStorageName == "hubic" {
|
||||
@@ -176,6 +176,21 @@ func loadStorage(localStoragePath string, threads int) (Storage, error) {
|
||||
}
|
||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||
return storage, err
|
||||
} else if *testStorageName == "storj" {
|
||||
storage, err := CreateStorjStorage(config["satellite"], config["key"], config["passphrase"], config["bucket"], config["storage_path"], threads)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||
return storage, err
|
||||
} else if *testStorageName == "smb" {
|
||||
port, _ := strconv.Atoi(config["port"])
|
||||
storage, err := CreateSambaStorage(config["server"], port, config["username"], config["password"], config["share"], config["storage_path"], threads)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||
return storage, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Invalid storage named: %s", *testStorageName)
|
||||
|
||||
Reference in New Issue
Block a user