Skip to content

Commit 3a81c10

Browse files
committed
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.
1 parent cdf8f5a commit 3a81c10

File tree

3 files changed

+305
-3
lines changed

3 files changed

+305
-3
lines changed

src/duplicacy_sambastorage.go

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Copyright (c) Acrosync LLC. All rights reserved.
2+
// Free for personal use and commercial trial
3+
// Commercial use requires per-user licenses available from https://duplicacy.com
4+
5+
package duplicacy
6+
7+
import (
8+
"io"
9+
"os"
10+
"fmt"
11+
"net"
12+
"path"
13+
"time"
14+
"strings"
15+
"syscall"
16+
"math/rand"
17+
18+
"github.com/hirochachacha/go-smb2"
19+
)
20+
21+
// SambaStorage is a local on-disk file storage implementing the Storage interface.
22+
type SambaStorage struct {
23+
StorageBase
24+
25+
share *smb2.Share
26+
storageDir string
27+
numberOfThreads int
28+
}
29+
30+
// CreateSambaStorage creates a file storage.
31+
func CreateSambaStorage(server string, port int, username string, password string, shareName string, storageDir string, threads int) (storage *SambaStorage, err error) {
32+
33+
connection, err := net.Dial("tcp", fmt.Sprintf("%s:%d", server, port))
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
dialer := &smb2.Dialer{
39+
Initiator: &smb2.NTLMInitiator{
40+
User: username,
41+
Password: password,
42+
},
43+
}
44+
45+
client, err := dialer.Dial(connection)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
share, err := client.Mount(shareName)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
// Random number fo generating the temporary chunk file suffix.
56+
rand.Seed(time.Now().UnixNano())
57+
58+
storage = &SambaStorage{
59+
share: share,
60+
numberOfThreads: threads,
61+
}
62+
63+
exist, isDir, _, err := storage.GetFileInfo(0, storageDir)
64+
if err != nil {
65+
return nil, fmt.Errorf("Failed to check the storage path %s: %v", storageDir, err)
66+
}
67+
68+
if !exist {
69+
return nil, fmt.Errorf("The storage path %s does not exist", storageDir)
70+
}
71+
72+
if !isDir {
73+
return nil, fmt.Errorf("The storage path %s is not a directory", storageDir)
74+
}
75+
76+
storage.storageDir = storageDir
77+
storage.DerivedStorage = storage
78+
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
79+
return storage, nil
80+
}
81+
82+
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively).
83+
func (storage *SambaStorage) ListFiles(threadIndex int, dir string) (files []string, sizes []int64, err error) {
84+
85+
fullPath := path.Join(storage.storageDir, dir)
86+
87+
list, err := storage.share.ReadDir(fullPath)
88+
if err != nil {
89+
if os.IsNotExist(err) {
90+
return nil, nil, nil
91+
}
92+
return nil, nil, err
93+
}
94+
95+
for _, f := range list {
96+
name := f.Name()
97+
if (f.IsDir() || f.Mode() & os.ModeSymlink != 0) && name[len(name)-1] != '/' {
98+
name += "/"
99+
}
100+
files = append(files, name)
101+
sizes = append(sizes, f.Size())
102+
}
103+
104+
return files, sizes, nil
105+
}
106+
107+
// DeleteFile deletes the file or directory at 'filePath'.
108+
func (storage *SambaStorage) DeleteFile(threadIndex int, filePath string) (err error) {
109+
err = storage.share.Remove(path.Join(storage.storageDir, filePath))
110+
if err == nil || os.IsNotExist(err) {
111+
return nil
112+
} else {
113+
return err
114+
}
115+
}
116+
117+
// MoveFile renames the file.
118+
func (storage *SambaStorage) MoveFile(threadIndex int, from string, to string) (err error) {
119+
return storage.share.Rename(path.Join(storage.storageDir, from), path.Join(storage.storageDir, to))
120+
}
121+
122+
// CreateDirectory creates a new directory.
123+
func (storage *SambaStorage) CreateDirectory(threadIndex int, dir string) (err error) {
124+
fmt.Printf("Creating directory %s\n", dir)
125+
err = storage.share.Mkdir(path.Join(storage.storageDir, dir), 0744)
126+
if err != nil && os.IsExist(err) {
127+
return nil
128+
} else {
129+
return err
130+
}
131+
}
132+
133+
// GetFileInfo returns the information about the file or directory at 'filePath'.
134+
func (storage *SambaStorage) GetFileInfo(threadIndex int, filePath string) (exist bool, isDir bool, size int64, err error) {
135+
stat, err := storage.share.Stat(path.Join(storage.storageDir, filePath))
136+
if err != nil {
137+
if os.IsNotExist(err) {
138+
return false, false, 0, nil
139+
} else {
140+
return false, false, 0, err
141+
}
142+
}
143+
144+
return true, stat.IsDir(), stat.Size(), nil
145+
}
146+
147+
// DownloadFile reads the file at 'filePath' into the chunk.
148+
func (storage *SambaStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
149+
150+
file, err := storage.share.Open(path.Join(storage.storageDir, filePath))
151+
152+
if err != nil {
153+
return err
154+
}
155+
156+
defer file.Close()
157+
if _, err = RateLimitedCopy(chunk, file, storage.DownloadRateLimit/storage.numberOfThreads); err != nil {
158+
return err
159+
}
160+
161+
return nil
162+
163+
}
164+
165+
// UploadFile writes 'content' to the file at 'filePath'
166+
func (storage *SambaStorage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {
167+
168+
fullPath := path.Join(storage.storageDir, filePath)
169+
170+
if len(strings.Split(filePath, "/")) > 2 {
171+
dir := path.Dir(fullPath)
172+
stat, err := storage.share.Stat(dir)
173+
if err != nil {
174+
if !os.IsNotExist(err) {
175+
return err
176+
}
177+
err = storage.share.MkdirAll(dir, 0744)
178+
if err != nil {
179+
return err
180+
}
181+
} else {
182+
if !stat.IsDir() && stat.Mode() & os.ModeSymlink == 0 {
183+
return fmt.Errorf("The path %s is not a directory or symlink", dir)
184+
}
185+
}
186+
}
187+
188+
letters := "abcdefghijklmnopqrstuvwxyz"
189+
suffix := make([]byte, 8)
190+
for i := range suffix {
191+
suffix[i] = letters[rand.Intn(len(letters))]
192+
}
193+
194+
temporaryFile := fullPath + "." + string(suffix) + ".tmp"
195+
196+
file, err := storage.share.Create(temporaryFile)
197+
if err != nil {
198+
return err
199+
}
200+
201+
reader := CreateRateLimitedReader(content, storage.UploadRateLimit/storage.numberOfThreads)
202+
_, err = io.Copy(file, reader)
203+
if err != nil {
204+
file.Close()
205+
return err
206+
}
207+
208+
if err = file.Sync(); err != nil {
209+
pathErr, ok := err.(*os.PathError)
210+
isNotSupported := ok && pathErr.Op == "sync" && pathErr.Err == syscall.ENOTSUP
211+
if !isNotSupported {
212+
_ = file.Close()
213+
return err
214+
}
215+
}
216+
217+
err = file.Close()
218+
if err != nil {
219+
return err
220+
}
221+
222+
err = storage.share.Rename(temporaryFile, fullPath)
223+
if err != nil {
224+
225+
if _, e := storage.share.Stat(fullPath); e == nil {
226+
storage.share.Remove(temporaryFile)
227+
return nil
228+
} else {
229+
return err
230+
}
231+
}
232+
233+
return nil
234+
}
235+
236+
// If a local snapshot cache is needed for the storage to avoid downloading/uploading chunks too often when
237+
// managing snapshots.
238+
func (storage *SambaStorage) IsCacheNeeded() bool { return true }
239+
240+
// If the 'MoveFile' method is implemented.
241+
func (storage *SambaStorage) IsMoveFileImplemented() bool { return true }
242+
243+
// If the storage can guarantee strong consistency.
244+
func (storage *SambaStorage) IsStrongConsistent() bool { return true }
245+
246+
// If the storage supports fast listing of files names.
247+
func (storage *SambaStorage) IsFastListing() bool { return false }
248+
249+
// Enable the test mode.
250+
func (storage *SambaStorage) EnableTestMode() {}

src/duplicacy_storage.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,43 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
757757
return nil
758758
}
759759
return storjStorage
760+
} else if matched[1] == "smb" {
761+
server := matched[3]
762+
username := matched[2]
763+
if username == "" {
764+
LOG_ERROR("STORAGE_CREATE", "No username is provided to access the SAMBA storage")
765+
return nil
766+
}
767+
username = username[:len(username)-1]
768+
storageDir := matched[5]
769+
port := 445
770+
771+
if strings.Contains(server, ":") {
772+
index := strings.Index(server, ":")
773+
port, _ = strconv.Atoi(server[index+1:])
774+
server = server[:index]
775+
}
776+
777+
if !strings.Contains(storageDir, "/") {
778+
LOG_ERROR("STORAGE_CREATE", "No share name specified for the SAMBA storage")
779+
return nil
780+
}
781+
782+
index := strings.Index(storageDir, "/")
783+
shareName := storageDir[:index]
784+
storageDir = storageDir[index+1:]
785+
786+
prompt := fmt.Sprintf("Enter the SAMBA password:")
787+
password := GetPassword(preference, "smb_password", prompt, true, resetPassword)
788+
sambaStorage, err := CreateSambaStorage(server, port, username, password, shareName, storageDir, threads)
789+
if err != nil {
790+
LOG_ERROR("STORAGE_CREATE", "Failed to load the SAMBA storage at %s: %v", storageURL, err)
791+
return nil
792+
}
793+
SavePassword(preference, "smb_password", password)
794+
return sambaStorage
795+
796+
760797
} else {
761798
LOG_ERROR("STORAGE_CREATE", "The storage type '%s' is not supported", matched[1])
762799
return nil

src/duplicacy_storage_test.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,15 @@ func loadStorage(localStoragePath string, threads int) (Storage, error) {
136136
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
137137
return storage, err
138138
} else if *testStorageName == "one" {
139-
storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads)
139+
storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads, "", "", "")
140140
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
141141
return storage, err
142142
} else if *testStorageName == "odb" {
143-
storage, err := CreateOneDriveStorage(config["token_file"], true, config["storage_path"], threads)
143+
storage, err := CreateOneDriveStorage(config["token_file"], true, config["storage_path"], threads, "", "", "")
144144
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
145145
return storage, err
146146
} else if *testStorageName == "one" {
147-
storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads)
147+
storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads, "", "", "")
148148
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
149149
return storage, err
150150
} else if *testStorageName == "hubic" {
@@ -176,6 +176,21 @@ func loadStorage(localStoragePath string, threads int) (Storage, error) {
176176
}
177177
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
178178
return storage, err
179+
} else if *testStorageName == "storj" {
180+
storage, err := CreateStorjStorage(config["satellite"], config["key"], config["passphrase"], config["bucket"], config["storage_path"], threads)
181+
if err != nil {
182+
return nil, err
183+
}
184+
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
185+
return storage, err
186+
} else if *testStorageName == "smb" {
187+
port, _ := strconv.Atoi(config["port"])
188+
storage, err := CreateSambaStorage(config["server"], port, config["username"], config["password"], config["share"], config["storage_path"], threads)
189+
if err != nil {
190+
return nil, err
191+
}
192+
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
193+
return storage, err
179194
}
180195

181196
return nil, fmt.Errorf("Invalid storage named: %s", *testStorageName)

0 commit comments

Comments
 (0)