mirror of https://github.com/perkeep/perkeep.git
427 lines
10 KiB
Go
427 lines
10 KiB
Go
/*
|
|
Copyright 2013 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 importer imports content from third-party websites.
|
|
//
|
|
// TODO(bradfitz): Finish this. Barely started.
|
|
package importer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"camlistore.org/pkg/blob"
|
|
"camlistore.org/pkg/blobserver"
|
|
"camlistore.org/pkg/httputil"
|
|
"camlistore.org/pkg/jsonconfig"
|
|
"camlistore.org/pkg/jsonsign/signhandler"
|
|
"camlistore.org/pkg/schema"
|
|
"camlistore.org/pkg/search"
|
|
"camlistore.org/pkg/server"
|
|
"camlistore.org/pkg/syncutil"
|
|
)
|
|
|
|
// A Host is the environment hosting an importer.
|
|
type Host struct {
|
|
BaseURL string
|
|
|
|
imp Importer
|
|
target blobserver.StatReceiver
|
|
search *search.Handler
|
|
signer *schema.Signer
|
|
|
|
// client optionally specifies how to fetch external network
|
|
// resources. If nil, http.DefaultClient is used.
|
|
client *http.Client
|
|
transport http.RoundTripper
|
|
|
|
mu sync.Mutex
|
|
running bool
|
|
stopreq chan struct{} // closed to signal importer to stop and return an error
|
|
lastProgress *ProgressMessage
|
|
lastRunErr error
|
|
}
|
|
|
|
func (h *Host) String() string {
|
|
return fmt.Sprintf("%T(%s)", h, h.imp)
|
|
}
|
|
|
|
func (h *Host) Target() blobserver.StatReceiver {
|
|
return h.target
|
|
}
|
|
|
|
func (h *Host) Search() *search.Handler {
|
|
return h.search
|
|
}
|
|
|
|
func (h *Host) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if httputil.PathSuffix(r) == "" {
|
|
switch r.FormValue("mode") {
|
|
case "":
|
|
case "start":
|
|
h.start()
|
|
case "stop":
|
|
h.stop()
|
|
default:
|
|
fmt.Fprintf(w, "Unknown mode")
|
|
}
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
fmt.Fprintf(w, "I am an importer of type %T; running=%v; last progress=%#v",
|
|
h.imp, h.running, h.lastProgress)
|
|
} else {
|
|
// TODO(aa): Remove this temporary hack once the UI has a way to configure importers.
|
|
h.imp.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
func (h *Host) start() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if h.running {
|
|
return
|
|
}
|
|
h.running = true
|
|
stopCh := make(chan struct{})
|
|
h.stopreq = stopCh
|
|
go func() {
|
|
log.Printf("Starting importer %s", h)
|
|
err := h.imp.Run(stopCh)
|
|
if err != nil {
|
|
log.Printf("Importer %s error: %v", h, err)
|
|
} else {
|
|
log.Printf("Importer %s finished.", h)
|
|
}
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.running = false
|
|
h.lastRunErr = err
|
|
}()
|
|
}
|
|
|
|
func (h *Host) stop() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if !h.running {
|
|
return
|
|
}
|
|
h.running = false
|
|
close(h.stopreq)
|
|
}
|
|
|
|
// HTTPClient returns the HTTP client to use.
|
|
func (h *Host) HTTPClient() *http.Client {
|
|
if h.client == nil {
|
|
return http.DefaultClient
|
|
}
|
|
return h.client
|
|
}
|
|
|
|
// HTTPTransport returns the HTTP transport to use.
|
|
func (h *Host) HTTPTransport() http.RoundTripper {
|
|
if h.transport == nil {
|
|
return http.DefaultTransport
|
|
}
|
|
return h.transport
|
|
}
|
|
|
|
type ProgressMessage struct {
|
|
ItemsDone, ItemsTotal int
|
|
BytesDone, BytesTotal int64
|
|
}
|
|
|
|
func (h *Host) upload(bb *schema.Builder) (br blob.Ref, err error) {
|
|
signed, err := bb.Sign(h.signer)
|
|
if err != nil {
|
|
return
|
|
}
|
|
sb, err := blobserver.ReceiveString(h.target, signed)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return sb.Ref, nil
|
|
}
|
|
|
|
// NewObject creates a new permanode and returns its Object wrapper.
|
|
func (h *Host) NewObject() (*Object, error) {
|
|
pn, err := h.upload(schema.NewUnsignedPermanode())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// No need to do a describe query against it: we know it's
|
|
// empty (has no claims against it yet).
|
|
return &Object{h: h, pn: pn}, nil
|
|
}
|
|
|
|
// An Object is wrapper around a permanode that the importer uses
|
|
// to synchronize.
|
|
type Object struct {
|
|
h *Host
|
|
pn blob.Ref // permanode ref
|
|
|
|
mu sync.RWMutex
|
|
attr map[string][]string
|
|
}
|
|
|
|
// PermanodeRef returns the permanode that this object wraps.
|
|
func (o *Object) PermanodeRef() blob.Ref {
|
|
return o.pn
|
|
}
|
|
|
|
// Attr returns the object's attribute value for the provided attr,
|
|
// or the empty string if unset. To distinguish between unset,
|
|
// an empty string, or multiple attribute values, use Attrs.
|
|
func (o *Object) Attr(attr string) string {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
if v := o.attr[attr]; len(v) > 0 {
|
|
return v[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Attrs returns the attribute values for the provided attr.
|
|
func (o *Object) Attrs(attr string) []string {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.attr[attr]
|
|
}
|
|
|
|
// SetAttr sets the attribute key to value.
|
|
func (o *Object) SetAttr(key, value string) error {
|
|
if o.Attr(key) == value {
|
|
return nil
|
|
}
|
|
_, err := o.h.upload(schema.NewSetAttributeClaim(o.pn, key, value))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
if o.attr == nil {
|
|
o.attr = make(map[string][]string)
|
|
}
|
|
o.attr[key] = []string{value}
|
|
return nil
|
|
}
|
|
|
|
// SetAttrs sets multiple attributes. The provided keyval should be an even number of alternating key/value pairs to set.
|
|
func (o *Object) SetAttrs(keyval ...string) error {
|
|
if len(keyval)%2 == 1 {
|
|
panic("importer.SetAttrs: odd argument count")
|
|
}
|
|
|
|
g := syncutil.Group{}
|
|
for i := 0; i < len(keyval); i += 2 {
|
|
key, val := keyval[i], keyval[i+1]
|
|
if val != o.Attr(key) {
|
|
g.Go(func() error {
|
|
return o.SetAttr(key, val)
|
|
})
|
|
}
|
|
}
|
|
return g.Err()
|
|
}
|
|
|
|
// ChildPathObject returns (creating if necessary) the child object
|
|
// from the permanode o, given by the "camliPath:xxxx" attribute,
|
|
// where xxx is the provided path.
|
|
func (o *Object) ChildPathObject(path string) (*Object, error) {
|
|
attrName := "camliPath:" + path
|
|
if v := o.Attr(attrName); v != "" {
|
|
br, ok := blob.Parse(v)
|
|
if ok {
|
|
return o.h.ObjectFromRef(br)
|
|
}
|
|
}
|
|
|
|
childBlobRef, err := o.h.upload(schema.NewUnsignedPermanode())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := o.SetAttr(attrName, childBlobRef.String()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Object{
|
|
h: o.h,
|
|
pn: childBlobRef,
|
|
}, nil
|
|
}
|
|
|
|
// RootObject returns the root permanode for this importer account.
|
|
func (h *Host) RootObject() (*Object, error) {
|
|
res, err := h.search.GetPermanodesWithAttr(&search.WithAttrRequest{
|
|
N: 2, // only expect 1
|
|
Attr: "camliImportRoot",
|
|
Value: h.imp.Prefix(),
|
|
})
|
|
if err != nil {
|
|
log.Printf("RootObject searching GetPermanodesWithAttr: %v", err)
|
|
return nil, err
|
|
}
|
|
if len(res.WithAttr) == 0 {
|
|
obj, err := h.NewObject()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Printf("No root object found. Created %v", obj.pn)
|
|
if err := obj.SetAttr("camliImportRoot", h.imp.Prefix()); err != nil {
|
|
return nil, err
|
|
}
|
|
return obj, nil
|
|
}
|
|
if len(res.WithAttr) > 1 {
|
|
return nil, fmt.Errorf("Found %d import roots for %q; want 1", len(res.WithAttr), h.imp.Prefix())
|
|
}
|
|
pn := res.WithAttr[0].Permanode
|
|
return h.ObjectFromRef(pn)
|
|
}
|
|
|
|
// ObjectFromRef returns the object given by the named permanode
|
|
func (h *Host) ObjectFromRef(permanodeRef blob.Ref) (*Object, error) {
|
|
res, err := h.search.Describe(&search.DescribeRequest{
|
|
BlobRef: permanodeRef,
|
|
Depth: 1,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
db, ok := res.Meta[permanodeRef.String()]
|
|
if !ok {
|
|
return nil, fmt.Errorf("permanode %v wasn't in Describe response", permanodeRef)
|
|
}
|
|
if db.Permanode == nil {
|
|
return nil, fmt.Errorf("permanode %v had no DescribedPermanode in Describe response", permanodeRef)
|
|
}
|
|
return &Object{
|
|
h: h,
|
|
pn: permanodeRef,
|
|
attr: map[string][]string(db.Permanode.Attr),
|
|
}, nil
|
|
}
|
|
|
|
// ErrInterrupted should be returned by importers
|
|
// when an Interrupt fires.
|
|
var ErrInterrupted = errors.New("import interrupted by request")
|
|
|
|
// An Interrupt is passed to importers for them to monitor
|
|
// requests to stop importing. The channel is closed as
|
|
// a signal to stop.
|
|
type Interrupt <-chan struct{}
|
|
|
|
// ShouldStop returns whether the interrupt has fired.
|
|
// If so, importers should return ErrInterrupted.
|
|
func (i Interrupt) ShouldStop() bool {
|
|
select {
|
|
case <-i:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// An Importer imports from a third-party site.
|
|
type Importer interface {
|
|
// Run runs a full or increment import.
|
|
Run(Interrupt) error
|
|
|
|
// Prefix returns the unique prefix for this importer.
|
|
// It should be of the form "serviceType:username".
|
|
// Further colons are added to form the names of planned
|
|
// permanodes.
|
|
Prefix() string
|
|
|
|
// CanHandleURL returns whether a URL (such as one a user is
|
|
// viewing in their browser and dragged onto Camlistore) is a
|
|
// form recognized by this importer. If so, its full metadata
|
|
// and full data (e.g. unscaled image) can be fetched, rather
|
|
// than just fetching the HTML of the URL.
|
|
//
|
|
// TODO: implement and use this. For now importers can return
|
|
// stub these and return false/errors. They're unused.
|
|
CanHandleURL(url string) bool
|
|
ImportURL(url string) error
|
|
|
|
ServeHTTP(w http.ResponseWriter, r *http.Request)
|
|
}
|
|
|
|
// Constructor is the function type that importers must register at init time.
|
|
type Constructor func(jsonconfig.Obj, *Host) (Importer, error)
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
ctors = make(map[string]Constructor)
|
|
)
|
|
|
|
func Register(name string, fn Constructor) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if _, dup := ctors[name]; dup {
|
|
panic("Dup registration of importer " + name)
|
|
}
|
|
ctors[name] = fn
|
|
}
|
|
|
|
func Create(name string, hl blobserver.Loader, baseURL string, cfg jsonconfig.Obj) (*Host, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
fn := ctors[name]
|
|
if fn == nil {
|
|
return nil, fmt.Errorf("Unknown importer type %q", name)
|
|
}
|
|
h := &Host{
|
|
BaseURL: baseURL,
|
|
}
|
|
imp, err := fn(cfg, h)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
h.imp = imp
|
|
return h, nil
|
|
}
|
|
|
|
func (h *Host) InitHandler(hl blobserver.FindHandlerByTyper) error {
|
|
_, handler, err := hl.FindHandlerByType("root")
|
|
if err != nil || handler == nil {
|
|
return errors.New("importer requires a 'root' handler")
|
|
}
|
|
rh := handler.(*server.RootHandler)
|
|
searchHandler, ok := rh.SearchHandler()
|
|
if !ok {
|
|
return errors.New("importer requires a 'root' handler with 'searchRoot' defined.")
|
|
}
|
|
h.search = searchHandler
|
|
if rh.Storage == nil {
|
|
return errors.New("importer requires a 'root' handler with 'blobRoot' defined.")
|
|
}
|
|
h.target = rh.Storage
|
|
|
|
_, handler, _ = hl.FindHandlerByType("jsonsign")
|
|
if sigh, ok := handler.(*signhandler.Handler); ok {
|
|
h.signer = sigh.Signer()
|
|
}
|
|
if h.signer == nil {
|
|
return errors.New("importer requires a 'jsonsign' handler")
|
|
}
|
|
|
|
return nil
|
|
}
|