// 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 ( "fmt" "strings" ) type HubicStorage struct { RateLimitedStorage client *HubicClient storageDir string numberOfThreads int } // CreateHubicStorage creates an Hubic storage object. func CreateHubicStorage(tokenFile string, storagePath string, threads int) (storage *HubicStorage, err error) { for len(storagePath) > 0 && storagePath[len(storagePath) - 1] == '/' { storagePath = storagePath[:len(storagePath) - 1] } client, err := NewHubicClient(tokenFile) if err != nil { return nil, err } exists, isDir, _, err := client.GetFileInfo(storagePath) if err != nil { return nil, err } if !exists { return nil, fmt.Errorf("Path '%s' doesn't exist", storagePath) } if !isDir { return nil, fmt.Errorf("Path '%s' is not a directory", storagePath) } storage = &HubicStorage { client: client, storageDir: storagePath, numberOfThreads: threads, } for _, path := range []string { "chunks", "snapshots" } { dir := storagePath + "/" + path exists, isDir, _, err := client.GetFileInfo(dir) if err != nil { return nil, err } if !exists { err = client.CreateDirectory(storagePath + "/" + path) if err != nil { return nil, err } } else if !isDir { return nil, fmt.Errorf("%s is not a directory", dir) } } return storage, nil } // ListFiles return the list of files and subdirectories under 'dir' (non-recursively) func (storage *HubicStorage) ListFiles(threadIndex int, dir string) ([]string, []int64, error) { for len(dir) > 0 && dir[len(dir) - 1] == '/' { dir = dir[:len(dir) - 1] } if dir == "snapshots" { entries, err := storage.client.ListEntries(storage.storageDir + "/" + dir) if err != nil { return nil, nil, err } subDirs := []string{} for _, entry := range entries { if entry.Type == "application/directory" { subDirs = append(subDirs, entry.Name + "/") } } return subDirs, nil, nil } else if strings.HasPrefix(dir, "snapshots/") { entries, err := storage.client.ListEntries(storage.storageDir + "/" + dir) if err != nil { return nil, nil, err } files := []string{} for _, entry := range entries { if entry.Type == "application/directory" { continue } files = append(files, entry.Name) } return files, nil, nil } else { files := []string{} sizes := []int64{} entries, err := storage.client.ListEntries(storage.storageDir + "/chunks") if err != nil { return nil, nil, err } for _, entry := range entries { if entry.Type == "application/directory" { continue } files = append(files, entry.Name) sizes = append(sizes, entry.Size) } return files, sizes, nil } } // DeleteFile deletes the file or directory at 'filePath'. func (storage *HubicStorage) DeleteFile(threadIndex int, filePath string) (err error) { err = storage.client.DeleteFile(storage.storageDir + "/" + filePath) if e, ok := err.(HubicError); ok && e.Status == 404 { LOG_DEBUG("HUBIC_DELETE", "Ignore 404 error") return nil } return err } // MoveFile renames the file. func (storage *HubicStorage) MoveFile(threadIndex int, from string, to string) (err error) { fromPath := storage.storageDir + "/" + from toPath := storage.storageDir + "/" + to return storage.client.MoveFile(fromPath, toPath) } // CreateDirectory creates a new directory. func (storage *HubicStorage) CreateDirectory(threadIndex int, dir string) (err error) { for len(dir) > 0 && dir[len(dir) - 1] == '/' { dir = dir[:len(dir) - 1] } return storage.client.CreateDirectory(storage.storageDir + "/" + dir) } // GetFileInfo returns the information about the file or directory at 'filePath'. func (storage *HubicStorage) GetFileInfo(threadIndex int, filePath string) (exist bool, isDir bool, size int64, err error) { for len(filePath) > 0 && filePath[len(filePath) - 1] == '/' { filePath = filePath[:len(filePath) - 1] } 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. func (storage *HubicStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) { readCloser, _, err := storage.client.DownloadFile(storage.storageDir + "/" + filePath) if err != nil { return err } defer readCloser.Close() _, err = RateLimitedCopy(chunk, readCloser, storage.DownloadRateLimit / storage.numberOfThreads) return err } // UploadFile writes 'content' to the file at 'filePath'. func (storage *HubicStorage) UploadFile(threadIndex int, filePath string, content []byte) (err error) { return storage.client.UploadFile(storage.storageDir + "/" + filePath, content, storage.UploadRateLimit / storage.numberOfThreads) } // If a local snapshot cache is needed for the storage to avoid downloading/uploading chunks too often when // managing snapshots. func (storage *HubicStorage) IsCacheNeeded() (bool) { return true } // If the 'MoveFile' method is implemented. func (storage *HubicStorage) IsMoveFileImplemented() (bool) { return true } // If the storage can guarantee strong consistency. func (storage *HubicStorage) IsStrongConsistent() (bool) { return false } // If the storage supports fast listing of files names. func (storage *HubicStorage) IsFastListing() (bool) { return true } // Enable the test mode. func (storage *HubicStorage) EnableTestMode() { storage.client.TestMode = true }