mirror of https://github.com/perkeep/perkeep.git
226 lines
6.2 KiB
Go
226 lines
6.2 KiB
Go
/*
|
|
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 client
|
|
|
|
import (
|
|
"bytes"
|
|
"camli/blobref"
|
|
"camli/http"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"json"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
type UploadHandle struct {
|
|
BlobRef *blobref.BlobRef
|
|
Size int64
|
|
Contents io.Reader
|
|
}
|
|
|
|
type PutResult struct {
|
|
BlobRef *blobref.BlobRef
|
|
Size int64
|
|
Skipped bool // already present on blobserver
|
|
}
|
|
|
|
// Note: must not touch data after calling this.
|
|
func NewUploadHandleFromString(data string) *UploadHandle {
|
|
s1 := sha1.New()
|
|
s1.Write([]byte(data))
|
|
bref := blobref.FromHash("sha1", s1)
|
|
buf := bytes.NewBufferString(data)
|
|
return &UploadHandle{BlobRef: bref, Size: int64(len(data)), Contents: buf}
|
|
}
|
|
|
|
func encodeBase64(s string) string {
|
|
buf := make([]byte, base64.StdEncoding.EncodedLen(len(s)))
|
|
base64.StdEncoding.Encode(buf, []byte(s))
|
|
return string(buf)
|
|
}
|
|
|
|
func (c *Client) jsonFromResponse(resp *http.Response) (map[string]interface{}, os.Error) {
|
|
if resp.StatusCode != 200 {
|
|
log.Printf("Failed to JSON from response; status code is %d", resp.StatusCode)
|
|
io.Copy(os.Stderr, resp.Body)
|
|
return nil, os.NewError(fmt.Sprintf("HTTP response code is %d; no JSON to parse.", resp.StatusCode))
|
|
}
|
|
// TODO: LimitReader here for paranoia
|
|
buf := new(bytes.Buffer)
|
|
io.Copy(buf, resp.Body)
|
|
resp.Body.Close()
|
|
jmap := make(map[string]interface{})
|
|
if jerr := json.Unmarshal(buf.Bytes(), &jmap); jerr != nil {
|
|
return nil, jerr
|
|
}
|
|
return jmap, nil
|
|
}
|
|
|
|
func (c *Client) Upload(h *UploadHandle) (*PutResult, os.Error) {
|
|
error := func(msg string, e os.Error) (*PutResult, os.Error) {
|
|
err := os.NewError(fmt.Sprintf("Error uploading blob %s: %s; err=%s",
|
|
h.BlobRef, msg, e))
|
|
c.log.Print(err.String())
|
|
return nil, err
|
|
}
|
|
|
|
c.statsMutex.Lock()
|
|
c.stats.UploadRequests.Blobs++
|
|
c.stats.UploadRequests.Bytes += h.Size
|
|
c.statsMutex.Unlock()
|
|
|
|
blobRefString := h.BlobRef.String()
|
|
|
|
// Pre-upload. Check whether the blob already exists on the
|
|
// server and if not, the URL to upload it to.
|
|
url := fmt.Sprintf("%s/camli/preupload", c.server)
|
|
requestBody := "camliversion=1&blob1="+blobRefString
|
|
req := http.NewPostRequest(
|
|
url,
|
|
"application/x-www-form-urlencoded",
|
|
strings.NewReader(requestBody))
|
|
req.Header["Authorization"] = c.authHeader()
|
|
req.ContentLength = int64(len(requestBody))
|
|
req.TransferEncoding = nil
|
|
|
|
resp, err := req.Send()
|
|
if err != nil {
|
|
return error("preupload http error", err)
|
|
}
|
|
|
|
pur, err := c.jsonFromResponse(resp)
|
|
if err != nil {
|
|
return error("preupload json parse error", err)
|
|
}
|
|
|
|
uploadUrl, ok := pur["uploadUrl"].(string)
|
|
if uploadUrl == "" {
|
|
return error("preupload json validity error: no 'uploadUrl'", nil)
|
|
}
|
|
|
|
alreadyHave, ok := pur["alreadyHave"].([]interface{})
|
|
if !ok {
|
|
return error("preupload json validity error: no 'alreadyHave'", nil)
|
|
}
|
|
|
|
pr := &PutResult{BlobRef: h.BlobRef, Size: h.Size}
|
|
|
|
for _, haveObj := range alreadyHave {
|
|
haveObj := haveObj.(map[string]interface{})
|
|
if haveObj["blobRef"].(string) == h.BlobRef.String() {
|
|
pr.Skipped = true
|
|
return pr, nil
|
|
}
|
|
}
|
|
|
|
// TODO: use a proper random boundary
|
|
boundary := "sdf8sd8f7s9df9s7df9sd7sdf9s879vs7d8v7sd8v7sd8v"
|
|
|
|
multiPartHeader := fmt.Sprintf(
|
|
"--%s\r\nContent-Type: application/octet-stream\r\n" +
|
|
"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n\r\n",
|
|
boundary,
|
|
h.BlobRef, h.BlobRef)
|
|
multiPartFooter := "\r\n--"+boundary+"--\r\n"
|
|
|
|
c.log.Printf("Uploading to URL: %s", uploadUrl)
|
|
req = http.NewPostRequest(uploadUrl,
|
|
"multipart/form-data; boundary="+boundary,
|
|
io.MultiReader(
|
|
strings.NewReader(multiPartHeader),
|
|
h.Contents,
|
|
strings.NewReader(multiPartFooter)))
|
|
req.Header["Authorization"] = c.authHeader()
|
|
req.ContentLength = int64(len(multiPartHeader)) + h.Size + int64(len(multiPartFooter))
|
|
req.TransferEncoding = nil
|
|
resp, err = req.Send()
|
|
if err != nil {
|
|
return error("upload http error", err)
|
|
}
|
|
|
|
// The only valid HTTP responses are 200 and 303.
|
|
if resp.StatusCode != 200 && resp.StatusCode != 303 {
|
|
return error(fmt.Sprintf("invalid http response %d in upload response", resp.StatusCode), nil)
|
|
}
|
|
|
|
if resp.StatusCode == 303 {
|
|
otherLocation, ok := resp.Header["Location"]
|
|
if !ok {
|
|
return error("303 without a Location", nil)
|
|
}
|
|
relUrl, err := http.ParseURL(otherLocation)
|
|
if err != nil {
|
|
return error("303 Location URL parse error", err)
|
|
}
|
|
baseUrl, _ := http.ParseURL(uploadUrl)
|
|
absUrl := baseUrl.Add(relUrl)
|
|
otherLocation = absUrl.String()
|
|
resp, _, err = http.Get(otherLocation)
|
|
if err != nil {
|
|
return error("error following 303 redirect after upload", err)
|
|
}
|
|
}
|
|
|
|
ures, err := c.jsonFromResponse(resp)
|
|
if err != nil {
|
|
return error("json parse from upload error", err)
|
|
}
|
|
|
|
errorText, ok := ures["errorText"].(string)
|
|
if ok {
|
|
c.log.Printf("Blob server reports error: %s", errorText)
|
|
}
|
|
|
|
received, ok := ures["received"].([]interface{})
|
|
if !ok {
|
|
return error("upload json validity error: no 'received'", nil)
|
|
}
|
|
|
|
for _, rit := range received {
|
|
it, ok := rit.(map[string]interface{})
|
|
if !ok {
|
|
return error("upload json validity error: 'received' is malformed", nil)
|
|
}
|
|
if it["blobRef"] == blobRefString {
|
|
switch size := it["size"].(type) {
|
|
case nil:
|
|
return error("upload json validity error: 'received' is missing 'size'", nil)
|
|
case float64:
|
|
if int64(size) == h.Size {
|
|
// Success!
|
|
c.statsMutex.Lock()
|
|
c.stats.Uploads.Blobs++
|
|
c.stats.Uploads.Bytes += h.Size
|
|
c.statsMutex.Unlock()
|
|
return pr, nil
|
|
} else {
|
|
return error(fmt.Sprintf("Server got blob, but reports wrong length (%v; expected %d)",
|
|
size, h.Size), nil)
|
|
}
|
|
default:
|
|
return error("unsupported type of 'size' in received response", nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, os.NewError("Server didn't receive blob.")
|
|
}
|