├── integrationtest ├── fixtures │ ├── 0byte │ ├── README │ ├── syncfolder │ │ └── README.syncfolder │ ├── simplefolder │ │ └── README.simplefolder │ ├── conflictfolder │ │ ├── v1 │ │ │ └── README.conflictfolder │ │ └── v2 │ │ │ └── README.conflictfolder │ └── recursivefolder │ │ ├── README.recursivefolder │ │ └── subfolder │ │ └── README.subfolder ├── acd.goconvey ├── acd-token.json.enc ├── doc.go ├── tree_sync_test.go ├── simple_upload_test.go ├── folder_upload_test.go └── init_test.go ├── doc.go ├── cmd └── acd │ └── main.go ├── .arcconfig ├── node ├── doc.go ├── util.go ├── remove.go ├── download.go ├── util_test.go ├── cache.go ├── find_test.go ├── find.go ├── mock.go ├── node.go ├── sync.go ├── upload.go └── tree.go ├── nodetree.go ├── .gitignore ├── internal ├── log │ ├── level_string.go │ └── extension.go └── constants │ └── errors.go ├── .travis.yml ├── list_test.go ├── list.go ├── default_paths_linux.go ├── default_paths_darwin.go ├── LICENSE ├── endpoints.go ├── response_checker.go ├── .arclint ├── download.go ├── cli ├── app.go ├── ls.go └── cp.go ├── README.md ├── token └── source.go ├── upload.go ├── account.go └── client.go /integrationtest/fixtures/0byte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integrationtest/acd.goconvey: -------------------------------------------------------------------------------- 1 | -short 2 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package acd represent an Amazon Cloud Drive client. 2 | package acd // import "gopkg.in/acd.v0" 3 | -------------------------------------------------------------------------------- /integrationtest/acd-token.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-acd/acd/HEAD/integrationtest/acd-token.json.enc -------------------------------------------------------------------------------- /cmd/acd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "gopkg.in/acd.v0/cli" 4 | 5 | func main() { 6 | app := cli.New() 7 | app.RunAndExitOnError() 8 | } 9 | -------------------------------------------------------------------------------- /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "load": [ 3 | "../../github.com/kalbasit/arcanist-go" 4 | ], 5 | "phabricator.uri": "http://phabricator.nasreddine.com/", 6 | "repository.callsign": "GOACD", 7 | "unit.engine": "GoTestEngine" 8 | } 9 | -------------------------------------------------------------------------------- /node/doc.go: -------------------------------------------------------------------------------- 1 | // Package node represents the Amazon Cloud Drive nodes documented at 2 | // https://developer.amazon.com/public/apis/experience/cloud-drive/content/nodes 3 | // It also provides the Tree struct which allows you to refer to the entire 4 | // filesystem as a file tree as defined by the Amazon documentation. 5 | package node // import "gopkg.in/acd.v0/node" 6 | -------------------------------------------------------------------------------- /nodetree.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import "gopkg.in/acd.v0/node" 4 | 5 | // FetchNodeTree fetches and caches the NodeTree. 6 | func (c *Client) FetchNodeTree() error { 7 | nt, err := node.NewTree(c, c.cacheFile) 8 | if err != nil { 9 | return err 10 | } 11 | 12 | c.NodeTree = nt 13 | return nil 14 | } 15 | 16 | // GetNodeTree returns the NodeTree. 17 | func (c *Client) GetNodeTree() *node.Tree { 18 | return c.NodeTree 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | cmd/acd/acd 7 | integrationtest/acd-token.json 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | -------------------------------------------------------------------------------- /node/util.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | // diff returns all of the strings that exist in s1 but do not exist in s2 4 | func diffSliceStr(s1, s2 []string) []string { 5 | var vs []string 6 | var found bool 7 | 8 | for _, v1 := range s1 { 9 | found = false 10 | for _, v2 := range s2 { 11 | if v1 == v2 { 12 | found = true 13 | break 14 | } 15 | } 16 | 17 | if !found { 18 | vs = append(vs, v1) 19 | } 20 | } 21 | 22 | return vs 23 | } 24 | -------------------------------------------------------------------------------- /internal/log/level_string.go: -------------------------------------------------------------------------------- 1 | // generated by stringer -type=Level; DO NOT EDIT 2 | 3 | package log 4 | 5 | import "fmt" 6 | 7 | const _Level_name = "DisableLogLevelFatalLevelErrorLevelInfoLevelDebugLevel" 8 | 9 | var _Level_index = [...]uint8{0, 15, 25, 35, 44, 54} 10 | 11 | func (i Level) String() string { 12 | if i >= Level(len(_Level_index)-1) { 13 | return fmt.Sprintf("Level(%d)", i) 14 | } 15 | return _Level_name[_Level_index[i]:_Level_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /integrationtest/doc.go: -------------------------------------------------------------------------------- 1 | // Package integrationtest is the integration test of the library. 2 | // This package assumes the existance of a valid 3 | // integrationtest/acd-token.json file with permissions 0600. 4 | // 5 | // The integration uses a real Amazon Cloud Drive account and manipulate files 6 | // under the folder /acd_test_folder. Due to the nature of these tests, it is 7 | // not recommended to run them against an account that has real data on it. The 8 | // ACD team and contributors are not responsible for any data loss due to these 9 | // tests. 10 | package integrationtest 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.4 4 | - tip 5 | matrix: 6 | allow_failures: 7 | - go: tip 8 | before_install: 9 | - openssl aes-256-cbc -K $encrypted_9933eea0afad_key -iv $encrypted_9933eea0afad_iv -in integrationtest/acd-token.json.enc -out integrationtest/acd-token.json -d 10 | - chmod 0600 integrationtest/acd-token.json 11 | - go get -u golang.org/x/tools/cmd/cover 12 | - mkdir -p $GOPATH/src/gopkg.in && cd .. && mv ${TRAVIS_BUILD_DIR} $GOPATH/src/gopkg.in/acd.v0 && export TRAVIS_BUILD_DIR=$GOPATH/src/gopkg.in/acd.v0 && cd ${TRAVIS_BUILD_DIR} 13 | script: go test -v -race -cover -bench=. ./... 14 | -------------------------------------------------------------------------------- /list_test.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "gopkg.in/acd.v0/node" 8 | ) 9 | 10 | func TestList(t *testing.T) { 11 | c := &Client{ 12 | NodeTree: node.Mocked, 13 | } 14 | 15 | tests := map[string][]string{ 16 | "/": []string{"README.md", "pictures"}, 17 | "/pictures": []string{"logo.png"}, 18 | } 19 | 20 | for path, want := range tests { 21 | var names []string 22 | nodes, err := c.List(path) 23 | if err != nil { 24 | t.Errorf("c.List(%q) error: %s", path, err) 25 | } 26 | for _, node := range nodes { 27 | names = append(names, node.Name) 28 | } 29 | if got := names; !reflect.DeepEqual(want, got) { 30 | t.Errorf("c.List(%q): want %v got %v", "/", want, got) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /node/remove.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "gopkg.in/acd.v0/internal/constants" 8 | "gopkg.in/acd.v0/internal/log" 9 | ) 10 | 11 | // Remove deletes a node from the server. 12 | // This function does not update the NodeTree, the caller should do so! 13 | func (n *Node) Remove() error { 14 | putURL := n.client.GetMetadataURL(fmt.Sprintf("/trash/%s", n.ID)) 15 | req, err := http.NewRequest("PUT", putURL, nil) 16 | if err != nil { 17 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 18 | return constants.ErrCreatingHTTPRequest 19 | } 20 | res, err := n.client.Do(req) 21 | if err != nil { 22 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 23 | return constants.ErrDoingHTTPRequest 24 | } 25 | if err := n.client.CheckResponse(res); err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "gopkg.in/acd.v0/internal/constants" 5 | "gopkg.in/acd.v0/internal/log" 6 | "gopkg.in/acd.v0/node" 7 | ) 8 | 9 | // List returns nodes.Nodes for all of the nodes underneath the path. It's up 10 | // to the caller to differentiate between a file, a folder or an asset by using 11 | // (*node.Node).IsFile(), (*node.Node).IsDir() and/or (*node.Node).IsAsset(). 12 | // A dir has sub-nodes accessible via (*node.Node).Nodes, you do not need to 13 | // call this this function for every sub-node. 14 | func (c *Client) List(path string) (node.Nodes, error) { 15 | rootNode, err := c.GetNodeTree().FindNode(path) 16 | if err != nil { 17 | return nil, err 18 | } 19 | if !rootNode.IsDir() { 20 | log.Errorf("%s: %s", constants.ErrPathIsNotFolder, path) 21 | return nil, constants.ErrPathIsNotFolder 22 | } 23 | 24 | return rootNode.Nodes, nil 25 | } 26 | -------------------------------------------------------------------------------- /default_paths_linux.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | const tokenFilename = "acd-token.json" 9 | const configFilename = "acd.json" 10 | const cacheFilename = "com.appspot.go-acd.cache" 11 | 12 | // DefaultConfigFile returns the default path for the configuration file. This is os-dependent setting. 13 | func DefaultConfigFile() string { 14 | homePath := os.Getenv("HOME") 15 | return path.Join(homePath, ".config", configFilename) 16 | } 17 | 18 | // DefaultTokenFile returns the default path for the token file. This is os-dependent setting. 19 | func DefaultTokenFile() string { 20 | homePath := os.Getenv("HOME") 21 | return path.Join(homePath, ".config", tokenFilename) 22 | } 23 | 24 | // DefaultCacheFile returns the default path for the cache file. This is os-dependent setting. 25 | func DefaultCacheFile() string { 26 | homePath := os.Getenv("HOME") 27 | return path.Join(homePath, ".cache", cacheFilename) 28 | } 29 | -------------------------------------------------------------------------------- /default_paths_darwin.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | const tokenFilename = "acd-token.json" 9 | const configFilename = "acd.json" 10 | const cacheFilename = "com.appspot.go-acd.cache" 11 | 12 | // DefaultConfigFile returns the default path for the configuration file. This is os-dependent setting. 13 | func DefaultConfigFile() string { 14 | homePath := os.Getenv("HOME") 15 | return path.Join(homePath, ".config", configFilename) 16 | } 17 | 18 | // DefaultTokenFile returns the default path for the token file. This is os-dependent setting. 19 | func DefaultTokenFile() string { 20 | homePath := os.Getenv("HOME") 21 | return path.Join(homePath, ".config", tokenFilename) 22 | } 23 | 24 | // DefaultCacheFile returns the default path for the cache file. This is os-dependent setting. 25 | func DefaultCacheFile() string { 26 | homePath := os.Getenv("HOME") 27 | return path.Join(homePath, "Library", "Caches", cacheFilename) 28 | } 29 | -------------------------------------------------------------------------------- /node/download.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | "gopkg.in/acd.v0/internal/constants" 9 | "gopkg.in/acd.v0/internal/log" 10 | ) 11 | 12 | // Download downloads the node and returns the body as io.ReadCloser or an 13 | // error. The caller is responsible for closing the reader. 14 | func (n *Node) Download() (io.ReadCloser, error) { 15 | if n.IsDir() { 16 | log.Errorf("%s: cannot download a folder", constants.ErrPathIsFolder) 17 | return nil, constants.ErrPathIsFolder 18 | } 19 | url := n.client.GetContentURL(fmt.Sprintf("nodes/%s/content", n.ID)) 20 | req, err := http.NewRequest("GET", url, nil) 21 | if err != nil { 22 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 23 | return nil, constants.ErrCreatingHTTPRequest 24 | } 25 | res, err := n.client.Do(req) 26 | if err != nil { 27 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 28 | return nil, constants.ErrDoingHTTPRequest 29 | } 30 | if err := n.client.CheckResponse(res); err != nil { 31 | return nil, err 32 | } 33 | 34 | return res.Body, nil 35 | } 36 | -------------------------------------------------------------------------------- /node/util_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestDiffSliceStr(t *testing.T) { 9 | type sliceString []string 10 | slices := [][][]string{ 11 | [][]string{ 12 | []string{"a", "b", "c"}, 13 | []string{"a", "b"}, 14 | }, 15 | [][]string{ 16 | []string{"a", "b", "c"}, 17 | []string{"a", "b", "c"}, 18 | }, 19 | [][]string{ 20 | []string{"b", "c"}, 21 | []string{"a", "b", "c"}, 22 | }, 23 | } 24 | diffs := [][]string{ 25 | []string{"c"}, 26 | []string{}, 27 | []string{}, 28 | } 29 | 30 | for i, ss := range slices { 31 | want, got := diffs[i], diffSliceStr(ss[0], ss[1]) 32 | 33 | // when we get an empty slice, we are actually getting an uninitialized one 34 | // for reflect.DeepEqual an initialized and an uninitialized slices are not 35 | // equal so we must initialize got so it reflect does not bark 36 | if len(got) == 0 { 37 | got = make([]string, 0) 38 | } 39 | 40 | if !reflect.DeepEqual(want, got) { 41 | t.Errorf("diffSliceStr(%v, %v): want %v, got %v", ss[0], ss[1], want, got) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Wael Nasreddine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /node/cache.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "encoding/gob" 5 | "os" 6 | 7 | "gopkg.in/acd.v0/internal/constants" 8 | "gopkg.in/acd.v0/internal/log" 9 | ) 10 | 11 | func (nt *Tree) loadCache() error { 12 | f, err := os.Open(nt.cacheFile) 13 | if err != nil { 14 | log.Debugf("error opening the cache file %q: %s", nt.cacheFile, constants.ErrLoadingCache) 15 | return constants.ErrLoadingCache 16 | } 17 | if err := gob.NewDecoder(f).Decode(nt); err != nil { 18 | log.Debugf("error decoding the cache file %q: %s", nt.cacheFile, err) 19 | return constants.ErrLoadingCache 20 | } 21 | log.Debugf("loaded NodeTree from cache file %q.", nt.cacheFile) 22 | nt.setClient(nt.Node) 23 | nt.buildNodeMap(nt.Node) 24 | 25 | return nil 26 | } 27 | 28 | func (nt *Tree) saveCache() error { 29 | f, err := os.Create(nt.cacheFile) 30 | if err != nil { 31 | log.Errorf("%s: %s", constants.ErrCreateFile, nt.cacheFile) 32 | return constants.ErrCreateFile 33 | } 34 | if err := gob.NewEncoder(f).Encode(nt); err != nil { 35 | log.Errorf("%s: %s", constants.ErrGOBEncoding, err) 36 | return constants.ErrGOBEncoding 37 | } 38 | log.Debugf("saved NodeTree to cache file %q.", nt.cacheFile) 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /endpoints.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "gopkg.in/acd.v0/internal/constants" 8 | "gopkg.in/acd.v0/internal/log" 9 | ) 10 | 11 | // GetMetadataURL returns the metadata url. 12 | func (c *Client) GetMetadataURL(path string) string { 13 | return c.metadataURL + path 14 | } 15 | 16 | // GetContentURL returns the content url. 17 | func (c *Client) GetContentURL(path string) string { 18 | return c.contentURL + path 19 | } 20 | 21 | func setEndpoints(c *Client) error { 22 | req, err := http.NewRequest("GET", endpointURL, nil) 23 | if err != nil { 24 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 25 | return constants.ErrCreatingHTTPRequest 26 | } 27 | 28 | var er endpointResponse 29 | res, err := c.Do(req) 30 | if err != nil { 31 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 32 | return constants.ErrDoingHTTPRequest 33 | } 34 | defer res.Body.Close() 35 | if err := json.NewDecoder(res.Body).Decode(&er); err != nil { 36 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 37 | return constants.ErrJSONDecodingResponseBody 38 | } 39 | 40 | c.contentURL = er.ContentURL 41 | c.metadataURL = er.MetadataURL 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /node/find_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import "testing" 4 | 5 | func TestFindNode(t *testing.T) { 6 | // tests are [path -> ID] 7 | tests := map[string]string{ 8 | "/": "/", 9 | "/README.md": "/README.md", 10 | "/rEaDme.MD": "/README.md", 11 | "//rEaDme.MD": "/README.md", 12 | "///REadmE.Md": "/README.md", 13 | "/pictuREs": "/pictures", 14 | "/pictures/loGO.png": "/pictures/logo.png", 15 | "/pictures//loGO.png": "/pictures/logo.png", 16 | } 17 | 18 | for path, ID := range tests { 19 | n, err := Mocked.FindNode(path) 20 | if err != nil { 21 | t.Fatalf("MockNodeTree.FindNode(%q) error: %s", path, err) 22 | } 23 | if want, got := ID, n.ID; want != got { 24 | t.Errorf("MockNodeTree.FindNode(%q).ID: want %s got %s", path, want, got) 25 | } 26 | } 27 | } 28 | 29 | func TestFindById(t *testing.T) { 30 | tests := []string{ 31 | "/", 32 | "/README.md", 33 | "/pictures", 34 | "/pictures/logo.png", 35 | } 36 | 37 | for _, test := range tests { 38 | n, err := Mocked.FindByID(test) 39 | if err != nil { 40 | t.Errorf("MockNodeTree.FindByID(%q) error: %s", test, err) 41 | } 42 | if want, got := test, n.ID; want != got { 43 | t.Errorf("MockNodeTree.FindByID(%q).ID: want %s got %s", test, want, got) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /integrationtest/fixtures/README: -------------------------------------------------------------------------------- 1 | The Amazon Cloud Drive API lets your customers access the photos, videos, and 2 | documents they have saved in Amazon Cloud Drive, and gives you the ability to 3 | interact with millions of Amazon customers. With access to the free Amazon 4 | Cloud Drive API you can put your own creative spin on how they upload, view, 5 | edit, download, and organize their digital content using your app. 6 | 7 | Built on the highly reliable and scalable AWS platform, the Amazon Cloud Drive 8 | API puts the power of a large-scale cloud services platform in your hands. You 9 | can offer confidence and peace of mind to your customer, as their content is 10 | safe and always accessible from Amazon Cloud Drive. Quit worrying about the 11 | cost of storage and unleash your creativity to build the best experience for 12 | your customers. For example, you could develop a mobile app to let customers 13 | edit all of their photos and videos into a single movie, or a web app that 14 | allows customers to organize their content around geolocation, or a desktop app 15 | that reimagines the way customers interact locally with their data in the 16 | cloud. 17 | 18 | If you're new to the Amazon Cloud Drive API, see our Developer Guide and tour 19 | Getting Started. 20 | 21 | https://developer.amazon.com/public/apis/experience/cloud-drive 22 | -------------------------------------------------------------------------------- /integrationtest/fixtures/syncfolder/README.syncfolder: -------------------------------------------------------------------------------- 1 | The Amazon Cloud Drive API lets your customers access the photos, videos, and 2 | documents they have saved in Amazon Cloud Drive, and gives you the ability to 3 | interact with millions of Amazon customers. With access to the free Amazon 4 | Cloud Drive API you can put your own creative spin on how they upload, view, 5 | edit, download, and organize their digital content using your app. 6 | 7 | Built on the highly reliable and scalable AWS platform, the Amazon Cloud Drive 8 | API puts the power of a large-scale cloud services platform in your hands. You 9 | can offer confidence and peace of mind to your customer, as their content is 10 | safe and always accessible from Amazon Cloud Drive. Quit worrying about the 11 | cost of storage and unleash your creativity to build the best experience for 12 | your customers. For example, you could develop a mobile app to let customers 13 | edit all of their photos and videos into a single movie, or a web app that 14 | allows customers to organize their content around geolocation, or a desktop app 15 | that reimagines the way customers interact locally with their data in the 16 | cloud. 17 | 18 | If you're new to the Amazon Cloud Drive API, see our Developer Guide and tour 19 | Getting Started. 20 | 21 | https://developer.amazon.com/public/apis/experience/cloud-drive 22 | syncfolder/README.syncfolder 23 | -------------------------------------------------------------------------------- /response_checker.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | 7 | "gopkg.in/acd.v0/internal/constants" 8 | "gopkg.in/acd.v0/internal/log" 9 | ) 10 | 11 | // CheckResponse validates the response from the Amazon Cloud Drive API. It 12 | // does that by looking at the response's status code and it returns an error 13 | // for any code lower than 200 or greater than 300 14 | func (c *Client) CheckResponse(res *http.Response) error { 15 | if 200 <= res.StatusCode && res.StatusCode <= 299 { 16 | return nil 17 | } 18 | errBody := "no response body" 19 | defer res.Body.Close() 20 | if data, err := ioutil.ReadAll(res.Body); err == nil { 21 | errBody = string(data) 22 | } 23 | var err error 24 | switch res.StatusCode { 25 | case http.StatusBadRequest: 26 | err = constants.ErrResponseBadInput 27 | case http.StatusUnauthorized: 28 | err = constants.ErrResponseInvalidToken 29 | case http.StatusForbidden: 30 | err = constants.ErrResponseForbidden 31 | case http.StatusConflict: 32 | err = constants.ErrResponseDuplicateExists 33 | case http.StatusInternalServerError: 34 | err = constants.ErrResponseInternalServerError 35 | case http.StatusServiceUnavailable: 36 | err = constants.ErrResponseUnavailable 37 | default: 38 | err = constants.ErrResponseUnknown 39 | } 40 | 41 | log.Errorf("{code: %s} %s: %s", res.Status, err, errBody) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /integrationtest/fixtures/simplefolder/README.simplefolder: -------------------------------------------------------------------------------- 1 | The Amazon Cloud Drive API lets your customers access the photos, videos, and 2 | documents they have saved in Amazon Cloud Drive, and gives you the ability to 3 | interact with millions of Amazon customers. With access to the free Amazon 4 | Cloud Drive API you can put your own creative spin on how they upload, view, 5 | edit, download, and organize their digital content using your app. 6 | 7 | Built on the highly reliable and scalable AWS platform, the Amazon Cloud Drive 8 | API puts the power of a large-scale cloud services platform in your hands. You 9 | can offer confidence and peace of mind to your customer, as their content is 10 | safe and always accessible from Amazon Cloud Drive. Quit worrying about the 11 | cost of storage and unleash your creativity to build the best experience for 12 | your customers. For example, you could develop a mobile app to let customers 13 | edit all of their photos and videos into a single movie, or a web app that 14 | allows customers to organize their content around geolocation, or a desktop app 15 | that reimagines the way customers interact locally with their data in the 16 | cloud. 17 | 18 | If you're new to the Amazon Cloud Drive API, see our Developer Guide and tour 19 | Getting Started. 20 | 21 | https://developer.amazon.com/public/apis/experience/cloud-drive 22 | simplefolder/README.simplefolder 23 | -------------------------------------------------------------------------------- /integrationtest/fixtures/conflictfolder/v1/README.conflictfolder: -------------------------------------------------------------------------------- 1 | The Amazon Cloud Drive API lets your customers access the photos, videos, and 2 | documents they have saved in Amazon Cloud Drive, and gives you the ability to 3 | interact with millions of Amazon customers. With access to the free Amazon 4 | Cloud Drive API you can put your own creative spin on how they upload, view, 5 | edit, download, and organize their digital content using your app. 6 | 7 | Built on the highly reliable and scalable AWS platform, the Amazon Cloud Drive 8 | API puts the power of a large-scale cloud services platform in your hands. You 9 | can offer confidence and peace of mind to your customer, as their content is 10 | safe and always accessible from Amazon Cloud Drive. Quit worrying about the 11 | cost of storage and unleash your creativity to build the best experience for 12 | your customers. For example, you could develop a mobile app to let customers 13 | edit all of their photos and videos into a single movie, or a web app that 14 | allows customers to organize their content around geolocation, or a desktop app 15 | that reimagines the way customers interact locally with their data in the 16 | cloud. 17 | 18 | If you're new to the Amazon Cloud Drive API, see our Developer Guide and tour 19 | Getting Started. 20 | 21 | https://developer.amazon.com/public/apis/experience/cloud-drive 22 | conflictfolder/a/README.conflictfolder 23 | -------------------------------------------------------------------------------- /integrationtest/fixtures/conflictfolder/v2/README.conflictfolder: -------------------------------------------------------------------------------- 1 | The Amazon Cloud Drive API lets your customers access the photos, videos, and 2 | documents they have saved in Amazon Cloud Drive, and gives you the ability to 3 | interact with millions of Amazon customers. With access to the free Amazon 4 | Cloud Drive API you can put your own creative spin on how they upload, view, 5 | edit, download, and organize their digital content using your app. 6 | 7 | Built on the highly reliable and scalable AWS platform, the Amazon Cloud Drive 8 | API puts the power of a large-scale cloud services platform in your hands. You 9 | can offer confidence and peace of mind to your customer, as their content is 10 | safe and always accessible from Amazon Cloud Drive. Quit worrying about the 11 | cost of storage and unleash your creativity to build the best experience for 12 | your customers. For example, you could develop a mobile app to let customers 13 | edit all of their photos and videos into a single movie, or a web app that 14 | allows customers to organize their content around geolocation, or a desktop app 15 | that reimagines the way customers interact locally with their data in the 16 | cloud. 17 | 18 | If you're new to the Amazon Cloud Drive API, see our Developer Guide and tour 19 | Getting Started. 20 | 21 | https://developer.amazon.com/public/apis/experience/cloud-drive 22 | conflictfolder/b/README.conflictfolder 23 | -------------------------------------------------------------------------------- /integrationtest/fixtures/recursivefolder/README.recursivefolder: -------------------------------------------------------------------------------- 1 | The Amazon Cloud Drive API lets your customers access the photos, videos, and 2 | documents they have saved in Amazon Cloud Drive, and gives you the ability to 3 | interact with millions of Amazon customers. With access to the free Amazon 4 | Cloud Drive API you can put your own creative spin on how they upload, view, 5 | edit, download, and organize their digital content using your app. 6 | 7 | Built on the highly reliable and scalable AWS platform, the Amazon Cloud Drive 8 | API puts the power of a large-scale cloud services platform in your hands. You 9 | can offer confidence and peace of mind to your customer, as their content is 10 | safe and always accessible from Amazon Cloud Drive. Quit worrying about the 11 | cost of storage and unleash your creativity to build the best experience for 12 | your customers. For example, you could develop a mobile app to let customers 13 | edit all of their photos and videos into a single movie, or a web app that 14 | allows customers to organize their content around geolocation, or a desktop app 15 | that reimagines the way customers interact locally with their data in the 16 | cloud. 17 | 18 | If you're new to the Amazon Cloud Drive API, see our Developer Guide and tour 19 | Getting Started. 20 | 21 | https://developer.amazon.com/public/apis/experience/cloud-drive 22 | recursivefolder/README.recursivefolder 23 | -------------------------------------------------------------------------------- /.arclint: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "(^Godeps/)", 4 | "(^\\.arc/__*)", 5 | "(^\\.arc/.*)", 6 | "(^\\.gitmodules$)" 7 | ], 8 | "linters": { 9 | "chmod": { 10 | "type": "chmod" 11 | }, 12 | "filename": { 13 | "type": "filename" 14 | }, 15 | "gofmt": { 16 | "include": [ 17 | "(\\.go$)" 18 | ], 19 | "type": "gofmt" 20 | }, 21 | "golint": { 22 | "include": [ 23 | "(\\.go$)" 24 | ], 25 | "type": "golint" 26 | }, 27 | "govet": { 28 | "include": [ 29 | "(\\.go$)" 30 | ], 31 | "type": "govet" 32 | }, 33 | "json": { 34 | "include": [ 35 | "(^\\.arcconfig$)", 36 | "(^\\.arclint$)", 37 | "(\\.json$)" 38 | ], 39 | "type": "json" 40 | }, 41 | "merge-conflict": { 42 | "type": "merge-conflict" 43 | }, 44 | "spelling": { 45 | "type": "spelling" 46 | }, 47 | "text": { 48 | "exclude": [ 49 | "(\\.go$)", 50 | "(^Makefile$)", 51 | "(^.travis.yml$)" 52 | ], 53 | "type": "text" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /integrationtest/fixtures/recursivefolder/subfolder/README.subfolder: -------------------------------------------------------------------------------- 1 | The Amazon Cloud Drive API lets your customers access the photos, videos, and 2 | documents they have saved in Amazon Cloud Drive, and gives you the ability to 3 | interact with millions of Amazon customers. With access to the free Amazon 4 | Cloud Drive API you can put your own creative spin on how they upload, view, 5 | edit, download, and organize their digital content using your app. 6 | 7 | Built on the highly reliable and scalable AWS platform, the Amazon Cloud Drive 8 | API puts the power of a large-scale cloud services platform in your hands. You 9 | can offer confidence and peace of mind to your customer, as their content is 10 | safe and always accessible from Amazon Cloud Drive. Quit worrying about the 11 | cost of storage and unleash your creativity to build the best experience for 12 | your customers. For example, you could develop a mobile app to let customers 13 | edit all of their photos and videos into a single movie, or a web app that 14 | allows customers to organize their content around geolocation, or a desktop app 15 | that reimagines the way customers interact locally with their data in the 16 | cloud. 17 | 18 | If you're new to the Amazon Cloud Drive API, see our Developer Guide and tour 19 | Getting Started. 20 | 21 | https://developer.amazon.com/public/apis/experience/cloud-drive 22 | recursivefolder/subfolder/README.subfolder 23 | -------------------------------------------------------------------------------- /node/find.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "gopkg.in/acd.v0/internal/constants" 8 | "gopkg.in/acd.v0/internal/log" 9 | ) 10 | 11 | // FindNode finds a node for a particular path. 12 | // TODO(kalbasit): This does not perform well, this should be cached in a map 13 | // path->node and calculated on load (fresh, cache, refresh). 14 | func (nt *Tree) FindNode(path string) (*Node, error) { 15 | // replace multiple n*/ with / 16 | re := regexp.MustCompile("/[/]*") 17 | path = string(re.ReplaceAll([]byte(path), []byte("/"))) 18 | // chop off the first /. 19 | path = strings.TrimPrefix(path, "/") 20 | // did we ask for the root node? 21 | if path == "" { 22 | return nt.Node, nil 23 | } 24 | 25 | // initialize our search from the root node 26 | node := nt.Node 27 | 28 | // iterate over the path parts until we find the path (or not). 29 | parts := strings.Split(path, "/") 30 | for _, part := range parts { 31 | var found bool 32 | for _, n := range node.Nodes { 33 | // does node.name matches our query? 34 | if strings.ToLower(n.Name) == strings.ToLower(part) { 35 | node = n 36 | found = true 37 | break 38 | } 39 | } 40 | 41 | if !found { 42 | log.Errorf("%s: %s", constants.ErrNodeNotFound, path) 43 | return nil, constants.ErrNodeNotFound 44 | } 45 | } 46 | 47 | return node, nil 48 | } 49 | 50 | // FindByID returns the node identified by the ID. 51 | func (nt *Tree) FindByID(id string) (*Node, error) { 52 | n, found := nt.nodeMap[id] 53 | if !found { 54 | log.Errorf("%s: ID %q", constants.ErrNodeNotFound, id) 55 | return nil, constants.ErrNodeNotFound 56 | } 57 | 58 | return n, nil 59 | } 60 | -------------------------------------------------------------------------------- /integrationtest/tree_sync_test.go: -------------------------------------------------------------------------------- 1 | package integrationtest 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "io" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestTreeSync(t *testing.T) { 12 | if testing.Short() { 13 | t.Skip("skipping integration test") 14 | } 15 | needCleaning = true 16 | 17 | var ( 18 | readmeFile = "fixtures/syncfolder/README.syncfolder" 19 | remoteReadmeFile = remotePath(readmeFile) 20 | ) 21 | 22 | // create two cached clients 23 | c1, err := newCachedClient(true) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if err := c1.FetchNodeTree(); err != nil { 28 | t.Fatal(err) 29 | } 30 | c2, err := newCachedClient(true) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | if err := c2.FetchNodeTree(); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | // using the first client upload the README file 39 | in, err := os.Open(readmeFile) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | inhash := md5.New() 44 | in.Seek(0, 0) 45 | io.Copy(inhash, in) 46 | inmd5 := hex.EncodeToString(inhash.Sum(nil)) 47 | in.Seek(0, 0) 48 | if err := c1.Upload(remoteReadmeFile, false, in); err != nil { 49 | t.Errorf("error uploading %s to %s: %s", readmeFile, remoteReadmeFile, err) 50 | } 51 | 52 | // using the second client, sync and find the node 53 | if err := c2.NodeTree.Sync(); err != nil { 54 | t.Fatal(err) 55 | } 56 | readmeNode, err := c2.NodeTree.FindNode(remoteReadmeFile) 57 | if err != nil { 58 | t.Fatalf("c2.NodeTree.FindNode(%q) error: %s", remoteReadmeFile, err) 59 | } 60 | if want, got := inmd5, readmeNode.ContentProperties.MD5; want != got { 61 | t.Errorf("c.NodeTree.FindNode(%q).ContentProperties.MD5: want %s got %s", remoteReadmeFile, want, got) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path" 8 | 9 | "gopkg.in/acd.v0/internal/constants" 10 | "gopkg.in/acd.v0/internal/log" 11 | ) 12 | 13 | // Download returns an io.ReadCloser for path. The caller is responsible for 14 | // closing the body. 15 | func (c *Client) Download(path string) (io.ReadCloser, error) { 16 | log.Debugf("downloading %q", path) 17 | 18 | node, err := c.NodeTree.FindNode(path) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return node.Download() 24 | } 25 | 26 | // DownloadFolder downloads an entire folder to a path, if recursive is true, 27 | // it will also download all subfolders. 28 | func (c *Client) DownloadFolder(localPath, remotePath string, recursive bool) error { 29 | log.Debugf("downloading %q to %q", localPath, remotePath) 30 | 31 | if err := os.Mkdir(localPath, os.FileMode(0755)); err != nil && !os.IsExist(err) { 32 | log.Errorf("%s: %s", constants.ErrCreateFolder, err) 33 | return constants.ErrCreateFolder 34 | } 35 | rootNode, err := c.GetNodeTree().FindNode(remotePath) 36 | if err != nil { 37 | return nil 38 | } 39 | for _, node := range rootNode.Nodes { 40 | flp := path.Join(localPath, node.Name) 41 | frp := fmt.Sprintf("%s/%s", remotePath, node.Name) 42 | if node.IsDir() { 43 | if recursive { 44 | if err := c.DownloadFolder(flp, frp, recursive); err != nil { 45 | return err 46 | } 47 | } 48 | 49 | continue 50 | } 51 | 52 | con, err := node.Download() 53 | if err != nil { 54 | return err 55 | } 56 | f, err := os.Create(flp) 57 | if err != nil { 58 | log.Errorf("%s: %s", constants.ErrCreateFile, flp) 59 | return constants.ErrCreateFile 60 | } 61 | log.Debugf("saving %s as %s", frp, flp) 62 | _, err = io.Copy(f, con) 63 | f.Close() 64 | con.Close() 65 | if err != nil { 66 | log.Errorf("%s: %s", constants.ErrWritingFileContents, err) 67 | return err 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /cli/app.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codegangsta/cli" 7 | "gopkg.in/acd.v0" 8 | "gopkg.in/acd.v0/internal/log" 9 | ) 10 | 11 | // TODO(kalbasit): I do not like this API code not even a bit. 12 | // a) I used codegangsta/cli wrong or overthought it. 13 | // b) codegangsta/cli is not the right library for this project. 14 | // This entire package should be re-written and TESTED!. 15 | 16 | var ( 17 | commands []cli.Command 18 | acdClient *acd.Client 19 | ) 20 | 21 | // New creates a new CLI application. 22 | func New() *cli.App { 23 | app := cli.NewApp() 24 | app.Author = "Wael Nasreddine" 25 | app.Email = "wael.nasreddine@gmail.com" 26 | app.Version = "0.1.0" 27 | app.EnableBashCompletion = true 28 | app.Name = "acd" 29 | app.Flags = []cli.Flag{ 30 | cli.StringFlag{ 31 | Name: "config-file, c", 32 | Value: acd.DefaultConfigFile(), 33 | Usage: "the path of the configuration file", 34 | }, 35 | 36 | cli.IntFlag{ 37 | Name: "log-level, l", 38 | Value: int(log.FatalLevel), 39 | Usage: fmt.Sprintf("possible log levels: %s", log.Levels()), 40 | }, 41 | } 42 | 43 | app.Before = beforeCommand 44 | app.After = afterCommand 45 | app.Commands = commands 46 | return app 47 | } 48 | 49 | func registerCommand(c cli.Command) { 50 | commands = append(commands, c) 51 | } 52 | 53 | func beforeCommand(c *cli.Context) error { 54 | var err error 55 | 56 | // set the log level 57 | log.SetLevel(log.Level(c.Int("log-level"))) 58 | 59 | // create a new client 60 | if acdClient, err = acd.New(c.String("config-file")); err != nil { 61 | return fmt.Errorf("error creating a new ACD client: %s", err) 62 | } 63 | 64 | // fetch the nodetree 65 | if err = acdClient.FetchNodeTree(); err != nil { 66 | return fmt.Errorf("error fetch the node tree: %s", err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func afterCommand(_ *cli.Context) error { 73 | if acdClient != nil { 74 | return acdClient.Close() 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /cli/ls.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "gopkg.in/acd.v0/node" 9 | 10 | "github.com/codegangsta/cli" 11 | ) 12 | 13 | var ( 14 | paths []string 15 | 16 | lsCommand = cli.Command{ 17 | Name: "ls", 18 | Usage: "list directory contents", 19 | Description: "ls list directory contents, multiple directories can be given. A directory must be prefixed by acd://", 20 | Action: lsAction, 21 | BashComplete: lsBashComplete, 22 | Before: lsBefore, 23 | Flags: []cli.Flag{ 24 | cli.BoolFlag{ 25 | Name: "long, l", 26 | Usage: "list in long format", 27 | }, 28 | }, 29 | } 30 | ) 31 | 32 | func init() { 33 | registerCommand(lsCommand) 34 | } 35 | 36 | func lsAction(c *cli.Context) { 37 | for _, p := range paths { 38 | nodes, err := acdClient.List(p) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | if len(paths) > 1 { 44 | fmt.Printf("%s:\n", p) 45 | } 46 | 47 | if c.Bool("long") { 48 | lsLong(nodes) 49 | } else { 50 | lsShort(nodes) 51 | } 52 | 53 | if len(paths) > 1 { 54 | fmt.Println() 55 | } 56 | } 57 | } 58 | 59 | func lsBefore(c *cli.Context) error { 60 | for _, arg := range c.Args() { 61 | if !strings.HasPrefix(arg, "acd://") { 62 | continue 63 | } 64 | 65 | paths = append(paths, strings.TrimPrefix(arg, "acd://")) 66 | } 67 | if len(paths) == 0 { 68 | return fmt.Errorf("ls: at least one path prefixed by acd:// is required. Given: %v", c.Args()) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func lsBashComplete(c *cli.Context) { 75 | } 76 | 77 | func lsLong(nodes node.Nodes) { 78 | for _, n := range nodes { 79 | if n.IsDir() { 80 | fmt.Print("d") 81 | } else { 82 | fmt.Print("-") 83 | } 84 | 85 | fmt.Printf("\t%d", n.Size()) 86 | fmt.Printf("\t%s", n.ModTime()) 87 | fmt.Printf("\t%s\n", n.Name) 88 | } 89 | } 90 | 91 | func lsShort(nodes node.Nodes) { 92 | sep := "" 93 | for _, n := range nodes { 94 | fmt.Printf("%s%s", sep, n.Name) 95 | sep = " " 96 | } 97 | fmt.Println("") 98 | } 99 | -------------------------------------------------------------------------------- /node/mock.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import "time" 4 | 5 | var ( 6 | rootNode = &Node{ 7 | ID: "/", 8 | Kind: "FOLDER", 9 | Parents: []string{}, 10 | Status: "AVAILABLE", 11 | CreatedBy: "CloudDriveFiles", 12 | CreationDate: time.Now(), 13 | ModifiedDate: time.Now(), 14 | Version: 1, 15 | Root: true, 16 | Nodes: Nodes{ 17 | &Node{ 18 | ID: "/README.md", 19 | Name: "README.md", 20 | Kind: "FILE", 21 | Parents: []string{"/"}, 22 | Status: "AVAILABLE", 23 | CreatedBy: "CloudDriveFiles", 24 | CreationDate: time.Now(), 25 | ModifiedDate: time.Now(), 26 | Version: 1, 27 | ContentProperties: ContentProperties{ 28 | Version: 1, 29 | Extension: "md", 30 | Size: 740, 31 | MD5: "11c8fac0d43831697251fd0b869e77d7", 32 | ContentType: "text/plain", 33 | ContentDate: time.Now(), 34 | }, 35 | }, 36 | 37 | &Node{ 38 | ID: "/pictures", 39 | Name: "pictures", 40 | Kind: "FOLDER", 41 | Parents: []string{"/"}, 42 | Status: "AVAILABLE", 43 | CreatedBy: "CloudDriveFiles", 44 | CreationDate: time.Now(), 45 | ModifiedDate: time.Now(), 46 | Version: 1, 47 | Nodes: Nodes{ 48 | &Node{ 49 | ID: "/pictures/logo.png", 50 | Name: "logo.png", 51 | Kind: "FILE", 52 | Parents: []string{"/pictures"}, 53 | Status: "AVAILABLE", 54 | CreatedBy: "CloudDriveFiles", 55 | CreationDate: time.Now(), 56 | ModifiedDate: time.Now(), 57 | Version: 1, 58 | ContentProperties: ContentProperties{ 59 | Version: 1, 60 | Extension: "png", 61 | Size: 18750, 62 | MD5: "c2c88b2bc3574122210c9f0cb45b0593", 63 | ContentType: "image/png", 64 | ContentDate: time.Now(), 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | } 71 | 72 | // Mocked is a valid tree (mock). The IDs are the fully-qualified path of 73 | // the file or folder to make testing easier. 74 | // / 75 | // |-- README.md 76 | // |-- pictures 77 | // |-- | 78 | // | -- logo.png 79 | Mocked = &Tree{ 80 | Node: rootNode, 81 | nodeMap: map[string]*Node{ 82 | "/": rootNode, 83 | "/README.md": rootNode.Nodes[0], 84 | "/pictures": rootNode.Nodes[1], 85 | "/pictures/logo.png": rootNode.Nodes[1].Nodes[0], 86 | }, 87 | } 88 | ) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Cloud Drive client for Go 2 | [![Build Status](https://travis-ci.org/go-acd/acd.svg?branch=master)](https://travis-ci.org/go-acd/acd) [![GoDoc](https://godoc.org/gopkg.in/acd.v0?status.png)](https://godoc.org/gopkg.in/acd.v0) 3 | 4 | Amazon Cloud Drive uses 5 | [oAuth 2.0 for authentication](https://developer.amazon.com/public/apis/experience/cloud-drive/content/restful-api-getting-started). 6 | The [token server](https://github.com/go-acd/token-server) takes care of 7 | the oAuth authentication. For your convenience, an instance of the 8 | server is deployed at: 9 | 10 | https://go-acd.appspot.com 11 | 12 | # Install 13 | 14 | This project is go-gettable: 15 | 16 | ``` 17 | go get gopkg.in/acd.v0/... 18 | ``` 19 | 20 | # Usage 21 | 22 | In order to use this library, you must authenticate through the [token server](https://go-acd.appspot.com). 23 | 24 | ## CLI 25 | 26 | Run `acd help` for usage. 27 | 28 | ## Library 29 | 30 | Consult the [Godoc](https://godoc.org/gopkg.in/acd.v0) for information 31 | on how to use the library. 32 | 33 | # Contributions 34 | 35 | Contributions are welcome as pull requests. 36 | 37 | # Commit Style Guideline 38 | 39 | We follow a rough convention for commit messages borrowed from Deis who 40 | borrowed theirs from CoreOS, who borrowed theirs from AngularJS. This is 41 | an example of a commit: 42 | 43 | feat(token): remove dependency on file system. 44 | 45 | use an IO.Reader and IO.Writer to deal with the token. 46 | 47 | To make it more formal, it looks something like this: 48 | {type}({scope}): {subject} 49 | 50 | {body} 51 | 52 | {footer} 53 | 54 | The {scope} can be anything specifying place of the commit change. 55 | 56 | The {subject} needs to use imperative, present tense: “change”, not “changed” nor 57 | “changes”. The first letter should not be capitalized, and there is no dot (.) at the end. 58 | 59 | Just like the {subject}, the message {body} needs to be in the present tense, and includes 60 | the motivation for the change, as well as a contrast with the previous behavior. The first 61 | letter in a paragraph must be capitalized. 62 | 63 | All breaking changes need to be mentioned in the {footer} with the description of the 64 | change, the justification behind the change and any migration notes required. 65 | 66 | Any line of the commit message cannot be longer than 72 characters, with the subject line 67 | limited to 50 characters. This allows the message to be easier to read on github as well 68 | as in various git tools. 69 | 70 | The allowed {types} are as follows: 71 | 72 | feat -> feature 73 | fix -> bug fix 74 | docs -> documentation 75 | style -> formatting 76 | ref -> refactoring code 77 | test -> adding missing tests 78 | chore -> maintenance 79 | 80 | # Credits 81 | 82 | Although this project was built from scratch, it was inspired by the 83 | following: 84 | 85 | - [sgeb/go-acd](https://github.com/sgeb/go-acd) 86 | - [yadayada/acd_cli](https://github.com/yadayada/acd_cli) 87 | - [caseymrm/drivesink](https://github.com/caseymrm/drivesink) 88 | 89 | # License ![License](https://img.shields.io/badge/license-MIT-blue.svg?style=plastic) 90 | 91 | The MIT License (MIT) - see LICENSE for more details 92 | -------------------------------------------------------------------------------- /internal/log/extension.go: -------------------------------------------------------------------------------- 1 | //go:generate stringer -type=Level 2 | 3 | // Package log provides a common logging package for ACD with logging level. 4 | package log 5 | 6 | import ( 7 | "fmt" 8 | stdLog "log" 9 | ) 10 | 11 | // Level is a custom type representing a log level. 12 | type Level uint8 13 | 14 | const ( 15 | // DisableLogLevel disables logging completely. 16 | DisableLogLevel Level = iota 17 | 18 | // FatalLevel represents a fatal message. 19 | FatalLevel 20 | 21 | // ErrorLevel represents an error message. 22 | ErrorLevel 23 | 24 | // InfoLevel represents an info message. 25 | InfoLevel 26 | 27 | // DebugLevel represents a debug message. 28 | DebugLevel 29 | ) 30 | 31 | var ( 32 | // Level defines the log level. Default: Error 33 | level = ErrorLevel 34 | 35 | levelPrefix = map[Level]string{ 36 | DisableLogLevel: "", 37 | FatalLevel: "[FATAL] ", 38 | ErrorLevel: "[ERROR] ", 39 | InfoLevel: "[INFO] ", 40 | DebugLevel: "[DEBUG] ", 41 | } 42 | ) 43 | 44 | // Levels returns a string of all possible levels 45 | func Levels() string { 46 | return fmt.Sprintf("%d:%s, %d:%s, %d:%s, %d:%s, %d:%s", 47 | DisableLogLevel, DisableLogLevel, 48 | FatalLevel, FatalLevel, 49 | ErrorLevel, ErrorLevel, 50 | InfoLevel, InfoLevel, 51 | DebugLevel, DebugLevel) 52 | } 53 | 54 | // SetLevel sets the log level to l. 55 | func SetLevel(l Level) { 56 | level = l 57 | } 58 | 59 | // GetLevel sets the log level to l. 60 | func GetLevel() Level { 61 | return level 62 | } 63 | 64 | // Printf calls Printf only if the level is equal or lower than the set level. 65 | // If the level is FatalLevel, it will call Fatalf regardless... 66 | func Printf(l Level, format string, v ...interface{}) { 67 | if l == FatalLevel { 68 | stdLog.Fatalf(format, v...) 69 | return 70 | } 71 | 72 | if l <= level { 73 | defer stdLog.SetPrefix(stdLog.Prefix()) 74 | stdLog.SetPrefix(levelPrefix[l]) 75 | stdLog.Printf(format, v...) 76 | } 77 | } 78 | 79 | // Print calls Print only if the level is equal or lower than the set level. 80 | // If the level is FatalLevel, it will call Fatal regardless... 81 | func Print(l Level, v ...interface{}) { 82 | if l == FatalLevel { 83 | stdLog.Fatal(v...) 84 | return 85 | } 86 | 87 | if l <= level { 88 | defer stdLog.SetPrefix(stdLog.Prefix()) 89 | stdLog.SetPrefix(levelPrefix[l]) 90 | stdLog.Print(v...) 91 | } 92 | } 93 | 94 | // Fatalf wraps Printf 95 | func Fatalf(format string, v ...interface{}) { Printf(FatalLevel, format, v...) } 96 | 97 | // Errorf wraps Printf 98 | func Errorf(format string, v ...interface{}) { Printf(ErrorLevel, format, v...) } 99 | 100 | // Infof wraps Printf 101 | func Infof(format string, v ...interface{}) { Printf(InfoLevel, format, v...) } 102 | 103 | // Debugf wraps Printf 104 | func Debugf(format string, v ...interface{}) { Printf(DebugLevel, format, v...) } 105 | 106 | // Fatal wraps Print 107 | func Fatal(v ...interface{}) { Print(FatalLevel, v...) } 108 | 109 | // Error wraps Print 110 | func Error(v ...interface{}) { Print(ErrorLevel, v...) } 111 | 112 | // Info wraps Print 113 | func Info(v ...interface{}) { Print(InfoLevel, v...) } 114 | 115 | // Debug wraps Print 116 | func Debug(v ...interface{}) { Print(DebugLevel, v...) } 117 | -------------------------------------------------------------------------------- /integrationtest/simple_upload_test.go: -------------------------------------------------------------------------------- 1 | package integrationtest 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "gopkg.in/acd.v0/internal/constants" 11 | ) 12 | 13 | func TestSimpleUpload(t *testing.T) { 14 | if testing.Short() { 15 | t.Skip("skipping integration test") 16 | } 17 | needCleaning = true 18 | 19 | var ( 20 | readmeFile = "fixtures/README" 21 | remoteReadmeFile = remotePath(readmeFile) 22 | ) 23 | 24 | // open the README file 25 | in, err := os.Open(readmeFile) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer in.Close() 30 | inhash := md5.New() 31 | in.Seek(0, 0) 32 | io.Copy(inhash, in) 33 | inmd5 := hex.EncodeToString(inhash.Sum(nil)) 34 | 35 | // test uploading 36 | c, err := newCachedClient(true) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if err := c.FetchNodeTree(); err != nil { 41 | t.Fatal(err) 42 | } 43 | in.Seek(0, 0) 44 | if err := c.Upload(remoteReadmeFile, false, in); err != nil { 45 | t.Errorf("error uploading %s to %s: %s", readmeFile, remoteReadmeFile, err) 46 | } 47 | 48 | // test the NodeTree is updated 49 | node, err := c.NodeTree.FindNode(remoteReadmeFile) 50 | if err != nil { 51 | t.Errorf("c.NodeTree.FindNode(%q): got error %s", remoteReadmeFile, err) 52 | } 53 | if want, got := inmd5, node.ContentProperties.MD5; want != got { 54 | t.Errorf("c.NodeTree.FindNode(%q).ContentProperties.MD5: want %s got %s", remoteReadmeFile, want, got) 55 | } 56 | 57 | // test the cache is being saved updated 58 | c.Close() 59 | c, err = newCachedClient(false) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | if err := c.FetchNodeTree(); err != nil { 64 | t.Fatal(err) 65 | } 66 | node, err = c.NodeTree.FindNode(remoteReadmeFile) 67 | if err != nil { 68 | t.Errorf("reloaded cache, c.NodeTree.FindNode(%q): got error %s", remoteReadmeFile, err) 69 | } 70 | if want, got := inmd5, node.ContentProperties.MD5; want != got { 71 | t.Errorf("reloaded cache, c.NodeTree.FindNode(%q).ContentProperties.MD5: want %s got %s", remoteReadmeFile, want, got) 72 | } 73 | 74 | // check the file exists on the server 75 | uc, err := newUncachedClient() 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if err := uc.FetchNodeTree(); err != nil { 80 | t.Fatal(err) 81 | } 82 | out, err := c.Download(remoteReadmeFile) 83 | if err != nil { 84 | t.Errorf("error uploading %s to %s: %s", readmeFile, remoteReadmeFile, err) 85 | } 86 | outhash := md5.New() 87 | io.Copy(outhash, out) 88 | outmd5 := hex.EncodeToString(outhash.Sum(nil)) 89 | 90 | if want, got := inmd5, outmd5; want != got { 91 | t.Errorf("c.Upload() hashes: want %s got %s", want, got) 92 | } 93 | } 94 | 95 | func Test0ByteUpload(t *testing.T) { 96 | if testing.Short() { 97 | t.Skip("skipping integration test") 98 | } 99 | needCleaning = true 100 | 101 | var ( 102 | zeroByteFile = "fixtures/0byte" 103 | remoteZeroByteFile = remotePath(zeroByteFile) 104 | ) 105 | 106 | // open the 0byte file 107 | in, err := os.Open(zeroByteFile) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | defer in.Close() 112 | 113 | // test uploading 114 | c, err := newUncachedClient() 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | if err := c.FetchNodeTree(); err != nil { 119 | t.Fatal(err) 120 | } 121 | if want, got := constants.ErrNoContentsToUpload, c.Upload(remoteZeroByteFile, false, in); want != got { 122 | t.Errorf("uploading a 0-byte file: want %s got %s", want, got) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /token/source.go: -------------------------------------------------------------------------------- 1 | // Package token represents an oauth2.TokenSource which has the ability to 2 | // refresh the access token through the oauth server. 3 | package token // import "gopkg.in/acd.v0/token" 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "net/http" 9 | "os" 10 | 11 | "golang.org/x/oauth2" 12 | "gopkg.in/acd.v0/internal/constants" 13 | "gopkg.in/acd.v0/internal/log" 14 | ) 15 | 16 | const refreshURL = "https://go-acd.appspot.com/refresh" 17 | 18 | // Source provides a Source with support for refreshing from the acd server. 19 | type Source struct { 20 | path string 21 | token *oauth2.Token 22 | } 23 | 24 | // New returns a new Source implementing oauth2.TokenSource. The path must 25 | // exist on the filesystem and must be of permissions 0600. 26 | func New(path string) (*Source, error) { 27 | if _, err := os.Stat(path); os.IsNotExist(err) { 28 | log.Errorf("%s: %s", constants.ErrFileNotFound, path) 29 | return nil, constants.ErrFileNotFound 30 | } 31 | 32 | ts := &Source{ 33 | path: path, 34 | token: new(oauth2.Token), 35 | } 36 | ts.readToken() 37 | 38 | return ts, nil 39 | } 40 | 41 | // Token returns an oauth2.Token. If the cached token (in (*Source).path) has 42 | // expired, it will fetch the token from the server and cache it before 43 | // returning it. 44 | func (ts *Source) Token() (*oauth2.Token, error) { 45 | if !ts.token.Valid() { 46 | log.Debug("token is not valid, it has probably expired") 47 | if err := ts.refreshToken(); err != nil { 48 | return nil, err 49 | } 50 | 51 | if err := ts.saveToken(); err != nil { 52 | return nil, err 53 | } 54 | } 55 | 56 | return ts.token, nil 57 | } 58 | 59 | func (ts *Source) readToken() error { 60 | log.Debugf("reading the token from %s", ts.path) 61 | f, err := os.Open(ts.path) 62 | if err != nil { 63 | log.Errorf("%s: %s", constants.ErrOpenFile, ts.path) 64 | return constants.ErrOpenFile 65 | } 66 | if err := json.NewDecoder(f).Decode(ts.token); err != nil { 67 | log.Errorf("%s: %s", constants.ErrJSONDecoding, err) 68 | return constants.ErrJSONDecoding 69 | } 70 | 71 | log.Debug("token loaded successfully") 72 | return nil 73 | } 74 | 75 | func (ts *Source) saveToken() error { 76 | log.Debugf("saving the token to %s", ts.path) 77 | f, err := os.Create(ts.path) 78 | if err != nil { 79 | log.Errorf("%s: %s", constants.ErrCreateFile, ts.path) 80 | return constants.ErrCreateFile 81 | } 82 | if err := json.NewEncoder(f).Encode(ts.token); err != nil { 83 | log.Errorf("%s: %s", constants.ErrJSONEncoding, err) 84 | return constants.ErrJSONEncoding 85 | } 86 | 87 | log.Debug("token saved successfully") 88 | return nil 89 | } 90 | 91 | func (ts *Source) refreshToken() error { 92 | log.Debugf("refreshing the token from %q", refreshURL) 93 | 94 | data, err := json.Marshal(ts.token) 95 | if err != nil { 96 | log.Errorf("%s: %s", constants.ErrJSONEncoding, err) 97 | return constants.ErrJSONEncoding 98 | } 99 | req, err := http.NewRequest("POST", refreshURL, bytes.NewBuffer(data)) 100 | if err != nil { 101 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 102 | return constants.ErrCreatingHTTPRequest 103 | } 104 | req.Header.Set("Content-Type", "application/json") 105 | res, err := (&http.Client{}).Do(req) 106 | if err != nil { 107 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 108 | return constants.ErrDoingHTTPRequest 109 | } 110 | defer res.Body.Close() 111 | if err := json.NewDecoder(res.Body).Decode(ts.token); err != nil { 112 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 113 | return constants.ErrJSONDecodingResponseBody 114 | } 115 | log.Debug("token was refreshed successfully") 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /integrationtest/folder_upload_test.go: -------------------------------------------------------------------------------- 1 | package integrationtest 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "testing" 12 | ) 13 | 14 | func TestSimpleFolderUpload(t *testing.T) { 15 | if testing.Short() { 16 | t.Skip("skipping integration test") 17 | } 18 | needCleaning = true 19 | 20 | testUploadFolder(t, "fixtures/simplefolder", false, false) 21 | } 22 | 23 | func TestRecursiveFolderUpload(t *testing.T) { 24 | if testing.Short() { 25 | t.Skip("skipping integration test") 26 | } 27 | needCleaning = true 28 | 29 | testUploadFolder(t, "fixtures/recursivefolder", true, false) 30 | } 31 | 32 | func TestConflictFolderUpload(t *testing.T) { 33 | if testing.Short() { 34 | t.Skip("skipping integration test") 35 | } 36 | needCleaning = true 37 | 38 | testUploadFolder(t, "fixtures/conflictfolder/v1", true, false) 39 | testUploadFolder(t, "fixtures/conflictfolder/v2", true, true) 40 | } 41 | 42 | func testUploadFolder(t *testing.T, localFolder string, recursive, overwrite bool) { 43 | var ( 44 | remoteFolder = remotePath(localFolder) 45 | files = listFiles(localFolder) 46 | md5s = make(map[string]string, len(files)) 47 | ) 48 | 49 | for _, file := range files { 50 | f, err := os.Open(fmt.Sprintf("%s/%s", localFolder, file)) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | hash := md5.New() 55 | io.Copy(hash, f) 56 | md5s[file] = hex.EncodeToString(hash.Sum(nil)) 57 | } 58 | 59 | // test uploading 60 | c, err := newCachedClient(true) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if err := c.FetchNodeTree(); err != nil { 65 | t.Fatal(err) 66 | } 67 | if err := c.UploadFolder(localFolder, remoteFolder, recursive, overwrite); err != nil { 68 | t.Errorf("error uploading %s to %s: %s", localFolder, remoteFolder, err) 69 | } 70 | 71 | // test the NodeTree is updated 72 | for _, file := range files { 73 | remoteFile := fmt.Sprintf("%s/%s", remoteFolder, file) 74 | node, err := c.NodeTree.FindNode(remoteFile) 75 | if err != nil { 76 | t.Errorf("c.NodeTree.FindNode(%s): got error %s", remoteFile, err) 77 | } 78 | // run the following test only if we find the node 79 | if err == nil { 80 | if want, got := md5s[file], node.ContentProperties.MD5; want != got { 81 | t.Errorf("c.NodeTree.FindNode(%s).ContentProperties.MD5: want %s got %s", file, want, got) 82 | } 83 | } 84 | } 85 | 86 | // test the cache is being saved updated 87 | c.Close() 88 | c, err = newCachedClient(false) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | if err := c.FetchNodeTree(); err != nil { 93 | t.Fatal(err) 94 | } 95 | for _, file := range files { 96 | remoteFile := fmt.Sprintf("%s/%s", remoteFolder, file) 97 | node, err := c.NodeTree.FindNode(remoteFile) 98 | if err != nil { 99 | t.Errorf("reloaded cache, c.NodeTree.FindNode(%s): got error %s", file, err) 100 | } 101 | // run the following test only if we find the node 102 | if err == nil { 103 | if want, got := md5s[file], node.ContentProperties.MD5; want != got { 104 | t.Errorf("reloaded cache, c.NodeTree.FindNode(%s).ContentProperties.MD5: want %s got %s", file, want, got) 105 | } 106 | } 107 | } 108 | 109 | // download the folder from the server and verify the contents byte-per-byte 110 | localPath, err := ioutil.TempDir("", "acd-folder-upload-test-") 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | if err := c.DownloadFolder(localPath, remoteFolder, recursive); err != nil { 115 | t.Errorf("c.DownloadFolder(%s, %s, %t) error: %s", localPath, remoteFolder, recursive, err) 116 | } 117 | for _, file := range files { 118 | localFile := path.Join(localPath, file) 119 | f, err := os.Open(localFile) 120 | if err != nil { 121 | if os.IsNotExist(err) { 122 | t.Errorf("cannot open the downloaded file at %s: does not exist", localFile) 123 | } else { 124 | t.Errorf("cannot open the downloaded file at %s: %s", localFile, err) 125 | } 126 | } 127 | hash := md5.New() 128 | io.Copy(hash, f) 129 | 130 | if want, got := md5s[file], hex.EncodeToString(hash.Sum(nil)); want != got { 131 | t.Errorf("c.DownloadFolder(%s, %s, %t), md5 of %s: want %s got %s", localPath, remoteFolder, recursive, localFile, want, got) 132 | } 133 | } 134 | if err := os.RemoveAll(localPath); err != nil { 135 | t.Logf("error removing the temporary folder %q, please remove it manually: %s", localPath, err) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /upload.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "io" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | 12 | "gopkg.in/acd.v0/internal/constants" 13 | "gopkg.in/acd.v0/internal/log" 14 | "gopkg.in/acd.v0/node" 15 | ) 16 | 17 | // Upload uploads io.Reader to the path defined by the filename. It will create 18 | // any non-existing folders. 19 | func (c *Client) Upload(filename string, overwrite bool, r io.Reader) error { 20 | var ( 21 | err error 22 | logLevel = log.GetLevel() 23 | fileNode *node.Node 24 | node *node.Node 25 | ) 26 | 27 | node, err = c.NodeTree.MkdirAll(path.Dir(filename)) 28 | if err != nil { 29 | return err 30 | } 31 | { 32 | log.SetLevel(log.DisableLogLevel) 33 | fileNode, err = c.NodeTree.FindNode(filename) 34 | log.SetLevel(logLevel) 35 | } 36 | if err == nil { 37 | if !overwrite { 38 | log.Errorf("%s: %s", constants.ErrFileExists, filename) 39 | return constants.ErrFileExists 40 | } 41 | if err = fileNode.Overwrite(r); err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | if _, err = node.Upload(path.Base(filename), r); err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // UploadFolder uploads an entire folder. 55 | // If recursive is true, it will recurse through the entire filetree under 56 | // localPath. If overwrite is false and an existing file with the same md5 was 57 | // found, an error will be returned. 58 | func (c *Client) UploadFolder(localPath, remotePath string, recursive, overwrite bool) error { 59 | log.Debugf("uploading %q to %q", localPath, remotePath) 60 | if err := filepath.Walk(localPath, c.uploadFolderFunc(localPath, remotePath, recursive, overwrite)); err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func (c *Client) uploadFolderFunc(localPath, remoteBasePath string, recursive, overwrite bool) filepath.WalkFunc { 68 | return func(fpath string, info os.FileInfo, err error) error { 69 | var ( 70 | logLevel = log.GetLevel() 71 | fileNode *node.Node 72 | remoteNode *node.Node 73 | f *os.File 74 | ) 75 | 76 | parts := strings.SplitAfter(fpath, localPath) 77 | remoteFilename := remoteBasePath + strings.Join(parts[1:], "/") 78 | remotePath := path.Dir(remoteFilename) 79 | log.Debugf("localPath %q remotePath %q fpath %q remoteFilename %q recursive %t overwrite %t", 80 | localPath, remotePath, fpath, remoteFilename, recursive, overwrite) 81 | 82 | // is this a folder? 83 | if info.IsDir() { 84 | log.Debugf("%q is a folder, skipping", fpath) 85 | return nil 86 | } 87 | // are we not recursive and trying to upload a file down the tree? 88 | if !recursive && localPath != path.Dir(fpath) { 89 | log.Debugf("%q is inside a sub-folder but we are not running recursively, skipping") 90 | return nil 91 | } 92 | 93 | log.Infof("uploading %q to %q", fpath, remoteFilename) 94 | if remoteNode, err = c.NodeTree.MkdirAll(remotePath); err != nil { 95 | return err 96 | } 97 | 98 | if f, err = os.Open(fpath); err != nil { 99 | log.Errorf("%s: %s", constants.ErrOpenFile, fpath) 100 | return constants.ErrOpenFile 101 | } 102 | defer f.Close() 103 | 104 | // does the file already exist? 105 | { 106 | log.SetLevel(log.DisableLogLevel) 107 | fileNode, err = c.NodeTree.FindNode(remoteFilename) 108 | log.SetLevel(logLevel) 109 | } 110 | if err == nil { 111 | if fileNode.IsDir() { 112 | log.Errorf("%s: remoteFilename %q", constants.ErrFileExistsAndIsFolder, remoteFilename) 113 | return constants.ErrFileExistsAndIsFolder 114 | } 115 | hash := md5.New() 116 | f.Seek(0, 0) 117 | io.Copy(hash, f) 118 | if hex.EncodeToString(hash.Sum(nil)) == fileNode.ContentProperties.MD5 { 119 | log.Debugf("%q already exists and has the same content, skipping", fpath) 120 | return nil 121 | } 122 | 123 | log.Debugf("%q already exists, overwrite is %t", fpath, overwrite) 124 | if !overwrite { 125 | log.Errorf("%s: remoteFilename %q", constants.ErrFileExistsWithDifferentContents, remoteFilename) 126 | return constants.ErrFileExistsWithDifferentContents 127 | } 128 | 129 | f.Seek(0, 0) 130 | return fileNode.Overwrite(f) 131 | } 132 | 133 | f.Seek(0, 0) 134 | if _, err := remoteNode.Upload(path.Base(fpath), f); err != nil && err != constants.ErrNoContentsToUpload { 135 | return err 136 | } 137 | 138 | return nil 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "gopkg.in/acd.v0/internal/constants" 9 | "gopkg.in/acd.v0/internal/log" 10 | ) 11 | 12 | type ( 13 | // AccountInfo represents information about an Amazon Cloud Drive account. 14 | AccountInfo struct { 15 | TermsOfUse string `json:"termsOfUse"` 16 | Status string `json:"status"` 17 | } 18 | 19 | // AccountQuota represents information about the account quotas. 20 | AccountQuota struct { 21 | Quota uint64 `json:"quota"` 22 | LastCalculated time.Time `json:"lastCalculated"` 23 | Available uint64 `json:"available"` 24 | } 25 | 26 | // AccountUsage represents information about the account usage. 27 | AccountUsage struct { 28 | LastCalculated time.Time `json:"lastCalculated"` 29 | 30 | Doc struct { 31 | Billable struct { 32 | Bytes uint64 `json:"bytes"` 33 | Count uint32 `json:"count"` 34 | } `json:"billable"` 35 | Total struct { 36 | Bytes uint64 `json:"bytes"` 37 | Count uint32 `json:"count"` 38 | } `json:"total"` 39 | } `json:"doc"` 40 | 41 | Other struct { 42 | Billable struct { 43 | Bytes uint64 `json:"bytes"` 44 | Count uint32 `json:"count"` 45 | } `json:"billable"` 46 | Total struct { 47 | Bytes uint64 `json:"bytes"` 48 | Count uint32 `json:"count"` 49 | } `json:"total"` 50 | } `json:"other"` 51 | 52 | Photo struct { 53 | Billable struct { 54 | Bytes uint64 `json:"bytes"` 55 | Count uint32 `json:"count"` 56 | } `json:"billable"` 57 | Total struct { 58 | Bytes uint64 `json:"bytes"` 59 | Count uint32 `json:"count"` 60 | } `json:"total"` 61 | } `json:"photo"` 62 | 63 | Video struct { 64 | Billable struct { 65 | Bytes uint64 `json:"bytes"` 66 | Count uint32 `json:"count"` 67 | } `json:"billable"` 68 | Total struct { 69 | Bytes uint64 `json:"bytes"` 70 | Count uint32 `json:"count"` 71 | } `json:"total"` 72 | } `json:"video"` 73 | } 74 | ) 75 | 76 | // GetAccountInfo returns AccountInfo about the current account. 77 | func (c *Client) GetAccountInfo() (*AccountInfo, error) { 78 | var ai AccountInfo 79 | req, err := http.NewRequest("GET", c.metadataURL+"/account/info", nil) 80 | if err != nil { 81 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 82 | return nil, constants.ErrCreatingHTTPRequest 83 | } 84 | 85 | res, err := c.Do(req) 86 | if err != nil { 87 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 88 | return nil, constants.ErrDoingHTTPRequest 89 | } 90 | 91 | defer res.Body.Close() 92 | if err := json.NewDecoder(res.Body).Decode(&ai); err != nil { 93 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 94 | return nil, constants.ErrJSONDecodingResponseBody 95 | } 96 | 97 | return &ai, nil 98 | } 99 | 100 | // GetAccountQuota returns AccountQuota about the current account. 101 | func (c *Client) GetAccountQuota() (*AccountQuota, error) { 102 | var aq AccountQuota 103 | req, err := http.NewRequest("GET", c.metadataURL+"/account/quota", nil) 104 | if err != nil { 105 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 106 | return nil, constants.ErrCreatingHTTPRequest 107 | } 108 | 109 | res, err := c.Do(req) 110 | if err != nil { 111 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 112 | return nil, constants.ErrDoingHTTPRequest 113 | } 114 | 115 | defer res.Body.Close() 116 | if err := json.NewDecoder(res.Body).Decode(&aq); err != nil { 117 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 118 | return nil, constants.ErrJSONDecodingResponseBody 119 | } 120 | 121 | return &aq, nil 122 | } 123 | 124 | // GetAccountUsage returns AccountUsage about the current account. 125 | func (c *Client) GetAccountUsage() (*AccountUsage, error) { 126 | var au AccountUsage 127 | req, err := http.NewRequest("GET", c.metadataURL+"/account/usage", nil) 128 | if err != nil { 129 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 130 | return nil, constants.ErrCreatingHTTPRequest 131 | } 132 | 133 | res, err := c.Do(req) 134 | if err != nil { 135 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 136 | return nil, constants.ErrDoingHTTPRequest 137 | } 138 | 139 | defer res.Body.Close() 140 | if err := json.NewDecoder(res.Body).Decode(&au); err != nil { 141 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 142 | return nil, constants.ErrJSONDecodingResponseBody 143 | } 144 | 145 | return &au, nil 146 | } 147 | -------------------------------------------------------------------------------- /node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "gopkg.in/acd.v0/internal/constants" 9 | "gopkg.in/acd.v0/internal/log" 10 | ) 11 | 12 | type ( 13 | // Nodes is a slice of nodes 14 | Nodes []*Node 15 | 16 | // ContentProperties hold the properties of the node. 17 | ContentProperties struct { 18 | Version uint64 `json:"version,omitempty"` 19 | Extension string `json:"extension,omitempty"` 20 | Size uint64 `json:"size,omitempty"` 21 | MD5 string `json:"md5,omitempty"` 22 | ContentType string `json:"contentType,omitempty"` 23 | ContentDate time.Time `json:"contentDate,omitempty"` 24 | } 25 | 26 | // Node represents a digital asset on the Amazon Cloud Drive, including files 27 | // and folders, in a parent-child relationship. A node contains only metadata 28 | // (e.g. folder) or it contains metadata and content (e.g. file). 29 | Node struct { 30 | // Coming from Amazon 31 | ID string `json:"id,omitempty"` 32 | Name string `json:"name,omitempty"` 33 | Kind string `json:"kind,omitempty"` 34 | Parents []string `json:"Parents,omitempty"` 35 | Status string `json:"status,omitempty"` 36 | Labels []string `json:"labels,omitempty"` 37 | CreatedBy string `json:"createdBy,omitempty"` 38 | CreationDate time.Time `json:"creationDate,omitempty"` 39 | ModifiedDate time.Time `json:"modifiedDate,omitempty"` 40 | Version uint64 `json:"version,omitempty"` 41 | TempLink string `json:"tempLink,omitempty"` 42 | ContentProperties ContentProperties `json:"contentProperties,omitempty"` 43 | 44 | // Internal 45 | Nodes Nodes `json:"nodes,omitempty"` 46 | Root bool `json:"root,omitempty"` 47 | client client 48 | } 49 | 50 | newNode struct { 51 | Name string `json:"name,omitempty"` 52 | Kind string `json:"kind,omitempty"` 53 | Labels []string `json:"labels,omitempty"` 54 | Properties map[string]string `json:"properties"` 55 | Parents []string `json:"parents"` 56 | } 57 | 58 | client interface { 59 | GetMetadataURL(string) string 60 | GetContentURL(string) string 61 | Do(*http.Request) (*http.Response, error) 62 | CheckResponse(*http.Response) error 63 | GetNodeTree() *Tree 64 | } 65 | ) 66 | 67 | // Size returns the size of the node. 68 | func (n *Node) Size() int64 { 69 | return int64(n.ContentProperties.Size) 70 | } 71 | 72 | // ModTime returns the last modified time of the node. 73 | func (n *Node) ModTime() time.Time { 74 | return n.ModifiedDate 75 | } 76 | 77 | // IsFile returns whether the node represents a file. 78 | func (n *Node) IsFile() bool { 79 | return n.Kind == "FILE" 80 | } 81 | 82 | // IsDir returns whether the node represents a folder. 83 | func (n *Node) IsDir() bool { 84 | return n.Kind == "FOLDER" 85 | } 86 | 87 | // IsAsset returns whether the node represents an asset. 88 | func (n *Node) IsAsset() bool { 89 | return n.Kind == "ASSET" 90 | } 91 | 92 | // Available returns true if the node is available 93 | func (n *Node) Available() bool { 94 | return n.Status == "AVAILABLE" 95 | } 96 | 97 | // AddChild add a new child for the node 98 | func (n *Node) AddChild(child *Node) { 99 | log.Debugf("adding %s under %s", child.Name, n.Name) 100 | n.Nodes = append(n.Nodes, child) 101 | child.client = n.client 102 | } 103 | 104 | // RemoveChild remove a new child for the node 105 | func (n *Node) RemoveChild(child *Node) { 106 | found := false 107 | 108 | for i, n := range n.Nodes { 109 | if n == child { 110 | if i < len(n.Nodes)-1 { 111 | copy(n.Nodes[i:], n.Nodes[i+1:]) 112 | } 113 | n.Nodes[len(n.Nodes)-1] = nil 114 | n.Nodes = n.Nodes[:len(n.Nodes)-1] 115 | found = true 116 | break 117 | } 118 | } 119 | log.Debugf("removing %s from %s: %t", child.Name, n.Name, found) 120 | } 121 | 122 | func (n *Node) update(newNode *Node) error { 123 | // encode the newNode to JSON. 124 | v, err := json.Marshal(newNode) 125 | if err != nil { 126 | log.Errorf("error encoding the node to JSON: %s", err) 127 | return constants.ErrJSONEncoding 128 | } 129 | 130 | // decode it back to n 131 | if err := json.Unmarshal(v, n); err != nil { 132 | log.Errorf("error decoding the node from JSON: %s", err) 133 | return constants.ErrJSONDecoding 134 | } 135 | 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package acd 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "golang.org/x/oauth2" 10 | "gopkg.in/acd.v0/internal/constants" 11 | "gopkg.in/acd.v0/internal/log" 12 | "gopkg.in/acd.v0/node" 13 | "gopkg.in/acd.v0/token" 14 | ) 15 | 16 | type ( 17 | // Config represents the clients configuration. 18 | Config struct { 19 | // TokenFile represents the file containing the oauth settings which must 20 | // be present on disk and has permissions 0600. The file is used by the 21 | // token package to produce a valid access token by calling the oauthServer 22 | // with the refresh token. The default oauth server is hosted at 23 | // https://go-acd.appspot.com with the source code available at 24 | // https://github.com/go-acd/oauth-server. It's currently not possible to 25 | // change the oauth-server. Please feel free to add this feature if you 26 | // have a use-case for it. 27 | TokenFile string `json:"tokenFile"` 28 | 29 | // CacheFile represents the file used by the client to cache the NodeTree. 30 | // This file is not assumed to be present and will be created on the first 31 | // run. It is gob-encoded node.Node. 32 | CacheFile string `json:"cacheFile"` 33 | 34 | // Timeout configures the HTTP Client with a timeout after which the client 35 | // will cancel the request and return. A timeout of 0 (the default) means 36 | // no timeout. See http://godoc.org/net/http#Client for more information. 37 | Timeout time.Duration `json:"timeout"` 38 | } 39 | 40 | // Client provides a client for Amazon Cloud Drive. 41 | Client struct { 42 | // NodeTree is the tree of nodes as stored on the drive. This tree should 43 | // be fetched using (*Client).FetchNodeTree() as soon the client is 44 | // created. 45 | NodeTree *node.Tree 46 | 47 | config *Config 48 | httpClient *http.Client 49 | cacheFile string 50 | metadataURL string 51 | contentURL string 52 | } 53 | 54 | endpointResponse struct { 55 | ContentURL string `json:"contentUrl"` 56 | MetadataURL string `json:"metadataUrl"` 57 | CustomerExists bool `json:"customerExists"` 58 | } 59 | ) 60 | 61 | const endpointURL = "https://drive.amazonaws.com/drive/v1/account/endpoint" 62 | 63 | // New returns a new Amazon Cloud Drive "acd" Client. configFile must exist and must be a valid JSON decodable into Config. 64 | func New(configFile string) (*Client, error) { 65 | config, err := loadConfig(configFile) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | ts, err := token.New(config.TokenFile) 71 | if err != nil { 72 | return nil, err 73 | } 74 | c := &Client{ 75 | config: config, 76 | cacheFile: config.CacheFile, 77 | httpClient: &http.Client{ 78 | Timeout: config.Timeout, 79 | Transport: &oauth2.Transport{ 80 | Source: oauth2.ReuseTokenSource(nil, ts), 81 | }, 82 | }, 83 | } 84 | if err := setEndpoints(c); err != nil { 85 | return nil, err 86 | } 87 | 88 | return c, nil 89 | } 90 | 91 | // Close finalizes the acd. 92 | func (c *Client) Close() error { 93 | return c.NodeTree.Close() 94 | } 95 | 96 | // Do invokes net/http.Client.Do(). Refer to net/http.Client.Do() for documentation. 97 | func (c *Client) Do(r *http.Request) (*http.Response, error) { 98 | return c.httpClient.Do(r) 99 | } 100 | 101 | func loadConfig(configFile string) (*Config, error) { 102 | // validate the config file 103 | if err := validateFile(configFile, false); err != nil { 104 | return nil, err 105 | } 106 | 107 | cf, err := os.Open(configFile) 108 | if err != nil { 109 | log.Errorf("%s: %s", constants.ErrOpenFile, err) 110 | return nil, err 111 | } 112 | defer cf.Close() 113 | var config Config 114 | if err := json.NewDecoder(cf).Decode(&config); err != nil { 115 | log.Errorf("%s: %s", constants.ErrJSONDecoding, err) 116 | return nil, err 117 | } 118 | 119 | // validate the token file 120 | if err := validateFile(config.TokenFile, true); err != nil { 121 | return nil, err 122 | } 123 | 124 | return &config, nil 125 | } 126 | 127 | func validateFile(file string, checkPerms bool) error { 128 | stat, err := os.Stat(file) 129 | if err != nil { 130 | if os.IsNotExist(err) { 131 | log.Errorf("%s: %s -- %s", constants.ErrFileNotFound, err, file) 132 | return constants.ErrFileNotFound 133 | } 134 | log.Errorf("%s: %s -- %s", constants.ErrStatFile, err, file) 135 | return constants.ErrStatFile 136 | } 137 | if checkPerms && stat.Mode() != os.FileMode(0600) { 138 | log.Errorf("%s: want 0600 got %s", constants.ErrWrongPermissions, stat.Mode()) 139 | return constants.ErrWrongPermissions 140 | } 141 | 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /cli/cp.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path" 8 | "strings" 9 | 10 | "gopkg.in/acd.v0/internal/constants" 11 | "gopkg.in/acd.v0/internal/log" 12 | 13 | "github.com/codegangsta/cli" 14 | ) 15 | 16 | var ( 17 | cpCommand = cli.Command{ 18 | Name: "cp", 19 | Usage: "copy files", 20 | Description: "cp copy files, multiple files can be given. It follows the usage of cp whereas the last entry is the destination and has to be a folder if multiple files were given", 21 | Action: cpAction, 22 | BashComplete: cpBashComplete, 23 | Before: cpBefore, 24 | Flags: []cli.Flag{ 25 | cli.BoolFlag{ 26 | Name: "recursive, R", 27 | Usage: "cp recursively", 28 | }, 29 | }, 30 | } 31 | 32 | action string 33 | ) 34 | 35 | func init() { 36 | registerCommand(cpCommand) 37 | } 38 | 39 | func cpAction(c *cli.Context) { 40 | if strings.HasPrefix(c.Args()[len(c.Args())-1], "acd://") { 41 | cpUpload(c) 42 | } else { 43 | cpDownload(c) 44 | } 45 | } 46 | 47 | func cpUpload(c *cli.Context) { 48 | // make sure the destination is a folder if it exists upstream and more than 49 | // one file is scheduled to be copied. 50 | dest := strings.TrimPrefix(c.Args()[len(c.Args())-1], "acd://") 51 | destNode, err := acdClient.NodeTree.FindNode(dest) 52 | if err == nil { 53 | // make sure if the remote node exists, it is a folder. 54 | if len(c.Args()) > 2 { 55 | if !destNode.IsDir() { 56 | log.Fatalf("cp: target %q is not a directory", dest) 57 | } 58 | } 59 | } 60 | 61 | for _, src := range c.Args()[:len(c.Args())-1] { 62 | if strings.HasPrefix(src, "acd://") { 63 | fmt.Printf("cp: target %q is amazon, src cannot be amazon when destination is amazon. Skipping\n", src) 64 | continue 65 | } 66 | stat, err := os.Stat(src) 67 | if err != nil { 68 | if os.IsNotExist(err) { 69 | log.Fatalf("cp: %s: %s", constants.ErrFileNotFound, src) 70 | } 71 | 72 | log.Fatalf("cp: %s: %s", constants.ErrStatFile, src) 73 | } 74 | if stat.IsDir() { 75 | if !c.Bool("recursive") { 76 | fmt.Printf("cp: %q is a directory (not copied).", src) 77 | continue 78 | } 79 | destFile := dest 80 | if destNode != nil { 81 | if !destNode.IsDir() { 82 | log.Fatalf("cp: target %q is not a directory", dest) 83 | } 84 | destFile = fmt.Sprintf("%s/%s", dest, path.Base(src)) 85 | } 86 | acdClient.UploadFolder(src, destFile, true, true) 87 | continue 88 | } 89 | f, err := os.Open(src) 90 | if err != nil { 91 | log.Fatalf("%s: %s -- %s", constants.ErrOpenFile, err, src) 92 | } 93 | err = acdClient.Upload(dest, true, f) 94 | f.Close() 95 | if err != nil { 96 | log.Fatalf("%s: %s", err, dest) 97 | } 98 | } 99 | } 100 | 101 | func cpDownload(c *cli.Context) { 102 | dest := c.Args()[len(c.Args())-1] 103 | destDir := false 104 | destStat, err := os.Stat(dest) 105 | if err == nil && destStat.IsDir() { 106 | destDir = true 107 | } 108 | if len(c.Args()) > 2 { 109 | if err == nil && !destDir { 110 | log.Fatalf("cp: target %q is not a directory", dest) 111 | } 112 | } 113 | 114 | for _, src := range c.Args()[:len(c.Args())-1] { 115 | if !strings.HasPrefix(src, "acd://") { 116 | fmt.Printf("cp: source %q is local, src cannot be local when destination is local. Skipping\n", src) 117 | continue 118 | } 119 | srcPath := strings.TrimPrefix(src, "acd://") 120 | destPath := dest 121 | if destDir { 122 | destPath = path.Join(destPath, path.Base(srcPath)) 123 | } 124 | srcNode, err := acdClient.GetNodeTree().FindNode(srcPath) 125 | if err != nil { 126 | fmt.Printf("cp: source %q not found. Skipping", src) 127 | continue 128 | } 129 | if srcNode.IsDir() { 130 | acdClient.DownloadFolder(destPath, srcPath, c.Bool("recursive")) 131 | } else { 132 | content, err := acdClient.Download(srcPath) 133 | if err != nil { 134 | fmt.Printf("cp: error downloading source %q. Skipping", src) 135 | } 136 | // TODO: respect umask 137 | if err := os.MkdirAll(path.Dir(destPath), os.FileMode(0755)); err != nil { 138 | fmt.Printf("cp: error creating the parents folders of %q: %s. Skipping", destPath, err) 139 | continue 140 | } 141 | // TODO: respect umask 142 | f, err := os.Create(destPath) 143 | if err != nil { 144 | fmt.Printf("cp: error writing %q: %s. Skipping", destPath, err) 145 | continue 146 | } 147 | io.Copy(f, content) 148 | f.Close() 149 | } 150 | } 151 | } 152 | 153 | func cpBashComplete(c *cli.Context) { 154 | } 155 | 156 | func cpBefore(c *cli.Context) error { 157 | foundRemote := false 158 | for _, arg := range c.Args() { 159 | if strings.HasPrefix(arg, "acd://") { 160 | foundRemote = true 161 | break 162 | } 163 | } 164 | 165 | if !foundRemote { 166 | return fmt.Errorf("cp: at least one path prefixed by acd:// is required. Given: %v", c.Args()) 167 | } 168 | 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /integrationtest/init_test.go: -------------------------------------------------------------------------------- 1 | package integrationtest 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | "gopkg.in/acd.v0" 17 | "gopkg.in/acd.v0/internal/constants" 18 | "gopkg.in/acd.v0/internal/log" 19 | ) 20 | 21 | const ( 22 | devNullCacheFile string = "/dev/null" 23 | testFolderBasePath string = "/acd_test_folder" 24 | baseTokenFile string = "acd-token.json" 25 | ) 26 | 27 | var ( 28 | cacheFile string 29 | cacheFiles []string 30 | configFiles []string 31 | tokenFiles []string 32 | testFolderPath string 33 | needCleaning bool 34 | ) 35 | 36 | func TestMain(m *testing.M) { 37 | defer func() { 38 | if r := recover(); r != nil { 39 | cleanUp() 40 | } 41 | }() 42 | 43 | cacheFile = newTempFile("acd-cache-") 44 | cacheFiles = append(cacheFiles, cacheFile) 45 | testFolderPath = fmt.Sprintf("%s/%d", testFolderBasePath, time.Now().UnixNano()) 46 | 47 | // disable all logs 48 | log.SetLevel(log.DebugLevel) 49 | 50 | // run all the tests 51 | code := m.Run() 52 | 53 | // Cleanup after the run 54 | cleanUp() 55 | 56 | // exit with the return status 57 | os.Exit(code) 58 | } 59 | 60 | func cleanUp() { 61 | if needCleaning { 62 | // remove the test folder 63 | if err := removeTestFolder(); err != nil { 64 | log.Errorf("error removing the test folder: %s", err) 65 | } 66 | 67 | // avoid double cleaning 68 | needCleaning = false 69 | } 70 | 71 | // remove all cache files 72 | for _, cf := range cacheFiles { 73 | os.Remove(cf) 74 | } 75 | 76 | // remove all config files. 77 | for _, cf := range configFiles { 78 | os.Remove(cf) 79 | } 80 | 81 | // remove all token files. 82 | for _, cf := range tokenFiles { 83 | os.Remove(cf) 84 | } 85 | } 86 | 87 | func newTempFile(baseName string) string { 88 | f, _ := ioutil.TempFile("", baseName) 89 | f.Close() 90 | os.Remove(f.Name()) 91 | return f.Name() 92 | } 93 | 94 | func newCachedClient(ncf bool) (*acd.Client, error) { 95 | if ncf { 96 | cacheFile = newTempFile("acd-cache-") 97 | cacheFiles = append(cacheFiles, cacheFile) 98 | } 99 | return acd.New(newConfigFile(cacheFile)) 100 | } 101 | 102 | func newUncachedClient() (*acd.Client, error) { 103 | return acd.New(newConfigFile(devNullCacheFile)) 104 | } 105 | 106 | func newConfigFile(cacheFile string) string { 107 | tokenFile := newTempFile("acd-token-") 108 | tokenFiles = append(tokenFiles, tokenFile) 109 | configFile := newTempFile("acd-config-") 110 | configFiles = append(configFiles, configFile) 111 | 112 | of, err := os.Open(baseTokenFile) 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | defer of.Close() 117 | nf, err := os.OpenFile(tokenFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | defer nf.Close() 122 | io.Copy(nf, of) 123 | 124 | cf, err := os.Create(configFile) 125 | if err != nil { 126 | log.Fatal(err) 127 | } 128 | config := &acd.Config{ 129 | TokenFile: tokenFile, 130 | CacheFile: cacheFile, 131 | } 132 | defer cf.Close() 133 | if err := json.NewEncoder(cf).Encode(config); err != nil { 134 | log.Fatal(err) 135 | } 136 | 137 | return configFile 138 | } 139 | 140 | func removeTestFolder() error { 141 | c, err := newUncachedClient() 142 | if err != nil { 143 | return err 144 | } 145 | if err := c.FetchNodeTree(); err != nil { 146 | return err 147 | } 148 | node, err := c.NodeTree.FindNode(testFolderPath) 149 | if err != nil && err != constants.ErrNodeNotFound { 150 | return err 151 | } 152 | if node == nil { 153 | return constants.ErrNodeNotFound 154 | } 155 | if node.Name != path.Base(testFolderPath) { 156 | return fmt.Errorf("something is wrong, the node's name is %s and not %s", node.Name, testFolderPath) 157 | } 158 | 159 | return c.NodeTree.RemoveNode(node) 160 | } 161 | 162 | func remotePath(fp string) string { 163 | p := strings.Replace(fp, "fixtures/", "", 1) 164 | r, err := regexp.Compile(`/(v1|v2)`) 165 | if err != nil { 166 | panic(err) 167 | } 168 | if ok := r.MatchString(p); ok { 169 | p = strings.Replace(p, "/v1", "/", 1) 170 | p = strings.Replace(p, "/v2", "/", 1) 171 | } 172 | p = strings.TrimSuffix(p, "/") 173 | return fmt.Sprintf("%s/%s", testFolderPath, p) 174 | } 175 | 176 | // listFiles returns the list of all of the files in folder and it's subfolders 177 | // but it does not include the subfolders as entries. 178 | func listFiles(folder string) []string { 179 | var files []string 180 | filepath.Walk(folder, func(fp string, info os.FileInfo, err error) error { 181 | if info.IsDir() { 182 | return nil 183 | } 184 | 185 | parts := strings.SplitAfter(fp, fmt.Sprintf("%s%c", folder, os.PathSeparator)) 186 | nfp := strings.Join(parts[1:], string(os.PathSeparator)) 187 | files = append(files, nfp) 188 | return nil 189 | }) 190 | 191 | return files 192 | } 193 | -------------------------------------------------------------------------------- /node/sync.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "sort" 9 | 10 | "gopkg.in/acd.v0/internal/constants" 11 | "gopkg.in/acd.v0/internal/log" 12 | ) 13 | 14 | type ( 15 | changes struct { 16 | Checkpoint string `json:"checkpoint,omitempty"` 17 | Chunksize int `json:"chunkSize,omitempty"` 18 | MaxNodes int `json:"maxNodes,omitempty"` 19 | IncludePurged string `json:"includePurged,omitempty"` 20 | } 21 | 22 | changesResponse struct { 23 | Checkpoint string `json:"checkpoint,omitempty"` 24 | Nodes []*Node `json:"nodes,omitempty"` 25 | Reset bool `json:"reset,omitempty"` 26 | End bool `json:"end,omitempty"` 27 | } 28 | ) 29 | 30 | // Sync syncs the tree with the server. 31 | func (nt *Tree) Sync() error { 32 | postURL := nt.client.GetMetadataURL("changes") 33 | c := &changes{ 34 | Checkpoint: nt.Checkpoint, 35 | } 36 | jsonBytes, err := json.Marshal(c) 37 | if err != nil { 38 | log.Errorf("%s: %s", constants.ErrJSONEncoding, err) 39 | return constants.ErrJSONEncoding 40 | } 41 | req, err := http.NewRequest("POST", postURL, bytes.NewBuffer(jsonBytes)) 42 | if err != nil { 43 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 44 | return constants.ErrCreatingHTTPRequest 45 | } 46 | req.Header.Set("Content-Type", "application/json") 47 | res, err := nt.client.Do(req) 48 | if err != nil { 49 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 50 | return constants.ErrDoingHTTPRequest 51 | } 52 | if err := nt.client.CheckResponse(res); err != nil { 53 | return err 54 | } 55 | 56 | // return format should be: 57 | // {"checkpoint": str, "reset": bool, "nodes": []} 58 | // {"checkpoint": str, "reset": false, "nodes": []} 59 | // {"end": true} 60 | defer res.Body.Close() 61 | bodyBytes, err := ioutil.ReadAll(res.Body) 62 | if err != nil { 63 | log.Errorf("%s: %s", constants.ErrReadingResponseBody, err) 64 | return constants.ErrReadingResponseBody 65 | } 66 | for _, lineBytes := range bytes.Split(bodyBytes, []byte("\n")) { 67 | var cr changesResponse 68 | if err := json.Unmarshal(lineBytes, &cr); err != nil { 69 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 70 | return constants.ErrJSONDecodingResponseBody 71 | } 72 | if cr.Checkpoint != "" { 73 | log.Debugf("changes returned Checkpoint: %s", cr.Checkpoint) 74 | nt.Checkpoint = cr.Checkpoint 75 | } 76 | if cr.Reset { 77 | log.Debug("reset is required") 78 | return constants.ErrMustFetchFresh 79 | } 80 | if cr.End { 81 | break 82 | } 83 | if err := nt.updateNodes(cr.Nodes); err != nil { 84 | return err 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (nt *Tree) updateNodes(nodes []*Node) error { 92 | // first make sure our nodeMap is up to date 93 | for _, node := range nodes { 94 | log.Debugf("node %s ID %s has changed.", node.Name, node.ID) 95 | if _, found := nt.nodeMap[node.ID]; !found { 96 | // remove the parents from the node we are inserting so the next section 97 | // will detect the added parents and add those. 98 | newNode := &Node{} 99 | (*newNode) = *node 100 | newNode.Parents = []string{} 101 | nt.nodeMap[node.ID] = newNode 102 | } 103 | } 104 | 105 | // now let's update the nodes 106 | for _, node := range nodes { 107 | // make a copy of n 108 | newNode := &Node{} 109 | (*newNode) = *nt.nodeMap[node.ID] 110 | if err := newNode.update(node); err != nil { 111 | return err 112 | } 113 | 114 | // has this node been deleted? 115 | if !newNode.Available() { 116 | log.Debugf("node ID %s name %s has been deleted", newNode.ID, newNode.Name) 117 | for _, parentID := range append(newNode.Parents, nt.nodeMap[node.ID].Parents...) { 118 | parent, err := nt.FindByID(parentID) 119 | if err != nil { 120 | continue 121 | } 122 | parent.RemoveChild(nt.nodeMap[node.ID]) 123 | } 124 | 125 | // remove the node itself from the nodemap 126 | delete(nt.nodeMap, node.ID) 127 | 128 | continue 129 | } 130 | 131 | // add/remove parents 132 | sort.Strings(nt.nodeMap[node.ID].Parents) 133 | sort.Strings(newNode.Parents) 134 | if parentIDs := diffSliceStr(nt.nodeMap[node.ID].Parents, newNode.Parents); len(parentIDs) > 0 { 135 | for _, parentID := range parentIDs { 136 | log.Debugf("ParentID %s has been removed from %s ID %s", parentID, node.Name, node.ID) 137 | parent, err := nt.FindByID(parentID) 138 | if err != nil { 139 | continue 140 | } 141 | parent.RemoveChild(nt.nodeMap[node.ID]) 142 | } 143 | } 144 | if parentIDs := diffSliceStr(newNode.Parents, nt.nodeMap[node.ID].Parents); len(parentIDs) > 0 { 145 | for _, parentID := range parentIDs { 146 | log.Debugf("ParentID %s has been added to %s ID %s", parentID, node.Name, node.ID) 147 | parent, err := nt.FindByID(parentID) 148 | if err != nil { 149 | continue 150 | } 151 | parent.AddChild(nt.nodeMap[node.ID]) 152 | } 153 | } 154 | 155 | // finally update the node itself 156 | (*node) = *newNode 157 | } 158 | 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /node/upload.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "mime/multipart" 9 | "net/http" 10 | 11 | "gopkg.in/acd.v0/internal/constants" 12 | "gopkg.in/acd.v0/internal/log" 13 | ) 14 | 15 | // CreateFolder creates the named folder under the node 16 | func (n *Node) CreateFolder(name string) (*Node, error) { 17 | cn := &newNode{ 18 | Name: name, 19 | Kind: "FOLDER", 20 | Parents: []string{n.ID}, 21 | } 22 | jsonBytes, err := json.Marshal(cn) 23 | if err != nil { 24 | log.Errorf("%s: %s", constants.ErrJSONEncoding, err) 25 | return nil, constants.ErrJSONEncoding 26 | } 27 | 28 | req, err := http.NewRequest("POST", n.client.GetMetadataURL("nodes"), bytes.NewBuffer(jsonBytes)) 29 | if err != nil { 30 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 31 | return nil, constants.ErrCreatingHTTPRequest 32 | } 33 | 34 | req.Header.Set("Content-Type", "application/json") 35 | res, err := n.client.Do(req) 36 | if err != nil { 37 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 38 | return nil, constants.ErrDoingHTTPRequest 39 | } 40 | if err := n.client.CheckResponse(res); err != nil { 41 | return nil, err 42 | } 43 | 44 | defer res.Body.Close() 45 | var node Node 46 | if err := json.NewDecoder(res.Body).Decode(&node); err != nil { 47 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 48 | return nil, constants.ErrJSONDecodingResponseBody 49 | } 50 | n.AddChild(&node) 51 | 52 | return &node, nil 53 | } 54 | 55 | // Upload writes contents of r as name inside the current node. 56 | func (n *Node) Upload(name string, r io.Reader) (*Node, error) { 57 | metadata := &newNode{ 58 | Name: name, 59 | Kind: "FILE", 60 | Parents: []string{n.ID}, 61 | } 62 | metadataJSON, err := json.Marshal(metadata) 63 | if err != nil { 64 | log.Errorf("%s: %s", constants.ErrJSONEncoding, err) 65 | return nil, constants.ErrJSONEncoding 66 | } 67 | 68 | postURL := n.client.GetContentURL("nodes?suppress=deduplication") 69 | node, err := n.upload(postURL, "POST", string(metadataJSON), name, r) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | n.AddChild(node) 75 | return node, nil 76 | } 77 | 78 | // Overwrite writes contents of r as name inside the current node. 79 | func (n *Node) Overwrite(r io.Reader) error { 80 | putURL := n.client.GetContentURL(fmt.Sprintf("nodes/%s/content", n.ID)) 81 | node, err := n.upload(putURL, "PUT", "", n.Name, r) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | return n.update(node) 87 | } 88 | 89 | func (n *Node) upload(url, method, metadataJSON, name string, r io.Reader) (*Node, error) { 90 | bodyReader, bodyWriter := io.Pipe() 91 | errChan := make(chan error) 92 | bodyChan := make(chan io.ReadCloser) 93 | contentTypeChan := make(chan string) 94 | 95 | go n.bodyWriter(metadataJSON, name, r, bodyWriter, errChan, contentTypeChan) 96 | go func() { 97 | req, err := http.NewRequest(method, url, bodyReader) 98 | if err != nil { 99 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 100 | select { 101 | case errChan <- constants.ErrCreatingHTTPRequest: 102 | default: 103 | } 104 | return 105 | } 106 | req.Header.Add("Content-Type", <-contentTypeChan) 107 | res, err := n.client.Do(req) // this should block until the upload is finished. 108 | if err != nil { 109 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 110 | select { 111 | case errChan <- constants.ErrDoingHTTPRequest: 112 | default: 113 | } 114 | return 115 | } 116 | if err := n.client.CheckResponse(res); err != nil { 117 | select { 118 | case errChan <- err: 119 | default: 120 | } 121 | return 122 | } 123 | 124 | select { 125 | case bodyChan <- res.Body: 126 | default: 127 | } 128 | }() 129 | 130 | for { 131 | select { 132 | case err := <-errChan: 133 | if err != nil { 134 | return nil, err 135 | } 136 | case body := <-bodyChan: 137 | defer body.Close() 138 | var node Node 139 | if err := json.NewDecoder(body).Decode(&node); err != nil { 140 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 141 | return nil, constants.ErrJSONDecodingResponseBody 142 | } 143 | 144 | return &node, nil 145 | } 146 | } 147 | } 148 | 149 | func (n *Node) bodyWriter(metadataJSON, name string, r io.Reader, bodyWriter io.WriteCloser, errChan chan error, contentTypeChan chan string) { 150 | writer := multipart.NewWriter(bodyWriter) 151 | contentTypeChan <- writer.FormDataContentType() 152 | if metadataJSON != "" { 153 | if err := writer.WriteField("metadata", metadataJSON); err != nil { 154 | log.Errorf("%s: %s", constants.ErrWritingMetadata, err) 155 | select { 156 | case errChan <- constants.ErrWritingMetadata: 157 | default: 158 | } 159 | return 160 | } 161 | } 162 | 163 | part, err := writer.CreateFormFile("content", name) 164 | if err != nil { 165 | log.Errorf("%s: %s", constants.ErrCreatingWriterFromFile, err) 166 | select { 167 | case errChan <- err: 168 | default: 169 | } 170 | return 171 | } 172 | count, err := io.Copy(part, r) 173 | if err != nil { 174 | log.Errorf("%s: %s", constants.ErrWritingFileContents, err) 175 | select { 176 | case errChan <- constants.ErrWritingFileContents: 177 | default: 178 | } 179 | return 180 | } 181 | if count == 0 { 182 | select { 183 | case errChan <- constants.ErrNoContentsToUpload: 184 | default: 185 | } 186 | return 187 | } 188 | 189 | select { 190 | case errChan <- writer.Close(): 191 | default: 192 | } 193 | select { 194 | case errChan <- bodyWriter.Close(): 195 | default: 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /node/tree.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "gopkg.in/acd.v0/internal/constants" 11 | "gopkg.in/acd.v0/internal/log" 12 | ) 13 | 14 | type ( 15 | // Tree represents a node tree. 16 | Tree struct { 17 | *Node 18 | 19 | // Internal 20 | LastUpdated time.Time 21 | Checkpoint string 22 | 23 | client client 24 | cacheFile string 25 | nodeMap map[string]*Node 26 | } 27 | 28 | nodeList struct { 29 | ETagResponse string `json:"eTagResponse"` 30 | Count uint64 `json:"count,omitempty"` 31 | NextToken string `json:"nextToken,omitempty"` 32 | Nodes []*Node `json:"data,omitempty"` 33 | } 34 | ) 35 | 36 | // RemoveNode removes this node from the server and from the NodeTree. 37 | func (nt *Tree) RemoveNode(n *Node) error { 38 | if err := n.Remove(); err != nil { 39 | return err 40 | } 41 | 42 | for _, parentID := range n.Parents { 43 | parent, err := nt.FindByID(parentID) 44 | if err != nil { 45 | log.Debugf("parent ID %s not found", parentID) 46 | continue 47 | } 48 | parent.RemoveChild(n) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // NewTree returns the root node (the head of the tree). 55 | func NewTree(c client, cacheFile string) (*Tree, error) { 56 | nt := &Tree{ 57 | cacheFile: cacheFile, 58 | client: c, 59 | } 60 | if err := nt.loadOrFetch(); err != nil { 61 | return nil, err 62 | } 63 | if err := nt.saveCache(); err != nil { 64 | return nil, err 65 | } 66 | 67 | return nt, nil 68 | } 69 | 70 | // Close finalizes the NodeTree 71 | func (nt *Tree) Close() error { 72 | return nt.saveCache() 73 | } 74 | 75 | // MkdirAll creates a directory named path, along with any necessary parents, 76 | // and returns the directory node and nil, or else returns an error. If path is 77 | // already a directory, MkdirAll does nothing and returns the directory node 78 | // and nil. 79 | func (nt *Tree) MkdirAll(path string) (*Node, error) { 80 | var ( 81 | err error 82 | folderNode = nt.Node 83 | logLevel = log.GetLevel() 84 | nextNode *Node 85 | node *Node 86 | ) 87 | 88 | // Short-circuit if the node already exists! 89 | { 90 | log.SetLevel(log.DisableLogLevel) 91 | node, err = nt.FindNode(path) 92 | log.SetLevel(logLevel) 93 | } 94 | if err == nil { 95 | if node.IsDir() { 96 | return node, err 97 | } 98 | log.Errorf("%s: %s", constants.ErrFileExistsAndIsNotFolder, path) 99 | return nil, constants.ErrFileExistsAndIsNotFolder 100 | } 101 | 102 | // chop off the first /. 103 | if strings.HasPrefix(path, "/") { 104 | path = path[1:] 105 | } 106 | parts := strings.Split(path, "/") 107 | if len(parts) == 0 { 108 | log.Errorf("%s: %s", constants.ErrCannotCreateRootNode, path) 109 | return nil, constants.ErrCannotCreateRootNode 110 | } 111 | 112 | for i, part := range parts { 113 | { 114 | log.SetLevel(log.DisableLogLevel) 115 | nextNode, err = nt.FindNode(strings.Join(parts[:i+1], "/")) 116 | log.SetLevel(logLevel) 117 | } 118 | if err != nil && err != constants.ErrNodeNotFound { 119 | return nil, err 120 | } 121 | if err == constants.ErrNodeNotFound { 122 | nextNode, err = folderNode.CreateFolder(part) 123 | if err != nil { 124 | return nil, err 125 | } 126 | } 127 | 128 | if !nextNode.IsDir() { 129 | log.Errorf("%s: %s", constants.ErrCannotCreateANodeUnderAFile, strings.Join(parts[:i+1], "/")) 130 | return nil, constants.ErrCannotCreateANodeUnderAFile 131 | } 132 | 133 | folderNode = nextNode 134 | } 135 | 136 | return folderNode, nil 137 | } 138 | 139 | func (nt *Tree) setClient(n *Node) { 140 | n.client = nt.client 141 | for _, node := range n.Nodes { 142 | nt.setClient(node) 143 | } 144 | } 145 | 146 | func (nt *Tree) buildNodeMap(current *Node) { 147 | if nt.Node == current { 148 | nt.nodeMap = make(map[string]*Node) 149 | } 150 | nt.nodeMap[current.ID] = current 151 | for _, node := range current.Nodes { 152 | nt.buildNodeMap(node) 153 | } 154 | } 155 | 156 | func (nt *Tree) loadOrFetch() error { 157 | var err error 158 | if err = nt.loadCache(); err != nil { 159 | log.Debug(err) 160 | if err = nt.fetchFresh(); err != nil { 161 | return err 162 | } 163 | } 164 | 165 | if err = nt.Sync(); err != nil { 166 | switch err { 167 | case constants.ErrMustFetchFresh: 168 | if err = nt.fetchFresh(); err != nil { 169 | return err 170 | } 171 | return nt.Sync() 172 | default: 173 | return err 174 | } 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func (nt *Tree) fetchFresh() error { 181 | // grab the list of all of the nodes from the server. 182 | var nextToken string 183 | var nodes []*Node 184 | for { 185 | nl := nodeList{ 186 | Nodes: make([]*Node, 0, 200), 187 | } 188 | urlStr := nt.client.GetMetadataURL("nodes") 189 | u, err := url.Parse(urlStr) 190 | if err != nil { 191 | log.Errorf("%s: %s", constants.ErrParsingURL, urlStr) 192 | return constants.ErrParsingURL 193 | } 194 | 195 | v := url.Values{} 196 | v.Set("limit", "200") 197 | if nextToken != "" { 198 | v.Set("startToken", nextToken) 199 | } 200 | u.RawQuery = v.Encode() 201 | 202 | req, err := http.NewRequest("GET", u.String(), nil) 203 | if err != nil { 204 | log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err) 205 | return constants.ErrCreatingHTTPRequest 206 | } 207 | req.Header.Set("Content-Type", "application/json") 208 | res, err := nt.client.Do(req) 209 | if err != nil { 210 | log.Errorf("%s: %s", constants.ErrDoingHTTPRequest, err) 211 | return constants.ErrDoingHTTPRequest 212 | } 213 | 214 | defer res.Body.Close() 215 | if err := json.NewDecoder(res.Body).Decode(&nl); err != nil { 216 | log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err) 217 | return constants.ErrJSONDecodingResponseBody 218 | } 219 | 220 | nextToken = nl.NextToken 221 | nodes = append(nodes, nl.Nodes...) 222 | 223 | if nextToken == "" { 224 | break 225 | } 226 | } 227 | 228 | nodeMap := make(map[string]*Node, len(nodes)) 229 | for _, node := range nodes { 230 | if !node.Available() { 231 | continue 232 | } 233 | nt.setClient(node) 234 | nodeMap[node.ID] = node 235 | } 236 | 237 | for _, node := range nodeMap { 238 | if node.Name == "" && node.IsDir() && len(node.Parents) == 0 { 239 | nt.Node = node 240 | node.Root = true 241 | } 242 | 243 | for _, parentID := range node.Parents { 244 | if pn, found := nodeMap[parentID]; found { 245 | pn.Nodes = append(pn.Nodes, node) 246 | } 247 | } 248 | } 249 | 250 | nt.nodeMap = nodeMap 251 | return nil 252 | } 253 | -------------------------------------------------------------------------------- /internal/constants/errors.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import "errors" 4 | 5 | var ( 6 | // Response errors 7 | 8 | // ErrResponseUnknown is returned when the response status is not known. 9 | ErrResponseUnknown = errors.New("response returned an unknown status") 10 | // ErrResponseBadInput Bad input parameter. Error message should indicate 11 | // which one and why. 12 | ErrResponseBadInput = errors.New("response returned with status 400") 13 | // ErrResponseInvalidToken The client passed in the invalid Auth token. 14 | // Client should refresh the token and then try again. 15 | ErrResponseInvalidToken = errors.New("response returned with status 401") 16 | // ErrResponseForbidden Forbidden. 17 | ErrResponseForbidden = errors.New("response returned with status 403") 18 | // ErrResponseDuplicateExists Duplicate file exists. 19 | ErrResponseDuplicateExists = errors.New("response returned with status 409") 20 | // ErrResponseInternalServerError Servers are not working as expected. The 21 | // request is probably valid but needs to be requested again later. 22 | ErrResponseInternalServerError = errors.New("response returned with status 500") 23 | // ErrResponseUnavailable Service Unavailable. 24 | ErrResponseUnavailable = errors.New("response returned with status 503") 25 | // ErrJSONDecodingResponseBody is returned if there was an error decoding the 26 | // response body. 27 | ErrJSONDecodingResponseBody = errors.New("error while JSON-decoding the response body") 28 | // ErrReadingResponseBody is returned if ioutil.ReadAll() has failed. 29 | ErrReadingResponseBody = errors.New("error reading the entire response body") 30 | 31 | // Request errors 32 | 33 | // ErrCreatingHTTPRequest is returned if there was an error creating the HTTP 34 | // request. 35 | ErrCreatingHTTPRequest = errors.New("error creating HTTP request") 36 | // ErrDoingHTTPRequest is returned if there was an error doing the HTTP 37 | // request. 38 | ErrDoingHTTPRequest = errors.New("error doing the HTTP request") 39 | // ErrHTTPRequestTimeout is returned when an HTTP request has timed out. 40 | ErrHTTPRequestTimeout = errors.New("the request has timed out") 41 | 42 | // Downloading errors 43 | 44 | // ErrNodeDownload is returned if there was an error downloading the file. 45 | ErrNodeDownload = errors.New("error downloading the node") 46 | 47 | // Uploading errors 48 | 49 | // ErrFileExistsAndIsFolder is returned if attempting to upload a file but a 50 | // folder with the same path already exists. 51 | ErrFileExistsAndIsFolder = errors.New("the file exists and is a folder") 52 | // ErrFileExistsAndIsNotFolder is returned if attempting to create a folder 53 | // but a file with the same path already exists. 54 | ErrFileExistsAndIsNotFolder = errors.New("the file exists and is not a folder") 55 | // ErrFileExistsWithDifferentContents is returned if attempting to upload a 56 | // file but overwrite is disabled and the file already exists with different 57 | // contents. 58 | ErrFileExistsWithDifferentContents = errors.New("the files exists but is with different contents") 59 | // ErrWritingMetadata is returned if an error occurs whilst writing the metadata 60 | ErrWritingMetadata = errors.New("error writing the metadata") 61 | // ErrCreatingWriterFromFile is returned if an error happens when creating a writer from a file 62 | ErrCreatingWriterFromFile = errors.New("error creating a writer from a file") 63 | // ErrWritingFileContents is returned if an error happens when writing the file contents 64 | ErrWritingFileContents = errors.New("error writing the file contents") 65 | // ErrNoContentsToUpload is returned if the reader does not even have one byte. 66 | ErrNoContentsToUpload = errors.New("reader has not contents to upload") 67 | 68 | // JSON errors 69 | 70 | // ErrJSONEncoding is returned when an error occurs whilst encoding an object into JSON. 71 | ErrJSONEncoding = errors.New("error encoding an object to JSON") 72 | // ErrJSONDecoding is returned when an error occurs whilst decoding JSON to an object. 73 | ErrJSONDecoding = errors.New("error decoding JSON to an object") 74 | 75 | // GOB errors 76 | 77 | // ErrGOBEncoding is returned when an error occurs whilst encoding an object into GOB. 78 | ErrGOBEncoding = errors.New("error encoding an object to GOB") 79 | // ErrGOBDecoding is returned when an error occurs whilst decoding GOB to an object. 80 | ErrGOBDecoding = errors.New("error decoding GOB to an object") 81 | 82 | // Node errors 83 | 84 | // ErrNodeNotFound is returned when a node is not found. 85 | ErrNodeNotFound = errors.New("node not found") 86 | // ErrCannotCreateRootNode is returned if you attempt to create the root node 87 | ErrCannotCreateRootNode = errors.New("root node cannot be created") 88 | // ErrLoadingCache is returned when an error happens while loading from cacheFile 89 | ErrLoadingCache = errors.New("error loading from the cache file") 90 | // ErrMustFetchFresh is returned if the changes API requested a change. 91 | ErrMustFetchFresh = errors.New("must refresh the node tree") 92 | // ErrCannotCreateANodeUnderAFile is returned if you attempt to create a 93 | // folder/file under an existing file. 94 | ErrCannotCreateANodeUnderAFile = errors.New("cannot create a node under a file") 95 | 96 | // URL errors 97 | 98 | // ErrParsingURL is returned if an error occured whilst parsing a URL 99 | ErrParsingURL = errors.New("error parsing the URL") 100 | 101 | // File-related errors 102 | 103 | // ErrStatFile is returned if there was an error getting info about the file. 104 | ErrStatFile = errors.New("error stat() the file") 105 | // ErrOpenFile is returned if an error occurred while opening the file for reading 106 | ErrOpenFile = errors.New("error opening the file for reading") 107 | // ErrCreateFile is returned if an error is returned when trying to create a file 108 | ErrCreateFile = errors.New("error creating and/or truncating a file") 109 | // ErrCreateFolder is returned if an error occurred when trying to create a folder. 110 | ErrCreateFolder = errors.New("error creating a folder") 111 | // ErrFileExists is returned if the file already exists (on the server or locally). 112 | ErrFileExists = errors.New("the file already exists") 113 | // ErrFileNotFound is returned if no such file or directory. 114 | ErrFileNotFound = errors.New("no such file or directory") 115 | // ErrPathIsNotFolder is returned if the path is not a folder. 116 | ErrPathIsNotFolder = errors.New("path is not a folder") 117 | // ErrPathIsFolder is returned if the path is a folder. 118 | ErrPathIsFolder = errors.New("path is a folder") 119 | // ErrWrongPermissions is returned if the file has the wrong permissions. 120 | ErrWrongPermissions = errors.New("file has wrong permissions") 121 | ) 122 | --------------------------------------------------------------------------------