{"action":"created","comment":{"url":"https://api.github.com/repos/restic/restic/pulls/comments/92954585","pull_request_review_id":13475816,"id":92954585,"diff_hunk":"@@ -0,0 +1,288 @@\n+package b2\n+\n+import (\n+\t\"io\"\n+\t\"path\"\n+\t\"restic\"\n+\t\"strings\"\n+\n+\t\"restic/debug\"\n+\t\"restic/errors\"\n+\n+\tblazerb2 \"github.com/kurin/blazer/b2\"\n+\n+\t\"golang.org/x/net/context\"\n+)\n+\n+// b2 is a backend which stores the data on a B2 endpoint.\n+type b2 struct {\n+\tclient     *blazerb2.Client\n+\tbucket     *blazerb2.Bucket\n+\tbucketName string\n+\tprefix     string\n+\tcontext    context.Context\n+}\n+\n+// Open opens the B2 backend. The bucket is created if it does not exist yet.\n+func Open(cfg Config) (restic.Backend, error) {\n+\tdebug.Log(\"open, config %#v\", cfg)\n+\n+\tctx := context.Background()\n+\n+\tclient, err := blazerb2.NewClient(ctx, cfg.AccountID, cfg.Key)\n+\tif err != nil {\n+\t\treturn nil, errors.Wrap(err, \"blazerb2.NewClient\")\n+\t}\n+\n+\tbucket, err := client.NewBucket(ctx, cfg.Bucket, blazerb2.Private)\n+\tif err != nil {\n+\t\treturn nil, errors.Wrap(err, \"blazerb2.NewBucket\")\n+\t}\n+\n+\tbe := &b2{client: client, bucket: bucket, bucketName: cfg.Bucket, prefix: cfg.Prefix, context: ctx}\n+\n+\treturn be, nil\n+}\n+\n+// Resolve the backend path for based on file type ane name.\n+func (be *b2) b2path(t restic.FileType, name string) string {\n+\tif t == \"key\" && name == \"config\" {\n+\t\tt = restic.ConfigFile\n+\t}\n+\tif string(t) == restic.ConfigFile || name == \"\" {\n+\t\treturn path.Join(be.prefix, string(t))\n+\t}\n+\treturn path.Join(be.prefix, string(t), name)\n+}\n+\n+// Location returns this backend's location (the bucket name).\n+func (be *b2) Location() string {\n+\treturn be.bucketName\n+}\n+\n+// Load returns the data stored in the backend for h at the given offset\n+// and saves it in p. Load has the same semantics as io.ReaderAt.\n+func (be b2) Load(h restic.Handle, p []byte, off int64) (n int, err error) {\n+\tdebug.Log(\"Load: %v, offset %v, len %v\", h, off, len(p))\n+\tobjName := be.b2path(h.Type, h.Name)\n+\n+\tobj := be.bucket.Object(objName)\n+\tif err != nil {\n+\t\tdebug.Log(\"  err %v\", err)\n+\t\treturn 0, errors.Wrap(err, \"bucket.Object\")\n+\t}\n+\n+\tinfo, err := obj.Attrs(be.context)\n+\tif err != nil {\n+\t\treturn 0, errors.Wrap(err, \"obj.Attrs\")\n+\t}\n+\n+\t// handle negative offsets\n+\tif off < 0 {\n+\t\t// if the negative offset is larger than the object itself, read from\n+\t\t// the beginning.\n+\t\tif -off > info.Size {\n+\t\t\toff = 0\n+\t\t} else {\n+\t\t\t// otherwise compute the offset from the end of the file.\n+\t\t\toff = info.Size + off\n+\t\t}\n+\t}\n+\n+\t// return an error if the offset is beyond the end of the file\n+\tif off > info.Size {\n+\t\treturn 0, errors.Wrap(io.EOF, \"\")\n+\t}\n+\n+\tvar nextError error\n+\n+\t// manually create an io.ErrUnexpectedEOF\n+\tif off+int64(len(p)) > info.Size {\n+\t\tnewlen := info.Size - off\n+\t\tp = p[:newlen]\n+\n+\t\tnextError = io.ErrUnexpectedEOF\n+\n+\t\tdebug.Log(\"  capped buffer to %v byte\", len(p))\n+\t}\n+\n+\tr := obj.NewRangeReader(be.context, off, int64(len(p)))\n+\tdefer r.Close()\n+\n+\tbufsize := int64(len(p))\n+\tvar pos int64 = 0\n+\n+\t// loop to read chunks until the buffer is full or EOF is reached\n+\tfor {\n+\t\tn, err = r.Read(p[pos:])\n+\t\tpos += int64(n)\n+\t\tif pos == info.Size-off || pos == bufsize || err != nil {\n+\t\t\tif errors.Cause(err) == io.EOF {\n+\t\t\t\terr = nil\n+\t\t\t}\n+\t\t\tbreak\n+\t\t}\n+\t}\n+\n+\tif err == nil {\n+\t\terr = nextError\n+\t}\n+\n+\treturn int(pos), err\n+}\n+\n+// Save stores data in the backend at the handle.\n+func (be b2) Save(h restic.Handle, p []byte) (err error) {\n+\tdebug.Log(\"Save: %v with len %d\", h, len(p))\n+\tif err := h.Valid(); err != nil {\n+\t\treturn err\n+\t}\n+\n+\tobjName := be.b2path(h.Type, h.Name)\n+\n+\tobj := be.bucket.Object(objName)\n+\tif err != nil {\n+\t\tdebug.Log(\"  err %v\", err)\n+\t\treturn errors.Wrap(err, \"bucket.Object\")\n+\t}\n+\n+\t_, err = obj.Attrs(be.context)\n+\tif err == nil {\n+\t\tdebug.Log(\"  %v already exists\", h)\n+\t\treturn errors.New(\"key already exists\")\n+\t}\n+\n+\tdebug.Log(\"  Writing to %s\", objName)\n+\tw := obj.NewWriter(be.context)\n+\tn, err := w.Write(p)\n+\n+\tif err != nil {\n+\t\tdebug.Log(\"  Write error %v -> %v bytes, err %#v\", objName, n, err)\n+\t\tw.Close()\n+\t\treturn err\n+\t}\n+\n+\tdebug.Log(\"  Write successful %v -> %v bytes\", objName, n)\n+\tw.Close()\n+\treturn nil\n+}\n+\n+// Stat returns information about a blob.\n+func (be b2) Stat(h restic.Handle) (bi restic.FileInfo, err error) {\n+\tdebug.Log(\"Stat: %v\", h)\n+\tobjName := be.b2path(h.Type, h.Name)\n+\tobj := be.bucket.Object(objName)\n+\tinfo, err := obj.Attrs(be.context)\n+\tif err != nil {\n+\t\tdebug.Log(\"Attrs() err %v\", err)\n+\t\treturn restic.FileInfo{}, errors.Wrap(err, \"Stat\")\n+\t}\n+\treturn restic.FileInfo{Size: info.Size}, nil\n+}\n+\n+// Test returns true if a blob of the given type and name exists in the backend.\n+func (be *b2) Test(t restic.FileType, name string) (bool, error) {\n+\tfound := false\n+\tobjName := be.b2path(t, name)\n+\tobj := be.bucket.Object(objName)\n+\t_, err := obj.Attrs(be.context)\n+\tif err == nil {","path":"src/restic/backend/b2/b2.go","position":null,"original_position":189,"commit_id":"591f51c72e11a133f56468cf6ad4eb3b52669147","original_commit_id":"049859d323c81b999726a78e81ae94d1334ee52d","user":{"login":"arithmetric","id":280997,"avatar_url":"https://avatars.githubusercontent.com/u/280997?v=3","gravatar_id":"","url":"https://api.github.com/users/arithmetric","html_url":"https://github.com/arithmetric","followers_url":"https://api.github.com/users/arithmetric/followers","following_url":"https://api.github.com/users/arithmetric/following{/other_user}","gists_url":"https://api.github.com/users/arithmetric/gists{/gist_id}","starred_url":"https://api.github.com/users/arithmetric/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/arithmetric/subscriptions","organizations_url":"https://api.github.com/users/arithmetric/orgs","repos_url":"https://api.github.com/users/arithmetric/repos","events_url":"https://api.github.com/users/arithmetric/events{/privacy}","received_events_url":"https://api.github.com/users/arithmetric/received_events","type":"User","site_admin":false},"body":"I've made this change.","created_at":"2016-12-18T23:59:20Z","updated_at":"2016-12-18T23:59:20Z","html_url":"https://github.com/restic/restic/pull/694#discussion_r92954585","pull_request_url":"https://api.github.com/repos/restic/restic/pulls/694","_links":{"self":{"href":"https://api.github.com/repos/restic/restic/pulls/comments/92954585"},"html":{"href":"https://github.com/restic/restic/pull/694#discussion_r92954585"},"pull_request":{"href":"https://api.github.com/repos/restic/restic/pulls/694"}}},"pull_request":{"url":"https://api.github.com/repos/restic/restic/pulls/694","id":97487534,"html_url":"https://github.com/restic/restic/pull/694","diff_url":"https://github.com/restic/restic/pull/694.diff","patch_url":"https://github.com/restic/restic/pull/694.patch","issue_url":"https://api.github.com/repos/restic/restic/issues/694","number":694,"state":"open","locked":false,"title":"Backblaze B2 backend support","user":{"login":"arithmetric","id":280997,"avatar_url":"https://avatars.githubusercontent.com/u/280997?v=3","gravatar_id":"","url":"https://api.github.com/users/arithmetric","html_url":"https://github.com/arithmetric","followers_url":"https://api.github.com/users/arithmetric/followers","following_url":"https://api.github.com/users/arithmetric/following{/other_user}","gists_url":"https://api.github.com/users/arithmetric/gists{/gist_id}","starred_url":"https://api.github.com/users/arithmetric/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/arithmetric/subscriptions","organizations_url":"https://api.github.com/users/arithmetric/orgs","repos_url":"https://api.github.com/users/arithmetric/repos","events_url":"https://api.github.com/users/arithmetric/events{/privacy}","received_events_url":"https://api.github.com/users/arithmetric/received_events","type":"User","site_admin":false},"body":"This PR implements support for Backblaze B2 as a storage backend using kurin's [blazer](https://github.com/kurin/blazer) package. The code is based on the S3 backend.\r\n\r\nLike the S3 backend, the B2 account ID and key credentials should be specified by environment variables (`B2_ACCOUNT_ID` and `B2_ACCOUNT_KEY`), while the B2 bucket name and path prefix are specified through the repository URL in the format: `b2:<bucket name>` or `b2:<bucket name>:<prefix path>`.\r\n\r\nIf you register with Backblaze B2, you should be able to run the full test suite against the B2 API at least once daily and stay within the free limits. By raising the caps to a modest amount (less than $1 total), you would be able to run the full test suite multiple times.\r\n\r\nResolves #512.\r\n","created_at":"2016-12-12T02:48:52Z","updated_at":"2016-12-18T23:59:20Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f9d455c738064af4e64ba86419ab310e98689a80","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/restic/restic/pulls/694/commits","review_comments_url":"https://api.github.com/repos/restic/restic/pulls/694/comments","review_comment_url":"https://api.github.com/repos/restic/restic/pulls/comments{/number}","comments_url":"https://api.github.com/repos/restic/restic/issues/694/comments","statuses_url":"https://api.github.com/repos/restic/restic/statuses/591f51c72e11a133f56468cf6ad4eb3b52669147","head":{"label":"arithmetric:feature/support_backblaze_b2_backend","ref":"feature/support_backblaze_b2_backend","sha":"591f51c72e11a133f56468cf6ad4eb3b52669147","user":{"login":"arithmetric","id":280997,"avatar_url":"https://avatars.githubusercontent.com/u/280997?v=3","gravatar_id":"","url":"https://api.github.com/users/arithmetric","html_url":"https://github.com/arithmetric","followers_url":"https://api.github.com/users/arithmetric/followers","following_url":"https://api.github.com/users/arithmetric/following{/other_user}","gists_url":"https://api.github.com/users/arithmetric/gists{/gist_id}","starred_url":"https://api.github.com/users/arithmetric/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/arithmetric/subscriptions","organizations_url":"https://api.github.com/users/arithmetric/orgs","repos_url":"https://api.github.com/users/arithmetric/repos","events_url":"https://api.github.com/users/arithmetric/events{/privacy}","received_events_url":"https://api.github.com/users/arithmetric/received_events","type":"User","site_admin":false},"repo":{"id":75681102,"name":"restic","full_name":"arithmetric/restic","owner":{"login":"arithmetric","id":280997,"avatar_url":"https://avatars.githubusercontent.com/u/280997?v=3","gravatar_id":"","url":"https://api.github.com/users/arithmetric","html_url":"https://github.com/arithmetric","followers_url":"https://api.github.com/users/arithmetric/followers","following_url":"https://api.github.com/users/arithmetric/following{/other_user}","gists_url":"https://api.github.com/users/arithmetric/gists{/gist_id}","starred_url":"https://api.github.com/users/arithmetric/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/arithmetric/subscriptions","organizations_url":"https://api.github.com/users/arithmetric/orgs","repos_url":"https://api.github.com/users/arithmetric/repos","events_url":"https://api.github.com/users/arithmetric/events{/privacy}","received_events_url":"https://api.github.com/users/arithmetric/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/arithmetric/restic","description":"restic backup program","fork":true,"url":"https://api.github.com/repos/arithmetric/restic","forks_url":"https://api.github.com/repos/arithmetric/restic/forks","keys_url":"https://api.github.com/repos/arithmetric/restic/keys{/key_id}","collaborators_url":"https://api.github.com/repos/arithmetric/restic/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/arithmetric/restic/teams","hooks_url":"https://api.github.com/repos/arithmetric/restic/hooks","issue_events_url":"https://api.github.com/repos/arithmetric/restic/issues/events{/number}","events_url":"https://api.github.com/repos/arithmetric/restic/events","assignees_url":"https://api.github.com/repos/arithmetric/restic/assignees{/user}","branches_url":"https://api.github.com/repos/arithmetric/restic/branches{/branch}","tags_url":"https://api.github.com/repos/arithmetric/restic/tags","blobs_url":"https://api.github.com/repos/arithmetric/restic/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/arithmetric/restic/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/arithmetric/restic/git/refs{/sha}","trees_url":"https://api.github.com/repos/arithmetric/restic/git/trees{/sha}","statuses_url":"https://api.github.com/repos/arithmetric/restic/statuses/{sha}","languages_url":"https://api.github.com/repos/arithmetric/restic/languages","stargazers_url":"https://api.github.com/repos/arithmetric/restic/stargazers","contributors_url":"https://api.github.com/repos/arithmetric/restic/contributors","subscribers_url":"https://api.github.com/repos/arithmetric/restic/subscribers","subscription_url":"https://api.github.com/repos/arithmetric/restic/subscription","commits_url":"https://api.github.com/repos/arithmetric/restic/commits{/sha}","git_commits_url":"https://api.github.com/repos/arithmetric/restic/git/commits{/sha}","comments_url":"https://api.github.com/repos/arithmetric/restic/comments{/number}","issue_comment_url":"https://api.github.com/repos/arithmetric/restic/issues/comments{/number}","contents_url":"https://api.github.com/repos/arithmetric/restic/contents/{+path}","compare_url":"https://api.github.com/repos/arithmetric/restic/compare/{base}...{head}","merges_url":"https://api.github.com/repos/arithmetric/restic/merges","archive_url":"https://api.github.com/repos/arithmetric/restic/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/arithmetric/restic/downloads","issues_url":"https://api.github.com/repos/arithmetric/restic/issues{/number}","pulls_url":"https://api.github.com/repos/arithmetric/restic/pulls{/number}","milestones_url":"https://api.github.com/repos/arithmetric/restic/milestones{/number}","notifications_url":"https://api.github.com/repos/arithmetric/restic/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/arithmetric/restic/labels{/name}","releases_url":"https://api.github.com/repos/arithmetric/restic/releases{/id}","deployments_url":"https://api.github.com/repos/arithmetric/restic/deployments","created_at":"2016-12-06T01:10:35Z","updated_at":"2016-12-06T01:10:37Z","pushed_at":"2016-12-18T23:57:55Z","git_url":"git://github.com/arithmetric/restic.git","ssh_url":"git@github.com:arithmetric/restic.git","clone_url":"https://github.com/arithmetric/restic.git","svn_url":"https://github.com/arithmetric/restic","homepage":"https://restic.github.io/","size":14650,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"restic:master","ref":"master","sha":"51cd78e16cfc8975c34a7e11ea6f4aec0d886cfb","user":{"login":"restic","id":10073512,"avatar_url":"https://avatars.githubusercontent.com/u/10073512?v=3","gravatar_id":"","url":"https://api.github.com/users/restic","html_url":"https://github.com/restic","followers_url":"https://api.github.com/users/restic/followers","following_url":"https://api.github.com/users/restic/following{/other_user}","gists_url":"https://api.github.com/users/restic/gists{/gist_id}","starred_url":"https://api.github.com/users/restic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/restic/subscriptions","organizations_url":"https://api.github.com/users/restic/orgs","repos_url":"https://api.github.com/users/restic/repos","events_url":"https://api.github.com/users/restic/events{/privacy}","received_events_url":"https://api.github.com/users/restic/received_events","type":"Organization","site_admin":false},"repo":{"id":19205896,"name":"restic","full_name":"restic/restic","owner":{"login":"restic","id":10073512,"avatar_url":"https://avatars.githubusercontent.com/u/10073512?v=3","gravatar_id":"","url":"https://api.github.com/users/restic","html_url":"https://github.com/restic","followers_url":"https://api.github.com/users/restic/followers","following_url":"https://api.github.com/users/restic/following{/other_user}","gists_url":"https://api.github.com/users/restic/gists{/gist_id}","starred_url":"https://api.github.com/users/restic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/restic/subscriptions","organizations_url":"https://api.github.com/users/restic/orgs","repos_url":"https://api.github.com/users/restic/repos","events_url":"https://api.github.com/users/restic/events{/privacy}","received_events_url":"https://api.github.com/users/restic/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/restic/restic","description":"restic backup program","fork":false,"url":"https://api.github.com/repos/restic/restic","forks_url":"https://api.github.com/repos/restic/restic/forks","keys_url":"https://api.github.com/repos/restic/restic/keys{/key_id}","collaborators_url":"https://api.github.com/repos/restic/restic/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/restic/restic/teams","hooks_url":"https://api.github.com/repos/restic/restic/hooks","issue_events_url":"https://api.github.com/repos/restic/restic/issues/events{/number}","events_url":"https://api.github.com/repos/restic/restic/events","assignees_url":"https://api.github.com/repos/restic/restic/assignees{/user}","branches_url":"https://api.github.com/repos/restic/restic/branches{/branch}","tags_url":"https://api.github.com/repos/restic/restic/tags","blobs_url":"https://api.github.com/repos/restic/restic/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/restic/restic/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/restic/restic/git/refs{/sha}","trees_url":"https://api.github.com/repos/restic/restic/git/trees{/sha}","statuses_url":"https://api.github.com/repos/restic/restic/statuses/{sha}","languages_url":"https://api.github.com/repos/restic/restic/languages","stargazers_url":"https://api.github.com/repos/restic/restic/stargazers","contributors_url":"https://api.github.com/repos/restic/restic/contributors","subscribers_url":"https://api.github.com/repos/restic/restic/subscribers","subscription_url":"https://api.github.com/repos/restic/restic/subscription","commits_url":"https://api.github.com/repos/restic/restic/commits{/sha}","git_commits_url":"https://api.github.com/repos/restic/restic/git/commits{/sha}","comments_url":"https://api.github.com/repos/restic/restic/comments{/number}","issue_comment_url":"https://api.github.com/repos/restic/restic/issues/comments{/number}","contents_url":"https://api.github.com/repos/restic/restic/contents/{+path}","compare_url":"https://api.github.com/repos/restic/restic/compare/{base}...{head}","merges_url":"https://api.github.com/repos/restic/restic/merges","archive_url":"https://api.github.com/repos/restic/restic/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/restic/restic/downloads","issues_url":"https://api.github.com/repos/restic/restic/issues{/number}","pulls_url":"https://api.github.com/repos/restic/restic/pulls{/number}","milestones_url":"https://api.github.com/repos/restic/restic/milestones{/number}","notifications_url":"https://api.github.com/repos/restic/restic/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/restic/restic/labels{/name}","releases_url":"https://api.github.com/repos/restic/restic/releases{/id}","deployments_url":"https://api.github.com/repos/restic/restic/deployments","created_at":"2014-04-27T14:07:58Z","updated_at":"2016-12-18T07:57:39Z","pushed_at":"2016-12-18T23:57:56Z","git_url":"git://github.com/restic/restic.git","ssh_url":"git@github.com:restic/restic.git","clone_url":"https://github.com/restic/restic.git","svn_url":"https://github.com/restic/restic","homepage":"https://restic.github.io/","size":14619,"stargazers_count":1065,"watchers_count":1065,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":101,"mirror_url":null,"open_issues_count":138,"forks":101,"open_issues":138,"watchers":1065,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/restic/restic/pulls/694"},"html":{"href":"https://github.com/restic/restic/pull/694"},"issue":{"href":"https://api.github.com/repos/restic/restic/issues/694"},"comments":{"href":"https://api.github.com/repos/restic/restic/issues/694/comments"},"review_comments":{"href":"https://api.github.com/repos/restic/restic/pulls/694/comments"},"review_comment":{"href":"https://api.github.com/repos/restic/restic/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/restic/restic/pulls/694/commits"},"statuses":{"href":"https://api.github.com/repos/restic/restic/statuses/591f51c72e11a133f56468cf6ad4eb3b52669147"}}}}
{"actor":{"display_login":"arithmetric"}}