1
0
mirror of https://github.com/gilbertchen/duplicacy synced 2025-12-06 00:03:38 +00:00

Implement new chunk directory structure

This commit is contained in:
Gilbert Chen
2017-11-07 12:05:39 -05:00
parent 7e1fb6130a
commit 86767b3df6
22 changed files with 663 additions and 606 deletions

View File

@@ -69,7 +69,7 @@ func NewACDClient(tokenFile string) (*ACDClient, error) {
func (client *ACDClient) call(url string, method string, input interface{}, contentType string) (io.ReadCloser, int64, error) { func (client *ACDClient) call(url string, method string, input interface{}, contentType string) (io.ReadCloser, int64, error) {
LOG_DEBUG("ACD_CALL", "Calling %s", url) //LOG_DEBUG("ACD_CALL", "%s %s", method, url)
var response *http.Response var response *http.Response
@@ -256,7 +256,7 @@ type ACDListEntriesOutput struct {
Entries []ACDEntry `json:"data"` Entries []ACDEntry `json:"data"`
} }
func (client *ACDClient) ListEntries(parentID string, listFiles bool) ([]ACDEntry, error) { func (client *ACDClient) ListEntries(parentID string, listFiles bool, listDirectories bool) ([]ACDEntry, error) {
startToken := "" startToken := ""
@@ -264,20 +264,22 @@ func (client *ACDClient) ListEntries(parentID string, listFiles bool) ([]ACDEntr
for { for {
url := client.MetadataURL + "nodes/" + parentID + "/children?filters=" url := client.MetadataURL + "nodes/" + parentID + "/children?"
if listFiles { if listFiles && !listDirectories {
url += "kind:FILE" url += "filters=kind:FILE&"
} else { } else if !listFiles && listDirectories {
url += "kind:FOLDER" url += "filters=kind:FOLDER&"
} }
if startToken != "" { if startToken != "" {
url += "&startToken=" + startToken url += "startToken=" + startToken + "&"
} }
if client.TestMode { if client.TestMode {
url += "&limit=8" url += "limit=8"
} else {
url += "limit=200"
} }
readCloser, _, err := client.call(url, "GET", 0, "") readCloser, _, err := client.call(url, "GET", 0, "")

View File

@@ -103,7 +103,7 @@ func TestACDClient(t *testing.T) {
} }
} }
entries, err := acdClient.ListEntries(test1ID, true) entries, err := acdClient.ListEntries(test1ID, true, false)
if err != nil { if err != nil {
t.Errorf("Error list randomly generated files: %v", err) t.Errorf("Error list randomly generated files: %v", err)
return return
@@ -117,7 +117,7 @@ func TestACDClient(t *testing.T) {
} }
} }
entries, err = acdClient.ListEntries(test2ID, true) entries, err = acdClient.ListEntries(test2ID, true, false)
if err != nil { if err != nil {
t.Errorf("Error list randomly generated files: %v", err) t.Errorf("Error list randomly generated files: %v", err)
return return

View File

@@ -9,10 +9,11 @@ import (
"path" "path"
"strings" "strings"
"sync" "sync"
"time"
) )
type ACDStorage struct { type ACDStorage struct {
RateLimitedStorage StorageBase
client *ACDClient client *ACDClient
idCache map[string]string idCache map[string]string
@@ -35,11 +36,13 @@ func CreateACDStorage(tokenFile string, storagePath string, threads int) (storag
numberOfThreads: threads, numberOfThreads: threads,
} }
storagePathID, _, _, err := storage.getIDFromPath(0, storagePath) storagePathID, err := storage.getIDFromPath(0, storagePath, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Set 'storagePath' as the root of the storage and clean up the id cache accordingly
storage.idCache = make(map[string]string)
storage.idCache[""] = storagePathID storage.idCache[""] = storagePathID
for _, dir := range []string{"chunks", "fossils", "snapshots"} { for _, dir := range []string{"chunks", "fossils", "snapshots"} {
@@ -48,7 +51,6 @@ func CreateACDStorage(tokenFile string, storagePath string, threads int) (storag
return nil, err return nil, err
} }
if dirID == "" { if dirID == "" {
dirID, err = client.CreateDirectory(storagePathID, dir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -58,8 +60,9 @@ func CreateACDStorage(tokenFile string, storagePath string, threads int) (storag
storage.idCache[dir] = dirID storage.idCache[dir] = dirID
} }
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{0}, 0)
return storage, nil return storage, nil
} }
func (storage *ACDStorage) getPathID(path string) string { func (storage *ACDStorage) getPathID(path string) string {
@@ -88,6 +91,9 @@ func (storage *ACDStorage) deletePathID(path string) {
storage.idCacheLock.Unlock() storage.idCacheLock.Unlock()
} }
// convertFilePath converts the path for a fossil in the form of 'chunks/id.fsl' to 'fossils/id'. This is because
// ACD doesn't support file renaming. Instead, it only allows one file to be moved from one directory to another.
// By adding a layer of path conversion we're pretending that we can rename between 'chunks/id' and 'chunks/id.fsl'
func (storage *ACDStorage) convertFilePath(filePath string) string { func (storage *ACDStorage) convertFilePath(filePath string) string {
if strings.HasPrefix(filePath, "chunks/") && strings.HasSuffix(filePath, ".fsl") { if strings.HasPrefix(filePath, "chunks/") && strings.HasSuffix(filePath, ".fsl") {
return "fossils/" + filePath[len("chunks/"):len(filePath)-len(".fsl")] return "fossils/" + filePath[len("chunks/"):len(filePath)-len(".fsl")]
@@ -95,35 +101,80 @@ func (storage *ACDStorage) convertFilePath(filePath string) string {
return filePath return filePath
} }
func (storage *ACDStorage) getIDFromPath(threadIndex int, path string) (fileID string, isDir bool, size int64, err error) { // getIDFromPath returns the id of the given path. If 'createDirectories' is true, create the given path and all its
// parent directories if they don't exist. Note that if 'createDirectories' is false, it may return an empty 'fileID'
// if the file doesn't exist.
func (storage *ACDStorage) getIDFromPath(threadIndex int, filePath string, createDirectories bool) (fileID string, err error) {
if fileID, ok := storage.findPathID(filePath); ok {
return fileID, nil
}
parentID, ok := storage.findPathID("") parentID, ok := storage.findPathID("")
if !ok { if !ok {
parentID, isDir, size, err = storage.client.ListByName("", "") parentID, _, _, err = storage.client.ListByName("", "")
if err != nil { if err != nil {
return "", false, 0, err return "", err
} }
storage.savePathID("", parentID)
} }
names := strings.Split(path, "/") names := strings.Split(filePath, "/")
current := ""
for i, name := range names { for i, name := range names {
parentID, isDir, _, err = storage.client.ListByName(parentID, name)
if err != nil { current = path.Join(current, name)
return "", false, 0, err fileID, ok := storage.findPathID(current)
if ok {
parentID = fileID
continue
} }
if parentID == "" { isDir := false
if i == len(names)-1 { fileID, isDir, _, err = storage.client.ListByName(parentID, name)
return "", false, 0, nil if err != nil {
} else { return "", err
return "", false, 0, fmt.Errorf("File path '%s' does not exist", path) }
if fileID == "" {
if !createDirectories {
return "", nil
} }
// Create the current directory
fileID, err = storage.client.CreateDirectory(parentID, name)
if err != nil {
// Check if the directory has been created by another thread
if e, ok := err.(ACDError); !ok || e.Status != 409 {
return "", fmt.Errorf("Failed to create directory '%s': %v", current, err)
}
// A 409 means the directory may have already created by another thread. Wait 10 seconds
// until we seed the directory.
for i := 0; i < 10; i++ {
var createErr error
fileID, isDir, _, createErr = storage.client.ListByName(parentID, name)
if createErr != nil {
return "", createErr
}
if fileID == "" {
time.Sleep(time.Second)
} else {
break
}
}
if fileID == "" {
return "", fmt.Errorf("All attempts to create directory '%s' failed: %v", current, err)
}
} else {
isDir = true
}
} else {
storage.savePathID(current, fileID)
} }
if i != len(names)-1 && !isDir { if i != len(names)-1 && !isDir {
return "", false, 0, fmt.Errorf("Invalid path %s", path) return "", fmt.Errorf("Path '%s' is not a directory", current)
} }
parentID = fileID
} }
return parentID, isDir, size, err return parentID, nil
} }
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively) // ListFiles return the list of files and subdirectories under 'dir' (non-recursively)
@@ -136,7 +187,7 @@ func (storage *ACDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
if dir == "snapshots" { if dir == "snapshots" {
entries, err := storage.client.ListEntries(storage.getPathID(dir), false) entries, err := storage.client.ListEntries(storage.getPathID(dir), false, true)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -159,9 +210,10 @@ func (storage *ACDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
if pathID == "" { if pathID == "" {
return nil, nil, nil return nil, nil, nil
} }
storage.savePathID(dir, pathID)
} }
entries, err := storage.client.ListEntries(pathID, true) entries, err := storage.client.ListEntries(pathID, true, false)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -176,21 +228,33 @@ func (storage *ACDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
} else { } else {
files := []string{} files := []string{}
sizes := []int64{} sizes := []int64{}
for _, parent := range []string{"chunks", "fossils"} { parents := []string{"chunks", "fossils"}
entries, err := storage.client.ListEntries(storage.getPathID(parent), true) for i := 0; i < len(parents); i++ {
parent := parents[i]
pathID, ok := storage.findPathID(parent)
if !ok {
continue
}
entries, err := storage.client.ListEntries(pathID, true, true)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
for _, entry := range entries { for _, entry := range entries {
name := entry.Name if entry.Kind != "FOLDER" {
if parent == "fossils" { name := entry.Name
name += ".fsl" if strings.HasPrefix(parent, "fossils") {
name = parent + "/" + name + ".fsl"
name = name[len("fossils/"):]
} else {
name = parent + "/" + name
name = name[len("chunks/"):]
}
files = append(files, name)
sizes = append(sizes, entry.Size)
} else {
parents = append(parents, parent+"/"+entry.Name)
} }
storage.savePathID(parent+"/"+entry.Name, entry.ID) storage.savePathID(parent+"/"+entry.Name, entry.ID)
files = append(files, name)
sizes = append(sizes, entry.Size)
} }
} }
return files, sizes, nil return files, sizes, nil
@@ -201,17 +265,13 @@ func (storage *ACDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
// DeleteFile deletes the file or directory at 'filePath'. // DeleteFile deletes the file or directory at 'filePath'.
func (storage *ACDStorage) DeleteFile(threadIndex int, filePath string) (err error) { func (storage *ACDStorage) DeleteFile(threadIndex int, filePath string) (err error) {
filePath = storage.convertFilePath(filePath) filePath = storage.convertFilePath(filePath)
fileID, ok := storage.findPathID(filePath) fileID, err := storage.getIDFromPath(threadIndex, filePath, false)
if !ok { if err != nil {
fileID, _, _, err = storage.getIDFromPath(threadIndex, filePath) return err
if err != nil { }
return err if fileID == "" {
} LOG_TRACE("ACD_STORAGE", "File '%s' to be deleted does not exist", filePath)
if fileID == "" { return nil
LOG_TRACE("ACD_STORAGE", "File %s has disappeared before deletion", filePath)
return nil
}
storage.savePathID(filePath, fileID)
} }
err = storage.client.DeleteFile(fileID) err = storage.client.DeleteFile(fileID)
@@ -232,11 +292,19 @@ func (storage *ACDStorage) MoveFile(threadIndex int, from string, to string) (er
return fmt.Errorf("Attempting to rename file %s with unknown id", from) return fmt.Errorf("Attempting to rename file %s with unknown id", from)
} }
fromParentID := storage.getPathID("chunks") fromParent := path.Dir(from)
toParentID := storage.getPathID("fossils") fromParentID, err := storage.getIDFromPath(threadIndex, fromParent, false)
if err != nil {
return fmt.Errorf("Failed to retrieve the id of the parent directory '%s': %v", fromParent, err)
}
if fromParentID == "" {
return fmt.Errorf("The parent directory '%s' does not exist", fromParent)
}
if strings.HasPrefix(from, "fossils") { toParent := path.Dir(to)
fromParentID, toParentID = toParentID, fromParentID toParentID, err := storage.getIDFromPath(threadIndex, toParent, true)
if err != nil {
return fmt.Errorf("Failed to retrieve the id of the parent directory '%s': %v", toParent, err)
} }
err = storage.client.MoveFile(fileID, fromParentID, toParentID) err = storage.client.MoveFile(fileID, fromParentID, toParentID)
@@ -261,24 +329,25 @@ func (storage *ACDStorage) CreateDirectory(threadIndex int, dir string) (err err
dir = dir[:len(dir)-1] dir = dir[:len(dir)-1]
} }
if dir == "chunks" || dir == "snapshots" { parentPath := path.Dir(dir)
return nil if parentPath == "." {
parentPath = ""
}
parentID, ok := storage.findPathID(parentPath)
if !ok {
return fmt.Errorf("Path directory '%s' has unknown id", parentPath)
} }
if strings.HasPrefix(dir, "snapshots/") { name := path.Base(dir)
name := dir[len("snapshots/"):] dirID, err := storage.client.CreateDirectory(parentID, name)
dirID, err := storage.client.CreateDirectory(storage.getPathID("snapshots"), name) if err != nil {
if err != nil { if e, ok := err.(ACDError); ok && e.Status == 409 {
if e, ok := err.(ACDError); ok && e.Status == 409 { return nil
return nil } else {
} else { return err
return err
}
} }
storage.savePathID(dir, dirID)
return nil
} }
storage.savePathID(dir, dirID)
return nil return nil
} }
@@ -291,8 +360,21 @@ func (storage *ACDStorage) GetFileInfo(threadIndex int, filePath string) (exist
} }
filePath = storage.convertFilePath(filePath) filePath = storage.convertFilePath(filePath)
fileID := ""
fileID, isDir, size, err = storage.getIDFromPath(threadIndex, filePath) parentPath := path.Dir(filePath)
if parentPath == "." {
parentPath = ""
}
parentID, err := storage.getIDFromPath(threadIndex, parentPath, false)
if err != nil {
return false, false, 0, err
}
if parentID == "" {
return false, false, 0, nil
}
name := path.Base(filePath)
fileID, isDir, size, err := storage.client.ListByName(parentID, name)
if err != nil { if err != nil {
return false, false, 0, err return false, false, 0, err
} }
@@ -300,43 +382,18 @@ func (storage *ACDStorage) GetFileInfo(threadIndex int, filePath string) (exist
return false, false, 0, nil return false, false, 0, nil
} }
storage.savePathID(filePath, fileID)
return true, isDir, size, nil return true, isDir, size, nil
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *ACDStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
parentID := ""
filePath = "chunks/" + chunkID
realPath := filePath
if isFossil {
parentID = storage.getPathID("fossils")
filePath += ".fsl"
realPath = "fossils/" + chunkID + ".fsl"
} else {
parentID = storage.getPathID("chunks")
}
fileID := ""
fileID, _, size, err = storage.client.ListByName(parentID, chunkID)
if fileID != "" {
storage.savePathID(realPath, fileID)
}
return filePath, fileID != "", size, err
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *ACDStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *ACDStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
fileID, ok := storage.findPathID(filePath) fileID, err := storage.getIDFromPath(threadIndex, filePath, false)
if !ok { if err != nil {
fileID, _, _, err = storage.getIDFromPath(threadIndex, filePath) return err
if err != nil { }
return err if fileID == "" {
} return fmt.Errorf("File path '%s' does not exist", filePath)
if fileID == "" {
return fmt.Errorf("File path '%s' does not exist", filePath)
}
storage.savePathID(filePath, fileID)
} }
readCloser, _, err := storage.client.DownloadFile(fileID) readCloser, _, err := storage.client.DownloadFile(fileID)
@@ -353,22 +410,16 @@ func (storage *ACDStorage) DownloadFile(threadIndex int, filePath string, chunk
// UploadFile writes 'content' to the file at 'filePath'. // UploadFile writes 'content' to the file at 'filePath'.
func (storage *ACDStorage) UploadFile(threadIndex int, filePath string, content []byte) (err error) { func (storage *ACDStorage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {
parent := path.Dir(filePath) parent := path.Dir(filePath)
if parent == "." { if parent == "." {
parent = "" parent = ""
} }
parentID, ok := storage.findPathID(parent) parentID, err := storage.getIDFromPath(threadIndex, parent, true)
if err != nil {
if !ok { return err
parentID, _, _, err = storage.getIDFromPath(threadIndex, parent) }
if err != nil { if parentID == "" {
return err return fmt.Errorf("File path '%s' does not exist", parent)
}
if parentID == "" {
return fmt.Errorf("File path '%s' does not exist", parent)
}
storage.savePathID(parent, parentID)
} }
fileID, err := storage.client.UploadFile(parentID, path.Base(filePath), content, storage.UploadRateLimit/storage.numberOfThreads) fileID, err := storage.client.UploadFile(parentID, path.Base(filePath), content, storage.UploadRateLimit/storage.numberOfThreads)

View File

@@ -12,7 +12,7 @@ import (
) )
type AzureStorage struct { type AzureStorage struct {
RateLimitedStorage StorageBase
containers []*storage.Container containers []*storage.Container
} }
@@ -47,6 +47,8 @@ func CreateAzureStorage(accountName string, accountKey string,
containers: containers, containers: containers,
} }
azureStorage.DerivedStorage = azureStorage
azureStorage.SetDefaultNestingLevels([]int{0}, 0)
return return
} }
@@ -149,23 +151,6 @@ func (storage *AzureStorage) GetFileInfo(threadIndex int, filePath string) (exis
return true, false, blob.Properties.ContentLength, nil return true, false, blob.Properties.ContentLength, nil
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *AzureStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
filePath = "chunks/" + chunkID
if isFossil {
filePath += ".fsl"
}
exist, _, size, err = storage.GetFileInfo(threadIndex, filePath)
if err != nil {
return "", false, 0, err
} else {
return filePath, exist, size, err
}
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *AzureStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *AzureStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
readCloser, err := storage.containers[threadIndex].GetBlobReference(filePath).Get(nil) readCloser, err := storage.containers[threadIndex].GetBlobReference(filePath).Get(nil)

View File

@@ -9,7 +9,7 @@ import (
) )
type B2Storage struct { type B2Storage struct {
RateLimitedStorage StorageBase
clients []*B2Client clients []*B2Client
} }
@@ -38,6 +38,9 @@ func CreateB2Storage(accountID string, applicationKey string, bucket string, thr
storage = &B2Storage{ storage = &B2Storage{
clients: clients, clients: clients,
} }
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{0}, 0)
return storage, nil return storage, nil
} }
@@ -204,17 +207,6 @@ func (storage *B2Storage) GetFileInfo(threadIndex int, filePath string) (exist b
return true, false, entries[0].Size, nil return true, false, entries[0].Size, nil
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *B2Storage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
filePath = "chunks/" + chunkID
if isFossil {
filePath += ".fsl"
}
exist, _, size, err = storage.GetFileInfo(threadIndex, filePath)
return filePath, exist, size, err
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *B2Storage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *B2Storage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {

View File

@@ -79,7 +79,7 @@ func (manager *BackupManager) SetupSnapshotCache(storageName string) bool {
preferencePath := GetDuplicacyPreferencePath() preferencePath := GetDuplicacyPreferencePath()
cacheDir := path.Join(preferencePath, "cache", storageName) cacheDir := path.Join(preferencePath, "cache", storageName)
storage, err := CreateFileStorage(cacheDir, 2, false, 1) storage, err := CreateFileStorage(cacheDir, false, 1)
if err != nil { if err != nil {
LOG_ERROR("BACKUP_CACHE", "Failed to create the snapshot cache dir: %v", err) LOG_ERROR("BACKUP_CACHE", "Failed to create the snapshot cache dir: %v", err)
return false return false
@@ -93,6 +93,7 @@ func (manager *BackupManager) SetupSnapshotCache(storageName string) bool {
} }
} }
storage.SetDefaultNestingLevels([]int{1}, 1)
manager.snapshotCache = storage manager.snapshotCache = storage
manager.SnapshotManager.snapshotCache = storage manager.SnapshotManager.snapshotCache = storage
return true return true

View File

@@ -336,6 +336,30 @@ func TestBackupManager(t *testing.T) {
} }
} }
numberOfSnapshots := backupManager.SnapshotManager.ListSnapshots( /*snapshotID*/ "host1" /*revisionsToList*/, nil /*tag*/, "" /*showFiles*/, false /*showChunks*/, false)
if numberOfSnapshots != 3 {
t.Errorf("Expected 3 snapshots but got %d", numberOfSnapshots)
}
backupManager.SnapshotManager.CheckSnapshots( /*snapshotID*/ "host1" /*revisions*/, []int{1, 2, 3} /*tag*/, "",
/*showStatistics*/ false /*showTabular*/, false /*checkFiles*/, false /*searchFossils*/, false /*resurrect*/, false)
backupManager.SnapshotManager.PruneSnapshots("host1", "host1" /*revisions*/, []int{1} /*tags*/, nil /*retentions*/, nil,
/*exhaustive*/ false /*exclusive=*/, false /*ignoredIDs*/, nil /*dryRun*/, false /*deleteOnly*/, false /*collectOnly*/, false)
numberOfSnapshots = backupManager.SnapshotManager.ListSnapshots( /*snapshotID*/ "host1" /*revisionsToList*/, nil /*tag*/, "" /*showFiles*/, false /*showChunks*/, false)
if numberOfSnapshots != 2 {
t.Errorf("Expected 2 snapshots but got %d", numberOfSnapshots)
}
backupManager.SnapshotManager.CheckSnapshots( /*snapshotID*/ "host1" /*revisions*/, []int{2, 3} /*tag*/, "",
/*showStatistics*/ false /*showTabular*/, false /*checkFiles*/, false /*searchFossils*/, false /*resurrect*/, false)
backupManager.Backup(testDir+"/repository1" /*quickMode=*/, false, threads, "fourth", false, false)
backupManager.SnapshotManager.PruneSnapshots("host1", "host1" /*revisions*/, nil /*tags*/, nil /*retentions*/, nil,
/*exhaustive*/ false /*exclusive=*/, true /*ignoredIDs*/, nil /*dryRun*/, false /*deleteOnly*/, false /*collectOnly*/, false)
numberOfSnapshots = backupManager.SnapshotManager.ListSnapshots( /*snapshotID*/ "host1" /*revisionsToList*/, nil /*tag*/, "" /*showFiles*/, false /*showChunks*/, false)
if numberOfSnapshots != 3 {
t.Errorf("Expected 3 snapshots but got %d", numberOfSnapshots)
}
backupManager.SnapshotManager.CheckSnapshots( /*snapshotID*/ "host1" /*revisions*/, []int{2, 3, 4} /*tag*/, "",
/*showStatistics*/ false /*showTabular*/, false /*checkFiles*/, false /*searchFossils*/, false /*resurrect*/, false)
/*buf := make([]byte, 1<<16) /*buf := make([]byte, 1<<16)
runtime.Stack(buf, true) runtime.Stack(buf, true)
fmt.Printf("%s", buf)*/ fmt.Printf("%s", buf)*/

View File

@@ -326,7 +326,7 @@ func (downloader *ChunkDownloader) Download(threadIndex int, task ChunkDownloadT
} }
const MaxDownloadAttempts = 3 const MaxDownloadAttempts = 3
for downloadAttempt := 0;; downloadAttempt++ { for downloadAttempt := 0; ; downloadAttempt++ {
err = downloader.storage.DownloadFile(threadIndex, chunkPath, chunk) err = downloader.storage.DownloadFile(threadIndex, chunkPath, chunk)
if err != nil { if err != nil {
if err == io.ErrUnexpectedEOF && downloadAttempt < MaxDownloadAttempts { if err == io.ErrUnexpectedEOF && downloadAttempt < MaxDownloadAttempts {

View File

@@ -46,6 +46,8 @@ type Config struct {
ChunkSeed []byte `json:"chunk-seed"` ChunkSeed []byte `json:"chunk-seed"`
FixedNesting bool `json:"fixed-nesting"`
// Use HMAC-SHA256(hashKey, plaintext) as the chunk hash. // Use HMAC-SHA256(hashKey, plaintext) as the chunk hash.
// Use HMAC-SHA256(idKey, chunk hash) as the file name of the chunk // Use HMAC-SHA256(idKey, chunk hash) as the file name of the chunk
// For chunks, use HMAC-SHA256(chunkKey, chunk hash) as the encryption key // For chunks, use HMAC-SHA256(chunkKey, chunk hash) as the encryption key
@@ -63,7 +65,7 @@ type Config struct {
// for encrypting a non-chunk file // for encrypting a non-chunk file
FileKey []byte `json:"-"` FileKey []byte `json:"-"`
chunkPool chan *Chunk `json:"-"` chunkPool chan *Chunk
numberOfChunks int32 numberOfChunks int32
dryRun bool dryRun bool
} }
@@ -148,6 +150,7 @@ func CreateConfigFromParameters(compressionLevel int, averageChunkSize int, maxi
AverageChunkSize: averageChunkSize, AverageChunkSize: averageChunkSize,
MaximumChunkSize: maximumChunkSize, MaximumChunkSize: maximumChunkSize,
MinimumChunkSize: mininumChunkSize, MinimumChunkSize: mininumChunkSize,
FixedNesting: true,
} }
if isEncrypted { if isEncrypted {
@@ -380,6 +383,8 @@ func DownloadConfig(storage Storage, password string) (config *Config, isEncrypt
return nil, false, fmt.Errorf("Failed to parse the config file: %v", err) return nil, false, fmt.Errorf("Failed to parse the config file: %v", err)
} }
storage.SetNestingLevels(config)
return config, false, nil return config, false, nil
} }

View File

@@ -6,18 +6,17 @@ package duplicacy
import ( import (
"fmt" "fmt"
"path"
"strings" "strings"
"github.com/gilbertchen/go-dropbox" "github.com/gilbertchen/go-dropbox"
) )
type DropboxStorage struct { type DropboxStorage struct {
RateLimitedStorage StorageBase
clients []*dropbox.Files clients []*dropbox.Files
minimumNesting int // The minimum level of directories to dive into before searching for the chunk file. minimumNesting int // The minimum level of directories to dive into before searching for the chunk file.
storageDir string storageDir string
} }
// CreateDropboxStorage creates a dropbox storage object. // CreateDropboxStorage creates a dropbox storage object.
@@ -38,9 +37,9 @@ func CreateDropboxStorage(accessToken string, storageDir string, minimumNesting
} }
storage = &DropboxStorage{ storage = &DropboxStorage{
clients: clients, clients: clients,
storageDir: storageDir, storageDir: storageDir,
minimumNesting: minimumNesting, minimumNesting: minimumNesting,
} }
err = storage.CreateDirectory(0, "") err = storage.CreateDirectory(0, "")
@@ -48,6 +47,8 @@ func CreateDropboxStorage(accessToken string, storageDir string, minimumNesting
return nil, fmt.Errorf("Can't create storage directory: %v", err) return nil, fmt.Errorf("Can't create storage directory: %v", err)
} }
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{1}, 1)
return storage, nil return storage, nil
} }
@@ -181,63 +182,6 @@ func (storage *DropboxStorage) GetFileInfo(threadIndex int, filePath string) (ex
return true, output.Tag == "folder", int64(output.Size), nil return true, output.Tag == "folder", int64(output.Size), nil
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *DropboxStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
dir := "/chunks"
suffix := ""
if isFossil {
suffix = ".fsl"
}
for level := 0; level*2 < len(chunkID); level++ {
if level >= storage.minimumNesting {
filePath = path.Join(dir, chunkID[2*level:]) + suffix
var size int64
exist, _, size, err = storage.GetFileInfo(threadIndex, filePath)
if err != nil {
return "", false, 0, err
}
if exist {
return filePath, exist, size, nil
}
}
// Find the subdirectory the chunk file may reside.
subDir := path.Join(dir, chunkID[2*level:2*level+2])
exist, _, _, err = storage.GetFileInfo(threadIndex, subDir)
if err != nil {
return "", false, 0, err
}
if exist {
dir = subDir
continue
}
if level < storage.minimumNesting {
// Create the subdirectory if it doesn't exist.
err = storage.CreateDirectory(threadIndex, subDir)
if err != nil {
return "", false, 0, err
}
dir = subDir
continue
}
// Teh chunk must be under this subdirectory but it doesn't exist.
return path.Join(dir, chunkID[2*level:])[1:] + suffix, false, 0, nil
}
LOG_FATAL("CHUNK_FIND", "Chunk %s is still not found after having searched a maximum level of directories",
chunkID)
return "", false, 0, nil
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *DropboxStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *DropboxStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {

View File

@@ -11,21 +11,21 @@ import (
"math/rand" "math/rand"
"os" "os"
"path" "path"
"strings"
"time" "time"
) )
// FileStorage is a local on-disk file storage implementing the Storage interface. // FileStorage is a local on-disk file storage implementing the Storage interface.
type FileStorage struct { type FileStorage struct {
RateLimitedStorage StorageBase
minimumNesting int // The minimum level of directories to dive into before searching for the chunk file.
isCacheNeeded bool // Network storages require caching isCacheNeeded bool // Network storages require caching
storageDir string storageDir string
numberOfThreads int numberOfThreads int
} }
// CreateFileStorage creates a file storage. // CreateFileStorage creates a file storage.
func CreateFileStorage(storageDir string, minimumNesting int, isCacheNeeded bool, threads int) (storage *FileStorage, err error) { func CreateFileStorage(storageDir string, isCacheNeeded bool, threads int) (storage *FileStorage, err error) {
var stat os.FileInfo var stat os.FileInfo
@@ -51,7 +51,6 @@ func CreateFileStorage(storageDir string, minimumNesting int, isCacheNeeded bool
storage = &FileStorage{ storage = &FileStorage{
storageDir: storageDir, storageDir: storageDir,
minimumNesting: minimumNesting,
isCacheNeeded: isCacheNeeded, isCacheNeeded: isCacheNeeded,
numberOfThreads: threads, numberOfThreads: threads,
} }
@@ -59,6 +58,8 @@ func CreateFileStorage(storageDir string, minimumNesting int, isCacheNeeded bool
// Random number fo generating the temporary chunk file suffix. // Random number fo generating the temporary chunk file suffix.
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, nil return storage, nil
} }
@@ -126,67 +127,6 @@ func (storage *FileStorage) GetFileInfo(threadIndex int, filePath string) (exist
return true, stat.IsDir(), stat.Size(), nil return true, stat.IsDir(), stat.Size(), nil
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with the
// suffix '.fsl'.
func (storage *FileStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
dir := path.Join(storage.storageDir, "chunks")
suffix := ""
if isFossil {
suffix = ".fsl"
}
for level := 0; level*2 < len(chunkID); level++ {
if level >= storage.minimumNesting {
filePath = path.Join(dir, chunkID[2*level:]) + suffix
// Use Lstat() instead of Stat() since 1) Stat() doesn't work for deduplicated disks on Windows and 2) there isn't
// really a need to follow the link if filePath is a link.
stat, err := os.Lstat(filePath)
if err != nil {
LOG_DEBUG("FS_FIND", "File %s can't be found: %v", filePath, err)
} else if stat.IsDir() {
return filePath[len(storage.storageDir)+1:], false, 0, fmt.Errorf("The path %s is a directory", filePath)
} else {
return filePath[len(storage.storageDir)+1:], true, stat.Size(), nil
}
}
// Find the subdirectory the chunk file may reside.
subDir := path.Join(dir, chunkID[2*level:2*level+2])
stat, err := os.Stat(subDir)
if err == nil && stat.IsDir() {
dir = subDir
continue
}
if level < storage.minimumNesting {
// Create the subdirectory if it doesn't exist.
if err == nil && !stat.IsDir() {
return "", false, 0, fmt.Errorf("The path %s is not a directory", subDir)
}
err = os.Mkdir(subDir, 0744)
if err != nil {
// The directory may have been created by other threads so check it again.
stat, _ := os.Stat(subDir)
if stat == nil || !stat.IsDir() {
return "", false, 0, err
}
}
dir = subDir
continue
}
// The chunk must be under this subdirectory but it doesn't exist.
return path.Join(dir, chunkID[2*level:])[len(storage.storageDir)+1:] + suffix, false, 0, nil
}
return "", false, 0, fmt.Errorf("The maximum level of directories searched")
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *FileStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *FileStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
@@ -210,6 +150,26 @@ func (storage *FileStorage) UploadFile(threadIndex int, filePath string, content
fullPath := path.Join(storage.storageDir, filePath) fullPath := path.Join(storage.storageDir, filePath)
if len(strings.Split(filePath, "/")) > 2 {
dir := path.Dir(fullPath)
// Use Lstat() instead of Stat() since 1) Stat() doesn't work for deduplicated disks on Windows and 2) there isn't
// really a need to follow the link if filePath is a link.
stat, err := os.Lstat(dir)
if err != nil {
if !os.IsNotExist(err) {
return err
}
err = os.MkdirAll(dir, 0744)
if err != nil {
return err
}
} else {
if !stat.IsDir() {
fmt.Errorf("The path %s is not a directory", dir)
}
}
}
letters := "abcdefghijklmnopqrstuvwxyz" letters := "abcdefghijklmnopqrstuvwxyz"
suffix := make([]byte, 8) suffix := make([]byte, 8)
for i := range suffix { for i := range suffix {

View File

@@ -25,17 +25,18 @@ import (
) )
type GCDStorage struct { type GCDStorage struct {
RateLimitedStorage StorageBase
service *drive.Service service *drive.Service
idCache map[string]string idCache map[string]string
idCacheLock *sync.Mutex idCacheLock sync.Mutex
backoffs []int // desired backoff time in seconds for each thread backoffs []int // desired backoff time in seconds for each thread
attempts []int // number of failed attempts since last success for each thread attempts []int // number of failed attempts since last success for each thread
isConnected bool createDirectoryLock sync.Mutex
numberOfThreads int isConnected bool
TestMode bool numberOfThreads int
TestMode bool
} }
type GCDConfig struct { type GCDConfig struct {
@@ -91,7 +92,11 @@ func (storage *GCDStorage) shouldRetry(threadIndex int, err error) (bool, error)
retry = err.Temporary() retry = err.Temporary()
} }
if !retry || storage.attempts[threadIndex] >= MAX_ATTEMPTS { if !retry {
return false, err
}
if storage.attempts[threadIndex] >= MAX_ATTEMPTS {
LOG_INFO("GCD_RETRY", "[%d] Maximum number of retries reached (backoff: %d, attempts: %d)", LOG_INFO("GCD_RETRY", "[%d] Maximum number of retries reached (backoff: %d, attempts: %d)",
threadIndex, storage.backoffs[threadIndex], storage.attempts[threadIndex]) threadIndex, storage.backoffs[threadIndex], storage.attempts[threadIndex])
storage.backoffs[threadIndex] = 1 storage.backoffs[threadIndex] = 1
@@ -114,6 +119,9 @@ func (storage *GCDStorage) shouldRetry(threadIndex int, err error) (bool, error)
return true, nil return true, nil
} }
// convertFilePath converts the path for a fossil in the form of 'chunks/id.fsl' to 'fossils/id'. This is because
// ACD doesn't support file renaming. Instead, it only allows one file to be moved from one directory to another.
// By adding a layer of path conversion we're pretending that we can rename between 'chunks/id' and 'chunks/id.fsl'
func (storage *GCDStorage) convertFilePath(filePath string) string { func (storage *GCDStorage) convertFilePath(filePath string) string {
if strings.HasPrefix(filePath, "chunks/") && strings.HasSuffix(filePath, ".fsl") { if strings.HasPrefix(filePath, "chunks/") && strings.HasSuffix(filePath, ".fsl") {
return "fossils/" + filePath[len("chunks/"):len(filePath)-len(".fsl")] return "fossils/" + filePath[len("chunks/"):len(filePath)-len(".fsl")]
@@ -147,7 +155,7 @@ func (storage *GCDStorage) deletePathID(path string) {
storage.idCacheLock.Unlock() storage.idCacheLock.Unlock()
} }
func (storage *GCDStorage) listFiles(threadIndex int, parentID string, listFiles bool) ([]*drive.File, error) { func (storage *GCDStorage) listFiles(threadIndex int, parentID string, listFiles bool, listDirectories bool) ([]*drive.File, error) {
if parentID == "" { if parentID == "" {
return nil, fmt.Errorf("No parent ID provided") return nil, fmt.Errorf("No parent ID provided")
@@ -157,11 +165,11 @@ func (storage *GCDStorage) listFiles(threadIndex int, parentID string, listFiles
startToken := "" startToken := ""
query := "'" + parentID + "' in parents and " query := "'" + parentID + "' in parents "
if listFiles { if listFiles && !listDirectories {
query += "mimeType != 'application/vnd.google-apps.folder'" query += "and mimeType != 'application/vnd.google-apps.folder'"
} else { } else if !listFiles && !listDirectories {
query += "mimeType = 'application/vnd.google-apps.folder'" query += "and mimeType = 'application/vnd.google-apps.folder'"
} }
maxCount := int64(1000) maxCount := int64(1000)
@@ -222,7 +230,14 @@ func (storage *GCDStorage) listByName(threadIndex int, parentID string, name str
return file.Id, file.MimeType == "application/vnd.google-apps.folder", file.Size, nil return file.Id, file.MimeType == "application/vnd.google-apps.folder", file.Size, nil
} }
func (storage *GCDStorage) getIDFromPath(threadIndex int, path string) (string, error) { // getIDFromPath returns the id of the given path. If 'createDirectories' is true, create the given path and all its
// parent directories if they don't exist. Note that if 'createDirectories' is false, it may return an empty 'fileID'
// if the file doesn't exist.
func (storage *GCDStorage) getIDFromPath(threadIndex int, filePath string, createDirectories bool) (string, error) {
if fileID, ok := storage.findPathID(filePath); ok {
return fileID, nil
}
fileID := "root" fileID := "root"
@@ -230,22 +245,18 @@ func (storage *GCDStorage) getIDFromPath(threadIndex int, path string) (string,
fileID = rootID fileID = rootID
} }
names := strings.Split(path, "/") names := strings.Split(filePath, "/")
current := "" current := ""
for i, name := range names { for i, name := range names {
// Find the intermediate directory in the cache first.
if len(current) == 0 { current = path.Join(current, name)
current = name
} else {
current = current + "/" + name
}
currentID, ok := storage.findPathID(current) currentID, ok := storage.findPathID(current)
if ok { if ok {
fileID = currentID fileID = currentID
continue continue
} }
// Check if the directory exists.
var err error var err error
var isDir bool var isDir bool
fileID, isDir, _, err = storage.listByName(threadIndex, fileID, name) fileID, isDir, _, err = storage.listByName(threadIndex, fileID, name)
@@ -253,10 +264,30 @@ func (storage *GCDStorage) getIDFromPath(threadIndex int, path string) (string,
return "", err return "", err
} }
if fileID == "" { if fileID == "" {
return "", fmt.Errorf("Path %s doesn't exist", path) if !createDirectories {
return "", nil
}
// Only one thread can create the directory at a time -- GCD allows multiple directories
// to have the same name but different ids.
storage.createDirectoryLock.Lock()
err = storage.CreateDirectory(threadIndex, current)
storage.createDirectoryLock.Unlock()
if err != nil {
return "", fmt.Errorf("Failed to create directory '%s': %v", current, err)
}
currentID, ok = storage.findPathID(current)
if !ok {
return "", fmt.Errorf("Directory '%s' created by id not found", current)
}
fileID = currentID
continue
} else {
storage.savePathID(current, fileID)
} }
if i != len(names)-1 && !isDir { if i != len(names)-1 && !isDir {
return "", fmt.Errorf("Invalid path %s", path) return "", fmt.Errorf("Path '%s' is not a directory", current)
} }
} }
return fileID, nil return fileID, nil
@@ -275,13 +306,13 @@ func CreateGCDStorage(tokenFile string, storagePath string, threads int) (storag
return nil, err return nil, err
} }
config := oauth2.Config{ oauth2Config := oauth2.Config{
ClientID: gcdConfig.ClientID, ClientID: gcdConfig.ClientID,
ClientSecret: gcdConfig.ClientSecret, ClientSecret: gcdConfig.ClientSecret,
Endpoint: gcdConfig.Endpoint, Endpoint: gcdConfig.Endpoint,
} }
authClient := config.Client(context.Background(), &gcdConfig.Token) authClient := oauth2Config.Client(context.Background(), &gcdConfig.Token)
service, err := drive.New(authClient) service, err := drive.New(authClient)
if err != nil { if err != nil {
@@ -292,7 +323,6 @@ func CreateGCDStorage(tokenFile string, storagePath string, threads int) (storag
service: service, service: service,
numberOfThreads: threads, numberOfThreads: threads,
idCache: make(map[string]string), idCache: make(map[string]string),
idCacheLock: &sync.Mutex{},
backoffs: make([]int, threads), backoffs: make([]int, threads),
attempts: make([]int, threads), attempts: make([]int, threads),
} }
@@ -302,7 +332,7 @@ func CreateGCDStorage(tokenFile string, storagePath string, threads int) (storag
storage.attempts[i] = 0 storage.attempts[i] = 0
} }
storagePathID, err := storage.getIDFromPath(0, storagePath) storagePathID, err := storage.getIDFromPath(0, storagePath, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -328,8 +358,9 @@ func CreateGCDStorage(tokenFile string, storagePath string, threads int) (storag
storage.isConnected = true storage.isConnected = true
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{0}, 0)
return storage, nil return storage, nil
} }
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively) // ListFiles return the list of files and subdirectories under 'dir' (non-recursively)
@@ -340,7 +371,7 @@ func (storage *GCDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
if dir == "snapshots" { if dir == "snapshots" {
files, err := storage.listFiles(threadIndex, storage.getPathID(dir), false) files, err := storage.listFiles(threadIndex, storage.getPathID(dir), false, true)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -353,12 +384,15 @@ func (storage *GCDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
} }
return subDirs, nil, nil return subDirs, nil, nil
} else if strings.HasPrefix(dir, "snapshots/") { } else if strings.HasPrefix(dir, "snapshots/") {
pathID, err := storage.getIDFromPath(threadIndex, dir) pathID, err := storage.getIDFromPath(threadIndex, dir, false)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if pathID == "" {
return nil, nil, fmt.Errorf("Path '%s' does not exist", dir)
}
entries, err := storage.listFiles(threadIndex, pathID, true) entries, err := storage.listFiles(threadIndex, pathID, true, false)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -374,20 +408,33 @@ func (storage *GCDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
files := []string{} files := []string{}
sizes := []int64{} sizes := []int64{}
for _, parent := range []string{"chunks", "fossils"} { parents := []string{"chunks", "fossils"}
entries, err := storage.listFiles(threadIndex, storage.getPathID(parent), true) for i := 0; i < len(parents); i++ {
parent := parents[i]
pathID, ok := storage.findPathID(parent)
if !ok {
continue
}
entries, err := storage.listFiles(threadIndex, pathID, true, true)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
for _, entry := range entries { for _, entry := range entries {
name := entry.Name if entry.MimeType != "application/vnd.google-apps.folder" {
if parent == "fossils" { name := entry.Name
name += ".fsl" if strings.HasPrefix(parent, "fossils") {
name = parent + "/" + name + ".fsl"
name = name[len("fossils/"):]
} else {
name = parent + "/" + name
name = name[len("chunks/"):]
}
files = append(files, name)
sizes = append(sizes, entry.Size)
} else {
parents = append(parents, parent+"/"+entry.Name)
} }
storage.savePathID(parent+"/"+entry.Name, entry.Id) storage.savePathID(parent+"/"+entry.Name, entry.Id)
files = append(files, name)
sizes = append(sizes, entry.Size)
} }
} }
return files, sizes, nil return files, sizes, nil
@@ -398,13 +445,10 @@ func (storage *GCDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
// DeleteFile deletes the file or directory at 'filePath'. // DeleteFile deletes the file or directory at 'filePath'.
func (storage *GCDStorage) DeleteFile(threadIndex int, filePath string) (err error) { func (storage *GCDStorage) DeleteFile(threadIndex int, filePath string) (err error) {
filePath = storage.convertFilePath(filePath) filePath = storage.convertFilePath(filePath)
fileID, ok := storage.findPathID(filePath) fileID, err := storage.getIDFromPath(threadIndex, filePath, false)
if !ok { if err != nil {
fileID, err = storage.getIDFromPath(threadIndex, filePath) LOG_TRACE("GCD_STORAGE", "Ignored file deletion error: %v", err)
if err != nil { return nil
LOG_TRACE("GCD_STORAGE", "Ignored file deletion error: %v", err)
return nil
}
} }
for { for {
@@ -432,14 +476,22 @@ func (storage *GCDStorage) MoveFile(threadIndex int, from string, to string) (er
fileID, ok := storage.findPathID(from) fileID, ok := storage.findPathID(from)
if !ok { if !ok {
return fmt.Errorf("Attempting to rename file %s with unknown id", to) return fmt.Errorf("Attempting to rename file %s with unknown id", from)
} }
fromParentID := storage.getPathID("chunks") fromParent := path.Dir(from)
toParentID := storage.getPathID("fossils") fromParentID, err := storage.getIDFromPath(threadIndex, fromParent, false)
if err != nil {
return fmt.Errorf("Failed to retrieve the id of the parent directory '%s': %v", fromParent, err)
}
if fromParentID == "" {
return fmt.Errorf("The parent directory '%s' does not exist", fromParent)
}
if strings.HasPrefix(from, "fossils") { toParent := path.Dir(to)
fromParentID, toParentID = toParentID, fromParentID toParentID, err := storage.getIDFromPath(threadIndex, toParent, true)
if err != nil {
return fmt.Errorf("Failed to retrieve the id of the parent directory '%s': %v", toParent, err)
} }
for { for {
@@ -458,7 +510,7 @@ func (storage *GCDStorage) MoveFile(threadIndex int, from string, to string) (er
return nil return nil
} }
// CreateDirectory creates a new directory. // createDirectory creates a new directory.
func (storage *GCDStorage) CreateDirectory(threadIndex int, dir string) (err error) { func (storage *GCDStorage) CreateDirectory(threadIndex int, dir string) (err error) {
for len(dir) > 0 && dir[len(dir)-1] == '/' { for len(dir) > 0 && dir[len(dir)-1] == '/' {
@@ -477,13 +529,15 @@ func (storage *GCDStorage) CreateDirectory(threadIndex int, dir string) (err err
return nil return nil
} }
parentID := storage.getPathID("") parentDir := path.Dir(dir)
name := dir if parentDir == "." {
parentDir = ""
if strings.HasPrefix(dir, "snapshots/") {
parentID = storage.getPathID("snapshots")
name = dir[len("snapshots/"):]
} }
parentID := storage.getPathID(parentDir)
if parentID == "" {
return fmt.Errorf("Parent directory '%s' does not exist", parentDir)
}
name := path.Base(dir)
file := &drive.File{ file := &drive.File{
Name: name, Name: name,
@@ -495,10 +549,19 @@ func (storage *GCDStorage) CreateDirectory(threadIndex int, dir string) (err err
file, err = storage.service.Files.Create(file).Fields("id").Do() file, err = storage.service.Files.Create(file).Fields("id").Do()
if retry, err := storage.shouldRetry(threadIndex, err); err == nil && !retry { if retry, err := storage.shouldRetry(threadIndex, err); err == nil && !retry {
break break
} else if retry {
continue
} else { } else {
return err
// Check if the directory has already been created by other thread
exist, _, _, newErr := storage.GetFileInfo(threadIndex, dir)
if newErr == nil && exist {
return nil
}
if retry {
continue
} else {
return err
}
} }
} }
@@ -511,18 +574,21 @@ func (storage *GCDStorage) GetFileInfo(threadIndex int, filePath string) (exist
for len(filePath) > 0 && filePath[len(filePath)-1] == '/' { for len(filePath) > 0 && filePath[len(filePath)-1] == '/' {
filePath = filePath[:len(filePath)-1] filePath = filePath[:len(filePath)-1]
} }
filePath = storage.convertFilePath(filePath)
// GetFileInfo is never called on a fossil
fileID, ok := storage.findPathID(filePath) fileID, ok := storage.findPathID(filePath)
if !ok { if !ok {
dir := path.Dir(filePath) dir := path.Dir(filePath)
if dir == "." { if dir == "." {
dir = "" dir = ""
} }
dirID, err := storage.getIDFromPath(threadIndex, dir) dirID, err := storage.getIDFromPath(threadIndex, dir, false)
if err != nil { if err != nil {
return false, false, 0, err return false, false, 0, err
} }
if dirID == "" {
return false, false, 0, nil
}
fileID, isDir, size, err = storage.listByName(threadIndex, dirID, path.Base(filePath)) fileID, isDir, size, err = storage.listByName(threadIndex, dirID, path.Base(filePath))
if fileID != "" { if fileID != "" {
@@ -543,37 +609,15 @@ func (storage *GCDStorage) GetFileInfo(threadIndex int, filePath string) (exist
} }
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *GCDStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
parentID := ""
filePath = "chunks/" + chunkID
realPath := storage.convertFilePath(filePath)
if isFossil {
parentID = storage.getPathID("fossils")
filePath += ".fsl"
} else {
parentID = storage.getPathID("chunks")
}
fileID := ""
fileID, _, size, err = storage.listByName(threadIndex, parentID, chunkID)
if fileID != "" {
storage.savePathID(realPath, fileID)
}
return filePath, fileID != "", size, err
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *GCDStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *GCDStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
// We never download the fossil so there is no need to convert the path // We never download the fossil so there is no need to convert the path
fileID, ok := storage.findPathID(filePath) fileID, err := storage.getIDFromPath(threadIndex, filePath, false)
if !ok { if err != nil {
fileID, err = storage.getIDFromPath(threadIndex, filePath) return err
if err != nil { }
return err if fileID == "" {
} return fmt.Errorf("%s does not exist", filePath)
storage.savePathID(filePath, fileID)
} }
var response *http.Response var response *http.Response
@@ -605,13 +649,9 @@ func (storage *GCDStorage) UploadFile(threadIndex int, filePath string, content
parent = "" parent = ""
} }
parentID, ok := storage.findPathID(parent) parentID, err := storage.getIDFromPath(threadIndex, parent, true)
if !ok { if err != nil {
parentID, err = storage.getIDFromPath(threadIndex, parent) return err
if err != nil {
return err
}
storage.savePathID(parent, parentID)
} }
file := &drive.File{ file := &drive.File{

View File

@@ -24,7 +24,7 @@ import (
) )
type GCSStorage struct { type GCSStorage struct {
RateLimitedStorage StorageBase
bucket *gcs.BucketHandle bucket *gcs.BucketHandle
storageDir string storageDir string
@@ -101,8 +101,9 @@ func CreateGCSStorage(tokenFile string, bucketName string, storageDir string, th
numberOfThreads: threads, numberOfThreads: threads,
} }
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{0}, 0)
return storage, nil return storage, nil
} }
func (storage *GCSStorage) shouldRetry(backoff *int, err error) (bool, error) { func (storage *GCSStorage) shouldRetry(backoff *int, err error) (bool, error) {
@@ -238,19 +239,6 @@ func (storage *GCSStorage) GetFileInfo(threadIndex int, filePath string) (exist
return true, false, attributes.Size, nil return true, false, attributes.Size, nil
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *GCSStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
filePath = "chunks/" + chunkID
if isFossil {
filePath += ".fsl"
}
exist, _, size, err = storage.GetFileInfo(threadIndex, filePath)
return filePath, exist, size, err
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *GCSStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *GCSStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
readCloser, err := storage.bucket.Object(storage.storageDir + filePath).NewReader(context.Background()) readCloser, err := storage.bucket.Object(storage.storageDir + filePath).NewReader(context.Background())

View File

@@ -10,7 +10,7 @@ import (
) )
type HubicStorage struct { type HubicStorage struct {
RateLimitedStorage StorageBase
client *HubicClient client *HubicClient
storageDir string storageDir string
@@ -64,8 +64,9 @@ func CreateHubicStorage(tokenFile string, storagePath string, threads int) (stor
} }
} }
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{0}, 0)
return storage, nil return storage, nil
} }
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively) // ListFiles return the list of files and subdirectories under 'dir' (non-recursively)
@@ -158,18 +159,6 @@ func (storage *HubicStorage) GetFileInfo(threadIndex int, filePath string) (exis
return storage.client.GetFileInfo(storage.storageDir + "/" + filePath) return storage.client.GetFileInfo(storage.storageDir + "/" + filePath)
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *HubicStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
filePath = "chunks/" + chunkID
if isFossil {
filePath += ".fsl"
}
exist, _, size, err = storage.client.GetFileInfo(storage.storageDir + "/" + filePath)
return filePath, exist, size, err
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *HubicStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *HubicStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
readCloser, _, err := storage.client.DownloadFile(storage.storageDir + "/" + filePath) readCloser, _, err := storage.client.DownloadFile(storage.storageDir + "/" + filePath)

View File

@@ -11,7 +11,7 @@ import (
) )
type OneDriveStorage struct { type OneDriveStorage struct {
RateLimitedStorage StorageBase
client *OneDriveClient client *OneDriveClient
storageDir string storageDir string
@@ -65,10 +65,19 @@ func CreateOneDriveStorage(tokenFile string, storagePath string, threads int) (s
} }
} }
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{0}, 0)
return storage, nil return storage, nil
} }
func (storage *OneDriveStorage) convertFilePath(filePath string) string {
if strings.HasPrefix(filePath, "chunks/") && strings.HasSuffix(filePath, ".fsl") {
return "fossils/" + filePath[len("chunks/"):len(filePath)-len(".fsl")]
}
return filePath
}
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively) // ListFiles return the list of files and subdirectories under 'dir' (non-recursively)
func (storage *OneDriveStorage) ListFiles(threadIndex int, dir string) ([]string, []int64, error) { func (storage *OneDriveStorage) ListFiles(threadIndex int, dir string) ([]string, []int64, error) {
for len(dir) > 0 && dir[len(dir)-1] == '/' { for len(dir) > 0 && dir[len(dir)-1] == '/' {
@@ -105,19 +114,29 @@ func (storage *OneDriveStorage) ListFiles(threadIndex int, dir string) ([]string
} else { } else {
files := []string{} files := []string{}
sizes := []int64{} sizes := []int64{}
for _, parent := range []string{"chunks", "fossils"} { parents := []string{"chunks", "fossils"}
for i := 0; i < len(parents); i++ {
parent := parents[i]
entries, err := storage.client.ListEntries(storage.storageDir + "/" + parent) entries, err := storage.client.ListEntries(storage.storageDir + "/" + parent)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
for _, entry := range entries { for _, entry := range entries {
name := entry.Name if len(entry.Folder) == 0 {
if parent == "fossils" { name := entry.Name
name += ".fsl" if strings.HasPrefix(parent, "fossils") {
name = parent + "/" + name + ".fsl"
name = name[len("fossils/"):]
} else {
name = parent + "/" + name
name = name[len("chunks/"):]
}
files = append(files, name)
sizes = append(sizes, entry.Size)
} else {
parents = append(parents, parent+"/"+entry.Name)
} }
files = append(files, name)
sizes = append(sizes, entry.Size)
} }
} }
return files, sizes, nil return files, sizes, nil
@@ -127,9 +146,7 @@ func (storage *OneDriveStorage) ListFiles(threadIndex int, dir string) ([]string
// DeleteFile deletes the file or directory at 'filePath'. // DeleteFile deletes the file or directory at 'filePath'.
func (storage *OneDriveStorage) DeleteFile(threadIndex int, filePath string) (err error) { func (storage *OneDriveStorage) DeleteFile(threadIndex int, filePath string) (err error) {
if strings.HasSuffix(filePath, ".fsl") && strings.HasPrefix(filePath, "chunks/") { filePath = storage.convertFilePath(filePath)
filePath = "fossils/" + filePath[len("chunks/"):len(filePath)-len(".fsl")]
}
err = storage.client.DeleteFile(storage.storageDir + "/" + filePath) err = storage.client.DeleteFile(storage.storageDir + "/" + filePath)
if e, ok := err.(OneDriveError); ok && e.Status == 404 { if e, ok := err.(OneDriveError); ok && e.Status == 404 {
@@ -141,14 +158,11 @@ func (storage *OneDriveStorage) DeleteFile(threadIndex int, filePath string) (er
// MoveFile renames the file. // MoveFile renames the file.
func (storage *OneDriveStorage) MoveFile(threadIndex int, from string, to string) (err error) { func (storage *OneDriveStorage) MoveFile(threadIndex int, from string, to string) (err error) {
fromPath := storage.storageDir + "/" + from
toParent := storage.storageDir + "/fossils"
if strings.HasSuffix(from, ".fsl") {
fromPath = storage.storageDir + "/fossils/" + from[len("chunks/"):len(from)-len(".fsl")]
toParent = storage.storageDir + "/chunks"
}
err = storage.client.MoveFile(fromPath, toParent) fromPath := storage.storageDir + "/" + storage.convertFilePath(from)
toPath := storage.storageDir + "/" + storage.convertFilePath(to)
err = storage.client.MoveFile(fromPath, path.Dir(toPath))
if err != nil { if err != nil {
if e, ok := err.(OneDriveError); ok && e.Status == 409 { if e, ok := err.(OneDriveError); ok && e.Status == 409 {
LOG_DEBUG("ONEDRIVE_MOVE", "Ignore 409 conflict error") LOG_DEBUG("ONEDRIVE_MOVE", "Ignore 409 conflict error")
@@ -180,24 +194,13 @@ func (storage *OneDriveStorage) GetFileInfo(threadIndex int, filePath string) (e
for len(filePath) > 0 && filePath[len(filePath)-1] == '/' { for len(filePath) > 0 && filePath[len(filePath)-1] == '/' {
filePath = filePath[:len(filePath)-1] filePath = filePath[:len(filePath)-1]
} }
filePath = storage.convertFilePath(filePath)
fileID, isDir, size, err := storage.client.GetFileInfo(storage.storageDir + "/" + filePath) fileID, isDir, size, err := storage.client.GetFileInfo(storage.storageDir + "/" + filePath)
return fileID != "", isDir, size, err return fileID != "", isDir, size, err
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *OneDriveStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
filePath = "chunks/" + chunkID
realPath := storage.storageDir + "/" + filePath
if isFossil {
filePath += ".fsl"
realPath = storage.storageDir + "/fossils/" + chunkID
}
fileID, _, size, err := storage.client.GetFileInfo(realPath)
return filePath, fileID != "", size, err
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *OneDriveStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *OneDriveStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
readCloser, _, err := storage.client.DownloadFile(storage.storageDir + "/" + filePath) readCloser, _, err := storage.client.DownloadFile(storage.storageDir + "/" + filePath)

View File

@@ -13,7 +13,7 @@ import (
// S3CStorage is a storage backend for s3 compatible storages that require V2 Signing. // S3CStorage is a storage backend for s3 compatible storages that require V2 Signing.
type S3CStorage struct { type S3CStorage struct {
RateLimitedStorage StorageBase
buckets []*s3.Bucket buckets []*s3.Bucket
storageDir string storageDir string
@@ -58,6 +58,8 @@ func CreateS3CStorage(regionName string, endpoint string, bucketName string, sto
storageDir: storageDir, storageDir: storageDir,
} }
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{0}, 0)
return storage, nil return storage, nil
} }
@@ -154,25 +156,6 @@ func (storage *S3CStorage) GetFileInfo(threadIndex int, filePath string) (exist
} }
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *S3CStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
filePath = "chunks/" + chunkID
if isFossil {
filePath += ".fsl"
}
exist, _, size, err = storage.GetFileInfo(threadIndex, filePath)
if err != nil {
return "", false, 0, err
} else {
return filePath, exist, size, err
}
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *S3CStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *S3CStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {

View File

@@ -16,7 +16,7 @@ import (
) )
type S3Storage struct { type S3Storage struct {
RateLimitedStorage StorageBase
client *s3.S3 client *s3.S3
bucket string bucket string
@@ -53,7 +53,7 @@ func CreateS3Storage(regionName string, endpoint string, bucketName string, stor
} }
} }
config := &aws.Config{ s3Config := &aws.Config{
Region: aws.String(regionName), Region: aws.String(regionName),
Credentials: auth, Credentials: auth,
Endpoint: aws.String(endpoint), Endpoint: aws.String(endpoint),
@@ -66,12 +66,14 @@ func CreateS3Storage(regionName string, endpoint string, bucketName string, stor
} }
storage = &S3Storage{ storage = &S3Storage{
client: s3.New(session.New(config)), client: s3.New(session.New(s3Config)),
bucket: bucketName, bucket: bucketName,
storageDir: storageDir, storageDir: storageDir,
numberOfThreads: threads, numberOfThreads: threads,
} }
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{0}, 0)
return storage, nil return storage, nil
} }
@@ -188,25 +190,6 @@ func (storage *S3Storage) GetFileInfo(threadIndex int, filePath string) (exist b
} }
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *S3Storage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
filePath = "chunks/" + chunkID
if isFossil {
filePath += ".fsl"
}
exist, _, size, err = storage.GetFileInfo(threadIndex, filePath)
if err != nil {
return "", false, 0, err
} else {
return filePath, exist, size, err
}
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *S3Storage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *S3Storage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {

View File

@@ -12,6 +12,7 @@ import (
"os" "os"
"path" "path"
"runtime" "runtime"
"strings"
"time" "time"
"github.com/pkg/sftp" "github.com/pkg/sftp"
@@ -19,10 +20,10 @@ import (
) )
type SFTPStorage struct { type SFTPStorage struct {
RateLimitedStorage StorageBase
client *sftp.Client client *sftp.Client
minimumNesting int // The minimum level of directories to dive into before searching for the chunk file. minimumNesting int // The minimum level of directories to dive into before searching for the chunk file.
storageDir string storageDir string
numberOfThreads int numberOfThreads int
} }
@@ -45,18 +46,18 @@ func CreateSFTPStorage(server string, port int, username string, storageDir stri
hostKeyCallback func(hostname string, remote net.Addr, hostKeyCallback func(hostname string, remote net.Addr,
key ssh.PublicKey) error, threads int) (storage *SFTPStorage, err error) { key ssh.PublicKey) error, threads int) (storage *SFTPStorage, err error) {
config := &ssh.ClientConfig{ sftpConfig := &ssh.ClientConfig{
User: username, User: username,
Auth: authMethods, Auth: authMethods,
HostKeyCallback: hostKeyCallback, HostKeyCallback: hostKeyCallback,
} }
if server == "sftp.hidrive.strato.com" { if server == "sftp.hidrive.strato.com" {
config.Ciphers = []string{"aes128-cbc", "aes128-ctr", "aes256-ctr"} sftpConfig.Ciphers = []string{"aes128-cbc", "aes128-ctr", "aes256-ctr"}
} }
serverAddress := fmt.Sprintf("%s:%d", server, port) serverAddress := fmt.Sprintf("%s:%d", server, port)
connection, err := ssh.Dial("tcp", serverAddress, config) connection, err := ssh.Dial("tcp", serverAddress, sftpConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -92,6 +93,8 @@ func CreateSFTPStorage(server string, port int, username string, storageDir stri
runtime.SetFinalizer(storage, CloseSFTPStorage) runtime.SetFinalizer(storage, CloseSFTPStorage)
storage.DerivedStorage = storage
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, nil return storage, nil
} }
@@ -176,64 +179,6 @@ func (storage *SFTPStorage) GetFileInfo(threadIndex int, filePath string) (exist
return true, fileInfo.IsDir(), fileInfo.Size(), nil return true, fileInfo.IsDir(), fileInfo.Size(), nil
} }
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *SFTPStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
dir := path.Join(storage.storageDir, "chunks")
suffix := ""
if isFossil {
suffix = ".fsl"
}
for level := 0; level*2 < len(chunkID); level++ {
if level >= storage.minimumNesting {
filePath = path.Join(dir, chunkID[2*level:]) + suffix
if stat, err := storage.client.Stat(filePath); err == nil && !stat.IsDir() {
return filePath[len(storage.storageDir)+1:], true, stat.Size(), nil
} else if err == nil && stat.IsDir() {
return filePath[len(storage.storageDir)+1:], true, 0, fmt.Errorf("The path %s is a directory", filePath)
}
}
// Find the subdirectory the chunk file may reside.
subDir := path.Join(dir, chunkID[2*level:2*level+2])
stat, err := storage.client.Stat(subDir)
if err == nil && stat.IsDir() {
dir = subDir
continue
}
if level < storage.minimumNesting {
// Create the subdirectory if is doesn't exist.
if err == nil && !stat.IsDir() {
return "", false, 0, fmt.Errorf("The path %s is not a directory", subDir)
}
err = storage.client.Mkdir(subDir)
if err != nil {
// The directory may have been created by other threads so check it again.
stat, _ := storage.client.Stat(subDir)
if stat == nil || !stat.IsDir() {
return "", false, 0, fmt.Errorf("Failed to create the directory %s: %v", subDir, err)
}
}
dir = subDir
continue
}
// The chunk must be under this subdirectory but it doesn't exist.
return path.Join(dir, chunkID[2*level:])[len(storage.storageDir)+1:] + suffix, false, 0, nil
}
LOG_FATAL("CHUNK_FIND", "Chunk %s is still not found after having searched a maximum level of directories",
chunkID)
return "", false, 0, nil
}
// DownloadFile reads the file at 'filePath' into the chunk. // DownloadFile reads the file at 'filePath' into the chunk.
func (storage *SFTPStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { func (storage *SFTPStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
file, err := storage.client.Open(path.Join(storage.storageDir, filePath)) file, err := storage.client.Open(path.Join(storage.storageDir, filePath))
@@ -255,6 +200,30 @@ func (storage *SFTPStorage) UploadFile(threadIndex int, filePath string, content
fullPath := path.Join(storage.storageDir, filePath) fullPath := path.Join(storage.storageDir, filePath)
dirs := strings.Split(filePath, "/")
if len(dirs) > 1 {
fullDir := path.Dir(fullPath)
_, err := storage.client.Stat(fullDir)
if err != nil {
// The error may be caused by a non-existent fullDir, or a broken connection. In either case,
// we just assume it is the former because there isn't a way to tell which is the case.
for i, _ := range dirs[1 : len(dirs)-1] {
subDir := path.Join(storage.storageDir, path.Join(dirs[0:i+2]...))
// We don't check the error; just keep going blindly but always store the last err
err = storage.client.Mkdir(subDir)
}
// If there is an error creating the dirs, we check fullDir one more time, because another thread
// may happen to create the same fullDir ahead of this thread
if err != nil {
_, err := storage.client.Stat(fullDir)
if err != nil {
return err
}
}
}
}
letters := "abcdefghijklmnopqrstuvwxyz" letters := "abcdefghijklmnopqrstuvwxyz"
suffix := make([]byte, 8) suffix := make([]byte, 8)
for i := range suffix { for i := range suffix {
@@ -301,7 +270,14 @@ func (storage *SFTPStorage) IsMoveFileImplemented() bool { return true }
func (storage *SFTPStorage) IsStrongConsistent() bool { return true } func (storage *SFTPStorage) IsStrongConsistent() bool { return true }
// If the storage supports fast listing of files names. // If the storage supports fast listing of files names.
func (storage *SFTPStorage) IsFastListing() bool { return false } func (storage *SFTPStorage) IsFastListing() bool {
for _, level := range storage.readLevels {
if level > 1 {
return false
}
}
return true
}
// Enable the test mode. // Enable the test mode.
func (storage *SFTPStorage) EnableTestMode() {} func (storage *SFTPStorage) EnableTestMode() {}

View File

@@ -592,24 +592,6 @@ func (manager *SnapshotManager) ListAllFiles(storage Storage, top string) (allFi
allSizes = append(allSizes, sizes[i]) allSizes = append(allSizes, sizes[i])
} }
} }
if !manager.config.dryRun {
if top == "chunks/" {
// We're listing all chunks so this is the perfect place to detect if a directory contains too many
// chunks. Create sub-directories if necessary
if len(files) > 1024 && !storage.IsFastListing() {
for i := 0; i < 256; i++ {
subdir := dir + fmt.Sprintf("%02x\n", i)
manager.storage.CreateDirectory(0, subdir)
}
}
} else {
// Remove chunk sub-directories that are empty
if len(files) == 0 && strings.HasPrefix(dir, "chunks/") && dir != "chunks/" {
storage.DeleteFile(0, dir)
}
}
}
} }
return allFiles, allSizes return allFiles, allSizes

View File

@@ -95,14 +95,14 @@ func createTestSnapshotManager(testDir string) *SnapshotManager {
os.RemoveAll(testDir) os.RemoveAll(testDir)
os.MkdirAll(testDir, 0700) os.MkdirAll(testDir, 0700)
storage, _ := CreateFileStorage(testDir, 2, false, 1) storage, _ := CreateFileStorage(testDir, false, 1)
storage.CreateDirectory(0, "chunks") storage.CreateDirectory(0, "chunks")
storage.CreateDirectory(0, "snapshots") storage.CreateDirectory(0, "snapshots")
config := CreateConfig() config := CreateConfig()
snapshotManager := CreateSnapshotManager(config, storage) snapshotManager := CreateSnapshotManager(config, storage)
cacheDir := path.Join(testDir, "cache") cacheDir := path.Join(testDir, "cache")
snapshotCache, _ := CreateFileStorage(cacheDir, 2, false, 1) snapshotCache, _ := CreateFileStorage(cacheDir, false, 1)
snapshotCache.CreateDirectory(0, "chunks") snapshotCache.CreateDirectory(0, "chunks")
snapshotCache.CreateDirectory(0, "snapshots") snapshotCache.CreateDirectory(0, "snapshots")

View File

@@ -5,6 +5,7 @@
package duplicacy package duplicacy
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@@ -20,8 +21,10 @@ import (
) )
type Storage interface { type Storage interface {
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively) // ListFiles return the list of files and subdirectories under 'dir'. A subdirectories returned must have a trailing '/', with
ListFiles(threadIndex int, dir string) (files []string, size []int64, err error) // a size of 0. If 'dir' is 'snapshots', only subdirectories will be returned. If 'dir' is 'snapshots/repository_id', then only
// files will be returned. If 'dir' is 'chunks', the implementation can return the list either recusively or non-recusively.
ListFiles(threadIndex int, dir string) (files []string, sizes []int64, err error)
// DeleteFile deletes the file or directory at 'filePath'. // DeleteFile deletes the file or directory at 'filePath'.
DeleteFile(threadIndex int, filePath string) (err error) DeleteFile(threadIndex int, filePath string) (err error)
@@ -45,6 +48,9 @@ type Storage interface {
// UploadFile writes 'content' to the file at 'filePath'. // UploadFile writes 'content' to the file at 'filePath'.
UploadFile(threadIndex int, filePath string, content []byte) (err error) UploadFile(threadIndex int, filePath string, content []byte) (err error)
// SetNestingLevels sets up the chunk nesting structure.
SetNestingLevels(config *Config)
// If a local snapshot cache is needed for the storage to avoid downloading/uploading chunks too often when // If a local snapshot cache is needed for the storage to avoid downloading/uploading chunks too often when
// managing snapshots. // managing snapshots.
IsCacheNeeded() bool IsCacheNeeded() bool
@@ -65,16 +71,97 @@ type Storage interface {
SetRateLimits(downloadRateLimit int, uploadRateLimit int) SetRateLimits(downloadRateLimit int, uploadRateLimit int)
} }
type RateLimitedStorage struct { // StorageBase is the base struct from which all storages are derived from
DownloadRateLimit int type StorageBase struct {
UploadRateLimit int DownloadRateLimit int // Maximum download rate (bytes/seconds)
UploadRateLimit int // Maximum upload reate (bytes/seconds)
DerivedStorage Storage // Used as the pointer to the derived storage class
readLevels []int // At which nesting level to find the chunk with the given id
writeLevel int // Store the uploaded chunk to this level
} }
func (storage *RateLimitedStorage) SetRateLimits(downloadRateLimit int, uploadRateLimit int) { // SetRateLimits sets the maximum download and upload rates
func (storage *StorageBase) SetRateLimits(downloadRateLimit int, uploadRateLimit int) {
storage.DownloadRateLimit = downloadRateLimit storage.DownloadRateLimit = downloadRateLimit
storage.UploadRateLimit = uploadRateLimit storage.UploadRateLimit = uploadRateLimit
} }
// SetDefaultNestingLevels sets the default read and write levels. This is usually called by
// derived storages to set the levels with old values so that storages initialied by ealier versions
// will continue to work.
func (storage *StorageBase) SetDefaultNestingLevels(readLevels []int, writeLevel int) {
storage.readLevels = readLevels
storage.writeLevel = writeLevel
}
// SetNestingLevels sets the new read and write levels (normally both at 1) if the 'config' file has
// the 'fixed-nesting' key, or if a file named 'nesting' exists on the storage.
func (storage *StorageBase) SetNestingLevels(config *Config) {
// 'FixedNesting' is true only for the 'config' file with the new format (2.0.10+)
if config.FixedNesting {
storage.readLevels = nil
// Check if the 'nesting' file exist
exist, _, _, err := storage.DerivedStorage.GetFileInfo(0, "nesting")
if err == nil && exist {
nestingFile := CreateChunk(CreateConfig(), true)
if storage.DerivedStorage.DownloadFile(0, "config", nestingFile) == nil {
var nesting struct {
ReadLevels []int `json:"read-levels"`
WriteLevel int `json:"write-level"`
}
if json.Unmarshal(nestingFile.GetBytes(), &nesting) == nil {
storage.readLevels = nesting.ReadLevels
storage.writeLevel = nesting.WriteLevel
}
}
}
if len(storage.readLevels) == 0 {
storage.readLevels = []int{1}
storage.writeLevel = 1
}
}
LOG_DEBUG("STORAGE_NESTING", "Chunk read levels: %v, write level: %d", storage.readLevels, storage.writeLevel)
for _, level := range storage.readLevels {
if storage.writeLevel == level {
return
}
}
LOG_ERROR("STORAGE_NESTING", "The write level %d isn't in the read levels %v", storage.readLevels, storage.writeLevel)
}
// FindChunk finds the chunk with the specified id at the levels one by one as specified by 'readLevels'.
func (storage *StorageBase) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
chunkPaths := make([]string, 0)
for _, level := range storage.readLevels {
chunkPath := "chunks/"
for i := 0; i < level; i++ {
chunkPath += chunkID[2*i:2*i+2] + "/"
}
chunkPath += chunkID[2*level:]
if isFossil {
chunkPath += ".fsl"
}
exist, _, size, err = storage.DerivedStorage.GetFileInfo(threadIndex, chunkPath)
if err == nil && exist {
return chunkPath, exist, size, err
}
chunkPaths = append(chunkPaths, chunkPath)
}
for i, level := range storage.readLevels {
if storage.writeLevel == level {
return chunkPaths[i], false, 0, nil
}
}
return "", false, 0, fmt.Errorf("Invalid chunk nesting setup")
}
func checkHostKey(hostname string, remote net.Addr, key ssh.PublicKey) error { func checkHostKey(hostname string, remote net.Addr, key ssh.PublicKey) error {
if preferencePath == "" { if preferencePath == "" {
@@ -148,7 +235,7 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
} }
if isFileStorage { if isFileStorage {
fileStorage, err := CreateFileStorage(storageURL, 2, isCacheNeeded, threads) fileStorage, err := CreateFileStorage(storageURL, isCacheNeeded, threads)
if err != nil { if err != nil {
LOG_ERROR("STORAGE_CREATE", "Failed to load the file storage at %s: %v", storageURL, err) LOG_ERROR("STORAGE_CREATE", "Failed to load the file storage at %s: %v", storageURL, err)
return nil return nil
@@ -157,7 +244,7 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
} }
if strings.HasPrefix(storageURL, "flat://") { if strings.HasPrefix(storageURL, "flat://") {
fileStorage, err := CreateFileStorage(storageURL[7:], 0, false, threads) fileStorage, err := CreateFileStorage(storageURL[7:], false, threads)
if err != nil { if err != nil {
LOG_ERROR("STORAGE_CREATE", "Failed to load the file storage at %s: %v", storageURL, err) LOG_ERROR("STORAGE_CREATE", "Failed to load the file storage at %s: %v", storageURL, err)
return nil return nil
@@ -166,7 +253,7 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
} }
if strings.HasPrefix(storageURL, "samba://") { if strings.HasPrefix(storageURL, "samba://") {
fileStorage, err := CreateFileStorage(storageURL[8:], 2, true, threads) fileStorage, err := CreateFileStorage(storageURL[8:], true, threads)
if err != nil { if err != nil {
LOG_ERROR("STORAGE_CREATE", "Failed to load the file storage at %s: %v", storageURL, err) LOG_ERROR("STORAGE_CREATE", "Failed to load the file storage at %s: %v", storageURL, err)
return nil return nil

View File

@@ -15,7 +15,6 @@ import (
"path" "path"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"strings"
"testing" "testing"
"time" "time"
@@ -41,61 +40,100 @@ func init() {
func loadStorage(localStoragePath string, threads int) (Storage, error) { func loadStorage(localStoragePath string, threads int) (Storage, error) {
if testStorageName == "" || testStorageName == "file" { if testStorageName == "" || testStorageName == "file" {
return CreateFileStorage(localStoragePath, 2, false, threads) storage, err := CreateFileStorage(localStoragePath, false, threads)
if storage != nil {
// Use a read level of at least 2 because this will catch more errors than a read level of 1.
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
}
return storage, err
} }
config, err := ioutil.ReadFile("test_storage.conf") description, err := ioutil.ReadFile("test_storage.conf")
if err != nil { if err != nil {
return nil, err return nil, err
} }
storages := make(map[string]map[string]string) configs := make(map[string]map[string]string)
err = json.Unmarshal(config, &storages) err = json.Unmarshal(description, &configs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
storage, found := storages[testStorageName] config, found := configs[testStorageName]
if !found { if !found {
return nil, fmt.Errorf("No storage named '%s' found", testStorageName) return nil, fmt.Errorf("No storage named '%s' found", testStorageName)
} }
if testStorageName == "flat" { if testStorageName == "flat" {
return CreateFileStorage(localStoragePath, 0, false, threads) storage, err := CreateFileStorage(localStoragePath, false, threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "samba" { } else if testStorageName == "samba" {
return CreateFileStorage(localStoragePath, 2, true, threads) storage, err := CreateFileStorage(localStoragePath, true, threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "sftp" { } else if testStorageName == "sftp" {
port, _ := strconv.Atoi(storage["port"]) port, _ := strconv.Atoi(config["port"])
return CreateSFTPStorageWithPassword(storage["server"], port, storage["username"], storage["directory"], 2, storage["password"], threads) storage, err := CreateSFTPStorageWithPassword(config["server"], port, config["username"], config["directory"], 2, config["password"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "s3" || testStorageName == "wasabi" { } else if testStorageName == "s3" || testStorageName == "wasabi" {
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads, true, false) storage, err := CreateS3Storage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads, true, false)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "s3c" { } else if testStorageName == "s3c" {
return CreateS3CStorage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads) storage, err := CreateS3CStorage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "minio" { } else if testStorageName == "minio" {
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads, false, true) storage, err := CreateS3Storage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads, false, true)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "minios" { } else if testStorageName == "minios" {
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads, true, true) storage, err := CreateS3Storage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads, true, true)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "dropbox" { } else if testStorageName == "dropbox" {
return CreateDropboxStorage(storage["token"], storage["directory"], 1, threads) storage, err := CreateDropboxStorage(config["token"], config["directory"], 1, threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "b2" { } else if testStorageName == "b2" {
return CreateB2Storage(storage["account"], storage["key"], storage["bucket"], threads) storage, err := CreateB2Storage(config["account"], config["key"], config["bucket"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "gcs-s3" { } else if testStorageName == "gcs-s3" {
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads, true, false) storage, err := CreateS3Storage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads, true, false)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "gcs" { } else if testStorageName == "gcs" {
return CreateGCSStorage(storage["token_file"], storage["bucket"], storage["directory"], threads) storage, err := CreateGCSStorage(config["token_file"], config["bucket"], config["directory"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "gcs-sa" { } else if testStorageName == "gcs-sa" {
return CreateGCSStorage(storage["token_file"], storage["bucket"], storage["directory"], threads) storage, err := CreateGCSStorage(config["token_file"], config["bucket"], config["directory"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "azure" { } else if testStorageName == "azure" {
return CreateAzureStorage(storage["account"], storage["key"], storage["container"], threads) storage, err := CreateAzureStorage(config["account"], config["key"], config["container"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "acd" { } else if testStorageName == "acd" {
return CreateACDStorage(storage["token_file"], storage["storage_path"], threads) storage, err := CreateACDStorage(config["token_file"], config["storage_path"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "gcd" { } else if testStorageName == "gcd" {
return CreateGCDStorage(storage["token_file"], storage["storage_path"], threads) storage, err := CreateGCDStorage(config["token_file"], config["storage_path"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "one" { } else if testStorageName == "one" {
return CreateOneDriveStorage(storage["token_file"], storage["storage_path"], threads) storage, err := CreateOneDriveStorage(config["token_file"], config["storage_path"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else if testStorageName == "hubic" { } else if testStorageName == "hubic" {
return CreateHubicStorage(storage["token_file"], storage["storage_path"], threads) storage, err := CreateHubicStorage(config["token_file"], config["storage_path"], threads)
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
return storage, err
} else { } else {
return nil, fmt.Errorf("Invalid storage named: %s", testStorageName) return nil, fmt.Errorf("Invalid storage named: %s", testStorageName)
} }
@@ -266,6 +304,33 @@ func TestStorage(t *testing.T) {
storage.CreateDirectory(0, "snapshots/repository1") storage.CreateDirectory(0, "snapshots/repository1")
storage.CreateDirectory(0, "snapshots/repository2") storage.CreateDirectory(0, "snapshots/repository2")
storage.CreateDirectory(0, "shared")
// Upload to the same directory by multiple goroutines
count := 8
finished := make(chan int, count)
for i := 0; i < count; i++ {
go func(name string) {
err := storage.UploadFile(0, name, []byte("this is a test file"))
if err != nil {
t.Errorf("Error to upload '%s': %v", name, err)
}
finished <- 0
}(fmt.Sprintf("shared/a/b/c/%d", i))
}
for i := 0; i < count; i++ {
<-finished
}
for i := 0; i < count; i++ {
storage.DeleteFile(0, fmt.Sprintf("shared/a/b/c/%d", i))
}
storage.DeleteFile(0, "shared/a/b/c")
storage.DeleteFile(0, "shared/a/b")
storage.DeleteFile(0, "shared/a")
time.Sleep(time.Duration(delay) * time.Second) time.Sleep(time.Duration(delay) * time.Second)
{ {
@@ -338,7 +403,7 @@ func TestStorage(t *testing.T) {
} }
} }
numberOfFiles := 20 numberOfFiles := 10
maxFileSize := 64 * 1024 maxFileSize := 64 * 1024
if testQuickMode { if testQuickMode {
@@ -374,15 +439,7 @@ func TestStorage(t *testing.T) {
t.Errorf("Failed to upload the file %s: %v", filePath, err) t.Errorf("Failed to upload the file %s: %v", filePath, err)
return return
} }
LOG_INFO("STORAGE_CHUNK", "Uploaded chunk: %s, size: %d", chunkID, len(content)) LOG_INFO("STORAGE_CHUNK", "Uploaded chunk: %s, size: %d", filePath, len(content))
}
allChunks := []string{}
for _, file := range listChunks(storage) {
file = strings.Replace(file, "/", "", -1)
if len(file) == 64 {
allChunks = append(allChunks, file)
}
} }
LOG_INFO("STORAGE_FOSSIL", "Making %s a fossil", chunks[0]) LOG_INFO("STORAGE_FOSSIL", "Making %s a fossil", chunks[0])
@@ -412,7 +469,7 @@ func TestStorage(t *testing.T) {
t.Errorf("Error downloading file %s: %v", filePath, err) t.Errorf("Error downloading file %s: %v", filePath, err)
continue continue
} }
LOG_INFO("STORAGE_CHUNK", "Downloaded chunk: %s, size: %d", chunkID, chunk.GetLength()) LOG_INFO("STORAGE_CHUNK", "Downloaded chunk: %s, size: %d", filePath, chunk.GetLength())
} }
hasher := sha256.New() hasher := sha256.New()
@@ -447,6 +504,11 @@ func TestStorage(t *testing.T) {
} }
} }
allChunks := []string{}
for _, file := range listChunks(storage) {
allChunks = append(allChunks, file)
}
for _, file := range allChunks { for _, file := range allChunks {
err = storage.DeleteFile(0, "chunks/"+file) err = storage.DeleteFile(0, "chunks/"+file)