mirror of https://github.com/perkeep/perkeep.git
Merge branch 'master' of ssh://camlistore.org:29418/camlistore
This commit is contained in:
commit
a9853a4622
4
build.pl
4
build.pl
|
@ -638,7 +638,7 @@ TARGET: lib/go/camli/auth
|
|||
TARGET: lib/go/camli/blobref
|
||||
TARGET: lib/go/camli/blobserver
|
||||
TARGET: lib/go/camli/blobserver/cond
|
||||
TARGET: lib/go/camli/blobserver/googlestorage
|
||||
TARGET: lib/go/camli/blobserver/google
|
||||
TARGET: lib/go/camli/blobserver/handlers
|
||||
TARGET: lib/go/camli/blobserver/localdisk
|
||||
TARGET: lib/go/camli/blobserver/remote
|
||||
|
@ -651,6 +651,8 @@ TARGET: lib/go/camli/db
|
|||
TARGET: lib/go/camli/db/dbimpl
|
||||
TARGET: lib/go/camli/errorutil
|
||||
TARGET: lib/go/camli/fs
|
||||
TARGET: lib/go/camli/googlestorage
|
||||
=skip_tests
|
||||
TARGET: lib/go/camli/httputil
|
||||
TARGET: lib/go/camli/jsonconfig
|
||||
TARGET: lib/go/camli/jsonsign
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"camli/blobserver/googlestorage"
|
||||
"camli/blobserver/google"
|
||||
"camli/third_party/code.google.com/goauth2/oauth"
|
||||
)
|
||||
|
||||
|
@ -37,7 +37,7 @@ func main() {
|
|||
if clientId, clientSecret, err = getClientInfo(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
transport := googlestorage.MakeOauthTransport(clientId, clientSecret, "")
|
||||
transport := google.MakeOauthTransport(clientId, clientSecret, "")
|
||||
|
||||
var accessCode string
|
||||
if accessCode, err = getAccessCode(transport.Config); err != nil {
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package googlestorage
|
||||
package google
|
||||
|
||||
import (
|
||||
"camli/third_party/code.google.com/goauth2/oauth"
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright 2011 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package google
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"camli/blobref"
|
||||
"camli/blobserver"
|
||||
"camli/googlestorage"
|
||||
"camli/jsonconfig"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
hub *blobserver.SimpleBlobHub
|
||||
bucket string // the gs bucket containing blobs
|
||||
client *googlestorage.Client
|
||||
}
|
||||
|
||||
func newFromConfig(_ blobserver.Loader, config jsonconfig.Obj) (blobserver.Storage, os.Error) {
|
||||
auth := config.RequiredObject("auth")
|
||||
gs := &Storage{
|
||||
&blobserver.SimpleBlobHub{},
|
||||
config.RequiredString("bucket"),
|
||||
googlestorage.NewClient(MakeOauthTransport(
|
||||
auth.RequiredString("client_id"),
|
||||
auth.RequiredString("client_secret"),
|
||||
auth.RequiredString("refresh_token"),
|
||||
)),
|
||||
}
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := auth.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gs, nil
|
||||
}
|
||||
|
||||
func (gs *Storage) EnumerateBlobs(dest chan<- blobref.SizedBlobRef, after string, limit uint, waitSeconds int) os.Error {
|
||||
// TODO: Implement stub
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gs *Storage) ReceiveBlob(blob *blobref.BlobRef, source io.Reader) (blobref.SizedBlobRef, os.Error) {
|
||||
// TODO: Implement stub
|
||||
return blobref.SizedBlobRef{}, nil
|
||||
}
|
||||
|
||||
func (gs *Storage) Stat(dest chan<- blobref.SizedBlobRef, blobs []*blobref.BlobRef, waitSeconds int) os.Error {
|
||||
// TODO: Implement stub
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gs *Storage) FetchStreaming(blob *blobref.BlobRef) (io.ReadCloser, int64, os.Error) {
|
||||
// TODO: Implement stub
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (gs *Storage) Remove(blobs []*blobref.BlobRef) os.Error {
|
||||
// TODO: Implement stub
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gs *Storage) GetBlobHub() blobserver.BlobHub {
|
||||
return gs.hub
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
Implements the Storage interface for Google Storage.
|
||||
A GoogleStorage instance stores blobs in a single Google Storage bucket, with
|
||||
each blob keyed by its blobref.
|
||||
|
||||
Testing:
|
||||
|
||||
gsapi_test.go contains integration tests which run against Google Storage.
|
||||
In order to run these tests properly, you will need to:
|
||||
|
||||
1. Set up google storage. See:
|
||||
http://code.google.com/apis/storage/docs/signup.html
|
||||
|
||||
2. Upload the contents of the testdata dir to a google storage bucket. Note
|
||||
that all these files begin with 'test-': such files will be ignored when
|
||||
the bucket is used as blobserver storage.
|
||||
|
||||
3. Create the config file '~/.camli/gstestconfig.json'. The file should look
|
||||
something like this:
|
||||
|
||||
{
|
||||
"auth": {
|
||||
"client_id": "your client id",
|
||||
"client_secret": "your client secret",
|
||||
"refresh_token": "a refresh token"
|
||||
},
|
||||
"bucket": "bucket.example.com"
|
||||
}
|
||||
|
||||
You can use camgsinit to help obtain the auth config object.
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
Copyright 2011 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Implements the Google Storage API calls needed by camlistore.
|
||||
// This is intended to be exclude camlistore-specific logic.
|
||||
|
||||
package googlestorage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"http"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"xml"
|
||||
|
||||
"camli/third_party/code.google.com/goauth2/oauth"
|
||||
)
|
||||
|
||||
const (
|
||||
gsAccessURL = "https://commondatastorage.googleapis.com"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
transport *oauth.Transport
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
Bucket string
|
||||
Key string
|
||||
}
|
||||
|
||||
type SizedObject struct {
|
||||
Object
|
||||
Size int64
|
||||
}
|
||||
|
||||
func NewClient(transport *oauth.Transport) *Client {
|
||||
return &Client{transport, transport.Client()}
|
||||
}
|
||||
|
||||
func (gso Object) String() string {
|
||||
return fmt.Sprintf("%v/%v", gso.Bucket, gso.Key)
|
||||
}
|
||||
|
||||
func (sgso SizedObject) String() string {
|
||||
return fmt.Sprintf("%v/%v (%vB)", sgso.Bucket, sgso.Key, sgso.Size)
|
||||
}
|
||||
|
||||
// A close relative to http.Client.Do(), helping with token refresh logic.
|
||||
// If canResend is true and the initial request's response is an auth error
|
||||
// (401 or 403), oauth credentials will be refreshed and the request sent
|
||||
// again. This should only be done for requests with empty bodies, since the
|
||||
// Body will be consumed on the first attempt if it exists.
|
||||
// If canResend is false, and req would have been resent if canResend were
|
||||
// true, then shouldRetry will be true.
|
||||
// One of resp or err will always be nil.
|
||||
func (gsa *Client) doRequest(req *http.Request, canResend bool) (resp *http.Response, err os.Error, shouldRetry bool) {
|
||||
if resp, err = gsa.client.Do(req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
// Unauth. Perhaps tokens need refreshing?
|
||||
if err = gsa.transport.Refresh(); err != nil {
|
||||
return
|
||||
}
|
||||
// Refresh succeeded. req should be resent
|
||||
if !canResend {
|
||||
return resp, nil, true
|
||||
}
|
||||
// Resend req. First, need to close the soon-overwritten response Body
|
||||
resp.Body.Close()
|
||||
resp, err = gsa.client.Do(req)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Makes a simple body-less google storage request
|
||||
func (gsa *Client) simpleRequest(method, url string) (resp *http.Response, err os.Error) {
|
||||
// Construct the request
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Set("x-goog-api-version", "2")
|
||||
|
||||
resp, err, _ = gsa.doRequest(req, true)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch a GS object.
|
||||
// Bucket and Key fields are trusted to be valid.
|
||||
// Returns (object reader, object size, err). Reader must be closed.
|
||||
func (gsa *Client) GetObject(obj *Object) (io.ReadCloser, int64, os.Error) {
|
||||
log.Printf("Fetching object from Google Storage: %s/%s\n", obj.Bucket, obj.Key)
|
||||
|
||||
resp, err := gsa.simpleRequest("GET", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("GS GET request failed: %v\n", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, 0, fmt.Errorf("GS GET request failed status: %v\n", resp.Status)
|
||||
}
|
||||
|
||||
return resp.Body, resp.ContentLength, nil
|
||||
}
|
||||
|
||||
// Check for size / existence of a GS object.
|
||||
// Bucket and Key fields are trusted to be valid.
|
||||
// err signals io / authz errors, a nonexistant file is not an error.
|
||||
func (gsa *Client) StatObject(obj *Object) (size int64, exists bool, err os.Error) {
|
||||
log.Printf("Statting object in Google Storage: %s/%s\n", obj.Bucket, obj.Key)
|
||||
|
||||
resp, err := gsa.simpleRequest("HEAD", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close() // should be empty
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return
|
||||
}
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
if size, err = strconv.Atoi64(resp.Header["Content-Length"][0]); err != nil {
|
||||
return
|
||||
}
|
||||
return size, true, nil
|
||||
}
|
||||
|
||||
// Any response other than 404 or 200 is erroneous
|
||||
return 0, false, fmt.Errorf("Bad head response code: %v", resp.Status)
|
||||
}
|
||||
|
||||
// Upload a GS object. Bucket and Key are trusted to be valid.
|
||||
// shouldRetry will be true if the put failed due to authorization, but
|
||||
// credentials have been refreshed and another attempt is likely to succeed.
|
||||
// In this case, content will have been consumed.
|
||||
func (gsa *Client) PutObject(obj *Object, content io.ReadCloser) (shouldRetry bool, err os.Error) {
|
||||
log.Printf("Putting object in Google Storage: %s/%s\n", obj.Bucket, obj.Key)
|
||||
|
||||
objURL := gsAccessURL + "/" + obj.Bucket + "/" + obj.Key
|
||||
var req *http.Request
|
||||
if req, err = http.NewRequest("PUT", objURL, content); err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Set("x-goog-api-version", "2")
|
||||
|
||||
var resp *http.Response
|
||||
if resp, err, shouldRetry = gsa.doRequest(req, false); err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close() // should be empty
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return shouldRetry, fmt.Errorf("Bad put response code: %v", resp.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Removes a GS object.
|
||||
// Bucket and Key values are trusted to be valid.
|
||||
func (gsa *Client) DeleteObject(obj *Object) (err os.Error) {
|
||||
log.Printf("Deleting %v/%v\n", obj.Bucket, obj.Key)
|
||||
|
||||
// bucketURL := gsAccessURL + "/" + obj.Bucket + "/" + obj.Key
|
||||
resp, err := gsa.simpleRequest("DELETE", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
err = fmt.Errorf("Bad delete response code: %v", resp.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Used for unmarshalling XML returned by enumerate request
|
||||
type gsListResult struct {
|
||||
Contents []SizedObject
|
||||
}
|
||||
|
||||
// List the objects in a GS bucket.
|
||||
// If after is nonempty, listing will begin with lexically greater object names
|
||||
// If limit is nonzero, the length of the list will be limited to that number.
|
||||
func (gsa *Client) EnumerateObjects(bucket, after string, limit uint) ([]SizedObject, os.Error) {
|
||||
log.Printf("Fetching from %v: after '%v', limit %v\n", bucket, after, limit)
|
||||
|
||||
// Build url, with query params
|
||||
params := make([]string, 0, 2)
|
||||
if after != "" {
|
||||
params = append(params, "marker="+http.URLEscape(after))
|
||||
}
|
||||
if limit > 0 {
|
||||
params = append(params, fmt.Sprintf("max-keys=%v", limit))
|
||||
}
|
||||
query := ""
|
||||
if len(params) > 0 {
|
||||
query = "?" + strings.Join(params, "&")
|
||||
}
|
||||
|
||||
// Make the request
|
||||
resp, err := gsa.simpleRequest("GET", gsAccessURL+"/"+bucket+"/"+query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Bad enumerate response code: %v", resp.Status)
|
||||
}
|
||||
|
||||
// Parse the XML response
|
||||
result := &gsListResult{make([]SizedObject, 0, limit)}
|
||||
if err = xml.Unmarshal(resp.Body, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Fill in the Bucket on all the SizedObjects
|
||||
for i, _ := range result.Contents {
|
||||
result.Contents[i].Bucket = bucket
|
||||
}
|
||||
|
||||
return result.Contents, nil
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
Copyright 2011 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// FYI These tests are integration tests that need to run against google
|
||||
// storage. See the README for more details on necessary setup
|
||||
|
||||
package googlestorage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
"os"
|
||||
|
||||
"camli/jsonconfig"
|
||||
"camli/third_party/code.google.com/goauth2/oauth"
|
||||
)
|
||||
|
||||
const testObjectContent = "Google Storage Test\n"
|
||||
|
||||
type BufferCloser struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *BufferCloser) Close() os.Error {
|
||||
b.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reads google storage config and creates a Client. Exits on error.
|
||||
func doConfig(t *testing.T) (gsa *Client, bucket string) {
|
||||
cf, err := jsonconfig.ReadFile("testconfig.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read config: %v", err)
|
||||
}
|
||||
|
||||
var config jsonconfig.Obj
|
||||
config = cf.RequiredObject("gsconf")
|
||||
if err := cf.Validate(); err != nil {
|
||||
t.Fatalf("Invalid config: %v", err)
|
||||
}
|
||||
|
||||
auth := config.RequiredObject("auth")
|
||||
bucket = config.RequiredString("bucket")
|
||||
if err := config.Validate(); err != nil {
|
||||
t.Fatalf("Invalid config: %v", err)
|
||||
}
|
||||
|
||||
gsa = NewClient(&oauth.Transport{
|
||||
&oauth.Config{
|
||||
ClientId: auth.RequiredString("client_id"),
|
||||
ClientSecret: auth.RequiredString("client_secret"),
|
||||
Scope: "https://www.googleapis.com/auth/devstorage.read_write",
|
||||
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
TokenURL: "https://accounts.google.com/o/oauth2/token",
|
||||
RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
|
||||
},
|
||||
&oauth.Token{
|
||||
AccessToken: "",
|
||||
RefreshToken: auth.RequiredString("refresh_token"),
|
||||
TokenExpiry: 0,
|
||||
},
|
||||
nil,
|
||||
})
|
||||
|
||||
if err := auth.Validate(); err != nil {
|
||||
t.Fatalf("Invalid config: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestGetObject(t *testing.T) {
|
||||
gs, bucket := doConfig(t)
|
||||
|
||||
body, size, err := gs.GetObject(&Object{bucket, "test-get"})
|
||||
if err != nil {
|
||||
t.Fatalf("Fetch failed: %v\n", err)
|
||||
}
|
||||
|
||||
content := make([]byte, size)
|
||||
if _, err = body.Read(content); err != nil {
|
||||
t.Fatalf("Failed to read response body: %v:\n", err)
|
||||
}
|
||||
|
||||
if string(content) != testObjectContent {
|
||||
t.Fatalf("Object has incorrect content.\nExpected: '%v'\nFound: '%v'\n", testObjectContent, string(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatObject(t *testing.T) {
|
||||
gs, bucket := doConfig(t)
|
||||
|
||||
// Stat a nonexistant file
|
||||
size, exists, err := gs.StatObject(&Object{bucket, "test-shouldntexist"})
|
||||
if err != nil {
|
||||
t.Errorf("Stat failed: %v\n", err)
|
||||
}
|
||||
if exists {
|
||||
t.Errorf("Test object exists!")
|
||||
}
|
||||
if size != 0 {
|
||||
t.Errorf("Expected size to be 0, found %v\n", size)
|
||||
}
|
||||
|
||||
// Try statting an object which does exist
|
||||
size, exists, err = gs.StatObject(&Object{bucket, "test-stat"})
|
||||
if err != nil {
|
||||
t.Errorf("Stat failed: %v\n", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Errorf("Test object doesn't exist!")
|
||||
}
|
||||
if size != int64(len(testObjectContent)) {
|
||||
t.Errorf("Test object size is wrong: \nexpected: %v\nfound: %v\n",
|
||||
len(testObjectContent), size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutObject(t *testing.T) {
|
||||
gs, bucket := doConfig(t)
|
||||
|
||||
now := time.UTC()
|
||||
testKey := fmt.Sprintf("test-put-%v.%v.%v-%v.%v.%v",
|
||||
now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second)
|
||||
|
||||
shouldRetry, err := gs.PutObject(&Object{bucket, testKey},
|
||||
&BufferCloser{bytes.NewBufferString(testObjectContent)})
|
||||
if shouldRetry {
|
||||
shouldRetry, err = gs.PutObject(&Object{bucket, testKey},
|
||||
&BufferCloser{bytes.NewBufferString(testObjectContent)})
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to put object: %v", err)
|
||||
}
|
||||
|
||||
// Just stat to check that it actually uploaded, don't bother reading back
|
||||
size, exists, err := gs.StatObject(&Object{bucket, testKey})
|
||||
if !exists {
|
||||
t.Errorf("Test object doesn't exist!")
|
||||
}
|
||||
if size != int64(len(testObjectContent)) {
|
||||
t.Errorf("Test object size is wrong: \nexpected: %v\nfound: %v\n",
|
||||
len(testObjectContent), size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteObject(t *testing.T) {
|
||||
gs, bucket := doConfig(t)
|
||||
|
||||
// Try deleting a nonexitent file
|
||||
err := gs.DeleteObject(&Object{bucket, "test-shouldntexist"})
|
||||
if err == nil {
|
||||
t.Errorf("Tried to delete nonexistent object, succeeded.")
|
||||
}
|
||||
|
||||
// Create a file, try to delete it
|
||||
now := time.UTC()
|
||||
testKey := fmt.Sprintf("test-delete-%v.%v.%v-%v.%v.%v",
|
||||
now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second)
|
||||
_, err = gs.PutObject(&Object{bucket, testKey},
|
||||
&BufferCloser{bytes.NewBufferString("Delete Me")})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to put file to delete.")
|
||||
}
|
||||
err = gs.DeleteObject(&Object{bucket, testKey})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to delete object: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumerateBucket(t *testing.T) {
|
||||
gs, bucket := doConfig(t)
|
||||
|
||||
// Enumerate ALL the things!
|
||||
objs, err := gs.EnumerateObjects(bucket, "", 0)
|
||||
if err != nil {
|
||||
t.Errorf("Enumeration failed: %v\n", err)
|
||||
} else if len(objs) < 7 {
|
||||
// Minimum number of blobs, equal to the number of files in testdata
|
||||
t.Errorf("Expected at least 7 files, found %v", len(objs))
|
||||
}
|
||||
|
||||
// Test a limited enum
|
||||
objs, err = gs.EnumerateObjects(bucket, "", 5)
|
||||
if err != nil {
|
||||
t.Errorf("Enumeration failed: %v\n", err)
|
||||
} else if len(objs) != 5 {
|
||||
t.Errorf(
|
||||
"Limited enum returned wrong number of blobs.\nExpected: %v\nFound: %v",
|
||||
5, len(objs))
|
||||
}
|
||||
|
||||
// Test fetching a limited set from a known start point
|
||||
objs, err = gs.EnumerateObjects(bucket, "test-enum", 4)
|
||||
if err != nil {
|
||||
t.Errorf("Enumeration failed: %v\n", err)
|
||||
} else {
|
||||
for i := 0; i < 4; i += 1 {
|
||||
if objs[i].Key != fmt.Sprintf("test-enum-%v", i+1) {
|
||||
t.Errorf(
|
||||
"Enum from start point returned wrong key:\nExpected: test-enum-%v\nFound: %v",
|
||||
i+1, objs[i].Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"gsconf": ["_fileobj", "gstestconfig.json"]}
|
|
@ -0,0 +1 @@
|
|||
Google Storage Test
|
|
@ -0,0 +1 @@
|
|||
1
|
|
@ -0,0 +1 @@
|
|||
2
|
|
@ -0,0 +1 @@
|
|||
3
|
|
@ -0,0 +1 @@
|
|||
4
|
|
@ -0,0 +1 @@
|
|||
Google Storage Test
|
|
@ -0,0 +1 @@
|
|||
Google Storage Test
|
|
@ -144,7 +144,7 @@ func (t *Transport) Exchange(code string) (tok *Token, err os.Error) {
|
|||
}
|
||||
|
||||
// RoundTrip executes a single HTTP transaction using the Transport's
|
||||
// Token as authorization headers.
|
||||
// Token as authorization headers.
|
||||
func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err os.Error) {
|
||||
if t.Config == nil {
|
||||
return nil, os.NewError("no Config supplied")
|
||||
|
@ -159,18 +159,10 @@ func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err os.Er
|
|||
return
|
||||
}
|
||||
|
||||
// Refresh credentials if they're stale and try again
|
||||
if resp.StatusCode == 401 {
|
||||
if err = t.refresh(); err != nil {
|
||||
return
|
||||
}
|
||||
resp, err = t.transport().RoundTrip(req)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Transport) refresh() os.Error {
|
||||
func (t *Transport) Refresh() os.Error {
|
||||
return t.updateToken(t.Token, http.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {t.RefreshToken},
|
||||
|
|
|
@ -82,7 +82,8 @@ class CamliOp(object):
|
|||
server_address,
|
||||
buffer_size=BUFFER_SIZE,
|
||||
create_connection=httplib.HTTPConnection,
|
||||
auth=None):
|
||||
auth=None,
|
||||
basepath=False):
|
||||
"""Initializer.
|
||||
|
||||
Args:
|
||||
|
@ -91,6 +92,8 @@ class CamliOp(object):
|
|||
client-related operations.
|
||||
create_connection: Use for testing.
|
||||
auth: Optional. 'username:password' to use for HTTP basic auth.
|
||||
basepath: Optional path suffix. e.g. if the server is at
|
||||
"localhost:3179/bs", the basepath should be "/bs".
|
||||
"""
|
||||
self.server_address = server_address
|
||||
self.buffer_size = buffer_size
|
||||
|
@ -99,8 +102,16 @@ class CamliOp(object):
|
|||
self._authorization = ''
|
||||
if auth:
|
||||
if len(auth.split(':')) != 2:
|
||||
logging.fatal('Invalid auth string; should be username:password')
|
||||
# Default to dummy username; current server doesn't care
|
||||
# TODO(jrabbit): care when necessary
|
||||
auth = "username:" + auth #If username not given use the implicit default, 'username'
|
||||
self._authorization = ('Basic ' + base64.encodestring(auth).strip())
|
||||
if basepath:
|
||||
if '/' not in basepath:
|
||||
raise NameError("basepath must be in form '/bs'")
|
||||
if basepath[-1] == '/':
|
||||
basepath = basepath[:-1]
|
||||
self.basepath = basepath
|
||||
|
||||
def _setup_connection(self):
|
||||
"""Sets up the HTTP connection."""
|
||||
|
@ -144,8 +155,12 @@ class CamliOp(object):
|
|||
# after that we need to do batching in smaller groups.
|
||||
|
||||
self._setup_connection()
|
||||
if self.basepath:
|
||||
fullpath = self.basepath + '/camli/stat'
|
||||
else:
|
||||
fullpath = '/camli/stat'
|
||||
self.connection.request(
|
||||
'POST', '/camli/preupload', urllib.urlencode(preupload),
|
||||
'POST', fullpath, urllib.urlencode(preupload),
|
||||
{'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': self._authorization})
|
||||
response = self.connection.getresponse()
|
||||
|
@ -162,9 +177,9 @@ class CamliOp(object):
|
|||
raise PayloadError('Server returned bad preupload response: %r' % data)
|
||||
|
||||
logging.debug('Parsed preupload response: %r', response_dict)
|
||||
if 'alreadyHave' not in response_dict:
|
||||
if 'stat' not in response_dict:
|
||||
raise PayloadError(
|
||||
'Could not find "alreadyHave" in preupload response: %r' %
|
||||
'Could not find "stat" in preupload response: %r' %
|
||||
response_dict)
|
||||
if 'uploadUrl' not in response_dict:
|
||||
raise PayloadError(
|
||||
|
@ -172,7 +187,7 @@ class CamliOp(object):
|
|||
response_dict)
|
||||
|
||||
already_have_blobrefs = set()
|
||||
for blobref_json in response_dict['alreadyHave']:
|
||||
for blobref_json in response_dict['stat']:
|
||||
if 'blobRef' not in blobref_json:
|
||||
raise PayloadError(
|
||||
'Cannot find "blobRef" in preupload response: %r',
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
from setuptools import setup
|
||||
setup(
|
||||
name='camlistore-client',
|
||||
version='1.0.3dev',
|
||||
author='Brett Slatkin',
|
||||
author_email='bslatkin@gmail.com',
|
||||
maintainer='Jack Laxson',
|
||||
maintainer_email='jackjrabbit+camli@gmail.com',
|
||||
description="Client library for Camlistore.",
|
||||
url='http://camlistore.org',
|
||||
license='Apache v2',
|
||||
long_description='A convience library for python developers wishing to explore camlistore.',
|
||||
packages=['camli'],
|
||||
install_requires=['simplejson'],
|
||||
classifiers=['Environment :: Console', 'Topic :: Internet :: WWW/HTTP']
|
||||
)
|
|
@ -41,9 +41,7 @@ function handleFormGetTagged(e) {
|
|||
}
|
||||
|
||||
var tags = input.value.split(/\s*,\s*/);
|
||||
CamliSearch.query = tags[0];
|
||||
CamliSearch.type = "tag";
|
||||
doSearch();
|
||||
document.location.href = "search.html?q=" + tags[0] + "&t=tag"
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
|
@ -144,8 +142,8 @@ function handleCreateNewCollection(e) {
|
|||
}
|
||||
|
||||
function handleAddToCollection(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
addToCollection(false)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue