mirror of
https://github.com/gilbertchen/duplicacy
synced 2025-12-06 00:03:38 +00:00
* All APIs include UploadFile are done via the call() function * New retry mechanism limiting the maximum backoff each time to 1 minute * Add an env var DUPLICACY_B2_RETRIES to specify the number of retries * Handle special/unicode characters in repositor ids * Allow a directory in a bucket to be used as the storage destination
242 lines
6.1 KiB
Go
242 lines
6.1 KiB
Go
// Copyright (c) Acrosync LLC. All rights reserved.
|
|
// Free for personal use and commercial trial
|
|
// Commercial use requires per-user licenses available from https://duplicacy.com
|
|
|
|
package duplicacy
|
|
|
|
import (
|
|
"strings"
|
|
)
|
|
|
|
type B2Storage struct {
|
|
StorageBase
|
|
|
|
client *B2Client
|
|
}
|
|
|
|
// CreateB2Storage creates a B2 storage object.
|
|
func CreateB2Storage(accountID string, applicationKey string, bucket string, storageDir string, threads int) (storage *B2Storage, err error) {
|
|
|
|
client := NewB2Client(accountID, applicationKey, storageDir, threads)
|
|
|
|
err = client.AuthorizeAccount(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = client.FindBucket(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
storage = &B2Storage{
|
|
client: client,
|
|
}
|
|
|
|
storage.DerivedStorage = storage
|
|
storage.SetDefaultNestingLevels([]int{0}, 0)
|
|
return storage, nil
|
|
}
|
|
|
|
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively)
|
|
func (storage *B2Storage) ListFiles(threadIndex int, dir string) (files []string, sizes []int64, err error) {
|
|
for len(dir) > 0 && dir[len(dir)-1] == '/' {
|
|
dir = dir[:len(dir)-1]
|
|
}
|
|
length := len(dir) + 1
|
|
|
|
includeVersions := false
|
|
if dir == "chunks" {
|
|
includeVersions = true
|
|
}
|
|
|
|
entries, err := storage.client.ListFileNames(threadIndex, dir, false, includeVersions)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if dir == "snapshots" {
|
|
|
|
subDirs := make(map[string]bool)
|
|
|
|
for _, entry := range entries {
|
|
name := entry.FileName[length:]
|
|
subDir := strings.Split(name, "/")[0]
|
|
subDirs[subDir+"/"] = true
|
|
}
|
|
|
|
for subDir := range subDirs {
|
|
files = append(files, subDir)
|
|
}
|
|
} else if dir == "chunks" {
|
|
lastFile := ""
|
|
for _, entry := range entries {
|
|
if entry.FileName == lastFile {
|
|
continue
|
|
}
|
|
lastFile = entry.FileName
|
|
if entry.Action == "hide" {
|
|
files = append(files, entry.FileName[length:]+".fsl")
|
|
} else {
|
|
files = append(files, entry.FileName[length:])
|
|
}
|
|
sizes = append(sizes, entry.Size)
|
|
}
|
|
} else {
|
|
for _, entry := range entries {
|
|
files = append(files, entry.FileName[length:])
|
|
}
|
|
}
|
|
|
|
return files, sizes, nil
|
|
}
|
|
|
|
// DeleteFile deletes the file or directory at 'filePath'.
|
|
func (storage *B2Storage) DeleteFile(threadIndex int, filePath string) (err error) {
|
|
|
|
if strings.HasSuffix(filePath, ".fsl") {
|
|
filePath = filePath[:len(filePath)-len(".fsl")]
|
|
entries, err := storage.client.ListFileNames(threadIndex, filePath, true, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
toBeDeleted := false
|
|
|
|
for _, entry := range entries {
|
|
if entry.FileName != filePath || (!toBeDeleted && entry.Action != "hide") {
|
|
continue
|
|
}
|
|
|
|
toBeDeleted = true
|
|
|
|
err = storage.client.DeleteFile(threadIndex, filePath, entry.FileID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
|
|
} else {
|
|
entries, err := storage.client.ListFileNames(threadIndex, filePath, true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
return nil
|
|
}
|
|
return storage.client.DeleteFile(threadIndex, filePath, entries[0].FileID)
|
|
}
|
|
}
|
|
|
|
// MoveFile renames the file.
|
|
func (storage *B2Storage) MoveFile(threadIndex int, from string, to string) (err error) {
|
|
|
|
filePath := ""
|
|
|
|
if strings.HasSuffix(from, ".fsl") {
|
|
filePath = to
|
|
if from != to+".fsl" {
|
|
filePath = ""
|
|
}
|
|
} else if strings.HasSuffix(to, ".fsl") {
|
|
filePath = from
|
|
if to != from+".fsl" {
|
|
filePath = ""
|
|
}
|
|
}
|
|
|
|
if filePath == "" {
|
|
LOG_FATAL("STORAGE_MOVE", "Moving file '%s' to '%s' is not supported", from, to)
|
|
return nil
|
|
}
|
|
|
|
if filePath == from {
|
|
_, err = storage.client.HideFile(threadIndex, from)
|
|
return err
|
|
} else {
|
|
entries, err := storage.client.ListFileNames(threadIndex, filePath, true, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(entries) == 0 || entries[0].FileName != filePath || entries[0].Action != "hide" {
|
|
return nil
|
|
}
|
|
|
|
return storage.client.DeleteFile(threadIndex, filePath, entries[0].FileID)
|
|
}
|
|
}
|
|
|
|
// CreateDirectory creates a new directory.
|
|
func (storage *B2Storage) CreateDirectory(threadIndex int, dir string) (err error) {
|
|
return nil
|
|
}
|
|
|
|
// GetFileInfo returns the information about the file or directory at 'filePath'.
|
|
func (storage *B2Storage) GetFileInfo(threadIndex int, filePath string) (exist bool, isDir bool, size int64, err error) {
|
|
isFossil := false
|
|
if strings.HasSuffix(filePath, ".fsl") {
|
|
isFossil = true
|
|
filePath = filePath[:len(filePath)-len(".fsl")]
|
|
}
|
|
|
|
entries, err := storage.client.ListFileNames(threadIndex, filePath, true, isFossil)
|
|
if err != nil {
|
|
return false, false, 0, err
|
|
}
|
|
|
|
if len(entries) == 0 || entries[0].FileName != filePath {
|
|
return false, false, 0, nil
|
|
}
|
|
|
|
if isFossil {
|
|
if entries[0].Action == "hide" {
|
|
return true, false, entries[0].Size, nil
|
|
} else {
|
|
return false, false, 0, nil
|
|
}
|
|
}
|
|
return true, false, entries[0].Size, nil
|
|
}
|
|
|
|
// DownloadFile reads the file at 'filePath' into the chunk.
|
|
func (storage *B2Storage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
|
|
|
|
filePath = strings.Replace(filePath, " ", "%20", -1)
|
|
readCloser, _, err := storage.client.DownloadFile(threadIndex, filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer readCloser.Close()
|
|
|
|
_, err = RateLimitedCopy(chunk, readCloser, storage.DownloadRateLimit/storage.client.Threads)
|
|
return err
|
|
}
|
|
|
|
// UploadFile writes 'content' to the file at 'filePath'.
|
|
func (storage *B2Storage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {
|
|
filePath = strings.Replace(filePath, " ", "%20", -1)
|
|
return storage.client.UploadFile(threadIndex, filePath, content, storage.UploadRateLimit/storage.client.Threads)
|
|
}
|
|
|
|
// If a local snapshot cache is needed for the storage to avoid downloading/uploading chunks too often when
|
|
// managing snapshots.
|
|
func (storage *B2Storage) IsCacheNeeded() bool { return true }
|
|
|
|
// If the 'MoveFile' method is implemented.
|
|
func (storage *B2Storage) IsMoveFileImplemented() bool { return true }
|
|
|
|
// If the storage can guarantee strong consistency.
|
|
func (storage *B2Storage) IsStrongConsistent() bool { return true }
|
|
|
|
// If the storage supports fast listing of files names.
|
|
func (storage *B2Storage) IsFastListing() bool { return true }
|
|
|
|
// Enable the test mode.
|
|
func (storage *B2Storage) EnableTestMode() {
|
|
storage.client.TestMode = true
|
|
}
|