diff --git a/build.pl b/build.pl index e02b9da41..974f74585 100755 --- a/build.pl +++ b/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 diff --git a/clients/go/camgsinit/camgsinit.go b/clients/go/camgsinit/camgsinit.go index 0c895bc23..67fbabf2c 100644 --- a/clients/go/camgsinit/camgsinit.go +++ b/clients/go/camgsinit/camgsinit.go @@ -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 { diff --git a/lib/go/camli/blobserver/googlestorage/auth.go b/lib/go/camli/blobserver/google/auth.go similarity index 98% rename from lib/go/camli/blobserver/googlestorage/auth.go rename to lib/go/camli/blobserver/google/auth.go index 39a05511f..673a43240 100644 --- a/lib/go/camli/blobserver/googlestorage/auth.go +++ b/lib/go/camli/blobserver/google/auth.go @@ -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" diff --git a/lib/go/camli/blobserver/google/storage.go b/lib/go/camli/blobserver/google/storage.go new file mode 100644 index 000000000..1e268b3ba --- /dev/null +++ b/lib/go/camli/blobserver/google/storage.go @@ -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 +} diff --git a/lib/go/camli/googlestorage/README b/lib/go/camli/googlestorage/README new file mode 100644 index 000000000..496d130f8 --- /dev/null +++ b/lib/go/camli/googlestorage/README @@ -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. + diff --git a/lib/go/camli/googlestorage/googlestorage.go b/lib/go/camli/googlestorage/googlestorage.go new file mode 100644 index 000000000..7b98ab107 --- /dev/null +++ b/lib/go/camli/googlestorage/googlestorage.go @@ -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 +} diff --git a/lib/go/camli/googlestorage/googlestorage_test.go b/lib/go/camli/googlestorage/googlestorage_test.go new file mode 100644 index 000000000..84a2a1f2c --- /dev/null +++ b/lib/go/camli/googlestorage/googlestorage_test.go @@ -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) + } + } + } +} diff --git a/lib/go/camli/googlestorage/testconfig.json b/lib/go/camli/googlestorage/testconfig.json new file mode 100644 index 000000000..ec0ace405 --- /dev/null +++ b/lib/go/camli/googlestorage/testconfig.json @@ -0,0 +1 @@ +{"gsconf": ["_fileobj", "gstestconfig.json"]} diff --git a/lib/go/camli/googlestorage/testdata/test-enum b/lib/go/camli/googlestorage/testdata/test-enum new file mode 100644 index 000000000..f2227372e --- /dev/null +++ b/lib/go/camli/googlestorage/testdata/test-enum @@ -0,0 +1 @@ +Google Storage Test diff --git a/lib/go/camli/googlestorage/testdata/test-enum-1 b/lib/go/camli/googlestorage/testdata/test-enum-1 new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/lib/go/camli/googlestorage/testdata/test-enum-1 @@ -0,0 +1 @@ +1 diff --git a/lib/go/camli/googlestorage/testdata/test-enum-2 b/lib/go/camli/googlestorage/testdata/test-enum-2 new file mode 100644 index 000000000..0cfbf0888 --- /dev/null +++ b/lib/go/camli/googlestorage/testdata/test-enum-2 @@ -0,0 +1 @@ +2 diff --git a/lib/go/camli/googlestorage/testdata/test-enum-3 b/lib/go/camli/googlestorage/testdata/test-enum-3 new file mode 100644 index 000000000..00750edc0 --- /dev/null +++ b/lib/go/camli/googlestorage/testdata/test-enum-3 @@ -0,0 +1 @@ +3 diff --git a/lib/go/camli/googlestorage/testdata/test-enum-4 b/lib/go/camli/googlestorage/testdata/test-enum-4 new file mode 100644 index 000000000..b8626c4cf --- /dev/null +++ b/lib/go/camli/googlestorage/testdata/test-enum-4 @@ -0,0 +1 @@ +4 diff --git a/lib/go/camli/googlestorage/testdata/test-get b/lib/go/camli/googlestorage/testdata/test-get new file mode 100644 index 000000000..f2227372e --- /dev/null +++ b/lib/go/camli/googlestorage/testdata/test-get @@ -0,0 +1 @@ +Google Storage Test diff --git a/lib/go/camli/googlestorage/testdata/test-stat b/lib/go/camli/googlestorage/testdata/test-stat new file mode 100644 index 000000000..f2227372e --- /dev/null +++ b/lib/go/camli/googlestorage/testdata/test-stat @@ -0,0 +1 @@ +Google Storage Test diff --git a/lib/go/camli/third_party/code.google.com/goauth2/oauth/oauth.go b/lib/go/camli/third_party/code.google.com/goauth2/oauth/oauth.go index 5e9bb6dc6..5990008b7 100644 --- a/lib/go/camli/third_party/code.google.com/goauth2/oauth/oauth.go +++ b/lib/go/camli/third_party/code.google.com/goauth2/oauth/oauth.go @@ -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}, diff --git a/lib/python/camli/op.py b/lib/python/camli/op.py index 6c7854252..7a5884a0d 100755 --- a/lib/python/camli/op.py +++ b/lib/python/camli/op.py @@ -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', diff --git a/lib/python/setup.py b/lib/python/setup.py new file mode 100644 index 000000000..cefac2d88 --- /dev/null +++ b/lib/python/setup.py @@ -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'] +) \ No newline at end of file diff --git a/server/go/camlistored/ui/search.js b/server/go/camlistored/ui/search.js index 256af3a4b..3e04a4b8b 100644 --- a/server/go/camlistored/ui/search.js +++ b/server/go/camlistored/ui/search.js @@ -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) }