Merge 750c53d37d into 3b997c3e16
551
bzz/api.go
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/registrar"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
)
|
||||
|
||||
var (
|
||||
hashMatcher = regexp.MustCompile("^[0-9A-Fa-f]{64}")
|
||||
slashes = regexp.MustCompile("/+")
|
||||
domainAndVersion = regexp.MustCompile("[@:;,]+")
|
||||
)
|
||||
|
||||
/*
|
||||
Api implements webserver/file system related content storage and retrieval
|
||||
on top of the dpa
|
||||
it is the public interface of the dpa which is included in the ethereum stack
|
||||
*/
|
||||
type Api struct {
|
||||
Chunker *TreeChunker
|
||||
Port string
|
||||
Registrar registrar.VersionedRegistrar
|
||||
datadir string
|
||||
|
||||
dpa *DPA
|
||||
hive *hive
|
||||
netStore *netStore
|
||||
}
|
||||
|
||||
/*
|
||||
the api constructor initialises
|
||||
- the netstore endpoint for chunk store logic
|
||||
- the chunker (bzz hash)
|
||||
- the dpa - single document retrieval api
|
||||
*/
|
||||
func NewApi(datadir, port string) (self *Api, err error) {
|
||||
|
||||
self = &Api{
|
||||
Chunker: &TreeChunker{},
|
||||
Port: port,
|
||||
datadir: datadir,
|
||||
}
|
||||
|
||||
self.hive, err = newHive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.netStore, err = newNetStore(filepath.Join(datadir, "bzz"), self.hive)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.dpa = &DPA{
|
||||
Chunker: self.Chunker,
|
||||
ChunkStore: self.netStore,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Local swarm without netStore
|
||||
func NewLocalApi(datadir string) (self *Api, err error) {
|
||||
|
||||
self = &Api{
|
||||
Chunker: &TreeChunker{},
|
||||
}
|
||||
dbStore, err := newDbStore(datadir)
|
||||
dbStore.setCapacity(50000)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
memStore := newMemStore(dbStore)
|
||||
localStore := &localStore{
|
||||
memStore,
|
||||
dbStore,
|
||||
}
|
||||
|
||||
self.dpa = &DPA{
|
||||
Chunker: self.Chunker,
|
||||
ChunkStore: localStore,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Bzz returns the bzz protocol class instances of which run on every peer
|
||||
func (self *Api) Bzz() (p2p.Protocol, error) {
|
||||
return BzzProtocol(self.netStore)
|
||||
}
|
||||
|
||||
/*
|
||||
Start is called when the ethereum stack is started
|
||||
- calls Init() on treechunker
|
||||
- launches the dpa (listening for chunk store/retrieve requests)
|
||||
- launches the netStore (starts kademlia hive peer management)
|
||||
- starts an http server
|
||||
*/
|
||||
func (self *Api) Start(node *discover.Node, listenAddr string, connectPeer func(string) error) {
|
||||
var err error
|
||||
if node == nil {
|
||||
err = fmt.Errorf("basenode nil")
|
||||
} else if self.netStore == nil {
|
||||
err = fmt.Errorf("netStore is nil")
|
||||
} else if connectPeer == nil {
|
||||
err = fmt.Errorf("no connect peer function")
|
||||
} else if bytes.Equal(node.ID[:], zeroKey) {
|
||||
err = fmt.Errorf("zero ID invalid")
|
||||
} else { // this is how we calculate the bzz address of the node
|
||||
// ideally this should be using the swarm hash function
|
||||
|
||||
var port string
|
||||
_, port, err = net.SplitHostPort(listenAddr)
|
||||
if err == nil {
|
||||
intport, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid port in '%s'", listenAddr)
|
||||
} else {
|
||||
baseAddr := &peerAddr{
|
||||
ID: node.ID[:],
|
||||
IP: node.IP,
|
||||
Port: uint16(intport),
|
||||
}
|
||||
baseAddr.new()
|
||||
err = self.hive.start(baseAddr, filepath.Join(self.datadir, "bzz-peers.json"), connectPeer)
|
||||
if err == nil {
|
||||
err = self.netStore.start(baseAddr)
|
||||
if err == nil {
|
||||
glog.V(logger.Info).Infof("[BZZ] Swarm network started on bzz address: %064x", baseAddr.hash[:])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
glog.V(logger.Info).Infof("[BZZ] Swarm started started offline: %v", err)
|
||||
}
|
||||
self.Chunker.Init()
|
||||
self.dpa.Start()
|
||||
|
||||
if self.Port != "" {
|
||||
go startHttpServer(self, self.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Api) Stop() {
|
||||
self.dpa.Stop()
|
||||
self.netStore.stop()
|
||||
self.hive.stop()
|
||||
}
|
||||
|
||||
// Get uses iterative manifest retrieval and prefix matching
|
||||
// to resolve path to content using dpa retrieve
|
||||
func (self *Api) Get(bzzpath string) (content []byte, mimeType string, status int, size int, err error) {
|
||||
var reader SectionReader
|
||||
reader, mimeType, status, err = self.getPath("/" + bzzpath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
content = make([]byte, reader.Size())
|
||||
size, err = reader.Read(content)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Put provides singleton manifest creation and optional name registration
|
||||
// on top of dpa store
|
||||
func (self *Api) Put(content, contentType string) (string, error) {
|
||||
sr := io.NewSectionReader(strings.NewReader(content), 0, int64(len(content)))
|
||||
wg := &sync.WaitGroup{}
|
||||
key, err := self.dpa.Store(sr, wg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
manifest := fmt.Sprintf(`{"entries":[{"hash":"%064x","contentType":"%s"}]}`, key, contentType)
|
||||
sr = io.NewSectionReader(strings.NewReader(manifest), 0, int64(len(manifest)))
|
||||
key, err = self.dpa.Store(sr, wg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
wg.Wait()
|
||||
return fmt.Sprintf("%064x", key), nil
|
||||
}
|
||||
|
||||
func (self *Api) Modify(rootHash, path, contentHash, contentType string) (newRootHash string, err error) {
|
||||
root := common.Hex2Bytes(rootHash)
|
||||
trie, err := loadManifest(self.dpa, root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if contentHash != "" {
|
||||
entry := &manifestTrieEntry{
|
||||
Path: path,
|
||||
Hash: contentHash,
|
||||
ContentType: contentType,
|
||||
}
|
||||
trie.addEntry(entry)
|
||||
} else {
|
||||
trie.deleteEntry(path)
|
||||
}
|
||||
|
||||
err = trie.recalcAndStore()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return fmt.Sprintf("%064x", trie.hash), nil
|
||||
}
|
||||
|
||||
const maxParallelFiles = 5
|
||||
|
||||
// Download replicates the manifest path structure on the local filesystem
|
||||
// under localpath
|
||||
func (self *Api) Download(bzzpath, localpath string) (err error) {
|
||||
lpath, err := filepath.Abs(filepath.Clean(localpath))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = os.MkdirAll(lpath, os.ModePerm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
parts := slashes.Split(bzzpath, 3)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("Invalid bzz path")
|
||||
}
|
||||
hostPort := parts[1]
|
||||
var path string
|
||||
if len(parts) > 2 {
|
||||
path = regularSlashes(parts[2]) + "/"
|
||||
}
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: host: '%s', path '%s' requested.", hostPort, path)
|
||||
|
||||
//resolving host and port
|
||||
var key Key
|
||||
key, err = self.Resolve(hostPort)
|
||||
if err != nil {
|
||||
err = errResolve(err)
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: error : %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
trie, err := loadManifest(self.dpa, key)
|
||||
if err != nil {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: loadManifestTrie error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
type downloadListEntry struct {
|
||||
key Key
|
||||
path string
|
||||
}
|
||||
|
||||
var list []*downloadListEntry
|
||||
var mde, mderr error
|
||||
|
||||
prevPath := lpath
|
||||
err = trie.listWithPrefix(path, func(entry *manifestTrieEntry, suffix string) { // TODO: paralellize
|
||||
key := common.Hex2Bytes(entry.Hash)
|
||||
path := lpath + "/" + suffix
|
||||
dir := filepath.Dir(path)
|
||||
if dir != prevPath {
|
||||
mde = os.MkdirAll(dir, os.ModePerm)
|
||||
if mde != nil {
|
||||
mderr = mde
|
||||
}
|
||||
prevPath = dir
|
||||
}
|
||||
if (mde == nil) && (path != dir+"/") {
|
||||
list = append(list, &downloadListEntry{key: key, path: path})
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
err = mderr
|
||||
}
|
||||
|
||||
cnt := len(list)
|
||||
errors := make([]error, cnt)
|
||||
done := make(chan bool, maxParallelFiles)
|
||||
dcnt := 0
|
||||
|
||||
for i, entry := range list {
|
||||
if i >= dcnt+maxParallelFiles {
|
||||
<-done
|
||||
dcnt++
|
||||
}
|
||||
go func(i int, entry *downloadListEntry, done chan bool) {
|
||||
f, err := os.Create(entry.path) // TODO: path separators
|
||||
if err == nil {
|
||||
reader := self.dpa.Retrieve(entry.key)
|
||||
writer := bufio.NewWriter(f)
|
||||
_, err = io.CopyN(writer, reader, reader.Size()) // TODO: handle errors
|
||||
err2 := writer.Flush()
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
err2 = f.Close()
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
}
|
||||
|
||||
errors[i] = err
|
||||
done <- true
|
||||
}(i, entry, done)
|
||||
}
|
||||
for dcnt < cnt {
|
||||
<-done
|
||||
dcnt++
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i, _ := range list {
|
||||
if errors[i] != nil {
|
||||
return errors[i]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Upload replicates a local directory as a manifest file and uploads it
|
||||
// using dpa store
|
||||
// TODO: localpath should point to a manifest
|
||||
func (self *Api) Upload(lpath, index string) (string, error) {
|
||||
var list []*manifestTrieEntry
|
||||
localpath, err := filepath.Abs(filepath.Clean(lpath))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
f, err := os.Open(localpath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var start int
|
||||
if stat.IsDir() {
|
||||
start = len(localpath)
|
||||
glog.V(logger.Debug).Infof("[BZZ] uploading '%s'", localpath)
|
||||
err = filepath.Walk(localpath, func(path string, info os.FileInfo, err error) error {
|
||||
if (err == nil) && !info.IsDir() {
|
||||
//fmt.Printf("lp %s path %s\n", localpath, path)
|
||||
if len(path) <= start {
|
||||
return fmt.Errorf("Path is too short")
|
||||
}
|
||||
if path[:start] != localpath {
|
||||
return fmt.Errorf("Path prefix of '%s' does not match localpath '%s'", path, localpath)
|
||||
}
|
||||
entry := &manifestTrieEntry{
|
||||
Path: path,
|
||||
}
|
||||
list = append(list, entry)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
dir := filepath.Dir(localpath)
|
||||
start = len(dir)
|
||||
if len(localpath) <= start {
|
||||
return "", fmt.Errorf("Path is too short")
|
||||
}
|
||||
if localpath[:start] != dir {
|
||||
return "", fmt.Errorf("Path prefix of '%s' does not match dir '%s'", localpath, dir)
|
||||
}
|
||||
entry := &manifestTrieEntry{
|
||||
Path: localpath,
|
||||
}
|
||||
list = append(list, entry)
|
||||
}
|
||||
|
||||
cnt := len(list)
|
||||
errors := make([]error, cnt)
|
||||
done := make(chan bool, maxParallelFiles)
|
||||
dcnt := 0
|
||||
|
||||
for i, entry := range list {
|
||||
if i >= dcnt+maxParallelFiles {
|
||||
<-done
|
||||
dcnt++
|
||||
}
|
||||
go func(i int, entry *manifestTrieEntry, done chan bool) {
|
||||
f, err := os.Open(entry.Path)
|
||||
if err == nil {
|
||||
stat, _ := f.Stat()
|
||||
sr := io.NewSectionReader(f, 0, stat.Size())
|
||||
wg := &sync.WaitGroup{}
|
||||
var hash Key
|
||||
hash, err = self.dpa.Store(sr, wg)
|
||||
if hash != nil {
|
||||
list[i].Hash = fmt.Sprintf("%064x", hash)
|
||||
}
|
||||
wg.Wait()
|
||||
if err == nil {
|
||||
first512 := make([]byte, 512)
|
||||
fread, _ := sr.ReadAt(first512, 0)
|
||||
if fread > 0 {
|
||||
mimeType := http.DetectContentType(first512[:fread])
|
||||
if filepath.Ext(entry.Path) == ".css" {
|
||||
mimeType = "text/css"
|
||||
}
|
||||
list[i].ContentType = mimeType
|
||||
//fmt.Printf("%v %v %v\n", entry.Path, mimeType, filepath.Ext(entry.Path))
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
errors[i] = err
|
||||
done <- true
|
||||
}(i, entry, done)
|
||||
}
|
||||
for dcnt < cnt {
|
||||
<-done
|
||||
dcnt++
|
||||
}
|
||||
|
||||
trie := &manifestTrie{
|
||||
dpa: self.dpa,
|
||||
}
|
||||
for i, entry := range list {
|
||||
if errors[i] != nil {
|
||||
return "", errors[i]
|
||||
}
|
||||
entry.Path = regularSlashes(entry.Path[start:])
|
||||
if entry.Path == index {
|
||||
ientry := &manifestTrieEntry{
|
||||
Path: "",
|
||||
Hash: entry.Hash,
|
||||
ContentType: entry.ContentType,
|
||||
}
|
||||
trie.addEntry(ientry)
|
||||
}
|
||||
trie.addEntry(entry)
|
||||
}
|
||||
|
||||
err2 := trie.recalcAndStore()
|
||||
var hs string
|
||||
if err2 == nil {
|
||||
hs = fmt.Sprintf("%064x", trie.hash)
|
||||
}
|
||||
return hs, err2
|
||||
}
|
||||
|
||||
func (self *Api) Register(sender common.Address, domain string, hash common.Hash) (err error) {
|
||||
domainhash := common.BytesToHash(crypto.Sha3([]byte(domain)))
|
||||
|
||||
if self.Registrar != nil {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: host '%s' (hash: '%v') to be registered as '%v'", domain, domainhash.Hex(), hash.Hex())
|
||||
_, err = self.Registrar.Registry().SetHashToHash(sender, domainhash, hash)
|
||||
} else {
|
||||
err = fmt.Errorf("no registry: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type errResolve error
|
||||
|
||||
func (self *Api) Resolve(hostPort string) (contentHash Key, err error) {
|
||||
host := hostPort
|
||||
if hashMatcher.MatchString(host) {
|
||||
contentHash = Key(common.Hex2Bytes(host))
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: host is a contentHash: '%064x'", contentHash)
|
||||
} else {
|
||||
if self.Registrar != nil {
|
||||
var hash common.Hash
|
||||
var version *big.Int
|
||||
parts := domainAndVersion.Split(host, 3)
|
||||
if len(parts) > 1 && parts[1] != "" {
|
||||
host = parts[0]
|
||||
version = common.Big(parts[1])
|
||||
}
|
||||
hostHash := common.BytesToHash(crypto.Sha3([]byte(host)))
|
||||
hash, err = self.Registrar.Resolver(version).HashToHash(hostHash)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to resolve '%s': %v", hostPort, err)
|
||||
}
|
||||
contentHash = Key(hash.Bytes())
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: resolve host '%s' to contentHash: '%064x'", hostPort, contentHash)
|
||||
} else {
|
||||
err = fmt.Errorf("no resolver '%s': %v", hostPort, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Api) getPath(uri string) (reader SectionReader, mimeType string, status int, err error) {
|
||||
parts := slashes.Split(uri, 3)
|
||||
hostPort := parts[1]
|
||||
var path string
|
||||
if len(parts) > 2 {
|
||||
path = parts[2]
|
||||
}
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: host: '%s', path '%s' requested.", hostPort, path)
|
||||
|
||||
//resolving host and port
|
||||
var key Key
|
||||
key, err = self.Resolve(hostPort)
|
||||
if err != nil {
|
||||
err = errResolve(err)
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: error : %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
trie, err := loadManifest(self.dpa, key)
|
||||
if err != nil {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: loadManifestTrie error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: getEntry(%s)", path)
|
||||
entry, _ := trie.getEntry(path)
|
||||
if entry != nil {
|
||||
key = common.Hex2Bytes(entry.Hash)
|
||||
status = entry.Status
|
||||
mimeType = entry.ContentType
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: content lookup key: '%064x' (%v)", key, mimeType)
|
||||
reader = self.dpa.Retrieve(key)
|
||||
} else {
|
||||
err = fmt.Errorf("manifest entry for '%s' not found", path)
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
184
bzz/api_test.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
testDir string
|
||||
datadir = "/tmp/bzz"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_, filename, _, _ := runtime.Caller(1)
|
||||
testDir = path.Join(path.Dir(filename), "bzztest")
|
||||
}
|
||||
|
||||
func testApi() (api *Api, err error) {
|
||||
os.RemoveAll(datadir)
|
||||
api, err = NewLocalApi(datadir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
api.Start(nil, ":0", nil)
|
||||
return
|
||||
}
|
||||
|
||||
func TestApiPut(t *testing.T) {
|
||||
api, err := testApi()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
expContent := "hello"
|
||||
expMimeType := "text/plain"
|
||||
expStatus := 0
|
||||
expSize := len(expContent)
|
||||
bzzhash, err := api.Put(expContent, expMimeType)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
testGet(t, api, bzzhash, []byte(expContent), expMimeType, expStatus, expSize)
|
||||
}
|
||||
|
||||
func testGet(t *testing.T, api *Api, bzzhash string, expContent []byte, expMimeType string, expStatus int, expSize int) {
|
||||
content, mimeType, status, size, err := api.Get(bzzhash)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(content, expContent) {
|
||||
t.Errorf("incorrect content. expected '%s...', got '%s...'", string(expContent), string(content))
|
||||
}
|
||||
if mimeType != expMimeType {
|
||||
t.Errorf("incorrect mimeType. expected '%s', got '%s'", expMimeType, mimeType)
|
||||
}
|
||||
if status != expStatus {
|
||||
t.Errorf("incorrect status. expected '%d', got '%d'", expStatus, status)
|
||||
}
|
||||
if size != expSize {
|
||||
t.Errorf("incorrect size. expected '%d', got '%d'", expSize, size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiDirUpload(t *testing.T) {
|
||||
api, err := testApi()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
bzzhash, err := api.Upload(path.Join(testDir, "test0"), "")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||
testGet(t, api, path.Join(bzzhash, "index.html"), content, "text/html; charset=utf-8", 0, 202)
|
||||
|
||||
content, err = ioutil.ReadFile(path.Join(testDir, "test0", "index.css"))
|
||||
testGet(t, api, path.Join(bzzhash, "index.css"), content, "text/css", 0, 132)
|
||||
|
||||
content, err = ioutil.ReadFile(path.Join(testDir, "test0", "img", "logo.png"))
|
||||
testGet(t, api, path.Join(bzzhash, "img", "logo.png"), content, "image/png", 0, 18136)
|
||||
|
||||
_, _, _, _, err = api.Get(bzzhash)
|
||||
if err == nil {
|
||||
t.Errorf("expected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiDirUploadModify(t *testing.T) {
|
||||
api, err := testApi()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
bzzhash, err := api.Upload(path.Join(testDir, "test0"), "")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
bzzhash, err = api.Modify(bzzhash, "index.html", "", "")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
bzzhash, err = api.Modify(bzzhash, "index2.html", "9ea1f60ebd80786d6005f6b256376bdb494a82496cd86fe8c307cdfb23c99e71", "text/html; charset=utf-8")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
bzzhash, err = api.Modify(bzzhash, "img/logo.png", "9ea1f60ebd80786d6005f6b256376bdb494a82496cd86fe8c307cdfb23c99e71", "text/html; charset=utf-8")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||
testGet(t, api, path.Join(bzzhash, "index2.html"), content, "text/html; charset=utf-8", 0, 202)
|
||||
testGet(t, api, path.Join(bzzhash, "img", "logo.png"), content, "text/html; charset=utf-8", 0, 202)
|
||||
|
||||
content, err = ioutil.ReadFile(path.Join(testDir, "test0", "index.css"))
|
||||
testGet(t, api, path.Join(bzzhash, "index.css"), content, "text/css", 0, 132)
|
||||
|
||||
_, _, _, _, err = api.Get(bzzhash)
|
||||
if err == nil {
|
||||
t.Errorf("expected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiDirUploadWithRootFile(t *testing.T) {
|
||||
api, err := testApi()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
bzzhash, err := api.Upload(path.Join(testDir, "test0"), "index.html")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||
testGet(t, api, bzzhash, content, "text/html; charset=utf-8", 0, 202)
|
||||
}
|
||||
|
||||
func TestApiFileUpload(t *testing.T) {
|
||||
api, err := testApi()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
bzzhash, err := api.Upload(path.Join(testDir, "test0", "index.html"), "")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||
testGet(t, api, path.Join(bzzhash, "index.html"), content, "text/html; charset=utf-8", 0, 202)
|
||||
}
|
||||
|
||||
func TestApiFileUploadWithRootFile(t *testing.T) {
|
||||
api, err := testApi()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
bzzhash, err := api.Upload(path.Join(testDir, "test0", "index.html"), "index.html")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||
testGet(t, api, bzzhash, content, "text/html; charset=utf-8", 0, 202)
|
||||
}
|
||||
191
bzz/bzzcontract/bzzcontract_test.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package bzzcontract
|
||||
|
||||
// import (
|
||||
// "encoding/binary"
|
||||
// "fmt"
|
||||
// "io/ioutil"
|
||||
// "math/big"
|
||||
// "os"
|
||||
// "testing"
|
||||
|
||||
// "github.com/ethereum/go-ethereum/accounts"
|
||||
// "github.com/ethereum/go-ethereum/common"
|
||||
// "github.com/ethereum/go-ethereum/core"
|
||||
// //"github.com/ethereum/go-ethereum/core/state"
|
||||
// "github.com/ethereum/go-ethereum/core/types"
|
||||
// "github.com/ethereum/go-ethereum/crypto"
|
||||
// "github.com/ethereum/go-ethereum/eth"
|
||||
// "github.com/ethereum/go-ethereum/rpc"
|
||||
// xe "github.com/ethereum/go-ethereum/xeth"
|
||||
// )
|
||||
|
||||
// type testFrontend struct {
|
||||
// t *testing.T
|
||||
// ethereum *eth.Ethereum
|
||||
// xeth *xe.XEth
|
||||
// api *rpc.EthereumApi
|
||||
// coinbase string
|
||||
// }
|
||||
|
||||
// func (f *testFrontend) UnlockAccount(acc []byte) bool {
|
||||
// f.t.Logf("Unlocking account %v\n", common.Bytes2Hex(acc))
|
||||
// f.ethereum.AccountManager().Unlock(acc, "password")
|
||||
// return true
|
||||
// }
|
||||
|
||||
// func (f *testFrontend) ConfirmTransaction(tx string) bool {
|
||||
// return true
|
||||
// }
|
||||
|
||||
// var port = 30300
|
||||
|
||||
// func testEth(t *testing.T) (ethereum *eth.Ethereum, err error) {
|
||||
// os.RemoveAll("/tmp/eth/")
|
||||
// err = os.MkdirAll("/tmp/eth/keys/e273f01c99144c438695e10f24926dc1f9fbf62d/", os.ModePerm)
|
||||
// if err != nil {
|
||||
// t.Errorf("%v", err)
|
||||
// return
|
||||
// }
|
||||
// err = os.MkdirAll("/tmp/eth/data", os.ModePerm)
|
||||
// if err != nil {
|
||||
// t.Errorf("%v", err)
|
||||
// return
|
||||
// }
|
||||
// ks := crypto.NewKeyStorePlain("/tmp/eth/keys")
|
||||
// ioutil.WriteFile("/tmp/eth/keys/e273f01c99144c438695e10f24926dc1f9fbf62d/e273f01c99144c438695e10f24926dc1f9fbf62d",
|
||||
// []byte(`{"Id":"RhRXD+fNRKS4jx+7ZfEsNA==","Address":"4nPwHJkUTEOGleEPJJJtwfn79i0=","PrivateKey":"h4ACVpe74uIvi5Cg/2tX/Yrm2xdr3J7QoMbMtNX2CNc="}`), os.ModePerm)
|
||||
|
||||
// port++
|
||||
// ethereum, err = eth.New(ð.Config{
|
||||
// DataDir: "/tmp/eth",
|
||||
// AccountManager: accounts.NewManager(ks),
|
||||
// Port: fmt.Sprintf("%d", port),
|
||||
// MaxPeers: 10,
|
||||
// Name: "test",
|
||||
// })
|
||||
|
||||
// if err != nil {
|
||||
// t.Errorf("%v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
// func testInit(t *testing.T) (self *testFrontend) {
|
||||
|
||||
// ethereum, err := testEth(t)
|
||||
// if err != nil {
|
||||
// t.Errorf("error creating jsre, got %v", err)
|
||||
// return
|
||||
// }
|
||||
// err = ethereum.Start()
|
||||
// if err != nil {
|
||||
// t.Errorf("error starting ethereum: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// self = &testFrontend{t: t, ethereum: ethereum}
|
||||
// self.xeth = xe.New(ethereum, self)
|
||||
// self.api = rpc.NewEthereumApi(self.xeth)
|
||||
|
||||
// addr := self.xeth.Coinbase()
|
||||
// self.coinbase = addr
|
||||
// if addr != "0x"+core.TestAccount {
|
||||
// t.Errorf("CoinBase %v does not match TestAccount 0x%v", addr, core.TestAccount)
|
||||
// }
|
||||
// t.Logf("CoinBase is %v", addr)
|
||||
|
||||
// balance := self.xeth.BalanceAt(core.TestAccount)
|
||||
// t.Logf("Balance is %v", balance)
|
||||
|
||||
// return
|
||||
|
||||
// }
|
||||
|
||||
// func (self *testFrontend) insertTx(addr, contract, fnsig string, args []string) {
|
||||
|
||||
// //cb := common.HexToAddress(self.coinbase)
|
||||
// //coinbase := self.ethereum.ChainManager().State().GetStateObject(cb)
|
||||
|
||||
// hash := common.Bytes2Hex(crypto.Sha3([]byte(fnsig)))
|
||||
// data := "0x" + hash[0:8]
|
||||
// for _, arg := range args {
|
||||
// data = data + common.Bytes2Hex(common.Hex2BytesFixed(arg, 32))
|
||||
// }
|
||||
// self.t.Logf("Tx data: %v", data)
|
||||
|
||||
// jsontx := `
|
||||
// [{
|
||||
// "from": "` + addr + `",
|
||||
// "to": "0x` + contract + `",
|
||||
// "value": "100000000000",
|
||||
// "gas": "100000",
|
||||
// "gasPrice": "100000",
|
||||
// "data": "` + data + `"
|
||||
// }]
|
||||
// `
|
||||
// req := &rpc.RpcRequest{
|
||||
// Jsonrpc: "2.0",
|
||||
// Method: "eth_transact",
|
||||
// Params: []byte(jsontx),
|
||||
// Id: 6,
|
||||
// }
|
||||
|
||||
// var reply interface{}
|
||||
// err0 := self.api.GetRequestReply(req, &reply)
|
||||
// if err0 != nil {
|
||||
// self.t.Errorf("GetRequestReply error: %v", err0)
|
||||
// }
|
||||
|
||||
// //self.xeth.Transact(addr, contract, "100000000000", "100000", "100000", data)
|
||||
// }
|
||||
|
||||
// func (self *testFrontend) applyTxs() {
|
||||
|
||||
// cb := common.HexToAddress(self.coinbase)
|
||||
// stateDb := self.ethereum.ChainManager().State().Copy()
|
||||
// block := self.ethereum.ChainManager().NewBlock(cb)
|
||||
// coinbase := stateDb.GetStateObject(cb)
|
||||
// coinbase.SetGasPool(big.NewInt(1000000))
|
||||
// txs := self.ethereum.TxPool().GetTransactions()
|
||||
|
||||
// for i := 0; i < len(txs); i++ {
|
||||
// for _, tx := range txs {
|
||||
// if tx.Nonce() == uint64(i) {
|
||||
// _, gas, err := core.ApplyMessage(core.NewEnv(stateDb, self.ethereum.ChainManager(), tx, block), tx, coinbase)
|
||||
// //self.ethereum.TxPool().RemoveSet([]*types.Transaction{tx})
|
||||
// self.t.Logf("ApplyMessage: gas %v err %v", gas, err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// self.ethereum.TxPool().RemoveSet(txs)
|
||||
// self.xeth = self.xeth.WithState(stateDb)
|
||||
|
||||
// }
|
||||
|
||||
// func storageAddress(varidx uint32, key []byte) string {
|
||||
// data := make([]byte, 64)
|
||||
// binary.BigEndian.PutUint32(data[60:64], varidx)
|
||||
// copy(data[0:32], key[0:32])
|
||||
// return "0x" + common.Bytes2Hex(crypto.Sha3(data))
|
||||
// }
|
||||
|
||||
// func TestSwarmContract(t *testing.T) {
|
||||
// // t.Skip("skip until fixed")
|
||||
// tf := testInit(t)
|
||||
// defer tf.ethereum.Stop()
|
||||
|
||||
// tf.insertTx(tf.coinbase, core.ContractAddrSwarm, "signup(uint256)", []string{"1000"})
|
||||
// tf.applyTxs()
|
||||
|
||||
// addr := common.Hex2BytesFixed(tf.coinbase[2:], 32)
|
||||
// key := storageAddress(0, addr)
|
||||
// data := tf.xeth.StorageAt("0x"+core.ContractAddrSwarm, key)
|
||||
// key = key[:65] + "6"
|
||||
// data2 := tf.xeth.StorageAt("0x"+core.ContractAddrSwarm, key)
|
||||
|
||||
// t.Logf("addr = %x key = %v data = %v, %v", addr, key, data, data2)
|
||||
|
||||
// }
|
||||
161
bzz/bzzcontract/swarm.sol
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/// @title Swarm Distributed Preimage Archive
|
||||
/// @author Daniel A. Nagy <daniel@ethdev.com>
|
||||
contract Swarm
|
||||
{
|
||||
|
||||
uint constant GRACE = 50; // grace period for lost information in blocks
|
||||
uint constant REWARD_FRACTION = 10; // this fraction of a deposit is paid as reward
|
||||
|
||||
bytes32 constant MAGIC_NUMBER = "Swarm receipt";
|
||||
|
||||
struct Bee {
|
||||
uint deposit; // amount deposited by this member
|
||||
uint expiry; // expiration time of the deposit
|
||||
bytes32 missing; // member accused of losing this swarm chunk
|
||||
uint deadline; // block number before which chunk must be presented
|
||||
address reporter; // receipt reported by this address
|
||||
}
|
||||
|
||||
mapping (address => Bee) swarm;
|
||||
|
||||
// block number of transactions presenting chunks
|
||||
mapping (bytes32 => uint) presentedChunks;
|
||||
|
||||
function max(uint a, uint b) private returns (uint c) {
|
||||
if(a >= b) return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
/// @notice Sign up as a Swarm node for `time` seconds.
|
||||
/// No term extension for nodes with non-clean status.
|
||||
///
|
||||
/// @dev Guards against term overflow and unauthorized extension,
|
||||
/// but all funds are added to deposite irrespective of status.
|
||||
///
|
||||
/// @param time term of Swarm membership in seconds from now.
|
||||
function signup(uint time) {
|
||||
Bee b = swarm[tx.origin];
|
||||
if(isClean(msg.sender) && now + time > now) {
|
||||
b.expiry = max(b.expiry, now + time);
|
||||
}
|
||||
b.deposit += msg.value;
|
||||
}
|
||||
|
||||
/// @notice Withdraw from Swarm, refund deposit.
|
||||
///
|
||||
/// @dev Only allowed with clean status and expired term.
|
||||
function withdraw() {
|
||||
Bee b = swarm[tx.origin];
|
||||
if(now > b.expiry && isClean(msg.sender)) {
|
||||
msg.sender.send(b.deposit);
|
||||
b.deposit = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Total deposit for address `addr`.
|
||||
/// No change in state.
|
||||
///
|
||||
/// @dev Not meaningful for "Guilty" status.
|
||||
///
|
||||
/// @param addr queried address.
|
||||
///
|
||||
/// @return balance of queried address.
|
||||
function balance(address addr) returns (uint d) {
|
||||
Bee b = swarm[addr];
|
||||
return b.deposit;
|
||||
}
|
||||
|
||||
/// @notice Determine clean status of address `addr`.
|
||||
/// Changes the state, but only as a matter of optimization.
|
||||
/// Works as accessor.
|
||||
///
|
||||
/// @dev Defined as no signed receipt has been presented for missing chunk.
|
||||
///
|
||||
/// @param addr queried address.
|
||||
///
|
||||
/// @return true if status is "Clean".
|
||||
function isClean(address addr) returns (bool s) {
|
||||
Bee b = swarm[addr];
|
||||
if(b.missing != 0 && presentedChunks[b.missing] != 0) b.missing = 0;
|
||||
return b.missing == 0; // nothing they signed is missing
|
||||
}
|
||||
|
||||
/// @param suspect address of reported Swarm node
|
||||
event Report(address suspect);
|
||||
|
||||
/// @notice Find out what is missing in case of a Report event.
|
||||
///
|
||||
/// @return 0 if nothing is missing, swarm hash otherwise
|
||||
function whatIsMissing() returns (bytes32 h) {
|
||||
bytes32 missing = swarm[tx.origin].missing;
|
||||
if(presentedChunks[missing] != 0) missing = 0;
|
||||
return missing;
|
||||
}
|
||||
|
||||
/// @notice Report chunk `swarmHash` as missing.
|
||||
///
|
||||
/// @param swarmHash sha3 hash of the missing chunk
|
||||
/// @param expiry expiration time of receipt
|
||||
/// @param sig_v signature parameter v
|
||||
/// @param sig_r signature parameter r
|
||||
/// @param sig_s signature parameter s
|
||||
function reportMissingChunk(bytes32 swarmHash, uint expiry,
|
||||
uint8 sig_v, bytes32 sig_r, bytes32 sig_s) {
|
||||
if(expiry < now) return;
|
||||
bytes32 recptHash = sha3(MAGIC_NUMBER, swarmHash, expiry);
|
||||
address signer = ecrecover(recptHash, sig_v, sig_r, sig_s);
|
||||
if(!isClean(signer) || !expiresAfter(signer, now)) return;
|
||||
Bee b = swarm[signer];
|
||||
b.missing = swarmHash;
|
||||
b.deadline = block.number + GRACE;
|
||||
b.reporter = msg.sender;
|
||||
Report(signer);
|
||||
}
|
||||
|
||||
/// @notice Present a chunk in order to avoid losing deposit.
|
||||
///
|
||||
/// @param chunk chunk data
|
||||
function presentMissingChunk(bytes chunk) external {
|
||||
bytes32 swarmHash = sha3(chunk);
|
||||
presentedChunks[swarmHash] = block.number;
|
||||
}
|
||||
|
||||
/// @notice Determine guilty status of address `addr`.
|
||||
/// No change in state.
|
||||
///
|
||||
/// @dev Definition of guilty is failing to present missing chunk within grace period.
|
||||
///
|
||||
/// @param addr queried address.
|
||||
///
|
||||
/// @return true, if status is "Guilty".
|
||||
function isGuilty(address addr) returns (bool g){
|
||||
if(isClean(addr)) return false;
|
||||
Bee b = swarm[addr];
|
||||
return b.deadline < block.number;
|
||||
}
|
||||
|
||||
/// @notice Collect rewards for successfully prosecuting `addr`.
|
||||
///
|
||||
/// @dev This implies burning 9/10 of the security deposit.
|
||||
///
|
||||
/// @param addr guilty defendant address
|
||||
function claimReporterReward(address addr) {
|
||||
if(!isGuilty(addr)) return;
|
||||
Bee b = swarm[addr];
|
||||
msg.sender.send(b.deposit / REWARD_FRACTION); // reporter rewarded
|
||||
delete swarm[addr]; // rest of deposit burnt
|
||||
}
|
||||
|
||||
/// @notice Determine if the deposit for `addr` is unaccessible until `time`.
|
||||
/// No change in state.
|
||||
///
|
||||
/// @param addr queried address.
|
||||
///
|
||||
/// @param time queried time.
|
||||
///
|
||||
/// @return true if deposit expires after queried time.
|
||||
function expiresAfter(address addr, uint time) returns (bool s) {
|
||||
Bee b = swarm[addr];
|
||||
return b.expiry > time;
|
||||
}
|
||||
}
|
||||
BIN
bzz/bzzdemo/add.png
Normal file
|
After Width: | Height: | Size: 129 B |
BIN
bzz/bzzdemo/back.png
Normal file
|
After Width: | Height: | Size: 388 B |
BIN
bzz/bzzdemo/cut-left.png
Normal file
|
After Width: | Height: | Size: 221 B |
BIN
bzz/bzzdemo/cut-mov.png
Normal file
|
After Width: | Height: | Size: 197 B |
BIN
bzz/bzzdemo/cut-right.png
Normal file
|
After Width: | Height: | Size: 193 B |
BIN
bzz/bzzdemo/cut-top.png
Normal file
|
After Width: | Height: | Size: 205 B |
1
bzz/bzzdemo/data.json
Normal file
BIN
bzz/bzzdemo/delete.png
Normal file
|
After Width: | Height: | Size: 552 B |
BIN
bzz/bzzdemo/down.png
Normal file
|
After Width: | Height: | Size: 403 B |
BIN
bzz/bzzdemo/download.png
Normal file
|
After Width: | Height: | Size: 422 B |
BIN
bzz/bzzdemo/eye.png
Normal file
|
After Width: | Height: | Size: 525 B |
BIN
bzz/bzzdemo/imgs/apron.jpg
Normal file
|
After Width: | Height: | Size: 409 KiB |
BIN
bzz/bzzdemo/imgs/bekas1.jpg
Normal file
|
After Width: | Height: | Size: 542 KiB |
BIN
bzz/bzzdemo/imgs/bekas2.jpg
Normal file
|
After Width: | Height: | Size: 496 KiB |
BIN
bzz/bzzdemo/imgs/bekas_blanik.jpg
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
bzz/bzzdemo/imgs/hangar.jpg
Normal file
|
After Width: | Height: | Size: 425 KiB |
BIN
bzz/bzzdemo/imgs/hangar_inside.jpg
Normal file
|
After Width: | Height: | Size: 631 KiB |
BIN
bzz/bzzdemo/imgs/ruin.jpg
Normal file
|
After Width: | Height: | Size: 456 KiB |
291
bzz/bzzdemo/index.css
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
/* General reset */
|
||||
html, body
|
||||
{
|
||||
overflow: hidden; /* IE<9 */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
img
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Main gallery elements */
|
||||
#gallery h2
|
||||
{
|
||||
margin: 1em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#gallery
|
||||
{
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
#gallery.no-cursor *
|
||||
{
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
#gallery a, #gallery a:active, #gallery a:focus
|
||||
{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#gallery #background
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#gallery #noise
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url(noise.png);
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
#gallery #content
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#gallery #flash
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Main image */
|
||||
#gallery #content img.current
|
||||
{
|
||||
box-shadow: 0 0 2.5em rgba(0, 0, 0, 0.5);
|
||||
-moz-box-shadow: 0 0 2.5em rgba(0, 0, 0, 0.5);
|
||||
-webkit-box-shadow: 0 0 2.5em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#gallery #content #header
|
||||
{
|
||||
-webkit-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
background: #111; /* IE<9 */
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
font-family: sans-serif;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
#gallery #content #header img
|
||||
{
|
||||
vertical-align: middle;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
#gallery #content #header #throbber
|
||||
{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#gallery #content #header a
|
||||
{
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#gallery #content #header a:hover
|
||||
{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Navigation arrows */
|
||||
#gallery #content #left,
|
||||
#gallery #content #right
|
||||
{
|
||||
position: absolute;
|
||||
width: 5%;
|
||||
min-width: 2.5em;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#gallery #content #left
|
||||
{
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#gallery #content #right
|
||||
{
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#gallery #content #left div,
|
||||
#gallery #content #right div
|
||||
{
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
opacity: 0.3;
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
#gallery #content #left div:hover,
|
||||
#gallery #content #right div:hover
|
||||
{
|
||||
opacity: 0.6;
|
||||
filter: alpha(opacity=60);
|
||||
}
|
||||
|
||||
#gallery #content #left img,
|
||||
#gallery #content #right img
|
||||
{
|
||||
position: absolute;
|
||||
display: block;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#gallery #content #left img
|
||||
{
|
||||
left: 25%;
|
||||
}
|
||||
|
||||
#gallery #content #right img
|
||||
{
|
||||
right: 25%;
|
||||
}
|
||||
|
||||
/* Thumbnail list */
|
||||
#gallery #list
|
||||
{
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding-top: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
|
||||
box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
|
||||
-moz-box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
|
||||
-webkit-box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#gallery #list:focus
|
||||
{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Invidivual thumbnails */
|
||||
#gallery #list .thumb
|
||||
{
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
|
||||
box-shadow: 0.25em 0.25em 0.25em rgba(0, 0, 0, 0.5);
|
||||
-moz-box-shadow: 0.25em 0.25em 0.25em rgba(0, 0, 0, 0.5);
|
||||
-webkit-box-shadow: 0.25em 0.25em 0.25em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#gallery #list .thumb a
|
||||
{
|
||||
display: block;
|
||||
position: relative;
|
||||
border: 2px solid #111
|
||||
}
|
||||
|
||||
#gallery #list .thumb.current a
|
||||
{
|
||||
border: 2px solid #f00;
|
||||
}
|
||||
|
||||
#gallery #list .thumb a:hover,
|
||||
#gallery #list .thumb a:focus
|
||||
{
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
#gallery #list .thumb.current a:hover,
|
||||
#gallery #list .thumb.current a:focus
|
||||
{
|
||||
border: 2px solid #f00;
|
||||
}
|
||||
|
||||
/* Thumbnail styles */
|
||||
#gallery #list .thumb .ovr
|
||||
{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#gallery #list .thumb.cut-left .ovr
|
||||
{
|
||||
background: url(cut-left.png) top left repeat-y;
|
||||
}
|
||||
|
||||
#gallery #list .thumb.cut-right .ovr
|
||||
{
|
||||
background: url(cut-right.png) repeat-y top right;
|
||||
}
|
||||
|
||||
#gallery #list .thumb.cut-left.cut-right .ovr
|
||||
{
|
||||
background: url(cut-left.png) top left repeat-y, url(cut-right.png) repeat-y top right;
|
||||
}
|
||||
|
||||
#gallery #list .thumb.cut-top .ovr
|
||||
{
|
||||
background: url(cut-top.png) top left repeat-x;
|
||||
}
|
||||
|
||||
#gallery #list .thumb.cut-bottom .ovr
|
||||
{
|
||||
background: url(cut-right.png) repeat-x bottom left;
|
||||
}
|
||||
|
||||
#gallery #list .thumb.cut-top.cut-bottom .ovr
|
||||
{
|
||||
background: url(cut-left.png) top left repeat-x, url(cut-right.png) repeat-x bottom left;
|
||||
}
|
||||
|
||||
#gallery #list .thumb.movie .ovr
|
||||
{
|
||||
background: url(cut-mov.png) top left repeat-y, url(cut-mov.png) top right repeat-y;
|
||||
}
|
||||
20
bzz/bzzdemo/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<script src="mootools-core-1.4.js" type="text/javascript"></script>
|
||||
<script src="mootools-more-1.4.js" type="text/javascript"></script>
|
||||
<script src="mootools-idle.js" type="text/javascript"></script>
|
||||
<script src="mootools-mooswipe.js" type="text/javascript"></script>
|
||||
<script src="index.js" type="text/javascript"></script>
|
||||
<link href="index.css" rel="stylesheet" type="text/css"/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<h2>Frak! JavaScript required :'(</h2>
|
||||
</noscript>
|
||||
<div id="gallery"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
836
bzz/bzzdemo/index.js
Normal file
|
|
@ -0,0 +1,836 @@
|
|||
// fgallery: a modern, minimalist javascript photo gallery
|
||||
// Copyright(c) 2003-2014 by wave++ "Yuri D'Elia" <wavexx@thregr.org>
|
||||
// Distributed under GPL2 (see COPYING) WITHOUT ANY WARRANTY.
|
||||
var datafile = 'data.json';
|
||||
var padding = 22;
|
||||
var duration = 500;
|
||||
var thrdelay = 1500;
|
||||
var hidedelay = 3000;
|
||||
var prefetch = 1;
|
||||
var minupscale = 640 * 480;
|
||||
var thumbrt = 16/9 - 5/3;
|
||||
var cutrt = 0.15;
|
||||
|
||||
Element.Events.hashchange =
|
||||
{
|
||||
onAdd: function()
|
||||
{
|
||||
var hash = window.location.hash;
|
||||
|
||||
var hashchange = function()
|
||||
{
|
||||
if(hash == window.location.hash) return;
|
||||
else hash = window.location.hash;
|
||||
|
||||
var value = (!hash.indexOf('#')? hash.substr(1): hash);
|
||||
window.fireEvent('hashchange', value);
|
||||
document.fireEvent('hashchange', value);
|
||||
};
|
||||
|
||||
if("onhashchange" in window
|
||||
&& (!Browser.ie || Browser.version > 7))
|
||||
window.onhashchange = hashchange;
|
||||
else
|
||||
hashchange.periodical(50);
|
||||
}
|
||||
};
|
||||
|
||||
// some state variables
|
||||
var emain; // main object
|
||||
var eback; // background
|
||||
var enoise; // additive noise
|
||||
var eflash; // flashing object
|
||||
var ehdr; // header
|
||||
var elist; // thumbnail list
|
||||
var fscr; // thumbnail list scroll fx
|
||||
var econt; // picture container
|
||||
var ebuff; // picture buffer
|
||||
var eleft; // go left
|
||||
var eright; // go right
|
||||
var oimg; // old image
|
||||
var eimg; // new image
|
||||
var cthumb; // current thumbnail
|
||||
var mthumb; // missing thumbnails
|
||||
var eidx; // current index
|
||||
var tthr; // throbber timeout
|
||||
var imgs; // image list
|
||||
var first; // first image
|
||||
var idle; // idle timer
|
||||
var clayout; // current layout
|
||||
var csr; // current scaling ratio
|
||||
|
||||
function resize()
|
||||
{
|
||||
// best layout
|
||||
var msize = emain.getSize();
|
||||
var rt = (imgs.thumb.min[0] / imgs.thumb.min[1]);
|
||||
var maxw = msize.x - imgs.thumb.min[0] - padding;
|
||||
var maxh = msize.y * rt - imgs.thumb.min[1] - padding;
|
||||
var layout = (maxw >= maxh? 'horizontal': 'vertical');
|
||||
|
||||
// calculate a good multiplier for the thumbnail size
|
||||
var m = (layout == 'horizontal'?
|
||||
(msize.x * window.devicePixelRatio * thumbrt) / imgs.thumb.min[0]:
|
||||
(msize.y * window.devicePixelRatio * thumbrt) / imgs.thumb.min[1]);
|
||||
if(m >= 1)
|
||||
m = Math.pow(2, Math.floor(Math.log(m) / Math.LN2));
|
||||
else
|
||||
m = Math.pow(2, Math.ceil(Math.log(m) / Math.LN2));
|
||||
var sr = m / window.devicePixelRatio;
|
||||
|
||||
if(layout != clayout || sr != csr)
|
||||
{
|
||||
onLayoutChanged(layout, sr);
|
||||
if(cthumb) centerThumb(0);
|
||||
clayout = layout;
|
||||
csr = sr;
|
||||
}
|
||||
|
||||
// resize main container
|
||||
var epos = elist.getPosition();
|
||||
if(layout == 'horizontal')
|
||||
{
|
||||
econt.setStyles(
|
||||
{
|
||||
'width': epos.x,
|
||||
'height': msize.y
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
econt.setStyles(
|
||||
{
|
||||
width: msize.x,
|
||||
height: epos.y
|
||||
});
|
||||
}
|
||||
|
||||
if(oimg) resizeMainImg(oimg);
|
||||
if(eimg) resizeMainImg(eimg);
|
||||
}
|
||||
|
||||
function onLayoutChanged(layout, sr)
|
||||
{
|
||||
elist.setStyle('display', 'none');
|
||||
|
||||
// refit the thumbnails, cropping edges
|
||||
imgs.data.each(function(x, i)
|
||||
{
|
||||
var crop = x.thumb[1];
|
||||
var size = (x.thumb[2]? x.thumb[2]: crop);
|
||||
var offset = (x.thumb[3]? x.thumb[3]: [0, 0]);
|
||||
var center = (x.center? [x.center[0] / 1000, x.center[1] / 1000]: [0.5, 0.5]);
|
||||
|
||||
var maxw, maxh;
|
||||
if(layout == 'horizontal')
|
||||
{
|
||||
maxw = imgs.thumb.min[0];
|
||||
maxh = Math.round(maxw * (crop[1] / crop[0]));
|
||||
maxh = Math.max(maxh, imgs.thumb.min[1]);
|
||||
maxh = Math.min(maxh, imgs.thumb.max[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
maxh = imgs.thumb.min[1];
|
||||
maxw = Math.round(maxh * (crop[0] / crop[1]));
|
||||
maxw = Math.max(maxw, imgs.thumb.min[0]);
|
||||
maxw = Math.min(maxw, imgs.thumb.max[0]);
|
||||
}
|
||||
|
||||
x.eimg.setStyles(
|
||||
{
|
||||
'width': Math.round(maxw * sr),
|
||||
'height': Math.round(maxh * sr),
|
||||
'background-size': Math.round(crop[0] * sr) + "px " + Math.round(crop[1] * sr) + "px"
|
||||
});
|
||||
|
||||
// center cropped thumbnail
|
||||
var dx = maxw - crop[0];
|
||||
var cx = size[0] * center[0] - offset[0];
|
||||
cx = Math.round(crop[0] / 2 - cx + dx / 2);
|
||||
cx = Math.max(Math.min(0, cx), dx);
|
||||
|
||||
var dy = maxh - crop[1];
|
||||
var cy = size[1] * center[1] - offset[1];
|
||||
cy = Math.round(crop[1] / 2 - cy + dy / 2);
|
||||
cy = Math.max(Math.min(0, cy), dy);
|
||||
|
||||
x.eimg.setStyle('background-position', Math.round(cx * sr) + 'px ' + Math.round(cy * sr) + 'px');
|
||||
|
||||
// border styles
|
||||
var classes = ['cut-left', 'cut-right', 'cut-top', 'cut-bottom'];
|
||||
classes.each(function(c) { x.ethumb.removeClass(c); });
|
||||
|
||||
var wx = Math.round(size[0] * cutrt);
|
||||
if((offset[0] - cx) > wx) x.ethumb.addClass('cut-left');
|
||||
if((cx - offset[0] + size[0] - maxw) > wx) x.ethumb.addClass('cut-right');
|
||||
|
||||
var wy = Math.round(size[1] * cutrt);
|
||||
if((offset[1] - cy) > wy) x.ethumb.addClass('cut-top');
|
||||
if((cy - offset[1] + size[1] - maxh) > wy) x.ethumb.addClass('cut-bottom');
|
||||
});
|
||||
|
||||
// resize thumbnail list
|
||||
if(layout == 'horizontal')
|
||||
{
|
||||
elist.setStyles(
|
||||
{
|
||||
'top': 0,
|
||||
'left': 'auto',
|
||||
'right': 0,
|
||||
'bottom': 0,
|
||||
'overflow-y': 'scroll',
|
||||
'overflow-x': 'hidden',
|
||||
'white-space': 'pre-line'
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
elist.setStyles(
|
||||
{
|
||||
'top': 'auto',
|
||||
'left': 0,
|
||||
'right': 0,
|
||||
'bottom': 0,
|
||||
'overflow-y': 'hidden',
|
||||
'overflow-x': 'scroll',
|
||||
'white-space': 'nowrap'
|
||||
});
|
||||
}
|
||||
|
||||
elist.setStyle('display', 'block');
|
||||
}
|
||||
|
||||
function resizeMainImg(img)
|
||||
{
|
||||
var contSize = econt.getSize();
|
||||
var listSize = elist.getSize();
|
||||
var thumbWidth = (clayout == 'horizontal'? listSize.x: listSize.y);
|
||||
var data = imgs.data[img.idx].img;
|
||||
var width = data[1][0];
|
||||
var height = data[1][1];
|
||||
var imgrt = width / height;
|
||||
var pad = padding * 2;
|
||||
|
||||
if(imgrt > (contSize.x / contSize.y))
|
||||
{
|
||||
img.width = Math.max(thumbWidth + pad, contSize.x - pad);
|
||||
img.height = img.width / imgrt;
|
||||
}
|
||||
else
|
||||
{
|
||||
img.height = Math.max(thumbWidth + pad, contSize.y - pad);
|
||||
img.width = img.height * imgrt;
|
||||
}
|
||||
if(width * height <= minupscale && img.width > width)
|
||||
{
|
||||
img.width = width;
|
||||
img.height = height;
|
||||
}
|
||||
|
||||
img.setStyles(
|
||||
{
|
||||
'position': 'absolute',
|
||||
'top': contSize.y / 2 - img.height / 2,
|
||||
'left': contSize.x / 2 - img.width / 2
|
||||
});
|
||||
}
|
||||
|
||||
function ts()
|
||||
{
|
||||
var date = new Date();
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
function detectSlowness(start)
|
||||
{
|
||||
var end = ts();
|
||||
var delta = end - start;
|
||||
if(delta > duration * 2)
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
function centerThumb(duration)
|
||||
{
|
||||
var thumbPos = cthumb.getPosition();
|
||||
var thumbSize = cthumb.getSize();
|
||||
var listSize = elist.getSize();
|
||||
var listScroll = elist.getScroll();
|
||||
|
||||
var x = thumbPos.x + listScroll.x - listSize.x / 2 + thumbSize.x / 2;
|
||||
var y = thumbPos.y + listScroll.y - listSize.y / 2 + thumbSize.y / 2;
|
||||
|
||||
if(fscr) fscr.cancel();
|
||||
fscr = new Fx.Scroll(elist, { duration: duration }).start(x, y);
|
||||
}
|
||||
|
||||
function sendImgs(xhr, uri) {
|
||||
// set up request
|
||||
xhr.open("PUT", uri + "data.json", true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
// send the collected data as JSON
|
||||
xhr.send(JSON.stringify(imgs));
|
||||
}
|
||||
|
||||
function imageToUrl(img, w, h) {
|
||||
var can = document.createElement('canvas');
|
||||
can.width = w;
|
||||
can.height = h;
|
||||
var cntxt = can.getContext("2d");
|
||||
cntxt.drawImage(img, 0, 0, w, h);
|
||||
return can.toDataURL();
|
||||
}
|
||||
|
||||
function uploadFile(files, nr, uri) {
|
||||
if(files.length <= nr) {
|
||||
if(uri != "") {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() { if (xhr.readyState === 4) {
|
||||
var i = xhr.responseText;
|
||||
window.location.replace("/" + i + "/");
|
||||
}};
|
||||
sendImgs(xhr, uri);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var imageType = /^image\//;
|
||||
var file = files[nr];
|
||||
if(!imageType.test(file.type)) {
|
||||
uploadFile(files, nr + 1, uri);
|
||||
return;
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() { if (xhr.readyState === 4) {
|
||||
var i = xhr.responseText;
|
||||
|
||||
// insert image into index
|
||||
var img = new Image();
|
||||
img.onload = function() {
|
||||
var blur = imageToUrl(img, 5, 5);
|
||||
var thumbData = [];
|
||||
var thumbSize = 200;
|
||||
if(img.naturalWidth > img.naturalHeight) {
|
||||
// landscape thumbnail
|
||||
var h = img.naturalHeight * thumbSize / img.naturalWidth;
|
||||
thumbData[0] = imageToUrl(img, thumbSize, h);
|
||||
thumbData[1] = [thumbSize, h];
|
||||
} else {
|
||||
// portrait thumbnail
|
||||
var w = img.naturalWidth * thumbSize / img.naturalHeight;
|
||||
thumbData[0] = imageToUrl(img, w, thumbsize);
|
||||
thumbData[1] = [w, thumbSize];
|
||||
}
|
||||
// update index
|
||||
var imgData = [];
|
||||
imgData[0] = "imgs/" + file.name;
|
||||
imgData[1] = [img.naturalWidth, img.naturalHeight];
|
||||
imgs.data.splice(eidx, 0, {img: imgData, thumb: thumbData, blur: blur});
|
||||
uploadFile(files, nr + 1, "/" + i + "/");
|
||||
}
|
||||
img.src = "/" + i + "/imgs/" + file.name;
|
||||
return;
|
||||
}};
|
||||
xhr.open("PUT", uri + "imgs/" + file.name, true);
|
||||
xhr.setRequestHeader('Content-Type', file.type);
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(evt) {
|
||||
xhr.send(evt.target.result);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
function handleFiles(files) {
|
||||
uploadFile(files, 0, "");
|
||||
}
|
||||
|
||||
function deleteImg()
|
||||
{
|
||||
if(imgs.data.length < 2) return; // empty albums not allowed
|
||||
var fname = imgs.data[eidx].img[0];
|
||||
imgs.data.splice(eidx,1);
|
||||
|
||||
// construct an HTTP request
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
// set response handler
|
||||
xhr.onreadystatechange = function () { if (xhr.readyState === 4) {
|
||||
var i = xhr.responseText;
|
||||
var xhrd = new XMLHttpRequest();
|
||||
xhrd.onreadystatechange = function () { if (xhrd.readyState === 4) {
|
||||
var j = xhrd.responseText;
|
||||
window.location.replace("/" + j + "/");
|
||||
}};
|
||||
xhrd.open("DELETE", "/" + i + "/" + fname, true);
|
||||
xhrd.send();
|
||||
}};
|
||||
|
||||
sendImgs(xhr, "");
|
||||
}
|
||||
|
||||
function moveUpDown(off)
|
||||
{
|
||||
var me = imgs.data[eidx];
|
||||
imgs.data[eidx] = imgs.data[eidx + off];
|
||||
imgs.data[eidx + off] = me;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function () { if (xhr.readyState === 4) {
|
||||
var i = xhr.responseText;
|
||||
window.location.replace("/" + i + "/#" + (eidx + off));
|
||||
}};
|
||||
sendImgs(xhr, "");
|
||||
}
|
||||
|
||||
function moveUp()
|
||||
{
|
||||
moveUpDown(-1);
|
||||
}
|
||||
|
||||
function moveDown()
|
||||
{
|
||||
moveUpDown(1);
|
||||
}
|
||||
|
||||
function onMainReady()
|
||||
{
|
||||
resizeMainImg(eimg);
|
||||
eimg.setStyle('opacity', 0);
|
||||
eimg.addClass('current');
|
||||
eimg.inject(ebuff);
|
||||
|
||||
// setup header
|
||||
var dsc = [];
|
||||
if(imgs.index)
|
||||
dsc.push("<a title=\"Back to index\" href=\"" + encodeURI(imgs.index) + "\"><img src=\"back.png\"/></a>");
|
||||
|
||||
// delete image
|
||||
if(imgs.data.length > 1)
|
||||
dsc.push("<a title=\"Delete image\" onclick=\"deleteImg()\"><img src=\"delete.png\"/></a>");
|
||||
|
||||
// add image
|
||||
dsc.push("<input type=\"file\" id=\"fileElem\" multiple accept=\"image/*\" style=\"display:none\" onchange=\"handleFiles(this.files)\"><a href=\"#\" id=\"fileSelect\"><img src=\"add.png\"></a>");
|
||||
|
||||
// up image
|
||||
if(eidx > 0)
|
||||
dsc.push("<a title=\"Move up\" onclick=\"moveUp()\"><img src=\"up.png\"/></a>");
|
||||
|
||||
// down image
|
||||
if(eidx < imgs.data.length - 1)
|
||||
dsc.push("<a title=\"Move down\" onclick=\"moveDown()\"><img src=\"down.png\"/></a>");
|
||||
|
||||
if(imgs.data[eidx].file)
|
||||
{
|
||||
var img = imgs.data[eidx].file[0];
|
||||
dsc.push("<a title=\"Download image\" href=\"" + encodeURI(img) + "\"><img src=\"eye.png\"/></a>");
|
||||
eimg.addEvent('click', function() { window.location = img; });
|
||||
eimg.setStyle('cursor', 'pointer'); // fallback
|
||||
eimg.setStyle('cursor', 'zoom-in');
|
||||
}
|
||||
if(imgs.download)
|
||||
dsc.push("<a title=\"Download album\" href=\"" + encodeURI(imgs.download) + "\"><img src=\"download.png\"/></a>");
|
||||
if(imgs.data[eidx].date)
|
||||
dsc.push("<b>Date</b>: " + imgs.data[eidx].date);
|
||||
ehdr.set('html', dsc.join(' '));
|
||||
ehdr.setStyle('display', (dsc.length? 'block': 'none'));
|
||||
|
||||
// setup upload file selector
|
||||
var fileSelect = document.getElementById("fileSelect"),
|
||||
fileElem = document.getElementById("fileElem");
|
||||
fileSelect.addEventListener("click", function (e) {
|
||||
if (fileElem) {
|
||||
fileElem.click();
|
||||
}
|
||||
e.preventDefault(); // prevent navigation to "#"
|
||||
}, false);
|
||||
|
||||
// complete thumbnails
|
||||
var d = duration;
|
||||
if(first !== false)
|
||||
{
|
||||
first = false;
|
||||
loadAllThumbs();
|
||||
d = 0;
|
||||
}
|
||||
|
||||
// start animations
|
||||
if(oimg)
|
||||
{
|
||||
oimg.removeClass('current');
|
||||
var fx = oimg.get('tween');
|
||||
fx.cancel();
|
||||
fx.duration = d;
|
||||
fx.removeEvents('complete');
|
||||
fx.addEvent('complete', function(x) { x.destroy(); });
|
||||
fx.start('opacity', 0);
|
||||
oimg = undefined;
|
||||
}
|
||||
|
||||
var fx = new Fx.Tween(eimg, { duration: d });
|
||||
if(d)
|
||||
{
|
||||
var now = ts();
|
||||
fx.addEvent('complete', function()
|
||||
{
|
||||
detectSlowness(now);
|
||||
});
|
||||
}
|
||||
eimg.set('tween', fx);
|
||||
fx.start('opacity', 1);
|
||||
|
||||
var rp = Math.floor(Math.random() * 100);
|
||||
eback.src = imgs.data[eidx].blur;
|
||||
enoise.setStyle('background-position', rp + 'px ' + rp + 'px');
|
||||
|
||||
clearTimeout(tthr);
|
||||
idle.start();
|
||||
showHdr();
|
||||
centerThumb(d);
|
||||
|
||||
// prefetch next image
|
||||
if(prefetch && eidx != imgs.data.length - 1)
|
||||
{
|
||||
var data = imgs.data[eidx + 1];
|
||||
Asset.images([data.img[0], data.blur]);
|
||||
}
|
||||
}
|
||||
|
||||
function showThrobber()
|
||||
{
|
||||
var img = new Element('img', { id: 'throbber' });
|
||||
img.src = "throbber.gif";
|
||||
ehdr.empty();
|
||||
img.inject(ehdr);
|
||||
ehdr.setStyle('display', 'block');
|
||||
idle.stop();
|
||||
showHdr();
|
||||
}
|
||||
|
||||
function hideHdr()
|
||||
{
|
||||
if(idle.started && ehdr.getStyle('opacity') !== 0)
|
||||
ehdr.tween('opacity', [1, 0], { link: 'ignore' });
|
||||
}
|
||||
|
||||
function hideNav()
|
||||
{
|
||||
emain.addClass('no-cursor');
|
||||
eleft.tween('opacity', [1, 0], { link: 'ignore' });
|
||||
eright.tween('opacity', [1, 0], { link: 'ignore' });
|
||||
}
|
||||
|
||||
function showHdr()
|
||||
{
|
||||
ehdr.get('tween').cancel();
|
||||
ehdr.fade('show');
|
||||
}
|
||||
|
||||
function showNav()
|
||||
{
|
||||
emain.removeClass('no-cursor');
|
||||
eleft.get('tween').cancel();
|
||||
eleft.fade('show');
|
||||
eright.get('tween').cancel();
|
||||
eright.fade('show');
|
||||
}
|
||||
|
||||
function flash()
|
||||
{
|
||||
eflash.setStyle('display', 'block');
|
||||
eflash.tween('opacity', [1, 0]);
|
||||
}
|
||||
|
||||
function prev()
|
||||
{
|
||||
if(eidx != 0)
|
||||
switchTo(eidx - 1);
|
||||
else
|
||||
{
|
||||
flash();
|
||||
switchTo(imgs.data.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function next()
|
||||
{
|
||||
if(eidx != imgs.data.length - 1)
|
||||
switchTo(eidx + 1);
|
||||
else
|
||||
{
|
||||
flash();
|
||||
switchTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
function switchTo(i)
|
||||
{
|
||||
window.location.replace("#" + i);
|
||||
}
|
||||
|
||||
function load(i)
|
||||
{
|
||||
if(i == eidx) return;
|
||||
doLoad(i);
|
||||
}
|
||||
|
||||
function doLoad(i)
|
||||
{
|
||||
var data = imgs.data[i];
|
||||
var assets = Asset.images([data.img[0], data.blur],
|
||||
{
|
||||
onComplete: function() { if(i == eidx) onMainReady(); }
|
||||
});
|
||||
|
||||
if(!oimg) oimg = eimg;
|
||||
eimg = assets[0];
|
||||
eimg.idx = eidx = i;
|
||||
|
||||
if(cthumb) cthumb.removeClass('current');
|
||||
cthumb = imgs.data[eidx].ethumb;
|
||||
cthumb.addClass('current');
|
||||
|
||||
clearTimeout(tthr);
|
||||
tthr = showThrobber.delay(thrdelay);
|
||||
}
|
||||
|
||||
function getLocationIndex()
|
||||
{
|
||||
var hash = window.location.hash;
|
||||
var idx = parseInt(!hash.indexOf('#')? hash.substr(1): hash);
|
||||
if(isNaN(idx) || idx < 0)
|
||||
idx = 0;
|
||||
else if(idx >= imgs.data.length)
|
||||
idx = imgs.data.length - 1;
|
||||
return idx;
|
||||
}
|
||||
|
||||
function change()
|
||||
{
|
||||
load(getLocationIndex());
|
||||
}
|
||||
|
||||
function loadThumb(i)
|
||||
{
|
||||
var x = imgs.data[i];
|
||||
x.eimg.setStyle('background-image', 'url(' + encodeURI(x.thumb[0]) + ')');
|
||||
}
|
||||
|
||||
function loadAllThumbs()
|
||||
{
|
||||
mthumbs.each(loadThumb);
|
||||
mthumbs = [];
|
||||
}
|
||||
|
||||
function loadNextThumb()
|
||||
{
|
||||
if(mthumbs.length)
|
||||
{
|
||||
var i = mthumbs.shift();
|
||||
Asset.image(imgs.data[i].thumb[0],
|
||||
{
|
||||
onLoad: function()
|
||||
{
|
||||
loadThumb(i);
|
||||
loadNextThumb();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initGallery(data)
|
||||
{
|
||||
imgs = data;
|
||||
emain = $('gallery');
|
||||
emain.setStyle('display', 'none');
|
||||
|
||||
eback = new Element('img', { id: 'background' });
|
||||
eback.inject(emain);
|
||||
|
||||
enoise = new Element('div', { id: 'noise' });
|
||||
enoise.inject(emain);
|
||||
|
||||
econt = new Element('div', { id: 'content' });
|
||||
econt.inject(emain);
|
||||
|
||||
ebuff = new Element('div');
|
||||
ebuff.inject(econt);
|
||||
|
||||
eflash = new Element('div', { id: 'flash' });
|
||||
eflash.setStyles({ 'opacity': 0, 'display': 'none' });
|
||||
eflash.set('tween',
|
||||
{
|
||||
duration: duration,
|
||||
link: 'cancel',
|
||||
onComplete: function() { eflash.setStyle('display', 'none'); }
|
||||
});
|
||||
eflash.inject(econt);
|
||||
|
||||
eleft = new Element('a', { id: 'left' });
|
||||
eleft.adopt((new Element('div')).adopt(new Element('img', { 'src': 'left.png' })));
|
||||
eleft.inject(econt);
|
||||
|
||||
eright = new Element('a', { id: 'right' });
|
||||
eright.adopt((new Element('div')).adopt(new Element('img', { 'src': 'right.png' })));
|
||||
eright.inject(econt);
|
||||
|
||||
ehdr = new Element('div', { id: 'header' });
|
||||
ehdr.inject(econt);
|
||||
|
||||
elist = new Element('div', { id: 'list' });
|
||||
elist.inject(emain);
|
||||
|
||||
imgs.data.each(function(x, i)
|
||||
{
|
||||
var ethumb = new Element('div', { 'class': 'thumb' });
|
||||
x.ethumb = ethumb;
|
||||
|
||||
var a = new Element('a');
|
||||
a.addEvent('click', function() { switchTo(i); });
|
||||
a.href = "#" + i;
|
||||
|
||||
var img = new Element('div', { 'class': 'img' });
|
||||
x.eimg = img;
|
||||
img.inject(a);
|
||||
|
||||
var ovr = new Element('div', { 'class': 'ovr' });
|
||||
ovr.inject(a);
|
||||
|
||||
a.inject(ethumb);
|
||||
ethumb.inject(elist);
|
||||
elist.appendText("\n");
|
||||
});
|
||||
|
||||
emain.setStyles(
|
||||
{
|
||||
'display': 'block',
|
||||
'visibility': 'hidden',
|
||||
'min-width': imgs.thumb.min[0] + padding * 2,
|
||||
'min-height': imgs.thumb.min[1] + padding * 2
|
||||
});
|
||||
|
||||
// events and navigation shortcuts
|
||||
eleft.addEvent('click', prev);
|
||||
eright.addEvent('click', next);
|
||||
window.addEvent('resize', resize);
|
||||
window.addEvent('hashchange', change);
|
||||
|
||||
window.addEvent('keydown', function(ev)
|
||||
{
|
||||
if(ev.key == 'up' || ev.key == 'left')
|
||||
{
|
||||
ev.stop();
|
||||
prev();
|
||||
}
|
||||
else if(ev.key == 'down' || ev.key == 'right' || ev.key == 'space')
|
||||
{
|
||||
ev.stop();
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
econt.addEvent('mousewheel', function(ev)
|
||||
{
|
||||
if(ev.alt || ev.control || ev.meta || ev.shift)
|
||||
return;
|
||||
|
||||
ev.stop();
|
||||
if(ev.wheel > 0)
|
||||
prev();
|
||||
else
|
||||
next();
|
||||
});
|
||||
|
||||
new MooSwipe(econt,
|
||||
{
|
||||
onSwipeleft: next,
|
||||
onSwipedown: next,
|
||||
onSwiperight: prev,
|
||||
onSwipeup: prev
|
||||
});
|
||||
|
||||
// setup an idle callback for mouse movement only
|
||||
var idleTmp = new IdleTimer(window, {
|
||||
timeout: hidedelay,
|
||||
events: ['mousemove', 'mousedown', 'mousewheel']
|
||||
}).start();
|
||||
idleTmp.addEvent('idle', hideNav);
|
||||
idleTmp.addEvent('active', function() { showNav(); showHdr(); });
|
||||
|
||||
// general idle callback
|
||||
idle = new IdleTimer(window, { timeout: hidedelay }).start();
|
||||
idle.addEvent('idle', hideHdr);
|
||||
|
||||
// prepare first image
|
||||
first = getLocationIndex();
|
||||
resize();
|
||||
load(first);
|
||||
centerThumb(0);
|
||||
if(imgs.name) document.title = imgs.name;
|
||||
|
||||
// setup thumbnail loading sequence
|
||||
mthumbs = [];
|
||||
if(first < 5)
|
||||
{
|
||||
// optimize common initial case (viewing from the beginning)
|
||||
for(var i = 0; i != imgs.data.length; ++i)
|
||||
mthumbs.push(i);
|
||||
}
|
||||
else for(var i = 0; i != imgs.data.length; ++i)
|
||||
{
|
||||
// distance from current
|
||||
var d = (i / 2 >> 0);
|
||||
var k = first + (i % 2? d + 1: -d);
|
||||
if(k < 0)
|
||||
k = imgs.data.length + k;
|
||||
else if(k >= imgs.data.length)
|
||||
k = k - imgs.data.length;
|
||||
mthumbs.push(k);
|
||||
}
|
||||
loadNextThumb();
|
||||
|
||||
emain.setStyle('visibility', 'visible');
|
||||
}
|
||||
|
||||
function initFailure()
|
||||
{
|
||||
emain = $('gallery');
|
||||
emain.set('html', "<h2>Cannot load gallery data :'(</h2>");
|
||||
emain.setStyles(
|
||||
{
|
||||
'background': 'inherit',
|
||||
'display': 'block'
|
||||
});
|
||||
}
|
||||
|
||||
function init()
|
||||
{
|
||||
if(!("devicePixelRatio" in window))
|
||||
window.devicePixelRatio = 1;
|
||||
|
||||
// read the data
|
||||
new Request.JSON(
|
||||
{
|
||||
url: datafile,
|
||||
onRequest: function()
|
||||
{
|
||||
if(this.xhr.overrideMimeType)
|
||||
this.xhr.overrideMimeType('application/json');
|
||||
},
|
||||
isSuccess: function()
|
||||
{
|
||||
return (!this.status || (this.status >= 200 && this.status < 300));
|
||||
},
|
||||
onSuccess: initGallery,
|
||||
onFailure: initFailure
|
||||
}).get();
|
||||
|
||||
// preload some resources
|
||||
Asset.images(['throbber.gif',
|
||||
'left.png', 'right.png', 'delete.png', 'add.png',
|
||||
'eye.png', 'download.png', 'back.png', 'up.png', 'down.png',
|
||||
'cut-left.png', 'cut-right.png',
|
||||
'cut-top.png', 'cut-mov.png']);
|
||||
}
|
||||
|
||||
window.addEvent('domready', init);
|
||||
BIN
bzz/bzzdemo/left.png
Normal file
|
After Width: | Height: | Size: 686 B |
491
bzz/bzzdemo/mootools-core-1.4.js
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
/*
|
||||
---
|
||||
MooTools: the javascript framework
|
||||
|
||||
web build:
|
||||
- http://mootools.net/core/76bf47062d6c1983d66ce47ad66aa0e0
|
||||
|
||||
packager build:
|
||||
- packager build Core/Core Core/Array Core/String Core/Number Core/Function Core/Object Core/Event Core/Browser Core/Class Core/Class.Extras Core/Slick.Parser Core/Slick.Finder Core/Element Core/Element.Style Core/Element.Event Core/Element.Delegation Core/Element.Dimensions Core/Fx Core/Fx.CSS Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request Core/Request.HTML Core/Request.JSON Core/Cookie Core/JSON Core/DOMReady Core/Swiff
|
||||
|
||||
copyrights:
|
||||
- [MooTools](http://mootools.net)
|
||||
|
||||
licenses:
|
||||
- [MIT License](http://mootools.net/license.txt)
|
||||
...
|
||||
*/
|
||||
|
||||
(function(){this.MooTools={version:"1.4.5",build:"ab8ea8824dc3b24b6666867a2c4ed58ebb762cf0"};var o=this.typeOf=function(i){if(i==null){return"null";}if(i.$family!=null){return i.$family();
|
||||
}if(i.nodeName){if(i.nodeType==1){return"element";}if(i.nodeType==3){return(/\S/).test(i.nodeValue)?"textnode":"whitespace";}}else{if(typeof i.length=="number"){if(i.callee){return"arguments";
|
||||
}if("item" in i){return"collection";}}}return typeof i;};var j=this.instanceOf=function(t,i){if(t==null){return false;}var s=t.$constructor||t.constructor;
|
||||
while(s){if(s===i){return true;}s=s.parent;}if(!t.hasOwnProperty){return false;}return t instanceof i;};var f=this.Function;var p=true;for(var k in {toString:1}){p=null;
|
||||
}if(p){p=["hasOwnProperty","valueOf","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","constructor"];}f.prototype.overloadSetter=function(s){var i=this;
|
||||
return function(u,t){if(u==null){return this;}if(s||typeof u!="string"){for(var v in u){i.call(this,v,u[v]);}if(p){for(var w=p.length;w--;){v=p[w];if(u.hasOwnProperty(v)){i.call(this,v,u[v]);
|
||||
}}}}else{i.call(this,u,t);}return this;};};f.prototype.overloadGetter=function(s){var i=this;return function(u){var v,t;if(typeof u!="string"){v=u;}else{if(arguments.length>1){v=arguments;
|
||||
}else{if(s){v=[u];}}}if(v){t={};for(var w=0;w<v.length;w++){t[v[w]]=i.call(this,v[w]);}}else{t=i.call(this,u);}return t;};};f.prototype.extend=function(i,s){this[i]=s;
|
||||
}.overloadSetter();f.prototype.implement=function(i,s){this.prototype[i]=s;}.overloadSetter();var n=Array.prototype.slice;f.from=function(i){return(o(i)=="function")?i:function(){return i;
|
||||
};};Array.from=function(i){if(i==null){return[];}return(a.isEnumerable(i)&&typeof i!="string")?(o(i)=="array")?i:n.call(i):[i];};Number.from=function(s){var i=parseFloat(s);
|
||||
return isFinite(i)?i:null;};String.from=function(i){return i+"";};f.implement({hide:function(){this.$hidden=true;return this;},protect:function(){this.$protected=true;
|
||||
return this;}});var a=this.Type=function(u,t){if(u){var s=u.toLowerCase();var i=function(v){return(o(v)==s);};a["is"+u]=i;if(t!=null){t.prototype.$family=(function(){return s;
|
||||
}).hide();}}if(t==null){return null;}t.extend(this);t.$constructor=a;t.prototype.$constructor=t;return t;};var e=Object.prototype.toString;a.isEnumerable=function(i){return(i!=null&&typeof i.length=="number"&&e.call(i)!="[object Function]");
|
||||
};var q={};var r=function(i){var s=o(i.prototype);return q[s]||(q[s]=[]);};var b=function(t,x){if(x&&x.$hidden){return;}var s=r(this);for(var u=0;u<s.length;
|
||||
u++){var w=s[u];if(o(w)=="type"){b.call(w,t,x);}else{w.call(this,t,x);}}var v=this.prototype[t];if(v==null||!v.$protected){this.prototype[t]=x;}if(this[t]==null&&o(x)=="function"){m.call(this,t,function(i){return x.apply(i,n.call(arguments,1));
|
||||
});}};var m=function(i,t){if(t&&t.$hidden){return;}var s=this[i];if(s==null||!s.$protected){this[i]=t;}};a.implement({implement:b.overloadSetter(),extend:m.overloadSetter(),alias:function(i,s){b.call(this,i,this.prototype[s]);
|
||||
}.overloadSetter(),mirror:function(i){r(this).push(i);return this;}});new a("Type",a);var d=function(s,x,v){var u=(x!=Object),B=x.prototype;if(u){x=new a(s,x);
|
||||
}for(var y=0,w=v.length;y<w;y++){var C=v[y],A=x[C],z=B[C];if(A){A.protect();}if(u&&z){x.implement(C,z.protect());}}if(u){var t=B.propertyIsEnumerable(v[0]);
|
||||
x.forEachMethod=function(G){if(!t){for(var F=0,D=v.length;F<D;F++){G.call(B,B[v[F]],v[F]);}}for(var E in B){G.call(B,B[E],E);}};}return d;};d("String",String,["charAt","charCodeAt","concat","indexOf","lastIndexOf","match","quote","replace","search","slice","split","substr","substring","trim","toLowerCase","toUpperCase"])("Array",Array,["pop","push","reverse","shift","sort","splice","unshift","concat","join","slice","indexOf","lastIndexOf","filter","forEach","every","map","some","reduce","reduceRight"])("Number",Number,["toExponential","toFixed","toLocaleString","toPrecision"])("Function",f,["apply","call","bind"])("RegExp",RegExp,["exec","test"])("Object",Object,["create","defineProperty","defineProperties","keys","getPrototypeOf","getOwnPropertyDescriptor","getOwnPropertyNames","preventExtensions","isExtensible","seal","isSealed","freeze","isFrozen"])("Date",Date,["now"]);
|
||||
Object.extend=m.overloadSetter();Date.extend("now",function(){return +(new Date);});new a("Boolean",Boolean);Number.prototype.$family=function(){return isFinite(this)?"number":"null";
|
||||
}.hide();Number.extend("random",function(s,i){return Math.floor(Math.random()*(i-s+1)+s);});var g=Object.prototype.hasOwnProperty;Object.extend("forEach",function(i,t,u){for(var s in i){if(g.call(i,s)){t.call(u,i[s],s,i);
|
||||
}}});Object.each=Object.forEach;Array.implement({forEach:function(u,v){for(var t=0,s=this.length;t<s;t++){if(t in this){u.call(v,this[t],t,this);}}},each:function(i,s){Array.forEach(this,i,s);
|
||||
return this;}});var l=function(i){switch(o(i)){case"array":return i.clone();case"object":return Object.clone(i);default:return i;}};Array.implement("clone",function(){var s=this.length,t=new Array(s);
|
||||
while(s--){t[s]=l(this[s]);}return t;});var h=function(s,i,t){switch(o(t)){case"object":if(o(s[i])=="object"){Object.merge(s[i],t);}else{s[i]=Object.clone(t);
|
||||
}break;case"array":s[i]=t.clone();break;default:s[i]=t;}return s;};Object.extend({merge:function(z,u,t){if(o(u)=="string"){return h(z,u,t);}for(var y=1,s=arguments.length;
|
||||
y<s;y++){var w=arguments[y];for(var x in w){h(z,x,w[x]);}}return z;},clone:function(i){var t={};for(var s in i){t[s]=l(i[s]);}return t;},append:function(w){for(var v=1,t=arguments.length;
|
||||
v<t;v++){var s=arguments[v]||{};for(var u in s){w[u]=s[u];}}return w;}});["Object","WhiteSpace","TextNode","Collection","Arguments"].each(function(i){new a(i);
|
||||
});var c=Date.now();String.extend("uniqueID",function(){return(c++).toString(36);});})();Array.implement({every:function(c,d){for(var b=0,a=this.length>>>0;
|
||||
b<a;b++){if((b in this)&&!c.call(d,this[b],b,this)){return false;}}return true;},filter:function(d,f){var c=[];for(var e,b=0,a=this.length>>>0;b<a;b++){if(b in this){e=this[b];
|
||||
if(d.call(f,e,b,this)){c.push(e);}}}return c;},indexOf:function(c,d){var b=this.length>>>0;for(var a=(d<0)?Math.max(0,b+d):d||0;a<b;a++){if(this[a]===c){return a;
|
||||
}}return -1;},map:function(c,e){var d=this.length>>>0,b=Array(d);for(var a=0;a<d;a++){if(a in this){b[a]=c.call(e,this[a],a,this);}}return b;},some:function(c,d){for(var b=0,a=this.length>>>0;
|
||||
b<a;b++){if((b in this)&&c.call(d,this[b],b,this)){return true;}}return false;},clean:function(){return this.filter(function(a){return a!=null;});},invoke:function(a){var b=Array.slice(arguments,1);
|
||||
return this.map(function(c){return c[a].apply(c,b);});},associate:function(c){var d={},b=Math.min(this.length,c.length);for(var a=0;a<b;a++){d[c[a]]=this[a];
|
||||
}return d;},link:function(c){var a={};for(var e=0,b=this.length;e<b;e++){for(var d in c){if(c[d](this[e])){a[d]=this[e];delete c[d];break;}}}return a;},contains:function(a,b){return this.indexOf(a,b)!=-1;
|
||||
},append:function(a){this.push.apply(this,a);return this;},getLast:function(){return(this.length)?this[this.length-1]:null;},getRandom:function(){return(this.length)?this[Number.random(0,this.length-1)]:null;
|
||||
},include:function(a){if(!this.contains(a)){this.push(a);}return this;},combine:function(c){for(var b=0,a=c.length;b<a;b++){this.include(c[b]);}return this;
|
||||
},erase:function(b){for(var a=this.length;a--;){if(this[a]===b){this.splice(a,1);}}return this;},empty:function(){this.length=0;return this;},flatten:function(){var d=[];
|
||||
for(var b=0,a=this.length;b<a;b++){var c=typeOf(this[b]);if(c=="null"){continue;}d=d.concat((c=="array"||c=="collection"||c=="arguments"||instanceOf(this[b],Array))?Array.flatten(this[b]):this[b]);
|
||||
}return d;},pick:function(){for(var b=0,a=this.length;b<a;b++){if(this[b]!=null){return this[b];}}return null;},hexToRgb:function(b){if(this.length!=3){return null;
|
||||
}var a=this.map(function(c){if(c.length==1){c+=c;}return c.toInt(16);});return(b)?a:"rgb("+a+")";},rgbToHex:function(d){if(this.length<3){return null;}if(this.length==4&&this[3]==0&&!d){return"transparent";
|
||||
}var b=[];for(var a=0;a<3;a++){var c=(this[a]-0).toString(16);b.push((c.length==1)?"0"+c:c);}return(d)?b:"#"+b.join("");}});String.implement({test:function(a,b){return((typeOf(a)=="regexp")?a:new RegExp(""+a,b)).test(this);
|
||||
},contains:function(a,b){return(b)?(b+this+b).indexOf(b+a+b)>-1:String(this).indexOf(a)>-1;},trim:function(){return String(this).replace(/^\s+|\s+$/g,"");
|
||||
},clean:function(){return String(this).replace(/\s+/g," ").trim();},camelCase:function(){return String(this).replace(/-\D/g,function(a){return a.charAt(1).toUpperCase();
|
||||
});},hyphenate:function(){return String(this).replace(/[A-Z]/g,function(a){return("-"+a.charAt(0).toLowerCase());});},capitalize:function(){return String(this).replace(/\b[a-z]/g,function(a){return a.toUpperCase();
|
||||
});},escapeRegExp:function(){return String(this).replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1");},toInt:function(a){return parseInt(this,a||10);},toFloat:function(){return parseFloat(this);
|
||||
},hexToRgb:function(b){var a=String(this).match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);return(a)?a.slice(1).hexToRgb(b):null;},rgbToHex:function(b){var a=String(this).match(/\d{1,3}/g);
|
||||
return(a)?a.rgbToHex(b):null;},substitute:function(a,b){return String(this).replace(b||(/\\?\{([^{}]+)\}/g),function(d,c){if(d.charAt(0)=="\\"){return d.slice(1);
|
||||
}return(a[c]!=null)?a[c]:"";});}});Number.implement({limit:function(b,a){return Math.min(a,Math.max(b,this));},round:function(a){a=Math.pow(10,a||0).toFixed(a<0?-a:0);
|
||||
return Math.round(this*a)/a;},times:function(b,c){for(var a=0;a<this;a++){b.call(c,a,this);}},toFloat:function(){return parseFloat(this);},toInt:function(a){return parseInt(this,a||10);
|
||||
}});Number.alias("each","times");(function(b){var a={};b.each(function(c){if(!Number[c]){a[c]=function(){return Math[c].apply(null,[this].concat(Array.from(arguments)));
|
||||
};}});Number.implement(a);})(["abs","acos","asin","atan","atan2","ceil","cos","exp","floor","log","max","min","pow","sin","sqrt","tan"]);Function.extend({attempt:function(){for(var b=0,a=arguments.length;
|
||||
b<a;b++){try{return arguments[b]();}catch(c){}}return null;}});Function.implement({attempt:function(a,c){try{return this.apply(c,Array.from(a));}catch(b){}return null;
|
||||
},bind:function(e){var a=this,b=arguments.length>1?Array.slice(arguments,1):null,d=function(){};var c=function(){var g=e,h=arguments.length;if(this instanceof c){d.prototype=a.prototype;
|
||||
g=new d;}var f=(!b&&!h)?a.call(g):a.apply(g,b&&h?b.concat(Array.slice(arguments)):b||arguments);return g==e?f:g;};return c;},pass:function(b,c){var a=this;
|
||||
if(b!=null){b=Array.from(b);}return function(){return a.apply(c,b||arguments);};},delay:function(b,c,a){return setTimeout(this.pass((a==null?[]:a),c),b);
|
||||
},periodical:function(c,b,a){return setInterval(this.pass((a==null?[]:a),b),c);}});(function(){var a=Object.prototype.hasOwnProperty;Object.extend({subset:function(d,g){var f={};
|
||||
for(var e=0,b=g.length;e<b;e++){var c=g[e];if(c in d){f[c]=d[c];}}return f;},map:function(b,e,f){var d={};for(var c in b){if(a.call(b,c)){d[c]=e.call(f,b[c],c,b);
|
||||
}}return d;},filter:function(b,e,g){var d={};for(var c in b){var f=b[c];if(a.call(b,c)&&e.call(g,f,c,b)){d[c]=f;}}return d;},every:function(b,d,e){for(var c in b){if(a.call(b,c)&&!d.call(e,b[c],c)){return false;
|
||||
}}return true;},some:function(b,d,e){for(var c in b){if(a.call(b,c)&&d.call(e,b[c],c)){return true;}}return false;},keys:function(b){var d=[];for(var c in b){if(a.call(b,c)){d.push(c);
|
||||
}}return d;},values:function(c){var b=[];for(var d in c){if(a.call(c,d)){b.push(c[d]);}}return b;},getLength:function(b){return Object.keys(b).length;},keyOf:function(b,d){for(var c in b){if(a.call(b,c)&&b[c]===d){return c;
|
||||
}}return null;},contains:function(b,c){return Object.keyOf(b,c)!=null;},toQueryString:function(b,c){var d=[];Object.each(b,function(h,g){if(c){g=c+"["+g+"]";
|
||||
}var f;switch(typeOf(h)){case"object":f=Object.toQueryString(h,g);break;case"array":var e={};h.each(function(k,j){e[j]=k;});f=Object.toQueryString(e,g);
|
||||
break;default:f=g+"="+encodeURIComponent(h);}if(h!=null){d.push(f);}});return d.join("&");}});})();(function(){var j=this.document;var g=j.window=this;
|
||||
var a=navigator.userAgent.toLowerCase(),b=navigator.platform.toLowerCase(),h=a.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0],d=h[1]=="ie"&&j.documentMode;
|
||||
var n=this.Browser={extend:Function.prototype.extend,name:(h[1]=="version")?h[3]:h[1],version:d||parseFloat((h[1]=="opera"&&h[4])?h[4]:h[2]),Platform:{name:a.match(/ip(?:ad|od|hone)/)?"ios":(a.match(/(?:webos|android)/)||b.match(/mac|win|linux/)||["other"])[0]},Features:{xpath:!!(j.evaluate),air:!!(g.runtime),query:!!(j.querySelector),json:!!(g.JSON)},Plugins:{}};
|
||||
n[n.name]=true;n[n.name+parseInt(n.version,10)]=true;n.Platform[n.Platform.name]=true;n.Request=(function(){var p=function(){return new XMLHttpRequest();
|
||||
};var o=function(){return new ActiveXObject("MSXML2.XMLHTTP");};var e=function(){return new ActiveXObject("Microsoft.XMLHTTP");};return Function.attempt(function(){p();
|
||||
return p;},function(){o();return o;},function(){e();return e;});})();n.Features.xhr=!!(n.Request);var i=(Function.attempt(function(){return navigator.plugins["Shockwave Flash"].description;
|
||||
},function(){return new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version");})||"0 r0").match(/\d+/g);n.Plugins.Flash={version:Number(i[0]||"0."+i[1])||0,build:Number(i[2])||0};
|
||||
n.exec=function(o){if(!o){return o;}if(g.execScript){g.execScript(o);}else{var e=j.createElement("script");e.setAttribute("type","text/javascript");e.text=o;
|
||||
j.head.appendChild(e);j.head.removeChild(e);}return o;};String.implement("stripScripts",function(o){var e="";var p=this.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi,function(q,r){e+=r+"\n";
|
||||
return"";});if(o===true){n.exec(e);}else{if(typeOf(o)=="function"){o(e,p);}}return p;});n.extend({Document:this.Document,Window:this.Window,Element:this.Element,Event:this.Event});
|
||||
this.Window=this.$constructor=new Type("Window",function(){});this.$family=Function.from("window").hide();Window.mirror(function(e,o){g[e]=o;});this.Document=j.$constructor=new Type("Document",function(){});
|
||||
j.$family=Function.from("document").hide();Document.mirror(function(e,o){j[e]=o;});j.html=j.documentElement;if(!j.head){j.head=j.getElementsByTagName("head")[0];
|
||||
}if(j.execCommand){try{j.execCommand("BackgroundImageCache",false,true);}catch(f){}}if(this.attachEvent&&!this.addEventListener){var c=function(){this.detachEvent("onunload",c);
|
||||
j.head=j.html=j.window=null;};this.attachEvent("onunload",c);}var l=Array.from;try{l(j.html.childNodes);}catch(f){Array.from=function(o){if(typeof o!="string"&&Type.isEnumerable(o)&&typeOf(o)!="array"){var e=o.length,p=new Array(e);
|
||||
while(e--){p[e]=o[e];}return p;}return l(o);};var k=Array.prototype,m=k.slice;["pop","push","reverse","shift","sort","splice","unshift","concat","join","slice"].each(function(e){var o=k[e];
|
||||
Array[e]=function(p){return o.apply(Array.from(p),m.call(arguments,1));};});}})();(function(){var b={};var a=this.DOMEvent=new Type("DOMEvent",function(c,g){if(!g){g=window;
|
||||
}c=c||g.event;if(c.$extended){return c;}this.event=c;this.$extended=true;this.shift=c.shiftKey;this.control=c.ctrlKey;this.alt=c.altKey;this.meta=c.metaKey;
|
||||
var i=this.type=c.type;var h=c.target||c.srcElement;while(h&&h.nodeType==3){h=h.parentNode;}this.target=document.id(h);if(i.indexOf("key")==0){var d=this.code=(c.which||c.keyCode);
|
||||
this.key=b[d];if(i=="keydown"){if(d>111&&d<124){this.key="f"+(d-111);}else{if(d>95&&d<106){this.key=d-96;}}}if(this.key==null){this.key=String.fromCharCode(d).toLowerCase();
|
||||
}}else{if(i=="click"||i=="dblclick"||i=="contextmenu"||i=="DOMMouseScroll"||i.indexOf("mouse")==0){var j=g.document;j=(!j.compatMode||j.compatMode=="CSS1Compat")?j.html:j.body;
|
||||
this.page={x:(c.pageX!=null)?c.pageX:c.clientX+j.scrollLeft,y:(c.pageY!=null)?c.pageY:c.clientY+j.scrollTop};this.client={x:(c.pageX!=null)?c.pageX-g.pageXOffset:c.clientX,y:(c.pageY!=null)?c.pageY-g.pageYOffset:c.clientY};
|
||||
if(i=="DOMMouseScroll"||i=="mousewheel"){this.wheel=(c.wheelDelta)?c.wheelDelta/120:-(c.detail||0)/3;}this.rightClick=(c.which==3||c.button==2);if(i=="mouseover"||i=="mouseout"){var k=c.relatedTarget||c[(i=="mouseover"?"from":"to")+"Element"];
|
||||
while(k&&k.nodeType==3){k=k.parentNode;}this.relatedTarget=document.id(k);}}else{if(i.indexOf("touch")==0||i.indexOf("gesture")==0){this.rotation=c.rotation;
|
||||
this.scale=c.scale;this.targetTouches=c.targetTouches;this.changedTouches=c.changedTouches;var f=this.touches=c.touches;if(f&&f[0]){var e=f[0];this.page={x:e.pageX,y:e.pageY};
|
||||
this.client={x:e.clientX,y:e.clientY};}}}}if(!this.client){this.client={};}if(!this.page){this.page={};}});a.implement({stop:function(){return this.preventDefault().stopPropagation();
|
||||
},stopPropagation:function(){if(this.event.stopPropagation){this.event.stopPropagation();}else{this.event.cancelBubble=true;}return this;},preventDefault:function(){if(this.event.preventDefault){this.event.preventDefault();
|
||||
}else{this.event.returnValue=false;}return this;}});a.defineKey=function(d,c){b[d]=c;return this;};a.defineKeys=a.defineKey.overloadSetter(true);a.defineKeys({"38":"up","40":"down","37":"left","39":"right","27":"esc","32":"space","8":"backspace","9":"tab","46":"delete","13":"enter"});
|
||||
})();(function(){var a=this.Class=new Type("Class",function(h){if(instanceOf(h,Function)){h={initialize:h};}var g=function(){e(this);if(g.$prototyping){return this;
|
||||
}this.$caller=null;var i=(this.initialize)?this.initialize.apply(this,arguments):this;this.$caller=this.caller=null;return i;}.extend(this).implement(h);
|
||||
g.$constructor=a;g.prototype.$constructor=g;g.prototype.parent=c;return g;});var c=function(){if(!this.$caller){throw new Error('The method "parent" cannot be called.');
|
||||
}var g=this.$caller.$name,h=this.$caller.$owner.parent,i=(h)?h.prototype[g]:null;if(!i){throw new Error('The method "'+g+'" has no parent.');}return i.apply(this,arguments);
|
||||
};var e=function(g){for(var h in g){var j=g[h];switch(typeOf(j)){case"object":var i=function(){};i.prototype=j;g[h]=e(new i);break;case"array":g[h]=j.clone();
|
||||
break;}}return g;};var b=function(g,h,j){if(j.$origin){j=j.$origin;}var i=function(){if(j.$protected&&this.$caller==null){throw new Error('The method "'+h+'" cannot be called.');
|
||||
}var l=this.caller,m=this.$caller;this.caller=m;this.$caller=i;var k=j.apply(this,arguments);this.$caller=m;this.caller=l;return k;}.extend({$owner:g,$origin:j,$name:h});
|
||||
return i;};var f=function(h,i,g){if(a.Mutators.hasOwnProperty(h)){i=a.Mutators[h].call(this,i);if(i==null){return this;}}if(typeOf(i)=="function"){if(i.$hidden){return this;
|
||||
}this.prototype[h]=(g)?i:b(this,h,i);}else{Object.merge(this.prototype,h,i);}return this;};var d=function(g){g.$prototyping=true;var h=new g;delete g.$prototyping;
|
||||
return h;};a.implement("implement",f.overloadSetter());a.Mutators={Extends:function(g){this.parent=g;this.prototype=d(g);},Implements:function(g){Array.from(g).each(function(j){var h=new j;
|
||||
for(var i in h){f.call(this,i,h[i],true);}},this);}};})();(function(){this.Chain=new Class({$chain:[],chain:function(){this.$chain.append(Array.flatten(arguments));
|
||||
return this;},callChain:function(){return(this.$chain.length)?this.$chain.shift().apply(this,arguments):false;},clearChain:function(){this.$chain.empty();
|
||||
return this;}});var a=function(b){return b.replace(/^on([A-Z])/,function(c,d){return d.toLowerCase();});};this.Events=new Class({$events:{},addEvent:function(d,c,b){d=a(d);
|
||||
this.$events[d]=(this.$events[d]||[]).include(c);if(b){c.internal=true;}return this;},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]);}return this;
|
||||
},fireEvent:function(e,c,b){e=a(e);var d=this.$events[e];if(!d){return this;}c=Array.from(c);d.each(function(f){if(b){f.delay(b,this,c);}else{f.apply(this,c);
|
||||
}},this);return this;},removeEvent:function(e,d){e=a(e);var c=this.$events[e];if(c&&!d.internal){var b=c.indexOf(d);if(b!=-1){delete c[b];}}return this;
|
||||
},removeEvents:function(d){var e;if(typeOf(d)=="object"){for(e in d){this.removeEvent(e,d[e]);}return this;}if(d){d=a(d);}for(e in this.$events){if(d&&d!=e){continue;
|
||||
}var c=this.$events[e];for(var b=c.length;b--;){if(b in c){this.removeEvent(e,c[b]);}}}return this;}});this.Options=new Class({setOptions:function(){var b=this.options=Object.merge.apply(null,[{},this.options].append(arguments));
|
||||
if(this.addEvent){for(var c in b){if(typeOf(b[c])!="function"||!(/^on[A-Z]/).test(c)){continue;}this.addEvent(c,b[c]);delete b[c];}}return this;}});})();
|
||||
(function(){var k,n,l,g,a={},c={},m=/\\/g;var e=function(q,p){if(q==null){return null;}if(q.Slick===true){return q;}q=(""+q).replace(/^\s+|\s+$/g,"");g=!!p;
|
||||
var o=(g)?c:a;if(o[q]){return o[q];}k={Slick:true,expressions:[],raw:q,reverse:function(){return e(this.raw,true);}};n=-1;while(q!=(q=q.replace(j,b))){}k.length=k.expressions.length;
|
||||
return o[k.raw]=(g)?h(k):k;};var i=function(o){if(o==="!"){return" ";}else{if(o===" "){return"!";}else{if((/^!/).test(o)){return o.replace(/^!/,"");}else{return"!"+o;
|
||||
}}}};var h=function(u){var r=u.expressions;for(var p=0;p<r.length;p++){var t=r[p];var q={parts:[],tag:"*",combinator:i(t[0].combinator)};for(var o=0;o<t.length;
|
||||
o++){var s=t[o];if(!s.reverseCombinator){s.reverseCombinator=" ";}s.combinator=s.reverseCombinator;delete s.reverseCombinator;}t.reverse().push(q);}return u;
|
||||
};var f=function(o){return o.replace(/[-[\]{}()*+?.\\^$|,#\s]/g,function(p){return"\\"+p;});};var j=new RegExp("^(?:\\s*(,)\\s*|\\s*(<combinator>+)\\s*|(\\s+)|(<unicode>+|\\*)|\\#(<unicode>+)|\\.(<unicode>+)|\\[\\s*(<unicode1>+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(<unicode>+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)".replace(/<combinator>/,"["+f(">+~`!@$%^&={}\\;</")+"]").replace(/<unicode>/g,"(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])").replace(/<unicode1>/g,"(?:[:\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])"));
|
||||
function b(x,s,D,z,r,C,q,B,A,y,u,F,G,v,p,w){if(s||n===-1){k.expressions[++n]=[];l=-1;if(s){return"";}}if(D||z||l===-1){D=D||" ";var t=k.expressions[n];
|
||||
if(g&&t[l]){t[l].reverseCombinator=i(D);}t[++l]={combinator:D,tag:"*"};}var o=k.expressions[n][l];if(r){o.tag=r.replace(m,"");}else{if(C){o.id=C.replace(m,"");
|
||||
}else{if(q){q=q.replace(m,"");if(!o.classList){o.classList=[];}if(!o.classes){o.classes=[];}o.classList.push(q);o.classes.push({value:q,regexp:new RegExp("(^|\\s)"+f(q)+"(\\s|$)")});
|
||||
}else{if(G){w=w||p;w=w?w.replace(m,""):null;if(!o.pseudos){o.pseudos=[];}o.pseudos.push({key:G.replace(m,""),value:w,type:F.length==1?"class":"element"});
|
||||
}else{if(B){B=B.replace(m,"");u=(u||"").replace(m,"");var E,H;switch(A){case"^=":H=new RegExp("^"+f(u));break;case"$=":H=new RegExp(f(u)+"$");break;case"~=":H=new RegExp("(^|\\s)"+f(u)+"(\\s|$)");
|
||||
break;case"|=":H=new RegExp("^"+f(u)+"(-|$)");break;case"=":E=function(I){return u==I;};break;case"*=":E=function(I){return I&&I.indexOf(u)>-1;};break;
|
||||
case"!=":E=function(I){return u!=I;};break;default:E=function(I){return !!I;};}if(u==""&&(/^[*$^]=$/).test(A)){E=function(){return false;};}if(!E){E=function(I){return I&&H.test(I);
|
||||
};}if(!o.attributes){o.attributes=[];}o.attributes.push({key:B,operator:A,value:u,test:E});}}}}}return"";}var d=(this.Slick||{});d.parse=function(o){return e(o);
|
||||
};d.escapeRegExp=f;if(!this.Slick){this.Slick=d;}}).apply((typeof exports!="undefined")?exports:this);(function(){var k={},m={},d=Object.prototype.toString;
|
||||
k.isNativeCode=function(c){return(/\{\s*\[native code\]\s*\}/).test(""+c);};k.isXML=function(c){return(!!c.xmlVersion)||(!!c.xml)||(d.call(c)=="[object XMLDocument]")||(c.nodeType==9&&c.documentElement.nodeName!="HTML");
|
||||
};k.setDocument=function(w){var p=w.nodeType;if(p==9){}else{if(p){w=w.ownerDocument;}else{if(w.navigator){w=w.document;}else{return;}}}if(this.document===w){return;
|
||||
}this.document=w;var A=w.documentElement,o=this.getUIDXML(A),s=m[o],r;if(s){for(r in s){this[r]=s[r];}return;}s=m[o]={};s.root=A;s.isXMLDocument=this.isXML(w);
|
||||
s.brokenStarGEBTN=s.starSelectsClosedQSA=s.idGetsName=s.brokenMixedCaseQSA=s.brokenGEBCN=s.brokenCheckedQSA=s.brokenEmptyAttributeQSA=s.isHTMLDocument=s.nativeMatchesSelector=false;
|
||||
var q,u,y,z,t;var x,v="slick_uniqueid";var c=w.createElement("div");var n=w.body||w.getElementsByTagName("body")[0]||A;n.appendChild(c);try{c.innerHTML='<a id="'+v+'"></a>';
|
||||
s.isHTMLDocument=!!w.getElementById(v);}catch(C){}if(s.isHTMLDocument){c.style.display="none";c.appendChild(w.createComment(""));u=(c.getElementsByTagName("*").length>1);
|
||||
try{c.innerHTML="foo</foo>";x=c.getElementsByTagName("*");q=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");}catch(C){}s.brokenStarGEBTN=u||q;try{c.innerHTML='<a name="'+v+'"></a><b id="'+v+'"></b>';
|
||||
s.idGetsName=w.getElementById(v)===c.firstChild;}catch(C){}if(c.getElementsByClassName){try{c.innerHTML='<a class="f"></a><a class="b"></a>';c.getElementsByClassName("b").length;
|
||||
c.firstChild.className="b";z=(c.getElementsByClassName("b").length!=2);}catch(C){}try{c.innerHTML='<a class="a"></a><a class="f b a"></a>';y=(c.getElementsByClassName("a").length!=2);
|
||||
}catch(C){}s.brokenGEBCN=z||y;}if(c.querySelectorAll){try{c.innerHTML="foo</foo>";x=c.querySelectorAll("*");s.starSelectsClosedQSA=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");
|
||||
}catch(C){}try{c.innerHTML='<a class="MiX"></a>';s.brokenMixedCaseQSA=!c.querySelectorAll(".MiX").length;}catch(C){}try{c.innerHTML='<select><option selected="selected">a</option></select>';
|
||||
s.brokenCheckedQSA=(c.querySelectorAll(":checked").length==0);}catch(C){}try{c.innerHTML='<a class=""></a>';s.brokenEmptyAttributeQSA=(c.querySelectorAll('[class*=""]').length!=0);
|
||||
}catch(C){}}try{c.innerHTML='<form action="s"><input id="action"/></form>';t=(c.firstChild.getAttribute("action")!="s");}catch(C){}s.nativeMatchesSelector=A.matchesSelector||A.mozMatchesSelector||A.webkitMatchesSelector;
|
||||
if(s.nativeMatchesSelector){try{s.nativeMatchesSelector.call(A,":slick");s.nativeMatchesSelector=null;}catch(C){}}}try{A.slick_expando=1;delete A.slick_expando;
|
||||
s.getUID=this.getUIDHTML;}catch(C){s.getUID=this.getUIDXML;}n.removeChild(c);c=x=n=null;s.getAttribute=(s.isHTMLDocument&&t)?function(G,E){var H=this.attributeGetters[E];
|
||||
if(H){return H.call(G);}var F=G.getAttributeNode(E);return(F)?F.nodeValue:null;}:function(F,E){var G=this.attributeGetters[E];return(G)?G.call(F):F.getAttribute(E);
|
||||
};s.hasAttribute=(A&&this.isNativeCode(A.hasAttribute))?function(F,E){return F.hasAttribute(E);}:function(F,E){F=F.getAttributeNode(E);return !!(F&&(F.specified||F.nodeValue));
|
||||
};var D=A&&this.isNativeCode(A.contains),B=w&&this.isNativeCode(w.contains);s.contains=(D&&B)?function(E,F){return E.contains(F);}:(D&&!B)?function(E,F){return E===F||((E===w)?w.documentElement:E).contains(F);
|
||||
}:(A&&A.compareDocumentPosition)?function(E,F){return E===F||!!(E.compareDocumentPosition(F)&16);}:function(E,F){if(F){do{if(F===E){return true;}}while((F=F.parentNode));
|
||||
}return false;};s.documentSorter=(A.compareDocumentPosition)?function(F,E){if(!F.compareDocumentPosition||!E.compareDocumentPosition){return 0;}return F.compareDocumentPosition(E)&4?-1:F===E?0:1;
|
||||
}:("sourceIndex" in A)?function(F,E){if(!F.sourceIndex||!E.sourceIndex){return 0;}return F.sourceIndex-E.sourceIndex;}:(w.createRange)?function(H,F){if(!H.ownerDocument||!F.ownerDocument){return 0;
|
||||
}var G=H.ownerDocument.createRange(),E=F.ownerDocument.createRange();G.setStart(H,0);G.setEnd(H,0);E.setStart(F,0);E.setEnd(F,0);return G.compareBoundaryPoints(Range.START_TO_END,E);
|
||||
}:null;A=null;for(r in s){this[r]=s[r];}};var f=/^([#.]?)((?:[\w-]+|\*))$/,h=/\[.+[*$^]=(?:""|'')?\]/,g={};k.search=function(U,z,H,s){var p=this.found=(s)?null:(H||[]);
|
||||
if(!U){return p;}else{if(U.navigator){U=U.document;}else{if(!U.nodeType){return p;}}}var F,O,V=this.uniques={},I=!!(H&&H.length),y=(U.nodeType==9);if(this.document!==(y?U:U.ownerDocument)){this.setDocument(U);
|
||||
}if(I){for(O=p.length;O--;){V[this.getUID(p[O])]=true;}}if(typeof z=="string"){var r=z.match(f);simpleSelectors:if(r){var u=r[1],v=r[2],A,E;if(!u){if(v=="*"&&this.brokenStarGEBTN){break simpleSelectors;
|
||||
}E=U.getElementsByTagName(v);if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{if(u=="#"){if(!this.isHTMLDocument||!y){break simpleSelectors;
|
||||
}A=U.getElementById(v);if(!A){return p;}if(this.idGetsName&&A.getAttributeNode("id").nodeValue!=v){break simpleSelectors;}if(s){return A||null;}if(!(I&&V[this.getUID(A)])){p.push(A);
|
||||
}}else{if(u=="."){if(!this.isHTMLDocument||((!U.getElementsByClassName||this.brokenGEBCN)&&U.querySelectorAll)){break simpleSelectors;}if(U.getElementsByClassName&&!this.brokenGEBCN){E=U.getElementsByClassName(v);
|
||||
if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{var T=new RegExp("(^|\\s)"+e.escapeRegExp(v)+"(\\s|$)");E=U.getElementsByTagName("*");
|
||||
for(O=0;A=E[O++];){className=A.className;if(!(className&&T.test(className))){continue;}if(s){return A;}if(!(I&&V[this.getUID(A)])){p.push(A);}}}}}}if(I){this.sort(p);
|
||||
}return(s)?null:p;}querySelector:if(U.querySelectorAll){if(!this.isHTMLDocument||g[z]||this.brokenMixedCaseQSA||(this.brokenCheckedQSA&&z.indexOf(":checked")>-1)||(this.brokenEmptyAttributeQSA&&h.test(z))||(!y&&z.indexOf(",")>-1)||e.disableQSA){break querySelector;
|
||||
}var S=z,x=U;if(!y){var C=x.getAttribute("id"),t="slickid__";x.setAttribute("id",t);S="#"+t+" "+S;U=x.parentNode;}try{if(s){return U.querySelector(S)||null;
|
||||
}else{E=U.querySelectorAll(S);}}catch(Q){g[z]=1;break querySelector;}finally{if(!y){if(C){x.setAttribute("id",C);}else{x.removeAttribute("id");}U=x;}}if(this.starSelectsClosedQSA){for(O=0;
|
||||
A=E[O++];){if(A.nodeName>"@"&&!(I&&V[this.getUID(A)])){p.push(A);}}}else{for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}if(I){this.sort(p);
|
||||
}return p;}F=this.Slick.parse(z);if(!F.length){return p;}}else{if(z==null){return p;}else{if(z.Slick){F=z;}else{if(this.contains(U.documentElement||U,z)){(p)?p.push(z):p=z;
|
||||
return p;}else{return p;}}}}this.posNTH={};this.posNTHLast={};this.posNTHType={};this.posNTHTypeLast={};this.push=(!I&&(s||(F.length==1&&F.expressions[0].length==1)))?this.pushArray:this.pushUID;
|
||||
if(p==null){p=[];}var M,L,K;var B,J,D,c,q,G,W;var N,P,o,w,R=F.expressions;search:for(O=0;(P=R[O]);O++){for(M=0;(o=P[M]);M++){B="combinator:"+o.combinator;
|
||||
if(!this[B]){continue search;}J=(this.isXMLDocument)?o.tag:o.tag.toUpperCase();D=o.id;c=o.classList;q=o.classes;G=o.attributes;W=o.pseudos;w=(M===(P.length-1));
|
||||
this.bitUniques={};if(w){this.uniques=V;this.found=p;}else{this.uniques={};this.found=[];}if(M===0){this[B](U,J,D,q,G,W,c);if(s&&w&&p.length){break search;
|
||||
}}else{if(s&&w){for(L=0,K=N.length;L<K;L++){this[B](N[L],J,D,q,G,W,c);if(p.length){break search;}}}else{for(L=0,K=N.length;L<K;L++){this[B](N[L],J,D,q,G,W,c);
|
||||
}}}N=this.found;}}if(I||(F.expressions.length>1)){this.sort(p);}return(s)?(p[0]||null):p;};k.uidx=1;k.uidk="slick-uniqueid";k.getUIDXML=function(n){var c=n.getAttribute(this.uidk);
|
||||
if(!c){c=this.uidx++;n.setAttribute(this.uidk,c);}return c;};k.getUIDHTML=function(c){return c.uniqueNumber||(c.uniqueNumber=this.uidx++);};k.sort=function(c){if(!this.documentSorter){return c;
|
||||
}c.sort(this.documentSorter);return c;};k.cacheNTH={};k.matchNTH=/^([+-]?\d*)?([a-z]+)?([+-]\d+)?$/;k.parseNTHArgument=function(q){var o=q.match(this.matchNTH);
|
||||
if(!o){return false;}var p=o[2]||false;var n=o[1]||1;if(n=="-"){n=-1;}var c=+o[3]||0;o=(p=="n")?{a:n,b:c}:(p=="odd")?{a:2,b:1}:(p=="even")?{a:2,b:0}:{a:0,b:n};
|
||||
return(this.cacheNTH[q]=o);};k.createNTHPseudo=function(p,n,c,o){return function(s,q){var u=this.getUID(s);if(!this[c][u]){var A=s.parentNode;if(!A){return false;
|
||||
}var r=A[p],t=1;if(o){var z=s.nodeName;do{if(r.nodeName!=z){continue;}this[c][this.getUID(r)]=t++;}while((r=r[n]));}else{do{if(r.nodeType!=1){continue;
|
||||
}this[c][this.getUID(r)]=t++;}while((r=r[n]));}}q=q||"n";var v=this.cacheNTH[q]||this.parseNTHArgument(q);if(!v){return false;}var y=v.a,x=v.b,w=this[c][u];
|
||||
if(y==0){return x==w;}if(y>0){if(w<x){return false;}}else{if(x<w){return false;}}return((w-x)%y)==0;};};k.pushArray=function(p,c,r,o,n,q){if(this.matchSelector(p,c,r,o,n,q)){this.found.push(p);
|
||||
}};k.pushUID=function(q,c,s,p,n,r){var o=this.getUID(q);if(!this.uniques[o]&&this.matchSelector(q,c,s,p,n,r)){this.uniques[o]=true;this.found.push(q);}};
|
||||
k.matchNode=function(n,o){if(this.isHTMLDocument&&this.nativeMatchesSelector){try{return this.nativeMatchesSelector.call(n,o.replace(/\[([^=]+)=\s*([^'"\]]+?)\s*\]/g,'[$1="$2"]'));
|
||||
}catch(u){}}var t=this.Slick.parse(o);if(!t){return true;}var r=t.expressions,s=0,q;for(q=0;(currentExpression=r[q]);q++){if(currentExpression.length==1){var p=currentExpression[0];
|
||||
if(this.matchSelector(n,(this.isXMLDocument)?p.tag:p.tag.toUpperCase(),p.id,p.classes,p.attributes,p.pseudos)){return true;}s++;}}if(s==t.length){return false;
|
||||
}var c=this.search(this.document,t),v;for(q=0;v=c[q++];){if(v===n){return true;}}return false;};k.matchPseudo=function(q,c,p){var n="pseudo:"+c;if(this[n]){return this[n](q,p);
|
||||
}var o=this.getAttribute(q,c);return(p)?p==o:!!o;};k.matchSelector=function(o,v,c,p,q,s){if(v){var t=(this.isXMLDocument)?o.nodeName:o.nodeName.toUpperCase();
|
||||
if(v=="*"){if(t<"@"){return false;}}else{if(t!=v){return false;}}}if(c&&o.getAttribute("id")!=c){return false;}var r,n,u;if(p){for(r=p.length;r--;){u=this.getAttribute(o,"class");
|
||||
if(!(u&&p[r].regexp.test(u))){return false;}}}if(q){for(r=q.length;r--;){n=q[r];if(n.operator?!n.test(this.getAttribute(o,n.key)):!this.hasAttribute(o,n.key)){return false;
|
||||
}}}if(s){for(r=s.length;r--;){n=s[r];if(!this.matchPseudo(o,n.key,n.value)){return false;}}}return true;};var j={" ":function(q,w,n,r,s,u,p){var t,v,o;
|
||||
if(this.isHTMLDocument){getById:if(n){v=this.document.getElementById(n);if((!v&&q.all)||(this.idGetsName&&v&&v.getAttributeNode("id").nodeValue!=n)){o=q.all[n];
|
||||
if(!o){return;}if(!o[0]){o=[o];}for(t=0;v=o[t++];){var c=v.getAttributeNode("id");if(c&&c.nodeValue==n){this.push(v,w,null,r,s,u);break;}}return;}if(!v){if(this.contains(this.root,q)){return;
|
||||
}else{break getById;}}else{if(this.document!==q&&!this.contains(q,v)){return;}}this.push(v,w,null,r,s,u);return;}getByClass:if(r&&q.getElementsByClassName&&!this.brokenGEBCN){o=q.getElementsByClassName(p.join(" "));
|
||||
if(!(o&&o.length)){break getByClass;}for(t=0;v=o[t++];){this.push(v,w,n,null,s,u);}return;}}getByTag:{o=q.getElementsByTagName(w);if(!(o&&o.length)){break getByTag;
|
||||
}if(!this.brokenStarGEBTN){w=null;}for(t=0;v=o[t++];){this.push(v,w,n,r,s,u);}}},">":function(p,c,r,o,n,q){if((p=p.firstChild)){do{if(p.nodeType==1){this.push(p,c,r,o,n,q);
|
||||
}}while((p=p.nextSibling));}},"+":function(p,c,r,o,n,q){while((p=p.nextSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q);break;}}},"^":function(p,c,r,o,n,q){p=p.firstChild;
|
||||
if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:+"](p,c,r,o,n,q);}}},"~":function(q,c,s,p,n,r){while((q=q.nextSibling)){if(q.nodeType!=1){continue;
|
||||
}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}},"++":function(p,c,r,o,n,q){this["combinator:+"](p,c,r,o,n,q);
|
||||
this["combinator:!+"](p,c,r,o,n,q);},"~~":function(p,c,r,o,n,q){this["combinator:~"](p,c,r,o,n,q);this["combinator:!~"](p,c,r,o,n,q);},"!":function(p,c,r,o,n,q){while((p=p.parentNode)){if(p!==this.document){this.push(p,c,r,o,n,q);
|
||||
}}},"!>":function(p,c,r,o,n,q){p=p.parentNode;if(p!==this.document){this.push(p,c,r,o,n,q);}},"!+":function(p,c,r,o,n,q){while((p=p.previousSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q);
|
||||
break;}}},"!^":function(p,c,r,o,n,q){p=p.lastChild;if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:!+"](p,c,r,o,n,q);}}},"!~":function(q,c,s,p,n,r){while((q=q.previousSibling)){if(q.nodeType!=1){continue;
|
||||
}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}}};for(var i in j){k["combinator:"+i]=j[i];}var l={empty:function(c){var n=c.firstChild;
|
||||
return !(n&&n.nodeType==1)&&!(c.innerText||c.textContent||"").length;},not:function(c,n){return !this.matchNode(c,n);},contains:function(c,n){return(c.innerText||c.textContent||"").indexOf(n)>-1;
|
||||
},"first-child":function(c){while((c=c.previousSibling)){if(c.nodeType==1){return false;}}return true;},"last-child":function(c){while((c=c.nextSibling)){if(c.nodeType==1){return false;
|
||||
}}return true;},"only-child":function(o){var n=o;while((n=n.previousSibling)){if(n.nodeType==1){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeType==1){return false;
|
||||
}}return true;},"nth-child":k.createNTHPseudo("firstChild","nextSibling","posNTH"),"nth-last-child":k.createNTHPseudo("lastChild","previousSibling","posNTHLast"),"nth-of-type":k.createNTHPseudo("firstChild","nextSibling","posNTHType",true),"nth-last-of-type":k.createNTHPseudo("lastChild","previousSibling","posNTHTypeLast",true),index:function(n,c){return this["pseudo:nth-child"](n,""+(c+1));
|
||||
},even:function(c){return this["pseudo:nth-child"](c,"2n");},odd:function(c){return this["pseudo:nth-child"](c,"2n+1");},"first-of-type":function(c){var n=c.nodeName;
|
||||
while((c=c.previousSibling)){if(c.nodeName==n){return false;}}return true;},"last-of-type":function(c){var n=c.nodeName;while((c=c.nextSibling)){if(c.nodeName==n){return false;
|
||||
}}return true;},"only-of-type":function(o){var n=o,p=o.nodeName;while((n=n.previousSibling)){if(n.nodeName==p){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeName==p){return false;
|
||||
}}return true;},enabled:function(c){return !c.disabled;},disabled:function(c){return c.disabled;},checked:function(c){return c.checked||c.selected;},focus:function(c){return this.isHTMLDocument&&this.document.activeElement===c&&(c.href||c.type||this.hasAttribute(c,"tabindex"));
|
||||
},root:function(c){return(c===this.root);},selected:function(c){return c.selected;}};for(var b in l){k["pseudo:"+b]=l[b];}var a=k.attributeGetters={"for":function(){return("htmlFor" in this)?this.htmlFor:this.getAttribute("for");
|
||||
},href:function(){return("href" in this)?this.getAttribute("href",2):this.getAttribute("href");},style:function(){return(this.style)?this.style.cssText:this.getAttribute("style");
|
||||
},tabindex:function(){var c=this.getAttributeNode("tabindex");return(c&&c.specified)?c.nodeValue:null;},type:function(){return this.getAttribute("type");
|
||||
},maxlength:function(){var c=this.getAttributeNode("maxLength");return(c&&c.specified)?c.nodeValue:null;}};a.MAXLENGTH=a.maxLength=a.maxlength;var e=k.Slick=(this.Slick||{});
|
||||
e.version="1.1.7";e.search=function(n,o,c){return k.search(n,o,c);};e.find=function(c,n){return k.search(c,n,null,true);};e.contains=function(c,n){k.setDocument(c);
|
||||
return k.contains(c,n);};e.getAttribute=function(n,c){k.setDocument(n);return k.getAttribute(n,c);};e.hasAttribute=function(n,c){k.setDocument(n);return k.hasAttribute(n,c);
|
||||
};e.match=function(n,c){if(!(n&&c)){return false;}if(!c||c===n){return true;}k.setDocument(n);return k.matchNode(n,c);};e.defineAttributeGetter=function(c,n){k.attributeGetters[c]=n;
|
||||
return this;};e.lookupAttributeGetter=function(c){return k.attributeGetters[c];};e.definePseudo=function(c,n){k["pseudo:"+c]=function(p,o){return n.call(p,o);
|
||||
};return this;};e.lookupPseudo=function(c){var n=k["pseudo:"+c];if(n){return function(o){return n.call(this,o);};}return null;};e.override=function(n,c){k.override(n,c);
|
||||
return this;};e.isXML=k.isXML;e.uidOf=function(c){return k.getUIDHTML(c);};if(!this.Slick){this.Slick=e;}}).apply((typeof exports!="undefined")?exports:this);
|
||||
var Element=function(b,g){var h=Element.Constructors[b];if(h){return h(g);}if(typeof b!="string"){return document.id(b).set(g);}if(!g){g={};}if(!(/^[\w-]+$/).test(b)){var e=Slick.parse(b).expressions[0][0];
|
||||
b=(e.tag=="*")?"div":e.tag;if(e.id&&g.id==null){g.id=e.id;}var d=e.attributes;if(d){for(var a,f=0,c=d.length;f<c;f++){a=d[f];if(g[a.key]!=null){continue;
|
||||
}if(a.value!=null&&a.operator=="="){g[a.key]=a.value;}else{if(!a.value&&!a.operator){g[a.key]=true;}}}}if(e.classList&&g["class"]==null){g["class"]=e.classList.join(" ");
|
||||
}}return document.newElement(b,g);};if(Browser.Element){Element.prototype=Browser.Element.prototype;Element.prototype._fireEvent=(function(a){return function(b,c){return a.call(this,b,c);
|
||||
};})(Element.prototype.fireEvent);}new Type("Element",Element).mirror(function(a){if(Array.prototype[a]){return;}var b={};b[a]=function(){var h=[],e=arguments,j=true;
|
||||
for(var g=0,d=this.length;g<d;g++){var f=this[g],c=h[g]=f[a].apply(f,e);j=(j&&typeOf(c)=="element");}return(j)?new Elements(h):h;};Elements.implement(b);
|
||||
});if(!Browser.Element){Element.parent=Object;Element.Prototype={"$constructor":Element,"$family":Function.from("element").hide()};Element.mirror(function(a,b){Element.Prototype[a]=b;
|
||||
});}Element.Constructors={};var IFrame=new Type("IFrame",function(){var e=Array.link(arguments,{properties:Type.isObject,iframe:function(f){return(f!=null);
|
||||
}});var c=e.properties||{},b;if(e.iframe){b=document.id(e.iframe);}var d=c.onload||function(){};delete c.onload;c.id=c.name=[c.id,c.name,b?(b.id||b.name):"IFrame_"+String.uniqueID()].pick();
|
||||
b=new Element(b||"iframe",c);var a=function(){d.call(b.contentWindow);};if(window.frames[c.id]){a();}else{b.addListener("load",a);}return b;});var Elements=this.Elements=function(a){if(a&&a.length){var e={},d;
|
||||
for(var c=0;d=a[c++];){var b=Slick.uidOf(d);if(!e[b]){e[b]=true;this.push(d);}}}};Elements.prototype={length:0};Elements.parent=Array;new Type("Elements",Elements).implement({filter:function(a,b){if(!a){return this;
|
||||
}return new Elements(Array.filter(this,(typeOf(a)=="string")?function(c){return c.match(a);}:a,b));}.protect(),push:function(){var d=this.length;for(var b=0,a=arguments.length;
|
||||
b<a;b++){var c=document.id(arguments[b]);if(c){this[d++]=c;}}return(this.length=d);}.protect(),unshift:function(){var b=[];for(var c=0,a=arguments.length;
|
||||
c<a;c++){var d=document.id(arguments[c]);if(d){b.push(d);}}return Array.prototype.unshift.apply(this,b);}.protect(),concat:function(){var b=new Elements(this);
|
||||
for(var c=0,a=arguments.length;c<a;c++){var d=arguments[c];if(Type.isEnumerable(d)){b.append(d);}else{b.push(d);}}return b;}.protect(),append:function(c){for(var b=0,a=c.length;
|
||||
b<a;b++){this.push(c[b]);}return this;}.protect(),empty:function(){while(this.length){delete this[--this.length];}return this;}.protect()});(function(){var f=Array.prototype.splice,a={"0":0,"1":1,length:2};
|
||||
f.call(a,1,1);if(a[1]==1){Elements.implement("splice",function(){var g=this.length;var e=f.apply(this,arguments);while(g>=this.length){delete this[g--];
|
||||
}return e;}.protect());}Array.forEachMethod(function(g,e){Elements.implement(e,g);});Array.mirror(Elements);var d;try{d=(document.createElement("<input name=x>").name=="x");
|
||||
}catch(b){}var c=function(e){return(""+e).replace(/&/g,"&").replace(/"/g,""");};Document.implement({newElement:function(e,g){if(g&&g.checked!=null){g.defaultChecked=g.checked;
|
||||
}if(d&&g){e="<"+e;if(g.name){e+=' name="'+c(g.name)+'"';}if(g.type){e+=' type="'+c(g.type)+'"';}e+=">";delete g.name;delete g.type;}return this.id(this.createElement(e)).set(g);
|
||||
}});})();(function(){Slick.uidOf(window);Slick.uidOf(document);Document.implement({newTextNode:function(e){return this.createTextNode(e);},getDocument:function(){return this;
|
||||
},getWindow:function(){return this.window;},id:(function(){var e={string:function(E,D,l){E=Slick.find(l,"#"+E.replace(/(\W)/g,"\\$1"));return(E)?e.element(E,D):null;
|
||||
},element:function(D,E){Slick.uidOf(D);if(!E&&!D.$family&&!(/^(?:object|embed)$/i).test(D.tagName)){var l=D.fireEvent;D._fireEvent=function(F,G){return l(F,G);
|
||||
};Object.append(D,Element.Prototype);}return D;},object:function(D,E,l){if(D.toElement){return e.element(D.toElement(l),E);}return null;}};e.textnode=e.whitespace=e.window=e.document=function(l){return l;
|
||||
};return function(D,F,E){if(D&&D.$family&&D.uniqueNumber){return D;}var l=typeOf(D);return(e[l])?e[l](D,F,E||document):null;};})()});if(window.$==null){Window.implement("$",function(e,l){return document.id(e,l,this.document);
|
||||
});}Window.implement({getDocument:function(){return this.document;},getWindow:function(){return this;}});[Document,Element].invoke("implement",{getElements:function(e){return Slick.search(this,e,new Elements);
|
||||
},getElement:function(e){return document.id(Slick.find(this,e));}});var m={contains:function(e){return Slick.contains(this,e);}};if(!document.contains){Document.implement(m);
|
||||
}if(!document.createElement("div").contains){Element.implement(m);}var r=function(E,D){if(!E){return D;}E=Object.clone(Slick.parse(E));var l=E.expressions;
|
||||
for(var e=l.length;e--;){l[e][0].combinator=D;}return E;};Object.forEach({getNext:"~",getPrevious:"!~",getParent:"!"},function(e,l){Element.implement(l,function(D){return this.getElement(r(D,e));
|
||||
});});Object.forEach({getAllNext:"~",getAllPrevious:"!~",getSiblings:"~~",getChildren:">",getParents:"!"},function(e,l){Element.implement(l,function(D){return this.getElements(r(D,e));
|
||||
});});Element.implement({getFirst:function(e){return document.id(Slick.search(this,r(e,">"))[0]);},getLast:function(e){return document.id(Slick.search(this,r(e,">")).getLast());
|
||||
},getWindow:function(){return this.ownerDocument.window;},getDocument:function(){return this.ownerDocument;},getElementById:function(e){return document.id(Slick.find(this,"#"+(""+e).replace(/(\W)/g,"\\$1")));
|
||||
},match:function(e){return !e||Slick.match(this,e);}});if(window.$$==null){Window.implement("$$",function(e){if(arguments.length==1){if(typeof e=="string"){return Slick.search(this.document,e,new Elements);
|
||||
}else{if(Type.isEnumerable(e)){return new Elements(e);}}}return new Elements(arguments);});}var w={before:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e);
|
||||
}},after:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e.nextSibling);}},bottom:function(l,e){e.appendChild(l);},top:function(l,e){e.insertBefore(l,e.firstChild);
|
||||
}};w.inside=w.bottom;var j={},d={};var k={};Array.forEach(["type","value","defaultValue","accessKey","cellPadding","cellSpacing","colSpan","frameBorder","rowSpan","tabIndex","useMap"],function(e){k[e.toLowerCase()]=e;
|
||||
});k.html="innerHTML";k.text=(document.createElement("div").textContent==null)?"innerText":"textContent";Object.forEach(k,function(l,e){d[e]=function(D,E){D[l]=E;
|
||||
};j[e]=function(D){return D[l];};});var x=["compact","nowrap","ismap","declare","noshade","checked","disabled","readOnly","multiple","selected","noresize","defer","defaultChecked","autofocus","controls","autoplay","loop"];
|
||||
var h={};Array.forEach(x,function(e){var l=e.toLowerCase();h[l]=e;d[l]=function(D,E){D[e]=!!E;};j[l]=function(D){return !!D[e];};});Object.append(d,{"class":function(e,l){("className" in e)?e.className=(l||""):e.setAttribute("class",l);
|
||||
},"for":function(e,l){("htmlFor" in e)?e.htmlFor=l:e.setAttribute("for",l);},style:function(e,l){(e.style)?e.style.cssText=l:e.setAttribute("style",l);
|
||||
},value:function(e,l){e.value=(l!=null)?l:"";}});j["class"]=function(e){return("className" in e)?e.className||null:e.getAttribute("class");};var f=document.createElement("button");
|
||||
try{f.type="button";}catch(z){}if(f.type!="button"){d.type=function(e,l){e.setAttribute("type",l);};}f=null;var p=document.createElement("input");p.value="t";
|
||||
p.type="submit";if(p.value!="t"){d.type=function(l,e){var D=l.value;l.type=e;l.value=D;};}p=null;var q=(function(e){e.random="attribute";return(e.getAttribute("random")=="attribute");
|
||||
})(document.createElement("div"));Element.implement({setProperty:function(l,D){var E=d[l.toLowerCase()];if(E){E(this,D);}else{if(q){var e=this.retrieve("$attributeWhiteList",{});
|
||||
}if(D==null){this.removeAttribute(l);if(q){delete e[l];}}else{this.setAttribute(l,""+D);if(q){e[l]=true;}}}return this;},setProperties:function(e){for(var l in e){this.setProperty(l,e[l]);
|
||||
}return this;},getProperty:function(F){var D=j[F.toLowerCase()];if(D){return D(this);}if(q){var l=this.getAttributeNode(F),E=this.retrieve("$attributeWhiteList",{});
|
||||
if(!l){return null;}if(l.expando&&!E[F]){var G=this.outerHTML;if(G.substr(0,G.search(/\/?['"]?>(?![^<]*<['"])/)).indexOf(F)<0){return null;}E[F]=true;}}var e=Slick.getAttribute(this,F);
|
||||
return(!e&&!Slick.hasAttribute(this,F))?null:e;},getProperties:function(){var e=Array.from(arguments);return e.map(this.getProperty,this).associate(e);
|
||||
},removeProperty:function(e){return this.setProperty(e,null);},removeProperties:function(){Array.each(arguments,this.removeProperty,this);return this;},set:function(D,l){var e=Element.Properties[D];
|
||||
(e&&e.set)?e.set.call(this,l):this.setProperty(D,l);}.overloadSetter(),get:function(l){var e=Element.Properties[l];return(e&&e.get)?e.get.apply(this):this.getProperty(l);
|
||||
}.overloadGetter(),erase:function(l){var e=Element.Properties[l];(e&&e.erase)?e.erase.apply(this):this.removeProperty(l);return this;},hasClass:function(e){return this.className.clean().contains(e," ");
|
||||
},addClass:function(e){if(!this.hasClass(e)){this.className=(this.className+" "+e).clean();}return this;},removeClass:function(e){this.className=this.className.replace(new RegExp("(^|\\s)"+e+"(?:\\s|$)"),"$1");
|
||||
return this;},toggleClass:function(e,l){if(l==null){l=!this.hasClass(e);}return(l)?this.addClass(e):this.removeClass(e);},adopt:function(){var E=this,e,G=Array.flatten(arguments),F=G.length;
|
||||
if(F>1){E=e=document.createDocumentFragment();}for(var D=0;D<F;D++){var l=document.id(G[D],true);if(l){E.appendChild(l);}}if(e){this.appendChild(e);}return this;
|
||||
},appendText:function(l,e){return this.grab(this.getDocument().newTextNode(l),e);},grab:function(l,e){w[e||"bottom"](document.id(l,true),this);return this;
|
||||
},inject:function(l,e){w[e||"bottom"](this,document.id(l,true));return this;},replaces:function(e){e=document.id(e,true);e.parentNode.replaceChild(this,e);
|
||||
return this;},wraps:function(l,e){l=document.id(l,true);return this.replaces(l).grab(l,e);},getSelected:function(){this.selectedIndex;return new Elements(Array.from(this.options).filter(function(e){return e.selected;
|
||||
}));},toQueryString:function(){var e=[];this.getElements("input, select, textarea").each(function(D){var l=D.type;if(!D.name||D.disabled||l=="submit"||l=="reset"||l=="file"||l=="image"){return;
|
||||
}var E=(D.get("tag")=="select")?D.getSelected().map(function(F){return document.id(F).get("value");}):((l=="radio"||l=="checkbox")&&!D.checked)?null:D.get("value");
|
||||
Array.from(E).each(function(F){if(typeof F!="undefined"){e.push(encodeURIComponent(D.name)+"="+encodeURIComponent(F));}});});return e.join("&");}});var i={},A={};
|
||||
var B=function(e){return(A[e]||(A[e]={}));};var v=function(l){var e=l.uniqueNumber;if(l.removeEvents){l.removeEvents();}if(l.clearAttributes){l.clearAttributes();
|
||||
}if(e!=null){delete i[e];delete A[e];}return l;};var C={input:"checked",option:"selected",textarea:"value"};Element.implement({destroy:function(){var e=v(this).getElementsByTagName("*");
|
||||
Array.each(e,v);Element.dispose(this);return null;},empty:function(){Array.from(this.childNodes).each(Element.dispose);return this;},dispose:function(){return(this.parentNode)?this.parentNode.removeChild(this):this;
|
||||
},clone:function(G,E){G=G!==false;var L=this.cloneNode(G),D=[L],F=[this],J;if(G){D.append(Array.from(L.getElementsByTagName("*")));F.append(Array.from(this.getElementsByTagName("*")));
|
||||
}for(J=D.length;J--;){var H=D[J],K=F[J];if(!E){H.removeAttribute("id");}if(H.clearAttributes){H.clearAttributes();H.mergeAttributes(K);H.removeAttribute("uniqueNumber");
|
||||
if(H.options){var O=H.options,e=K.options;for(var I=O.length;I--;){O[I].selected=e[I].selected;}}}var l=C[K.tagName.toLowerCase()];if(l&&K[l]){H[l]=K[l];
|
||||
}}if(Browser.ie){var M=L.getElementsByTagName("object"),N=this.getElementsByTagName("object");for(J=M.length;J--;){M[J].outerHTML=N[J].outerHTML;}}return document.id(L);
|
||||
}});[Element,Window,Document].invoke("implement",{addListener:function(E,D){if(E=="unload"){var e=D,l=this;D=function(){l.removeListener("unload",D);e();
|
||||
};}else{i[Slick.uidOf(this)]=this;}if(this.addEventListener){this.addEventListener(E,D,!!arguments[2]);}else{this.attachEvent("on"+E,D);}return this;},removeListener:function(l,e){if(this.removeEventListener){this.removeEventListener(l,e,!!arguments[2]);
|
||||
}else{this.detachEvent("on"+l,e);}return this;},retrieve:function(l,e){var E=B(Slick.uidOf(this)),D=E[l];if(e!=null&&D==null){D=E[l]=e;}return D!=null?D:null;
|
||||
},store:function(l,e){var D=B(Slick.uidOf(this));D[l]=e;return this;},eliminate:function(e){var l=B(Slick.uidOf(this));delete l[e];return this;}});if(window.attachEvent&&!window.addEventListener){window.addListener("unload",function(){Object.each(i,v);
|
||||
if(window.CollectGarbage){CollectGarbage();}});}Element.Properties={};Element.Properties.style={set:function(e){this.style.cssText=e;},get:function(){return this.style.cssText;
|
||||
},erase:function(){this.style.cssText="";}};Element.Properties.tag={get:function(){return this.tagName.toLowerCase();}};Element.Properties.html={set:function(e){if(e==null){e="";
|
||||
}else{if(typeOf(e)=="array"){e=e.join("");}}this.innerHTML=e;},erase:function(){this.innerHTML="";}};var t=document.createElement("div");t.innerHTML="<nav></nav>";
|
||||
var a=(t.childNodes.length==1);if(!a){var s="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),b=document.createDocumentFragment(),u=s.length;
|
||||
while(u--){b.createElement(s[u]);}}t=null;var g=Function.attempt(function(){var e=document.createElement("table");e.innerHTML="<tr><td></td></tr>";return true;
|
||||
});var c=document.createElement("tr"),o="<td></td>";c.innerHTML=o;var y=(c.innerHTML==o);c=null;if(!g||!y||!a){Element.Properties.html.set=(function(l){var e={table:[1,"<table>","</table>"],select:[1,"<select>","</select>"],tbody:[2,"<table><tbody>","</tbody></table>"],tr:[3,"<table><tbody><tr>","</tr></tbody></table>"]};
|
||||
e.thead=e.tfoot=e.tbody;return function(D){var E=e[this.get("tag")];if(!E&&!a){E=[0,"",""];}if(!E){return l.call(this,D);}var H=E[0],G=document.createElement("div"),F=G;
|
||||
if(!a){b.appendChild(G);}G.innerHTML=[E[1],D,E[2]].flatten().join("");while(H--){F=F.firstChild;}this.empty().adopt(F.childNodes);if(!a){b.removeChild(G);
|
||||
}G=null;};})(Element.Properties.html.set);}var n=document.createElement("form");n.innerHTML="<select><option>s</option></select>";if(n.firstChild.value!="s"){Element.Properties.value={set:function(G){var l=this.get("tag");
|
||||
if(l!="select"){return this.setProperty("value",G);}var D=this.getElements("option");for(var E=0;E<D.length;E++){var F=D[E],e=F.getAttributeNode("value"),H=(e&&e.specified)?F.value:F.get("text");
|
||||
if(H==G){return F.selected=true;}}},get:function(){var D=this,l=D.get("tag");if(l!="select"&&l!="option"){return this.getProperty("value");}if(l=="select"&&!(D=D.getSelected()[0])){return"";
|
||||
}var e=D.getAttributeNode("value");return(e&&e.specified)?D.value:D.get("text");}};}n=null;if(document.createElement("div").getAttributeNode("id")){Element.Properties.id={set:function(e){this.id=this.getAttributeNode("id").value=e;
|
||||
},get:function(){return this.id||null;},erase:function(){this.id=this.getAttributeNode("id").value="";}};}})();(function(){var i=document.html;var d=document.createElement("div");
|
||||
d.style.color="red";d.style.color=null;var c=d.style.color=="red";d=null;Element.Properties.styles={set:function(k){this.setStyles(k);}};var h=(i.style.opacity!=null),e=(i.style.filter!=null),j=/alpha\(opacity=([\d.]+)\)/i;
|
||||
var a=function(l,k){l.store("$opacity",k);l.style.visibility=k>0||k==null?"visible":"hidden";};var f=(h?function(l,k){l.style.opacity=k;}:(e?function(l,k){var n=l.style;
|
||||
if(!l.currentStyle||!l.currentStyle.hasLayout){n.zoom=1;}if(k==null||k==1){k="";}else{k="alpha(opacity="+(k*100).limit(0,100).round()+")";}var m=n.filter||l.getComputedStyle("filter")||"";
|
||||
n.filter=j.test(m)?m.replace(j,k):m+k;if(!n.filter){n.removeAttribute("filter");}}:a));var g=(h?function(l){var k=l.style.opacity||l.getComputedStyle("opacity");
|
||||
return(k=="")?1:k.toFloat();}:(e?function(l){var m=(l.style.filter||l.getComputedStyle("filter")),k;if(m){k=m.match(j);}return(k==null||m==null)?1:(k[1]/100);
|
||||
}:function(l){var k=l.retrieve("$opacity");if(k==null){k=(l.style.visibility=="hidden"?0:1);}return k;}));var b=(i.style.cssFloat==null)?"styleFloat":"cssFloat";
|
||||
Element.implement({getComputedStyle:function(m){if(this.currentStyle){return this.currentStyle[m.camelCase()];}var l=Element.getDocument(this).defaultView,k=l?l.getComputedStyle(this,null):null;
|
||||
return(k)?k.getPropertyValue((m==b)?"float":m.hyphenate()):null;},setStyle:function(l,k){if(l=="opacity"){if(k!=null){k=parseFloat(k);}f(this,k);return this;
|
||||
}l=(l=="float"?b:l).camelCase();if(typeOf(k)!="string"){var m=(Element.Styles[l]||"@").split(" ");k=Array.from(k).map(function(o,n){if(!m[n]){return"";
|
||||
}return(typeOf(o)=="number")?m[n].replace("@",Math.round(o)):o;}).join(" ");}else{if(k==String(Number(k))){k=Math.round(k);}}this.style[l]=k;if((k==""||k==null)&&c&&this.style.removeAttribute){this.style.removeAttribute(l);
|
||||
}return this;},getStyle:function(q){if(q=="opacity"){return g(this);}q=(q=="float"?b:q).camelCase();var k=this.style[q];if(!k||q=="zIndex"){k=[];for(var p in Element.ShortStyles){if(q!=p){continue;
|
||||
}for(var o in Element.ShortStyles[p]){k.push(this.getStyle(o));}return k.join(" ");}k=this.getComputedStyle(q);}if(k){k=String(k);var m=k.match(/rgba?\([\d\s,]+\)/);
|
||||
if(m){k=k.replace(m[0],m[0].rgbToHex());}}if(Browser.opera||Browser.ie){if((/^(height|width)$/).test(q)&&!(/px$/.test(k))){var l=(q=="width")?["left","right"]:["top","bottom"],n=0;
|
||||
l.each(function(r){n+=this.getStyle("border-"+r+"-width").toInt()+this.getStyle("padding-"+r).toInt();},this);return this["offset"+q.capitalize()]-n+"px";
|
||||
}if(Browser.ie&&(/^border(.+)Width|margin|padding/).test(q)&&isNaN(parseFloat(k))){return"0px";}}return k;},setStyles:function(l){for(var k in l){this.setStyle(k,l[k]);
|
||||
}return this;},getStyles:function(){var k={};Array.flatten(arguments).each(function(l){k[l]=this.getStyle(l);},this);return k;}});Element.Styles={left:"@px",top:"@px",bottom:"@px",right:"@px",width:"@px",height:"@px",maxWidth:"@px",maxHeight:"@px",minWidth:"@px",minHeight:"@px",backgroundColor:"rgb(@, @, @)",backgroundPosition:"@px @px",color:"rgb(@, @, @)",fontSize:"@px",letterSpacing:"@px",lineHeight:"@px",clip:"rect(@px @px @px @px)",margin:"@px @px @px @px",padding:"@px @px @px @px",border:"@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)",borderWidth:"@px @px @px @px",borderStyle:"@ @ @ @",borderColor:"rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)",zIndex:"@",zoom:"@",fontWeight:"@",textIndent:"@px",opacity:"@"};
|
||||
Element.ShortStyles={margin:{},padding:{},border:{},borderWidth:{},borderStyle:{},borderColor:{}};["Top","Right","Bottom","Left"].each(function(q){var p=Element.ShortStyles;
|
||||
var l=Element.Styles;["margin","padding"].each(function(r){var s=r+q;p[r][s]=l[s]="@px";});var o="border"+q;p.border[o]=l[o]="@px @ rgb(@, @, @)";var n=o+"Width",k=o+"Style",m=o+"Color";
|
||||
p[o]={};p.borderWidth[n]=p[o][n]=l[n]="@px";p.borderStyle[k]=p[o][k]=l[k]="@";p.borderColor[m]=p[o][m]=l[m]="rgb(@, @, @)";});})();(function(){Element.Properties.events={set:function(b){this.addEvents(b);
|
||||
}};[Element,Window,Document].invoke("implement",{addEvent:function(f,h){var i=this.retrieve("events",{});if(!i[f]){i[f]={keys:[],values:[]};}if(i[f].keys.contains(h)){return this;
|
||||
}i[f].keys.push(h);var g=f,b=Element.Events[f],d=h,j=this;if(b){if(b.onAdd){b.onAdd.call(this,h,f);}if(b.condition){d=function(k){if(b.condition.call(this,k,f)){return h.call(this,k);
|
||||
}return true;};}if(b.base){g=Function.from(b.base).call(this,f);}}var e=function(){return h.call(j);};var c=Element.NativeEvents[g];if(c){if(c==2){e=function(k){k=new DOMEvent(k,j.getWindow());
|
||||
if(d.call(j,k)===false){k.stop();}};}this.addListener(g,e,arguments[2]);}i[f].values.push(e);return this;},removeEvent:function(e,d){var c=this.retrieve("events");
|
||||
if(!c||!c[e]){return this;}var h=c[e];var b=h.keys.indexOf(d);if(b==-1){return this;}var g=h.values[b];delete h.keys[b];delete h.values[b];var f=Element.Events[e];
|
||||
if(f){if(f.onRemove){f.onRemove.call(this,d,e);}if(f.base){e=Function.from(f.base).call(this,e);}}return(Element.NativeEvents[e])?this.removeListener(e,g,arguments[2]):this;
|
||||
},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]);}return this;},removeEvents:function(b){var d;if(typeOf(b)=="object"){for(d in b){this.removeEvent(d,b[d]);
|
||||
}return this;}var c=this.retrieve("events");if(!c){return this;}if(!b){for(d in c){this.removeEvents(d);}this.eliminate("events");}else{if(c[b]){c[b].keys.each(function(e){this.removeEvent(b,e);
|
||||
},this);delete c[b];}}return this;},fireEvent:function(e,c,b){var d=this.retrieve("events");if(!d||!d[e]){return this;}c=Array.from(c);d[e].keys.each(function(f){if(b){f.delay(b,this,c);
|
||||
}else{f.apply(this,c);}},this);return this;},cloneEvents:function(e,d){e=document.id(e);var c=e.retrieve("events");if(!c){return this;}if(!d){for(var b in c){this.cloneEvents(e,b);
|
||||
}}else{if(c[d]){c[d].keys.each(function(f){this.addEvent(d,f);},this);}}return this;}});Element.NativeEvents={click:2,dblclick:2,mouseup:2,mousedown:2,contextmenu:2,mousewheel:2,DOMMouseScroll:2,mouseover:2,mouseout:2,mousemove:2,selectstart:2,selectend:2,keydown:2,keypress:2,keyup:2,orientationchange:2,touchstart:2,touchmove:2,touchend:2,touchcancel:2,gesturestart:2,gesturechange:2,gestureend:2,focus:2,blur:2,change:2,reset:2,select:2,submit:2,paste:2,input:2,load:2,unload:1,beforeunload:2,resize:1,move:1,DOMContentLoaded:1,readystatechange:1,error:1,abort:1,scroll:1};
|
||||
Element.Events={mousewheel:{base:(Browser.firefox)?"DOMMouseScroll":"mousewheel"}};if("onmouseenter" in document.documentElement){Element.NativeEvents.mouseenter=Element.NativeEvents.mouseleave=2;
|
||||
}else{var a=function(b){var c=b.relatedTarget;if(c==null){return true;}if(!c){return false;}return(c!=this&&c.prefix!="xul"&&typeOf(this)!="document"&&!this.contains(c));
|
||||
};Element.Events.mouseenter={base:"mouseover",condition:a};Element.Events.mouseleave={base:"mouseout",condition:a};}if(!window.addEventListener){Element.NativeEvents.propertychange=2;
|
||||
Element.Events.change={base:function(){var b=this.type;return(this.get("tag")=="input"&&(b=="radio"||b=="checkbox"))?"propertychange":"change";},condition:function(b){return this.type!="radio"||(b.event.propertyName=="checked"&&this.checked);
|
||||
}};}})();(function(){var c=!!window.addEventListener;Element.NativeEvents.focusin=Element.NativeEvents.focusout=2;var k=function(l,m,n,o,p){while(p&&p!=l){if(m(p,o)){return n.call(p,o,p);
|
||||
}p=document.id(p.parentNode);}};var a={mouseenter:{base:"mouseover"},mouseleave:{base:"mouseout"},focus:{base:"focus"+(c?"":"in"),capture:true},blur:{base:c?"blur":"focusout",capture:true}};
|
||||
var b="$delegation:";var i=function(l){return{base:"focusin",remove:function(m,o){var p=m.retrieve(b+l+"listeners",{})[o];if(p&&p.forms){for(var n=p.forms.length;
|
||||
n--;){p.forms[n].removeEvent(l,p.fns[n]);}}},listen:function(x,r,v,n,t,s){var o=(t.get("tag")=="form")?t:n.target.getParent("form");if(!o){return;}var u=x.retrieve(b+l+"listeners",{}),p=u[s]||{forms:[],fns:[]},m=p.forms,w=p.fns;
|
||||
if(m.indexOf(o)!=-1){return;}m.push(o);var q=function(y){k(x,r,v,y,t);};o.addEvent(l,q);w.push(q);u[s]=p;x.store(b+l+"listeners",u);}};};var d=function(l){return{base:"focusin",listen:function(m,n,p,q,r){var o={blur:function(){this.removeEvents(o);
|
||||
}};o[l]=function(s){k(m,n,p,s,r);};q.target.addEvents(o);}};};if(!c){Object.append(a,{submit:i("submit"),reset:i("reset"),change:d("change"),select:d("select")});
|
||||
}var h=Element.prototype,f=h.addEvent,j=h.removeEvent;var e=function(l,m){return function(r,q,n){if(r.indexOf(":relay")==-1){return l.call(this,r,q,n);
|
||||
}var o=Slick.parse(r).expressions[0][0];if(o.pseudos[0].key!="relay"){return l.call(this,r,q,n);}var p=o.tag;o.pseudos.slice(1).each(function(s){p+=":"+s.key+(s.value?"("+s.value+")":"");
|
||||
});l.call(this,r,q);return m.call(this,p,o.pseudos[0].value,q);};};var g={addEvent:function(v,q,x){var t=this.retrieve("$delegates",{}),r=t[v];if(r){for(var y in r){if(r[y].fn==x&&r[y].match==q){return this;
|
||||
}}}var p=v,u=q,o=x,n=a[v]||{};v=n.base||p;q=function(B){return Slick.match(B,u);};var w=Element.Events[p];if(w&&w.condition){var l=q,m=w.condition;q=function(C,B){return l(C,B)&&m.call(C,B,v);
|
||||
};}var z=this,s=String.uniqueID();var A=n.listen?function(B,C){if(!C&&B&&B.target){C=B.target;}if(C){n.listen(z,q,x,B,C,s);}}:function(B,C){if(!C&&B&&B.target){C=B.target;
|
||||
}if(C){k(z,q,x,B,C);}};if(!r){r={};}r[s]={match:u,fn:o,delegator:A};t[p]=r;return f.call(this,v,A,n.capture);},removeEvent:function(r,n,t,u){var q=this.retrieve("$delegates",{}),p=q[r];
|
||||
if(!p){return this;}if(u){var m=r,w=p[u].delegator,l=a[r]||{};r=l.base||m;if(l.remove){l.remove(this,u);}delete p[u];q[m]=p;return j.call(this,r,w);}var o,v;
|
||||
if(t){for(o in p){v=p[o];if(v.match==n&&v.fn==t){return g.removeEvent.call(this,r,n,t,o);}}}else{for(o in p){v=p[o];if(v.match==n){g.removeEvent.call(this,r,n,v.fn,o);
|
||||
}}}return this;}};[Element,Window,Document].invoke("implement",{addEvent:e(f,g.addEvent),removeEvent:e(j,g.removeEvent)});})();(function(){var h=document.createElement("div"),e=document.createElement("div");
|
||||
h.style.height="0";h.appendChild(e);var d=(e.offsetParent===h);h=e=null;var l=function(m){return k(m,"position")!="static"||a(m);};var i=function(m){return l(m)||(/^(?:table|td|th)$/i).test(m.tagName);
|
||||
};Element.implement({scrollTo:function(m,n){if(a(this)){this.getWindow().scrollTo(m,n);}else{this.scrollLeft=m;this.scrollTop=n;}return this;},getSize:function(){if(a(this)){return this.getWindow().getSize();
|
||||
}return{x:this.offsetWidth,y:this.offsetHeight};},getScrollSize:function(){if(a(this)){return this.getWindow().getScrollSize();}return{x:this.scrollWidth,y:this.scrollHeight};
|
||||
},getScroll:function(){if(a(this)){return this.getWindow().getScroll();}return{x:this.scrollLeft,y:this.scrollTop};},getScrolls:function(){var n=this.parentNode,m={x:0,y:0};
|
||||
while(n&&!a(n)){m.x+=n.scrollLeft;m.y+=n.scrollTop;n=n.parentNode;}return m;},getOffsetParent:d?function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null;
|
||||
}var n=(k(m,"position")=="static")?i:l;while((m=m.parentNode)){if(n(m)){return m;}}return null;}:function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null;
|
||||
}try{return m.offsetParent;}catch(n){}return null;},getOffsets:function(){if(this.getBoundingClientRect&&!Browser.Platform.ios){var r=this.getBoundingClientRect(),o=document.id(this.getDocument().documentElement),q=o.getScroll(),t=this.getScrolls(),s=(k(this,"position")=="fixed");
|
||||
return{x:r.left.toInt()+t.x+((s)?0:q.x)-o.clientLeft,y:r.top.toInt()+t.y+((s)?0:q.y)-o.clientTop};}var n=this,m={x:0,y:0};if(a(this)){return m;}while(n&&!a(n)){m.x+=n.offsetLeft;
|
||||
m.y+=n.offsetTop;if(Browser.firefox){if(!c(n)){m.x+=b(n);m.y+=g(n);}var p=n.parentNode;if(p&&k(p,"overflow")!="visible"){m.x+=b(p);m.y+=g(p);}}else{if(n!=this&&Browser.safari){m.x+=b(n);
|
||||
m.y+=g(n);}}n=n.offsetParent;}if(Browser.firefox&&!c(this)){m.x-=b(this);m.y-=g(this);}return m;},getPosition:function(p){var q=this.getOffsets(),n=this.getScrolls();
|
||||
var m={x:q.x-n.x,y:q.y-n.y};if(p&&(p=document.id(p))){var o=p.getPosition();return{x:m.x-o.x-b(p),y:m.y-o.y-g(p)};}return m;},getCoordinates:function(o){if(a(this)){return this.getWindow().getCoordinates();
|
||||
}var m=this.getPosition(o),n=this.getSize();var p={left:m.x,top:m.y,width:n.x,height:n.y};p.right=p.left+p.width;p.bottom=p.top+p.height;return p;},computePosition:function(m){return{left:m.x-j(this,"margin-left"),top:m.y-j(this,"margin-top")};
|
||||
},setPosition:function(m){return this.setStyles(this.computePosition(m));}});[Document,Window].invoke("implement",{getSize:function(){var m=f(this);return{x:m.clientWidth,y:m.clientHeight};
|
||||
},getScroll:function(){var n=this.getWindow(),m=f(this);return{x:n.pageXOffset||m.scrollLeft,y:n.pageYOffset||m.scrollTop};},getScrollSize:function(){var o=f(this),n=this.getSize(),m=this.getDocument().body;
|
||||
return{x:Math.max(o.scrollWidth,m.scrollWidth,n.x),y:Math.max(o.scrollHeight,m.scrollHeight,n.y)};},getPosition:function(){return{x:0,y:0};},getCoordinates:function(){var m=this.getSize();
|
||||
return{top:0,left:0,bottom:m.y,right:m.x,height:m.y,width:m.x};}});var k=Element.getComputedStyle;function j(m,n){return k(m,n).toInt()||0;}function c(m){return k(m,"-moz-box-sizing")=="border-box";
|
||||
}function g(m){return j(m,"border-top-width");}function b(m){return j(m,"border-left-width");}function a(m){return(/^(?:body|html)$/i).test(m.tagName);
|
||||
}function f(m){var n=m.getDocument();return(!n.compatMode||n.compatMode=="CSS1Compat")?n.html:n.body;}})();Element.alias({position:"setPosition"});[Window,Document,Element].invoke("implement",{getHeight:function(){return this.getSize().y;
|
||||
},getWidth:function(){return this.getSize().x;},getScrollTop:function(){return this.getScroll().y;},getScrollLeft:function(){return this.getScroll().x;
|
||||
},getScrollHeight:function(){return this.getScrollSize().y;},getScrollWidth:function(){return this.getScrollSize().x;},getTop:function(){return this.getPosition().y;
|
||||
},getLeft:function(){return this.getPosition().x;}});(function(){var f=this.Fx=new Class({Implements:[Chain,Events,Options],options:{fps:60,unit:false,duration:500,frames:null,frameSkip:true,link:"ignore"},initialize:function(g){this.subject=this.subject||this;
|
||||
this.setOptions(g);},getTransition:function(){return function(g){return -(Math.cos(Math.PI*g)-1)/2;};},step:function(g){if(this.options.frameSkip){var h=(this.time!=null)?(g-this.time):0,i=h/this.frameInterval;
|
||||
this.time=g;this.frame+=i;}else{this.frame++;}if(this.frame<this.frames){var j=this.transition(this.frame/this.frames);this.set(this.compute(this.from,this.to,j));
|
||||
}else{this.frame=this.frames;this.set(this.compute(this.from,this.to,1));this.stop();}},set:function(g){return g;},compute:function(i,h,g){return f.compute(i,h,g);
|
||||
},check:function(){if(!this.isRunning()){return true;}switch(this.options.link){case"cancel":this.cancel();return true;case"chain":this.chain(this.caller.pass(arguments,this));
|
||||
return false;}return false;},start:function(k,j){if(!this.check(k,j)){return this;}this.from=k;this.to=j;this.frame=(this.options.frameSkip)?0:-1;this.time=null;
|
||||
this.transition=this.getTransition();var i=this.options.frames,h=this.options.fps,g=this.options.duration;this.duration=f.Durations[g]||g.toInt();this.frameInterval=1000/h;
|
||||
this.frames=i||Math.round(this.duration/this.frameInterval);this.fireEvent("start",this.subject);b.call(this,h);return this;},stop:function(){if(this.isRunning()){this.time=null;
|
||||
d.call(this,this.options.fps);if(this.frames==this.frame){this.fireEvent("complete",this.subject);if(!this.callChain()){this.fireEvent("chainComplete",this.subject);
|
||||
}}else{this.fireEvent("stop",this.subject);}}return this;},cancel:function(){if(this.isRunning()){this.time=null;d.call(this,this.options.fps);this.frame=this.frames;
|
||||
this.fireEvent("cancel",this.subject).clearChain();}return this;},pause:function(){if(this.isRunning()){this.time=null;d.call(this,this.options.fps);}return this;
|
||||
},resume:function(){if((this.frame<this.frames)&&!this.isRunning()){b.call(this,this.options.fps);}return this;},isRunning:function(){var g=e[this.options.fps];
|
||||
return g&&g.contains(this);}});f.compute=function(i,h,g){return(h-i)*g+i;};f.Durations={"short":250,normal:500,"long":1000};var e={},c={};var a=function(){var h=Date.now();
|
||||
for(var j=this.length;j--;){var g=this[j];if(g){g.step(h);}}};var b=function(h){var g=e[h]||(e[h]=[]);g.push(this);if(!c[h]){c[h]=a.periodical(Math.round(1000/h),g);
|
||||
}};var d=function(h){var g=e[h];if(g){g.erase(this);if(!g.length&&c[h]){delete e[h];c[h]=clearInterval(c[h]);}}};})();Fx.CSS=new Class({Extends:Fx,prepare:function(b,e,a){a=Array.from(a);
|
||||
var h=a[0],g=a[1];if(g==null){g=h;h=b.getStyle(e);var c=this.options.unit;if(c&&h.slice(-c.length)!=c&&parseFloat(h)!=0){b.setStyle(e,g+c);var d=b.getComputedStyle(e);
|
||||
if(!(/px$/.test(d))){d=b.style[("pixel-"+e).camelCase()];if(d==null){var f=b.style.left;b.style.left=g+c;d=b.style.pixelLeft;b.style.left=f;}}h=(g||1)/(parseFloat(d)||1)*(parseFloat(h)||0);
|
||||
b.setStyle(e,h+c);}}return{from:this.parse(h),to:this.parse(g)};},parse:function(a){a=Function.from(a)();a=(typeof a=="string")?a.split(" "):Array.from(a);
|
||||
return a.map(function(c){c=String(c);var b=false;Object.each(Fx.CSS.Parsers,function(f,e){if(b){return;}var d=f.parse(c);if(d||d===0){b={value:d,parser:f};
|
||||
}});b=b||{value:c,parser:Fx.CSS.Parsers.String};return b;});},compute:function(d,c,b){var a=[];(Math.min(d.length,c.length)).times(function(e){a.push({value:d[e].parser.compute(d[e].value,c[e].value,b),parser:d[e].parser});
|
||||
});a.$family=Function.from("fx:css:value");return a;},serve:function(c,b){if(typeOf(c)!="fx:css:value"){c=this.parse(c);}var a=[];c.each(function(d){a=a.concat(d.parser.serve(d.value,b));
|
||||
});return a;},render:function(a,d,c,b){a.setStyle(d,this.serve(c,b));},search:function(a){if(Fx.CSS.Cache[a]){return Fx.CSS.Cache[a];}var c={},b=new RegExp("^"+a.escapeRegExp()+"$");
|
||||
Array.each(document.styleSheets,function(f,e){var d=f.href;if(d&&d.contains("://")&&!d.contains(document.domain)){return;}var g=f.rules||f.cssRules;Array.each(g,function(k,h){if(!k.style){return;
|
||||
}var j=(k.selectorText)?k.selectorText.replace(/^\w+/,function(i){return i.toLowerCase();}):null;if(!j||!b.test(j)){return;}Object.each(Element.Styles,function(l,i){if(!k.style[i]||Element.ShortStyles[i]){return;
|
||||
}l=String(k.style[i]);c[i]=((/^rgb/).test(l))?l.rgbToHex():l;});});});return Fx.CSS.Cache[a]=c;}});Fx.CSS.Cache={};Fx.CSS.Parsers={Color:{parse:function(a){if(a.match(/^#[0-9a-f]{3,6}$/i)){return a.hexToRgb(true);
|
||||
}return((a=a.match(/(\d+),\s*(\d+),\s*(\d+)/)))?[a[1],a[2],a[3]]:false;},compute:function(c,b,a){return c.map(function(e,d){return Math.round(Fx.compute(c[d],b[d],a));
|
||||
});},serve:function(a){return a.map(Number);}},Number:{parse:parseFloat,compute:Fx.compute,serve:function(b,a){return(a)?b+a:b;}},String:{parse:Function.from(false),compute:function(b,a){return a;
|
||||
},serve:function(a){return a;}}};Fx.Tween=new Class({Extends:Fx.CSS,initialize:function(b,a){this.element=this.subject=document.id(b);this.parent(a);},set:function(b,a){if(arguments.length==1){a=b;
|
||||
b=this.property||this.options.property;}this.render(this.element,b,a,this.options.unit);return this;},start:function(c,e,d){if(!this.check(c,e,d)){return this;
|
||||
}var b=Array.flatten(arguments);this.property=this.options.property||b.shift();var a=this.prepare(this.element,this.property,b);return this.parent(a.from,a.to);
|
||||
}});Element.Properties.tween={set:function(a){this.get("tween").cancel().setOptions(a);return this;},get:function(){var a=this.retrieve("tween");if(!a){a=new Fx.Tween(this,{link:"cancel"});
|
||||
this.store("tween",a);}return a;}};Element.implement({tween:function(a,c,b){this.get("tween").start(a,c,b);return this;},fade:function(d){var e=this.get("tween"),g,c=["opacity"].append(arguments),a;
|
||||
if(c[1]==null){c[1]="toggle";}switch(c[1]){case"in":g="start";c[1]=1;break;case"out":g="start";c[1]=0;break;case"show":g="set";c[1]=1;break;case"hide":g="set";
|
||||
c[1]=0;break;case"toggle":var b=this.retrieve("fade:flag",this.getStyle("opacity")==1);g="start";c[1]=b?0:1;this.store("fade:flag",!b);a=true;break;default:g="start";
|
||||
}if(!a){this.eliminate("fade:flag");}e[g].apply(e,c);var f=c[c.length-1];if(g=="set"||f!=0){this.setStyle("visibility",f==0?"hidden":"visible");}else{e.chain(function(){this.element.setStyle("visibility","hidden");
|
||||
this.callChain();});}return this;},highlight:function(c,a){if(!a){a=this.retrieve("highlight:original",this.getStyle("background-color"));a=(a=="transparent")?"#fff":a;
|
||||
}var b=this.get("tween");b.start("background-color",c||"#ffff88",a).chain(function(){this.setStyle("background-color",this.retrieve("highlight:original"));
|
||||
b.callChain();}.bind(this));return this;}});Fx.Morph=new Class({Extends:Fx.CSS,initialize:function(b,a){this.element=this.subject=document.id(b);this.parent(a);
|
||||
},set:function(a){if(typeof a=="string"){a=this.search(a);}for(var b in a){this.render(this.element,b,a[b],this.options.unit);}return this;},compute:function(e,d,c){var a={};
|
||||
for(var b in e){a[b]=this.parent(e[b],d[b],c);}return a;},start:function(b){if(!this.check(b)){return this;}if(typeof b=="string"){b=this.search(b);}var e={},d={};
|
||||
for(var c in b){var a=this.prepare(this.element,c,b[c]);e[c]=a.from;d[c]=a.to;}return this.parent(e,d);}});Element.Properties.morph={set:function(a){this.get("morph").cancel().setOptions(a);
|
||||
return this;},get:function(){var a=this.retrieve("morph");if(!a){a=new Fx.Morph(this,{link:"cancel"});this.store("morph",a);}return a;}};Element.implement({morph:function(a){this.get("morph").start(a);
|
||||
return this;}});Fx.implement({getTransition:function(){var a=this.options.transition||Fx.Transitions.Sine.easeInOut;if(typeof a=="string"){var b=a.split(":");
|
||||
a=Fx.Transitions;a=a[b[0]]||a[b[0].capitalize()];if(b[1]){a=a["ease"+b[1].capitalize()+(b[2]?b[2].capitalize():"")];}}return a;}});Fx.Transition=function(c,b){b=Array.from(b);
|
||||
var a=function(d){return c(d,b);};return Object.append(a,{easeIn:a,easeOut:function(d){return 1-c(1-d,b);},easeInOut:function(d){return(d<=0.5?c(2*d,b):(2-c(2*(1-d),b)))/2;
|
||||
}});};Fx.Transitions={linear:function(a){return a;}};Fx.Transitions.extend=function(a){for(var b in a){Fx.Transitions[b]=new Fx.Transition(a[b]);}};Fx.Transitions.extend({Pow:function(b,a){return Math.pow(b,a&&a[0]||6);
|
||||
},Expo:function(a){return Math.pow(2,8*(a-1));},Circ:function(a){return 1-Math.sin(Math.acos(a));},Sine:function(a){return 1-Math.cos(a*Math.PI/2);},Back:function(b,a){a=a&&a[0]||1.618;
|
||||
return Math.pow(b,2)*((a+1)*b-a);},Bounce:function(f){var e;for(var d=0,c=1;1;d+=c,c/=2){if(f>=(7-4*d)/11){e=c*c-Math.pow((11-6*d-11*f)/4,2);break;}}return e;
|
||||
},Elastic:function(b,a){return Math.pow(2,10*--b)*Math.cos(20*b*Math.PI*(a&&a[0]||1)/3);}});["Quad","Cubic","Quart","Quint"].each(function(b,a){Fx.Transitions[b]=new Fx.Transition(function(c){return Math.pow(c,a+2);
|
||||
});});(function(){var d=function(){},a=("onprogress" in new Browser.Request);var c=this.Request=new Class({Implements:[Chain,Events,Options],options:{url:"",data:"",headers:{"X-Requested-With":"XMLHttpRequest",Accept:"text/javascript, text/html, application/xml, text/xml, */*"},async:true,format:false,method:"post",link:"ignore",isSuccess:null,emulation:true,urlEncoded:true,encoding:"utf-8",evalScripts:false,evalResponse:false,timeout:0,noCache:false},initialize:function(e){this.xhr=new Browser.Request();
|
||||
this.setOptions(e);this.headers=this.options.headers;},onStateChange:function(){var e=this.xhr;if(e.readyState!=4||!this.running){return;}this.running=false;
|
||||
this.status=0;Function.attempt(function(){var f=e.status;this.status=(f==1223)?204:f;}.bind(this));e.onreadystatechange=d;if(a){e.onprogress=e.onloadstart=d;
|
||||
}clearTimeout(this.timer);this.response={text:this.xhr.responseText||"",xml:this.xhr.responseXML};if(this.options.isSuccess.call(this,this.status)){this.success(this.response.text,this.response.xml);
|
||||
}else{this.failure();}},isSuccess:function(){var e=this.status;return(e>=200&&e<300);},isRunning:function(){return !!this.running;},processScripts:function(e){if(this.options.evalResponse||(/(ecma|java)script/).test(this.getHeader("Content-type"))){return Browser.exec(e);
|
||||
}return e.stripScripts(this.options.evalScripts);},success:function(f,e){this.onSuccess(this.processScripts(f),e);},onSuccess:function(){this.fireEvent("complete",arguments).fireEvent("success",arguments).callChain();
|
||||
},failure:function(){this.onFailure();},onFailure:function(){this.fireEvent("complete").fireEvent("failure",this.xhr);},loadstart:function(e){this.fireEvent("loadstart",[e,this.xhr]);
|
||||
},progress:function(e){this.fireEvent("progress",[e,this.xhr]);},timeout:function(){this.fireEvent("timeout",this.xhr);},setHeader:function(e,f){this.headers[e]=f;
|
||||
return this;},getHeader:function(e){return Function.attempt(function(){return this.xhr.getResponseHeader(e);}.bind(this));},check:function(){if(!this.running){return true;
|
||||
}switch(this.options.link){case"cancel":this.cancel();return true;case"chain":this.chain(this.caller.pass(arguments,this));return false;}return false;},send:function(o){if(!this.check(o)){return this;
|
||||
}this.options.isSuccess=this.options.isSuccess||this.isSuccess;this.running=true;var l=typeOf(o);if(l=="string"||l=="element"){o={data:o};}var h=this.options;
|
||||
o=Object.append({data:h.data,url:h.url,method:h.method},o);var j=o.data,f=String(o.url),e=o.method.toLowerCase();switch(typeOf(j)){case"element":j=document.id(j).toQueryString();
|
||||
break;case"object":case"hash":j=Object.toQueryString(j);}if(this.options.format){var m="format="+this.options.format;j=(j)?m+"&"+j:m;}if(this.options.emulation&&!["get","post"].contains(e)){var k="_method="+e;
|
||||
j=(j)?k+"&"+j:k;e="post";}if(this.options.urlEncoded&&["post","put"].contains(e)){var g=(this.options.encoding)?"; charset="+this.options.encoding:"";this.headers["Content-type"]="application/x-www-form-urlencoded"+g;
|
||||
}if(!f){f=document.location.pathname;}var i=f.lastIndexOf("/");if(i>-1&&(i=f.indexOf("#"))>-1){f=f.substr(0,i);}if(this.options.noCache){f+=(f.contains("?")?"&":"?")+String.uniqueID();
|
||||
}if(j&&e=="get"){f+=(f.contains("?")?"&":"?")+j;j=null;}var n=this.xhr;if(a){n.onloadstart=this.loadstart.bind(this);n.onprogress=this.progress.bind(this);
|
||||
}n.open(e.toUpperCase(),f,this.options.async,this.options.user,this.options.password);if(this.options.user&&"withCredentials" in n){n.withCredentials=true;
|
||||
}n.onreadystatechange=this.onStateChange.bind(this);Object.each(this.headers,function(q,p){try{n.setRequestHeader(p,q);}catch(r){this.fireEvent("exception",[p,q]);
|
||||
}},this);this.fireEvent("request");n.send(j);if(!this.options.async){this.onStateChange();}else{if(this.options.timeout){this.timer=this.timeout.delay(this.options.timeout,this);
|
||||
}}return this;},cancel:function(){if(!this.running){return this;}this.running=false;var e=this.xhr;e.abort();clearTimeout(this.timer);e.onreadystatechange=d;
|
||||
if(a){e.onprogress=e.onloadstart=d;}this.xhr=new Browser.Request();this.fireEvent("cancel");return this;}});var b={};["get","post","put","delete","GET","POST","PUT","DELETE"].each(function(e){b[e]=function(g){var f={method:e};
|
||||
if(g!=null){f.data=g;}return this.send(f);};});c.implement(b);Element.Properties.send={set:function(e){var f=this.get("send").cancel();f.setOptions(e);
|
||||
return this;},get:function(){var e=this.retrieve("send");if(!e){e=new c({data:this,link:"cancel",method:this.get("method")||"post",url:this.get("action")});
|
||||
this.store("send",e);}return e;}};Element.implement({send:function(e){var f=this.get("send");f.send({data:this,url:e||f.options.url});return this;}});})();
|
||||
Request.HTML=new Class({Extends:Request,options:{update:false,append:false,evalScripts:true,filter:false,headers:{Accept:"text/html, application/xml, text/xml, */*"}},success:function(f){var e=this.options,c=this.response;
|
||||
c.html=f.stripScripts(function(h){c.javascript=h;});var d=c.html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);if(d){c.html=d[1];}var b=new Element("div").set("html",c.html);
|
||||
c.tree=b.childNodes;c.elements=b.getElements(e.filter||"*");if(e.filter){c.tree=c.elements;}if(e.update){var g=document.id(e.update).empty();if(e.filter){g.adopt(c.elements);
|
||||
}else{g.set("html",c.html);}}else{if(e.append){var a=document.id(e.append);if(e.filter){c.elements.reverse().inject(a);}else{a.adopt(b.getChildren());}}}if(e.evalScripts){Browser.exec(c.javascript);
|
||||
}this.onSuccess(c.tree,c.elements,c.html,c.javascript);}});Element.Properties.load={set:function(a){var b=this.get("load").cancel();b.setOptions(a);return this;
|
||||
},get:function(){var a=this.retrieve("load");if(!a){a=new Request.HTML({data:this,link:"cancel",update:this,method:"get"});this.store("load",a);}return a;
|
||||
}};Element.implement({load:function(){this.get("load").send(Array.link(arguments,{data:Type.isObject,url:Type.isString}));return this;}});if(typeof JSON=="undefined"){this.JSON={};
|
||||
}(function(){var special={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};var escape=function(chr){return special[chr]||"\\u"+("0000"+chr.charCodeAt(0).toString(16)).slice(-4);
|
||||
};JSON.validate=function(string){string=string.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"");
|
||||
return(/^[\],:{}\s]*$/).test(string);};JSON.encode=JSON.stringify?function(obj){return JSON.stringify(obj);}:function(obj){if(obj&&obj.toJSON){obj=obj.toJSON();
|
||||
}switch(typeOf(obj)){case"string":return'"'+obj.replace(/[\x00-\x1f\\"]/g,escape)+'"';case"array":return"["+obj.map(JSON.encode).clean()+"]";case"object":case"hash":var string=[];
|
||||
Object.each(obj,function(value,key){var json=JSON.encode(value);if(json){string.push(JSON.encode(key)+":"+json);}});return"{"+string+"}";case"number":case"boolean":return""+obj;
|
||||
case"null":return"null";}return null;};JSON.decode=function(string,secure){if(!string||typeOf(string)!="string"){return null;}if(secure||JSON.secure){if(JSON.parse){return JSON.parse(string);
|
||||
}if(!JSON.validate(string)){throw new Error("JSON could not decode the input; security is enabled and the value is not secure.");}}return eval("("+string+")");
|
||||
};})();Request.JSON=new Class({Extends:Request,options:{secure:true},initialize:function(a){this.parent(a);Object.append(this.headers,{Accept:"application/json","X-Request":"JSON"});
|
||||
},success:function(c){var b;try{b=this.response.json=JSON.decode(c,this.options.secure);}catch(a){this.fireEvent("error",[c,a]);return;}if(b==null){this.onFailure();
|
||||
}else{this.onSuccess(b,c);}}});var Cookie=new Class({Implements:Options,options:{path:"/",domain:false,duration:false,secure:false,document:document,encode:true},initialize:function(b,a){this.key=b;
|
||||
this.setOptions(a);},write:function(b){if(this.options.encode){b=encodeURIComponent(b);}if(this.options.domain){b+="; domain="+this.options.domain;}if(this.options.path){b+="; path="+this.options.path;
|
||||
}if(this.options.duration){var a=new Date();a.setTime(a.getTime()+this.options.duration*24*60*60*1000);b+="; expires="+a.toGMTString();}if(this.options.secure){b+="; secure";
|
||||
}this.options.document.cookie=this.key+"="+b;return this;},read:function(){var a=this.options.document.cookie.match("(?:^|;)\\s*"+this.key.escapeRegExp()+"=([^;]*)");
|
||||
return(a)?decodeURIComponent(a[1]):null;},dispose:function(){new Cookie(this.key,Object.merge({},this.options,{duration:-1})).write("");return this;}});
|
||||
Cookie.write=function(b,c,a){return new Cookie(b,a).write(c);};Cookie.read=function(a){return new Cookie(a).read();};Cookie.dispose=function(b,a){return new Cookie(b,a).dispose();
|
||||
};(function(i,k){var l,f,e=[],c,b,d=k.createElement("div");var g=function(){clearTimeout(b);if(l){return;}Browser.loaded=l=true;k.removeListener("DOMContentLoaded",g).removeListener("readystatechange",a);
|
||||
k.fireEvent("domready");i.fireEvent("domready");};var a=function(){for(var m=e.length;m--;){if(e[m]()){g();return true;}}return false;};var j=function(){clearTimeout(b);
|
||||
if(!a()){b=setTimeout(j,10);}};k.addListener("DOMContentLoaded",g);var h=function(){try{d.doScroll();return true;}catch(m){}return false;};if(d.doScroll&&!h()){e.push(h);
|
||||
c=true;}if(k.readyState){e.push(function(){var m=k.readyState;return(m=="loaded"||m=="complete");});}if("onreadystatechange" in k){k.addListener("readystatechange",a);
|
||||
}else{c=true;}if(c){j();}Element.Events.domready={onAdd:function(m){if(l){m.call(this);}}};Element.Events.load={base:"load",onAdd:function(m){if(f&&this==i){m.call(this);
|
||||
}},condition:function(){if(this==i){g();delete Element.Events.load;}return true;}};i.addEvent("load",function(){f=true;});})(window,document);(function(){var Swiff=this.Swiff=new Class({Implements:Options,options:{id:null,height:1,width:1,container:null,properties:{},params:{quality:"high",allowScriptAccess:"always",wMode:"window",swLiveConnect:true},callBacks:{},vars:{}},toElement:function(){return this.object;
|
||||
},initialize:function(path,options){this.instance="Swiff_"+String.uniqueID();this.setOptions(options);options=this.options;var id=this.id=options.id||this.instance;
|
||||
var container=document.id(options.container);Swiff.CallBacks[this.instance]={};var params=options.params,vars=options.vars,callBacks=options.callBacks;
|
||||
var properties=Object.append({height:options.height,width:options.width},options.properties);var self=this;for(var callBack in callBacks){Swiff.CallBacks[this.instance][callBack]=(function(option){return function(){return option.apply(self.object,arguments);
|
||||
};})(callBacks[callBack]);vars[callBack]="Swiff.CallBacks."+this.instance+"."+callBack;}params.flashVars=Object.toQueryString(vars);if(Browser.ie){properties.classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000";
|
||||
params.movie=path;}else{properties.type="application/x-shockwave-flash";}properties.data=path;var build='<object id="'+id+'"';for(var property in properties){build+=" "+property+'="'+properties[property]+'"';
|
||||
}build+=">";for(var param in params){if(params[param]){build+='<param name="'+param+'" value="'+params[param]+'" />';}}build+="</object>";this.object=((container)?container.empty():new Element("div")).set("html",build).firstChild;
|
||||
},replaces:function(element){element=document.id(element,true);element.parentNode.replaceChild(this.toElement(),element);return this;},inject:function(element){document.id(element,true).appendChild(this.toElement());
|
||||
return this;},remote:function(){return Swiff.remote.apply(Swiff,[this.toElement()].append(arguments));}});Swiff.CallBacks={};Swiff.remote=function(obj,fn){var rs=obj.CallFunction('<invoke name="'+fn+'" returntype="javascript">'+__flash__argumentsToXML(arguments,2)+"</invoke>");
|
||||
return eval(rs);};})();
|
||||
198
bzz/bzzdemo/mootools-idle.js
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
---
|
||||
description: Determines when the user is idle (not interacting with the page) so that you can respond appropriately.
|
||||
|
||||
license:
|
||||
- MIT-style license
|
||||
|
||||
authors:
|
||||
- Espen 'Rexxars' Hovlandsdal (http://rexxars.com/)
|
||||
|
||||
requires:
|
||||
core/1.2.4:
|
||||
- Class.Extras
|
||||
- Element.Event
|
||||
|
||||
provides:
|
||||
- IdleTimer
|
||||
|
||||
inspiration:
|
||||
- Inspired by Nicholas C. Zakas' Idle Timer (http://yuilibrary.com/gallery/show/idletimer) Copyright (c) 2009 Nicholas C. Zakas, [YUI BSD](http://developer.yahoo.com/yui/license.html)
|
||||
- Also inspired by Paul Irish's jQuery idleTimer (http://paulirish.com/2009/jquery-idletimer-plugin/) Copyright (c) 2009 Paul Irish, [MIT License](http://opensource.org/licenses/mit-license.php)
|
||||
...
|
||||
*/
|
||||
|
||||
IdleTimer = new Class({
|
||||
|
||||
Implements: [Events, Options],
|
||||
|
||||
options: {
|
||||
/*
|
||||
onStart: function(){},
|
||||
onStop: function(){},
|
||||
onIdle: function(){},
|
||||
onActive: function(){},
|
||||
onTimeoutChanged: function(){},
|
||||
*/
|
||||
timeout: 60000,
|
||||
events: ['mousemove', 'keydown', 'mousewheel', 'mousedown', 'touchstart', 'touchmove']
|
||||
},
|
||||
|
||||
initialize: function(element, options) {
|
||||
this.setOptions(options);
|
||||
this.element = document.id(element);
|
||||
this.activeBound = this.active.bind(this);
|
||||
this.isIdle = false;
|
||||
this.started = false;
|
||||
this.lastPos = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops any existing timeouts and removes the bound events
|
||||
*/
|
||||
stop: function() {
|
||||
clearTimeout(this.timer);
|
||||
|
||||
// Remove bound events
|
||||
for(var i = 0; i < this.options.events.length; i++) {
|
||||
this.element.removeEvent(this.options.events[i], this.activeBound);
|
||||
}
|
||||
this.bound = false;
|
||||
this.started = false;
|
||||
this.lastPos = false;
|
||||
this.fireEvent('stop');
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggered when the user becomes active. May also be launched manually by scripts
|
||||
* if implementing some sort of custom events etc. An example would be flash files
|
||||
* which does not trigger the documents onmousemove event, you could have the flash
|
||||
* call this method to prevent the idle event from being triggered.
|
||||
*/
|
||||
active: function(e) {
|
||||
if(e.event.type == 'mousemove')
|
||||
{
|
||||
// Fix https://code.google.com/p/chromium/issues/detail?id=103041
|
||||
var pos = [e.event.clientX, e.event.clientY];
|
||||
if(this.lastPos === false ||
|
||||
(this.lastPos[0] != pos[0] && this.lastPos[1] != pos[1]))
|
||||
this.lastPos = pos;
|
||||
else
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
if(this.isIdle) this.fireEvent('active');
|
||||
this.isIdle = false;
|
||||
this.start();
|
||||
},
|
||||
|
||||
/**
|
||||
* Fired when the user becomes idle
|
||||
*/
|
||||
idle: function() {
|
||||
if(this.timer) clearTimeout(this.timer); // If called manually, timer will have to be removed
|
||||
this.isIdle = true;
|
||||
this.fireEvent('idle');
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the timer which eventually will reach idle() if the user is inactive
|
||||
*/
|
||||
start: function() {
|
||||
if(this.timer) clearTimeout(this.timer); // If called twice, timer will have to be removed
|
||||
this.timer = this.idle.delay(this.options.timeout, this);
|
||||
this.lastActive = Date.now();
|
||||
if(!this.bound) this.bind();
|
||||
this.started = true;
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind events to the element
|
||||
*/
|
||||
bind: function() {
|
||||
for(var i = 0; i < this.options.events.length; i++) {
|
||||
this.element.addEvent(this.options.events[i], this.activeBound);
|
||||
}
|
||||
this.bound = true;
|
||||
this.fireEvent('start');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns how many seconds/milliseconds have passed since the user was last idle
|
||||
*/
|
||||
getIdleTime: function(returnSeconds) {
|
||||
return returnSeconds ? Math.round((Date.now() - this.lastActive) / 1000) : Date.now() - this.lastActive;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the number of milliseconds is concidered "idle".
|
||||
* Will also attempt to fix any difference in the old and new timeout values,
|
||||
* unless you pass true as whenActive - in this case the new timeout will be
|
||||
* in play the next time the user is active again.
|
||||
*/
|
||||
setTimeout: function(newTime, whenActive) {
|
||||
var old = this.options.timeout;
|
||||
this.options.timeout = newTime;
|
||||
|
||||
if(whenActive) return this; // The developer wants to wait until the next time the user is active before setting the new timeout
|
||||
|
||||
// In all cases, we need a new timer
|
||||
clearTimeout(this.timer);
|
||||
|
||||
// Fire a new timeout event
|
||||
this.fireEvent('timeoutChanged', newTime);
|
||||
|
||||
// How much time has ellapsed since we were last active?
|
||||
var elapsed = this.getIdleTime();
|
||||
|
||||
if(elapsed < newTime && this.isIdle) {
|
||||
// Set as active, cause the new "idle" time has not been reached
|
||||
this.fireEvent('active');
|
||||
this.isIdle = false;
|
||||
} else if(elapsed >= newTime) {
|
||||
// We've not reached the limit before, but with the new timeout, we now have.
|
||||
this.idle();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Set new timer
|
||||
this.timer = this.idle.delay(newTime - elapsed, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Element.Properties.idle = {
|
||||
|
||||
set: function(options) {
|
||||
var idle = this.retrieve('idle');
|
||||
if (idle) idle.stop();
|
||||
return this.eliminate('idle').store('idle:options', options);
|
||||
},
|
||||
|
||||
get: function(options) {
|
||||
if (options || !this.retrieve('idle')) {
|
||||
if (options || !this.retrieve('idle:options')) this.set('idle', options);
|
||||
this.store('idle', new IdleTimer(this, this.retrieve('idle:options')));
|
||||
}
|
||||
return this.retrieve('idle');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Element.Events.idle = {
|
||||
onAdd: function(fn) {
|
||||
var global = this.get ? false : true;
|
||||
var idler = global ? window.idleTimer : this.get('idle');
|
||||
if(global && !idler) { idler = window.idleTimer = new IdleTimer(Browser.ie ? document : this); }
|
||||
if(!idler.started) idler.start();
|
||||
idler.addEvent('idle', fn);
|
||||
}
|
||||
};
|
||||
Element.Events.active = {
|
||||
onAdd: function(fn) {
|
||||
(this.get ? this.get('idle') : window.idleTimer).addEvent('active', fn);
|
||||
}
|
||||
};
|
||||
77
bzz/bzzdemo/mootools-mooswipe.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
---
|
||||
description: Swipe events for touch devices.
|
||||
|
||||
license: MIT-style.
|
||||
|
||||
authors:
|
||||
- Caleb Troughton
|
||||
|
||||
requires:
|
||||
core/1.2.4:
|
||||
- Element.Event
|
||||
- Class
|
||||
- Class.Extras
|
||||
|
||||
provides:
|
||||
MooSwipe
|
||||
*/
|
||||
var MooSwipe = MooSwipe || new Class({
|
||||
Implements: [Options, Events],
|
||||
|
||||
options: {
|
||||
//onSwipeleft: $empty,
|
||||
//onSwiperight: $empty,
|
||||
//onSwipeup: $empty,
|
||||
//onSwipedown: $empty,
|
||||
tolerance: 50,
|
||||
preventDefaults: true
|
||||
},
|
||||
|
||||
element: null,
|
||||
startX: null,
|
||||
startY: null,
|
||||
isMoving: false,
|
||||
|
||||
initialize: function(el, options) {
|
||||
this.setOptions(options);
|
||||
this.element = $(el);
|
||||
this.element.addListener('touchstart', this.onTouchStart.bind(this));
|
||||
},
|
||||
|
||||
cancelTouch: function() {
|
||||
this.element.removeListener('touchmove', this.onTouchMove);
|
||||
this.startX = null;
|
||||
this.startY = null;
|
||||
this.isMoving = false;
|
||||
},
|
||||
|
||||
onTouchMove: function(e) {
|
||||
if (e.touches.length == 1) {
|
||||
this.options.preventDefaults && e.preventDefault();
|
||||
if (this.isMoving) {
|
||||
var dx = this.startX - e.touches[0].pageX;
|
||||
var dy = this.startY - e.touches[0].pageY;
|
||||
if (Math.abs(dx) >= this.options.tolerance) {
|
||||
this.cancelTouch();
|
||||
this.fireEvent(dx > 0 ? 'swipeleft' : 'swiperight');
|
||||
} else if (Math.abs(dy) >= this.options.tolerance) {
|
||||
this.cancelTouch();
|
||||
this.fireEvent(dy > 0 ? 'swipedown' : 'swipeup');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.isMoving) {
|
||||
this.cancelTouch();
|
||||
}
|
||||
},
|
||||
|
||||
onTouchStart: function(e) {
|
||||
if (e.touches.length == 1) {
|
||||
this.startX = e.touches[0].pageX;
|
||||
this.startY = e.touches[0].pageY;
|
||||
this.isMoving = true;
|
||||
this.element.addListener('touchmove', this.onTouchMove.bind(this));
|
||||
}
|
||||
}
|
||||
});
|
||||
45
bzz/bzzdemo/mootools-more-1.4.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// MooTools: the javascript framework.
|
||||
// Load this file's selection again by visiting: http://mootools.net/more/79cd71c11847ba8e70a662f66829987d
|
||||
// Or build this file again with packager using: packager build More/Element.Measure More/Fx.Scroll More/Assets
|
||||
/*
|
||||
---
|
||||
copyrights:
|
||||
- [MooTools](http://mootools.net)
|
||||
|
||||
licenses:
|
||||
- [MIT License](http://mootools.net/license.txt)
|
||||
...
|
||||
*/
|
||||
MooTools.More={version:"1.4.0.1",build:"a4244edf2aa97ac8a196fc96082dd35af1abab87"};(function(){var b=function(e,d){var f=[];Object.each(d,function(g){Object.each(g,function(h){e.each(function(i){f.push(i+"-"+h+(i=="border"?"-width":""));
|
||||
});});});return f;};var c=function(f,e){var d=0;Object.each(e,function(h,g){if(g.test(f)){d=d+h.toInt();}});return d;};var a=function(d){return !!(!d||d.offsetHeight||d.offsetWidth);
|
||||
};Element.implement({measure:function(h){if(a(this)){return h.call(this);}var g=this.getParent(),e=[];while(!a(g)&&g!=document.body){e.push(g.expose());
|
||||
g=g.getParent();}var f=this.expose(),d=h.call(this);f();e.each(function(i){i();});return d;},expose:function(){if(this.getStyle("display")!="none"){return function(){};
|
||||
}var d=this.style.cssText;this.setStyles({display:"block",position:"absolute",visibility:"hidden"});return function(){this.style.cssText=d;}.bind(this);
|
||||
},getDimensions:function(d){d=Object.merge({computeSize:false},d);var i={x:0,y:0};var h=function(j,e){return(e.computeSize)?j.getComputedSize(e):j.getSize();
|
||||
};var f=this.getParent("body");if(f&&this.getStyle("display")=="none"){i=this.measure(function(){return h(this,d);});}else{if(f){try{i=h(this,d);}catch(g){}}}return Object.append(i,(i.x||i.x===0)?{width:i.x,height:i.y}:{x:i.width,y:i.height});
|
||||
},getComputedSize:function(d){d=Object.merge({styles:["padding","border"],planes:{height:["top","bottom"],width:["left","right"]},mode:"both"},d);var g={},e={width:0,height:0},f;
|
||||
if(d.mode=="vertical"){delete e.width;delete d.planes.width;}else{if(d.mode=="horizontal"){delete e.height;delete d.planes.height;}}b(d.styles,d.planes).each(function(h){g[h]=this.getStyle(h).toInt();
|
||||
},this);Object.each(d.planes,function(i,h){var k=h.capitalize(),j=this.getStyle(h);if(j=="auto"&&!f){f=this.getDimensions();}j=g[h]=(j=="auto")?f[h]:j.toInt();
|
||||
e["total"+k]=j;i.each(function(m){var l=c(m,g);e["computed"+m.capitalize()]=l;e["total"+k]+=l;});},this);return Object.append(e,g);}});})();(function(){Fx.Scroll=new Class({Extends:Fx,options:{offset:{x:0,y:0},wheelStops:true},initialize:function(c,b){this.element=this.subject=document.id(c);
|
||||
this.parent(b);if(typeOf(this.element)!="element"){this.element=document.id(this.element.getDocument().body);}if(this.options.wheelStops){var d=this.element,e=this.cancel.pass(false,this);
|
||||
this.addEvent("start",function(){d.addEvent("mousewheel",e);},true);this.addEvent("complete",function(){d.removeEvent("mousewheel",e);},true);}},set:function(){var b=Array.flatten(arguments);
|
||||
if(Browser.firefox){b=[Math.round(b[0]),Math.round(b[1])];}this.element.scrollTo(b[0],b[1]);return this;},compute:function(d,c,b){return[0,1].map(function(e){return Fx.compute(d[e],c[e],b);
|
||||
});},start:function(c,d){if(!this.check(c,d)){return this;}var b=this.element.getScroll();return this.parent([b.x,b.y],[c,d]);},calculateScroll:function(g,f){var d=this.element,b=d.getScrollSize(),h=d.getScroll(),j=d.getSize(),c=this.options.offset,i={x:g,y:f};
|
||||
for(var e in i){if(!i[e]&&i[e]!==0){i[e]=h[e];}if(typeOf(i[e])!="number"){i[e]=b[e]-j[e];}i[e]+=c[e];}return[i.x,i.y];},toTop:function(){return this.start.apply(this,this.calculateScroll(false,0));
|
||||
},toLeft:function(){return this.start.apply(this,this.calculateScroll(0,false));},toRight:function(){return this.start.apply(this,this.calculateScroll("right",false));
|
||||
},toBottom:function(){return this.start.apply(this,this.calculateScroll(false,"bottom"));},toElement:function(d,e){e=e?Array.from(e):["x","y"];var c=a(this.element)?{x:0,y:0}:this.element.getScroll();
|
||||
var b=Object.map(document.id(d).getPosition(this.element),function(g,f){return e.contains(f)?g+c[f]:false;});return this.start.apply(this,this.calculateScroll(b.x,b.y));
|
||||
},toElementEdge:function(d,g,e){g=g?Array.from(g):["x","y"];d=document.id(d);var i={},f=d.getPosition(this.element),j=d.getSize(),h=this.element.getScroll(),b=this.element.getSize(),c={x:f.x+j.x,y:f.y+j.y};
|
||||
["x","y"].each(function(k){if(g.contains(k)){if(c[k]>h[k]+b[k]){i[k]=c[k]-b[k];}if(f[k]<h[k]){i[k]=f[k];}}if(i[k]==null){i[k]=h[k];}if(e&&e[k]){i[k]=i[k]+e[k];
|
||||
}},this);if(i.x!=h.x||i.y!=h.y){this.start(i.x,i.y);}return this;},toElementCenter:function(e,f,h){f=f?Array.from(f):["x","y"];e=document.id(e);var i={},c=e.getPosition(this.element),d=e.getSize(),b=this.element.getScroll(),g=this.element.getSize();
|
||||
["x","y"].each(function(j){if(f.contains(j)){i[j]=c[j]-(g[j]-d[j])/2;}if(i[j]==null){i[j]=b[j];}if(h&&h[j]){i[j]=i[j]+h[j];}},this);if(i.x!=b.x||i.y!=b.y){this.start(i.x,i.y);
|
||||
}return this;}});function a(b){return(/^(?:body|html)$/i).test(b.tagName);}})();var Asset={javascript:function(d,b){if(!b){b={};}var a=new Element("script",{src:d,type:"text/javascript"}),e=b.document||document,c=b.onload||b.onLoad;
|
||||
delete b.onload;delete b.onLoad;delete b.document;if(c){if(typeof a.onreadystatechange!="undefined"){a.addEvent("readystatechange",function(){if(["loaded","complete"].contains(this.readyState)){c.call(this);
|
||||
}});}else{a.addEvent("load",c);}}return a.set(b).inject(e.head);},css:function(d,a){if(!a){a={};}var b=new Element("link",{rel:"stylesheet",media:"screen",type:"text/css",href:d});
|
||||
var c=a.onload||a.onLoad,e=a.document||document;delete a.onload;delete a.onLoad;delete a.document;if(c){b.addEvent("load",c);}return b.set(a).inject(e.head);
|
||||
},image:function(c,b){if(!b){b={};}var d=new Image(),a=document.id(d)||new Element("img");["load","abort","error"].each(function(e){var g="on"+e,f="on"+e.capitalize(),h=b[g]||b[f]||function(){};
|
||||
delete b[f];delete b[g];d[g]=function(){if(!d){return;}if(!a.parentNode){a.width=d.width;a.height=d.height;}d=d.onload=d.onabort=d.onerror=null;h.delay(1,a,a);
|
||||
a.fireEvent(e,a,1);};});d.src=a.src=c;if(d&&d.complete){d.onload.delay(1);}return a.set(b);},images:function(c,b){c=Array.from(c);var d=function(){},a=0;
|
||||
b=Object.merge({onComplete:d,onProgress:d,onError:d,properties:{}},b);return new Elements(c.map(function(f,e){return Asset.image(f,Object.append(b.properties,{onload:function(){a++;
|
||||
b.onProgress.call(this,a,e,f);if(a==c.length){b.onComplete();}},onerror:function(){a++;b.onError.call(this,a,e,f);if(a==c.length){b.onComplete();}}}));
|
||||
}));}};
|
||||
BIN
bzz/bzzdemo/noise.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
bzz/bzzdemo/right.png
Normal file
|
After Width: | Height: | Size: 669 B |
BIN
bzz/bzzdemo/throbber.gif
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
bzz/bzzdemo/up.png
Normal file
|
After Width: | Height: | Size: 367 B |
38
bzz/bzzhash/bzzhash.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// bzzhash
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/bzz"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: bzzhash <file name>")
|
||||
os.Exit(0)
|
||||
}
|
||||
f, err := os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Println("Error opening file " + os.Args[1])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stat, _ := f.Stat()
|
||||
sr := io.NewSectionReader(f, 0, stat.Size())
|
||||
chunker := &bzz.TreeChunker{}
|
||||
chunker.Init()
|
||||
hash := make([]byte, chunker.KeySize())
|
||||
errC := chunker.Split(hash, sr, nil, nil)
|
||||
err, ok := <-errC
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
}
|
||||
if !ok {
|
||||
fmt.Printf("%064x\n", hash)
|
||||
}
|
||||
}
|
||||
BIN
bzz/bzztest/test0/img/logo.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
9
bzz/bzztest/test0/index.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
h1 {
|
||||
color: black;
|
||||
font-size: 12px;
|
||||
background-color: orange;
|
||||
border: 4px solid black;
|
||||
}
|
||||
body {
|
||||
background-color: orange
|
||||
}
|
||||
10
bzz/bzztest/test0/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Swarm Test</h1>
|
||||
<img src="img/logo.gif" align="center", alt="Ethereum logo">
|
||||
</body>
|
||||
</html>
|
||||
15
bzz/bzzup/bzzhandler.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Register Swarm protocol handler</title>
|
||||
<script type="text/javascript">
|
||||
navigator.registerProtocolHandler("bzz",
|
||||
"http://localhost:8500/%s",
|
||||
"Swarm handler");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Register Swarm protocol handler</h1>
|
||||
<p>This web page will install a web protocol handler for the <code>bzz:</code> protocol.</p>
|
||||
</body>
|
||||
</html>
|
||||
42
bzz/bzzup/bzzup.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#! /bin/bash
|
||||
|
||||
INDEX='index.html'
|
||||
|
||||
delimiter='{"entries":[{'
|
||||
|
||||
if [ -f "$1" ]; then
|
||||
hash=`wget -q -O- --post-file="$1" http://localhost:8500/raw`
|
||||
mime=`mimetype -b "$1"`
|
||||
wget -q -O- --post-data="$delimiter\"hash\":\"$hash\",\"contentType\":\"$mime\"}]}" http://localhost:8500/raw
|
||||
echo
|
||||
|
||||
else
|
||||
|
||||
[ -d "$1" ] || exit -1
|
||||
|
||||
bzzroot="$1"
|
||||
[ "_$1" = _ ] && bzzroot=.
|
||||
|
||||
pushd "$bzzroot" > /dev/null
|
||||
|
||||
(for path in `find . -type f`
|
||||
do
|
||||
name=`echo "$path" | cut -c3-`
|
||||
[ _`basename "$name"` = "_$INDEX" ] && name=`dirname "$name"`
|
||||
echo -n "$delimiter"
|
||||
hash=`wget -q -O- --post-file="$path" http://localhost:8500/raw`
|
||||
mime=`mimetype -b "$path"`
|
||||
if [ "_$name" = '_.' ]; then
|
||||
echo -n "\"hash\":\"$hash\",\"contentType\":\"$mime\""
|
||||
else
|
||||
echo -n "\"hash\":\"$hash\",\"path\":\"$name\",\"contentType\":\"$mime\""
|
||||
fi
|
||||
delimiter='},{'
|
||||
|
||||
done
|
||||
echo -n '}]}') | wget -q -O- --post-data=`cat` http://localhost:8500/raw
|
||||
echo
|
||||
|
||||
popd > /dev/null
|
||||
|
||||
fi
|
||||
451
bzz/chunker.go
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
/*
|
||||
The distributed storage implemented in this package requires fix sized chunks of content.
|
||||
|
||||
Chunker is the interface to a component that is responsible for disassembling and assembling larger data.
|
||||
|
||||
TreeChunker implements a Chunker based on a tree structure defined as follows:
|
||||
|
||||
1 each node in the tree including the root and other branching nodes are stored as a chunk.
|
||||
|
||||
2 branching nodes encode data contents that includes the size of the dataslice covered by its entire subtree under the node as well as the hash keys of all its children :
|
||||
data_{i} := size(subtree_{i}) || key_{j} || key_{j+1} .... || key_{j+n-1}
|
||||
|
||||
3 Leaf nodes encode an actual subslice of the input data.
|
||||
|
||||
4 if data size is not more than maximum chunksize, the data is stored in a single chunk
|
||||
key = sha256(int64(size) + data)
|
||||
|
||||
2 if data size is more than chunksize*Branches^l, but no more than chunksize*
|
||||
Branches^(l+1), the data vector is split into slices of chunksize*
|
||||
Branches^l length (except the last one).
|
||||
key = sha256(int64(size) + key(slice0) + key(slice1) + ...)
|
||||
*/
|
||||
|
||||
package bzz
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
const (
|
||||
hasherfunc crypto.Hash = crypto.SHA256 // http://golang.org/pkg/hash/#Hash
|
||||
defaultBranches int64 = 128
|
||||
)
|
||||
|
||||
var (
|
||||
// hashSize int64 = hasherfunc.New().Size() // hasher knows about its own length in bytes
|
||||
// chunksize int64 = branches * hashSize // chunk is defined as this
|
||||
joinTimeout = 120 * time.Second
|
||||
splitTimeout = 120 * time.Second
|
||||
)
|
||||
|
||||
type Key []byte
|
||||
|
||||
func (key Key) Hex() string {
|
||||
return fmt.Sprintf("%064x", key[:])
|
||||
}
|
||||
|
||||
/*
|
||||
Chunker is the interface to a component that is responsible for disassembling and assembling larger data and indended to be the dependency of a DPA storage system with fixed maximum chunksize.
|
||||
|
||||
It relies on the underlying chunking model.
|
||||
|
||||
When calling Split, the caller provides a channel (chan *Chunk) on which it receives chunks to store. The DPA delegates to storage layers (implementing ChunkStore interface).
|
||||
|
||||
Split returns an error channel, which the caller can monitor.
|
||||
After getting notified that all the data has been split (the error channel is closed), the caller can safely read or save the root key. Optionally it times out if not all chunks get stored or not the entire stream of data has been processed. By inspecting the errc channel the caller can check if any explicit errors (typically IO read/write failures) occured during splitting.
|
||||
|
||||
When calling Join with a root key, the caller gets returned a seekable lazy reader. The caller again provides a channel on which the caller receives placeholder chunks with missing data. The DPA is supposed to forward this to the chunk stores and notify the chunker if the data has been delivered (i.e. retrieved from memory cache, disk-persisted db or cloud based swarm delivery). As the seekable reader is used, the chunker then puts these together the relevant parts on demand.
|
||||
*/
|
||||
type Chunker interface {
|
||||
/*
|
||||
When splitting, data is given as a SectionReader, and the key is a hashSize long byte slice (Key), the root hash of the entire content will fill this once processing finishes.
|
||||
New chunks to store are coming to caller via the chunk storage channel, which the caller provides.
|
||||
wg is a Waitgroup (can be nil) that can be used to block until the local storage finishes
|
||||
The caller gets returned an error channel, if an error is encountered during splitting, it is fed to errC error channel.
|
||||
A closed error signals process completion at which point the key can be considered final if there were no errors.
|
||||
*/
|
||||
Split(key Key, data SectionReader, chunkC chan *Chunk, wg *sync.WaitGroup) chan error
|
||||
/*
|
||||
Join reconstructs original content based on a root key.
|
||||
When joining, the caller gets returned a Lazy SectionReader, which is
|
||||
seekable and implements on-demand fetching of chunks as and where it is read.
|
||||
New chunks to retrieve are coming to caller via the Chunk channel, which the caller provides.
|
||||
If an error is encountered during joining, it appears as a reader error.
|
||||
The SectionReader.
|
||||
As a result, partial reads from a document are possible even if other parts
|
||||
are corrupt or lost.
|
||||
The chunks are not meant to be validated by the chunker when joining. This
|
||||
is because it is left to the DPA to decide which sources are trusted.
|
||||
*/
|
||||
Join(key Key, chunkC chan *Chunk) SectionReader
|
||||
|
||||
// returns the key length
|
||||
KeySize() int64
|
||||
}
|
||||
|
||||
/*
|
||||
Tree chunker is a concrete implementation of data chunking.
|
||||
This chunker works in a simple way, it builds a tree out of the document so that each node either represents a chunk of real data or a chunk of data representing an branching non-leaf node of the tree. In particular each such non-leaf chunk will represent is a concatenation of the hash of its respective children. This scheme simultaneously guarantees data integrity as well as self addressing. Abstract nodes are transparent since their represented size component is strictly greater than their maximum data size, since they encode a subtree.
|
||||
|
||||
If all is well it is possible to implement this by simply composing readers so that no extra allocation or buffering is necessary for the data splitting and joining. This means that in principle there can be direct IO between : memory, file system, network socket (bzz peers storage request is read from the socket). In practice there may be need for several stages of internal buffering.
|
||||
The hashing itself does use extra copies and allocation though, since it does need it.
|
||||
*/
|
||||
|
||||
type TreeChunker struct {
|
||||
Branches int64
|
||||
HashFunc crypto.Hash
|
||||
JoinTimeout time.Duration
|
||||
SplitTimeout time.Duration
|
||||
// calculated
|
||||
hashSize int64 // self.HashFunc.New().Size()
|
||||
chunkSize int64 // hashSize* Branches
|
||||
}
|
||||
|
||||
func (self *TreeChunker) Init() {
|
||||
if self.HashFunc == 0 {
|
||||
self.HashFunc = hasherfunc
|
||||
}
|
||||
if self.Branches == 0 {
|
||||
self.Branches = defaultBranches
|
||||
}
|
||||
if self.JoinTimeout == 0 {
|
||||
self.JoinTimeout = joinTimeout
|
||||
}
|
||||
if self.SplitTimeout == 0 {
|
||||
self.SplitTimeout = splitTimeout
|
||||
}
|
||||
self.hashSize = int64(self.HashFunc.New().Size())
|
||||
self.chunkSize = self.hashSize * self.Branches
|
||||
// dpaLogger.Debugf("Chunker initialised: branches: %v, hashsize: %v, chunksize: %v, join timeout: %v , split timeout: %v", self.Branches, self.hashSize, self.chunkSize, self.JoinTimeout, self.SplitTimeout)
|
||||
|
||||
}
|
||||
|
||||
func (self *TreeChunker) KeySize() int64 {
|
||||
return self.hashSize
|
||||
}
|
||||
|
||||
// String() for pretty printing
|
||||
func (self *Chunk) String() string {
|
||||
var size int64
|
||||
var n int
|
||||
return fmt.Sprintf("Key: [%x..] TreeSize: %v Chunksize: %v Data: %x\n", self.Key[:4], self.Size, size, self.SData[:n])
|
||||
}
|
||||
|
||||
// The treeChunkers own Hash hashes together
|
||||
// - the size (of the subtree encoded in the Chunk)
|
||||
// - the Chunk, ie. the contents read from the input reader
|
||||
func (self *TreeChunker) Hash(input []byte) []byte {
|
||||
hasher := self.HashFunc.New()
|
||||
hasher.Write(input)
|
||||
return hasher.Sum(nil)
|
||||
}
|
||||
|
||||
func (self *TreeChunker) Split(key Key, data SectionReader, chunkC chan *Chunk, swg *sync.WaitGroup) (errC chan error) {
|
||||
|
||||
if swg != nil {
|
||||
swg.Add(1)
|
||||
defer swg.Done()
|
||||
}
|
||||
|
||||
if self.chunkSize <= 0 {
|
||||
panic("chunker must be initialised")
|
||||
}
|
||||
|
||||
if int64(len(key)) != self.hashSize {
|
||||
panic(fmt.Sprintf("root key buffer must be allocated byte slice of length %d", self.hashSize))
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
errC = make(chan error)
|
||||
rerrC := make(chan error)
|
||||
timeout := time.After(self.SplitTimeout)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
|
||||
depth := 0
|
||||
treeSize := self.chunkSize
|
||||
size := data.Size()
|
||||
// takes lowest depth such that chunksize*HashCount^(depth+1) > size
|
||||
// power series, will find the order of magnitude of the data size in base hashCount or numbers of levels of branching in the resulting tree.
|
||||
|
||||
for ; treeSize < size; treeSize *= self.Branches {
|
||||
depth++
|
||||
}
|
||||
|
||||
// glog.V(logger.Detail).Infof("[BZZ] split request received for data (%v bytes, depth: %v)", size, depth)
|
||||
|
||||
//launch actual recursive function passing the workgroup
|
||||
self.split(depth, treeSize/self.Branches, key, data, chunkC, rerrC, wg, swg)
|
||||
}()
|
||||
|
||||
// closes internal error channel if all subprocesses in the workgroup finished
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(rerrC)
|
||||
|
||||
}()
|
||||
|
||||
// waiting for request to end with wg finishing, error, or timeout
|
||||
go func() {
|
||||
select {
|
||||
case err := <-rerrC:
|
||||
if err != nil {
|
||||
errC <- err
|
||||
} // otherwise splitting is complete
|
||||
case <-timeout:
|
||||
errC <- fmt.Errorf("split time out")
|
||||
}
|
||||
close(errC)
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *TreeChunker) split(depth int, treeSize int64, key Key, data SectionReader, chunkC chan *Chunk, errc chan error, parentWg *sync.WaitGroup, swg *sync.WaitGroup) {
|
||||
|
||||
defer parentWg.Done()
|
||||
|
||||
size := data.Size()
|
||||
var newChunk *Chunk
|
||||
var hash Key
|
||||
// glog.V(logger.Detail).Infof("[BZZ] depth: %v, max subtree size: %v, data size: %v", depth, treeSize, size)
|
||||
|
||||
for depth > 0 && size < treeSize {
|
||||
treeSize /= self.Branches
|
||||
depth--
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
// leaf nodes -> content chunks
|
||||
chunkData := make([]byte, data.Size()+8)
|
||||
binary.LittleEndian.PutUint64(chunkData[0:8], uint64(size))
|
||||
data.ReadAt(chunkData[8:], 0)
|
||||
hash = self.Hash(chunkData)
|
||||
// glog.V(logger.Detail).Infof("[BZZ] content chunk: max subtree size: %v, data size: %v", treeSize, size)
|
||||
newChunk = &Chunk{
|
||||
Key: hash,
|
||||
SData: chunkData,
|
||||
Size: size,
|
||||
}
|
||||
} else {
|
||||
// intermediate chunk containing child nodes hashes
|
||||
branchCnt := int64((size + treeSize - 1) / treeSize)
|
||||
// glog.V(logger.Detail).Infof("[BZZ] intermediate node: setting branches: %v, depth: %v, max subtree size: %v, data size: %v", branches, depth, treeSize, size)
|
||||
|
||||
var chunk []byte = make([]byte, branchCnt*self.hashSize+8)
|
||||
var pos, i int64
|
||||
|
||||
binary.LittleEndian.PutUint64(chunk[0:8], uint64(size))
|
||||
|
||||
childrenWg := &sync.WaitGroup{}
|
||||
var secSize int64
|
||||
for i < branchCnt {
|
||||
// the last item can have shorter data
|
||||
if size-pos < treeSize {
|
||||
secSize = size - pos
|
||||
} else {
|
||||
secSize = treeSize
|
||||
}
|
||||
// take the section of the data encoded in the subTree
|
||||
subTreeData := NewChunkReader(data, pos, secSize)
|
||||
// the hash of that data
|
||||
subTreeKey := chunk[8+i*self.hashSize : 8+(i+1)*self.hashSize]
|
||||
|
||||
childrenWg.Add(1)
|
||||
go self.split(depth-1, treeSize/self.Branches, subTreeKey, subTreeData, chunkC, errc, childrenWg, swg)
|
||||
|
||||
i++
|
||||
pos += treeSize
|
||||
}
|
||||
// wait for all the children to complete calculating their hashes and copying them onto sections of the chunk
|
||||
childrenWg.Wait()
|
||||
// now we got the hashes in the chunk, then hash the chunks
|
||||
hash = self.Hash(chunk)
|
||||
newChunk = &Chunk{
|
||||
Key: hash,
|
||||
SData: chunk,
|
||||
Size: size,
|
||||
wg: swg,
|
||||
}
|
||||
|
||||
if swg != nil {
|
||||
swg.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// send off new chunk to storage
|
||||
if chunkC != nil {
|
||||
chunkC <- newChunk
|
||||
}
|
||||
// report hash of this chunk one level up (keys corresponds to the proper subslice of the parent chunk)x
|
||||
copy(key, hash)
|
||||
|
||||
}
|
||||
|
||||
func (self *TreeChunker) Join(key Key, chunkC chan *Chunk) SectionReader {
|
||||
|
||||
return &LazyChunkReader{
|
||||
key: key,
|
||||
chunkC: chunkC,
|
||||
quitC: make(chan bool),
|
||||
errC: make(chan error),
|
||||
chunker: self,
|
||||
}
|
||||
}
|
||||
|
||||
// LazyChunkReader implements LazySectionReader
|
||||
type LazyChunkReader struct {
|
||||
key Key // root key
|
||||
chunkC chan *Chunk // chunk channel to send retrieve requests on
|
||||
size int64 // size of the entire subtree
|
||||
off int64 // offset
|
||||
quitC chan bool // channel to abort retrieval
|
||||
errC chan error // error channel to monitor retrieve errors
|
||||
chunker *TreeChunker // needs TreeChunker params TODO: should just take
|
||||
// the chunkSize, Branches etc as params
|
||||
}
|
||||
|
||||
func (self *LazyChunkReader) ReadAt(b []byte, off int64) (read int, err error) {
|
||||
self.errC = make(chan error)
|
||||
chunk := &Chunk{
|
||||
Key: self.key,
|
||||
C: make(chan bool), // close channel to signal data delivery
|
||||
}
|
||||
self.chunkC <- chunk // submit retrieval request, someone should be listening on the other side (or we will time out globally)
|
||||
glog.V(logger.Detail).Infof("[BZZ] readAt: reading %x into %d bytes at offset %d.", chunk.Key[:4], len(b), off)
|
||||
|
||||
// waiting for the chunk retrieval
|
||||
select {
|
||||
case <-self.quitC:
|
||||
// this is how we control process leakage (quitC is closed once join is finished (after timeout))
|
||||
// glog.V(logger.Detail).Infof("[BZZ] quit")
|
||||
return
|
||||
case <-chunk.C: // bells are ringing, data have been delivered
|
||||
// glog.V(logger.Detail).Infof("[BZZ] chunk data received for %x", chunk.Key[:4])
|
||||
}
|
||||
if len(chunk.SData) == 0 {
|
||||
// glog.V(logger.Detail).Infof("[BZZ] No payload in %x", chunk.Key)
|
||||
return 0, notFound
|
||||
}
|
||||
chunk.Size = int64(binary.LittleEndian.Uint64(chunk.SData[0:8]))
|
||||
self.size = chunk.Size
|
||||
if b == nil {
|
||||
// glog.V(logger.Detail).Infof("[BZZ] Size query for %x", chunk.Key[:4])
|
||||
return
|
||||
}
|
||||
want := int64(len(b))
|
||||
if off+want > self.size {
|
||||
want = self.size - off
|
||||
}
|
||||
var treeSize int64
|
||||
var depth int
|
||||
// calculate depth and max treeSize
|
||||
treeSize = self.chunker.chunkSize
|
||||
for ; treeSize < chunk.Size; treeSize *= self.chunker.Branches {
|
||||
depth++
|
||||
}
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go self.join(b, off, off+want, depth, treeSize/self.chunker.Branches, chunk, &wg)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(self.errC)
|
||||
}()
|
||||
select {
|
||||
case err = <-self.errC:
|
||||
// glog.V(logger.Detail).Infof("[BZZ] ReadAt received %v", err)
|
||||
read = len(b)
|
||||
if off+int64(read) == self.size {
|
||||
err = io.EOF
|
||||
}
|
||||
// glog.V(logger.Detail).Infof("[BZZ] ReadAt returning at %d: %v", read, err)
|
||||
case <-self.quitC:
|
||||
// glog.V(logger.Detail).Infof("[BZZ] ReadAt aborted at %d: %v", read, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *LazyChunkReader) join(b []byte, off int64, eoff int64, depth int, treeSize int64, chunk *Chunk, parentWg *sync.WaitGroup) {
|
||||
defer parentWg.Done()
|
||||
|
||||
// glog.V(logger.Detail).Infof("[BZZ] depth: %v, loff: %v, eoff: %v, chunk.Size: %v, treeSize: %v", depth, off, eoff, chunk.Size, treeSize)
|
||||
|
||||
chunk.Size = int64(binary.LittleEndian.Uint64(chunk.SData[0:8]))
|
||||
|
||||
// find appropriate block level
|
||||
for chunk.Size < treeSize && depth > 0 {
|
||||
treeSize /= self.chunker.Branches
|
||||
depth--
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
// glog.V(logger.Detail).Infof("[BZZ] depth: %v, len(b): %v, off: %v, eoff: %v, chunk.Size: %v, treeSize: %v", depth, len(b), off, eoff, chunk.Size, treeSize)
|
||||
if int64(len(b)) != eoff-off {
|
||||
//fmt.Printf("len(b) = %v off = %v eoff = %v", len(b), off, eoff)
|
||||
panic("len(b) does not match")
|
||||
}
|
||||
|
||||
copy(b, chunk.SData[8+off:8+eoff])
|
||||
return // simply give back the chunks reader for content chunks
|
||||
}
|
||||
|
||||
// subtree index
|
||||
start := off / treeSize
|
||||
end := (eoff + treeSize - 1) / treeSize
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
for i := start; i < end; i++ {
|
||||
|
||||
soff := i * treeSize
|
||||
roff := soff
|
||||
seoff := soff + treeSize
|
||||
|
||||
if soff < off {
|
||||
soff = off
|
||||
}
|
||||
if seoff > eoff {
|
||||
seoff = eoff
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(j int64) {
|
||||
childKey := chunk.SData[8+j*self.chunker.hashSize : 8+(j+1)*self.chunker.hashSize]
|
||||
// glog.V(logger.Detail).Infof("[BZZ] subtree index: %v -> %x", j, childKey[:4])
|
||||
|
||||
ch := &Chunk{
|
||||
Key: childKey,
|
||||
C: make(chan bool), // close channel to signal data delivery
|
||||
}
|
||||
// glog.V(logger.Detail).Infof("[BZZ] chunk data sent for %x (key interval in chunk %v-%v)", ch.Key[:4], j*self.chunker.hashSize, (j+1)*self.chunker.hashSize)
|
||||
self.chunkC <- ch // submit retrieval request, someone should be listening on the other side (or we will time out globally)
|
||||
|
||||
// waiting for the chunk retrieval
|
||||
select {
|
||||
case <-self.quitC:
|
||||
// this is how we control process leakage (quitC is closed once join is finished (after timeout))
|
||||
return
|
||||
case <-ch.C: // bells are ringing, data have been delivered
|
||||
// glog.V(logger.Detail).Infof("[BZZ] chunk data received")
|
||||
}
|
||||
if soff < off {
|
||||
soff = off
|
||||
}
|
||||
if len(ch.SData) == 0 {
|
||||
self.errC <- fmt.Errorf("chunk %v-%v not found", off, off+treeSize)
|
||||
return
|
||||
}
|
||||
self.join(b[soff-off:seoff-off], soff-roff, seoff-roff, depth-1, treeSize/self.chunker.Branches, ch, &wg)
|
||||
}(i)
|
||||
} //for
|
||||
wg.Wait()
|
||||
}
|
||||
220
bzz/chunker_test.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
// "fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Tests TreeChunker by splitting and joining a random byte slice
|
||||
*/
|
||||
|
||||
type chunkerTester struct {
|
||||
errors []error
|
||||
chunks []*Chunk
|
||||
timeout bool
|
||||
}
|
||||
|
||||
func (self *chunkerTester) checkChunks(t *testing.T, want int) {
|
||||
l := len(self.chunks)
|
||||
if l != want {
|
||||
t.Errorf("expected %v chunks, got %v", want, l)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *chunkerTester) Split(chunker *TreeChunker, l int) (key Key, input []byte) {
|
||||
// reset
|
||||
self.errors = nil
|
||||
self.chunks = nil
|
||||
self.timeout = false
|
||||
|
||||
data, slice := testDataReader(l)
|
||||
input = slice
|
||||
key = make([]byte, 32)
|
||||
chunkC := make(chan *Chunk, 1000)
|
||||
errC := chunker.Split(key, data, chunkC, nil)
|
||||
quitC := make(chan bool)
|
||||
timeout := time.After(600 * time.Second)
|
||||
|
||||
go func() {
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
self.timeout = true
|
||||
break LOOP
|
||||
|
||||
case chunk := <-chunkC:
|
||||
if chunk != nil {
|
||||
self.chunks = append(self.chunks, chunk)
|
||||
} else {
|
||||
break LOOP
|
||||
}
|
||||
|
||||
case err, ok := <-errC:
|
||||
if err != nil {
|
||||
self.errors = append(self.errors, err)
|
||||
}
|
||||
// fmt.Printf("err %v", err)
|
||||
if !ok {
|
||||
close(chunkC)
|
||||
errC = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
close(quitC)
|
||||
}()
|
||||
<-quitC // waiting for it to finish
|
||||
return
|
||||
}
|
||||
|
||||
func (self *chunkerTester) Join(chunker *TreeChunker, key Key, c int) SectionReader {
|
||||
// reset but not the chunks
|
||||
self.errors = nil
|
||||
self.timeout = false
|
||||
chunkC := make(chan *Chunk, 1000)
|
||||
|
||||
reader := chunker.Join(key, chunkC)
|
||||
|
||||
quitC := make(chan bool)
|
||||
timeout := time.After(600 * time.Second)
|
||||
i := 0
|
||||
go func() {
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case <-quitC:
|
||||
break LOOP
|
||||
|
||||
case <-timeout:
|
||||
self.timeout = true
|
||||
break LOOP
|
||||
|
||||
case chunk := <-chunkC:
|
||||
i++
|
||||
// dpaLogger.DebugDetailf("TESTER: chunk request %x", chunk.Key[:4])
|
||||
// this just mocks the behaviour of a chunk store retrieval
|
||||
var found bool
|
||||
for _, ch := range self.chunks {
|
||||
if bytes.Equal(chunk.Key, ch.Key) {
|
||||
found = true
|
||||
chunk.SData = ch.SData
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
// fmt.Printf("TESTER: chunk unknown for %x", chunk.Key[:4])
|
||||
}
|
||||
close(chunk.C)
|
||||
// dpaLogger.DebugDetailf("TESTER: chunk request served %x", chunk.Key[:4])
|
||||
}
|
||||
}
|
||||
}()
|
||||
return reader
|
||||
}
|
||||
|
||||
func testRandomData(chunker *TreeChunker, tester *chunkerTester, n int, chunks int, t *testing.T) {
|
||||
key, input := tester.Split(chunker, n)
|
||||
|
||||
t.Logf(" Key = %x\n", key)
|
||||
|
||||
tester.checkChunks(t, chunks)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
reader := tester.Join(chunker, key, 0)
|
||||
output := make([]byte, n)
|
||||
r, err := reader.Read(output)
|
||||
if r != n || err != io.EOF {
|
||||
t.Errorf("read error read: %v n = %v err = %v\n", r, n, err)
|
||||
}
|
||||
// t.Logf(" IN: %x\nOUT: %x\n", input, output)
|
||||
if !bytes.Equal(output, input) {
|
||||
t.Errorf("input and output mismatch\n IN: %x\nOUT: %x\n", input, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomData(t *testing.T) {
|
||||
chunker := &TreeChunker{
|
||||
Branches: 2,
|
||||
SplitTimeout: 10 * time.Second,
|
||||
JoinTimeout: 10 * time.Second,
|
||||
}
|
||||
chunker.Init()
|
||||
tester := &chunkerTester{}
|
||||
testRandomData(chunker, tester, 60, 1, t)
|
||||
testRandomData(chunker, tester, 179, 5, t)
|
||||
testRandomData(chunker, tester, 253, 7, t)
|
||||
// t.Logf("chunks %v", tester.chunks)
|
||||
}
|
||||
|
||||
func chunkerAndTester() (chunker *TreeChunker, tester *chunkerTester) {
|
||||
chunker = &TreeChunker{
|
||||
Branches: 2,
|
||||
SplitTimeout: 10 * time.Second,
|
||||
JoinTimeout: 10 * time.Second,
|
||||
}
|
||||
chunker.Init()
|
||||
tester = &chunkerTester{}
|
||||
return
|
||||
}
|
||||
|
||||
func readAll(reader SectionReader, result []byte) {
|
||||
size := int64(len(result))
|
||||
|
||||
var end int64
|
||||
for pos := int64(0); pos < size; pos += 1000 {
|
||||
if pos+1000 > size {
|
||||
end = size
|
||||
} else {
|
||||
end = pos + 1000
|
||||
}
|
||||
reader.ReadAt(result[pos:end], pos)
|
||||
}
|
||||
}
|
||||
|
||||
func benchReadAll(reader SectionReader) {
|
||||
size := reader.Size()
|
||||
output := make([]byte, 1000)
|
||||
for pos := int64(0); pos < size; pos += 1000 {
|
||||
reader.ReadAt(output, pos)
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkJoinRandomData(n int, chunks int, t *testing.B) {
|
||||
t.StopTimer()
|
||||
for i := 0; i < t.N; i++ {
|
||||
// fmt.Printf("round %v\n", i)
|
||||
chunker, tester := chunkerAndTester()
|
||||
key, _ := tester.Split(chunker, n)
|
||||
// fmt.Printf("split done %v, joining...\n", i)
|
||||
t.StartTimer()
|
||||
reader := tester.Join(chunker, key, i)
|
||||
// fmt.Printf("join done %v, reading...\n", i)
|
||||
benchReadAll(reader)
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkSplitRandomData(n int, chunks int, t *testing.B) {
|
||||
for i := 0; i < t.N; i++ {
|
||||
chunker, tester := chunkerAndTester()
|
||||
tester.Split(chunker, n)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJoinRandomData_100_2(t *testing.B) { benchmarkJoinRandomData(100, 3, t) }
|
||||
func BenchmarkJoinRandomData_1000_2(t *testing.B) { benchmarkJoinRandomData(1000, 3, t) }
|
||||
func BenchmarkJoinRandomData_10000_2(t *testing.B) { benchmarkJoinRandomData(10000, 3, t) }
|
||||
func BenchmarkJoinRandomData_100000_2(t *testing.B) { benchmarkJoinRandomData(100000, 3, t) }
|
||||
func BenchmarkJoinRandomData_1000000_2(t *testing.B) { benchmarkJoinRandomData(1000000, 3, t) }
|
||||
|
||||
func BenchmarkSplitRandomData_100_2(t *testing.B) { benchmarkSplitRandomData(100, 3, t) }
|
||||
func BenchmarkSplitRandomData_1000_2(t *testing.B) { benchmarkSplitRandomData(1000, 3, t) }
|
||||
func BenchmarkSplitRandomData_10000_2(t *testing.B) { benchmarkSplitRandomData(10000, 3, t) }
|
||||
func BenchmarkSplitRandomData_100000_2(t *testing.B) { benchmarkSplitRandomData(100000, 3, t) }
|
||||
func BenchmarkSplitRandomData_1000000_2(t *testing.B) { benchmarkSplitRandomData(1000000, 3, t) }
|
||||
func BenchmarkSplitRandomData_10000000_2(t *testing.B) { benchmarkSplitRandomData(10000000, 3, t) }
|
||||
|
||||
// go test -bench ./bzz -cpuprofile cpu.out -memprofile mem.out
|
||||
194
bzz/chunkreader.go
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Bounded interface {
|
||||
Size() int64
|
||||
}
|
||||
|
||||
type Sliced interface {
|
||||
Slice(int64, int64) (b []byte, err error)
|
||||
}
|
||||
|
||||
// Size, Seek, Read, ReadAt
|
||||
type SectionReader interface {
|
||||
Bounded
|
||||
io.Seeker
|
||||
io.Reader
|
||||
io.ReaderAt
|
||||
}
|
||||
|
||||
// ChunkReader implements SectionReader on a section
|
||||
// of an underlying ReaderAt.
|
||||
type ChunkReader struct {
|
||||
r io.ReaderAt
|
||||
base int64
|
||||
off int64
|
||||
limit int64
|
||||
}
|
||||
|
||||
// NewChunkReader returns a ChunkReader that reads from r
|
||||
// starting at offset off and stops with EOF after n bytes.
|
||||
func NewChunkReader(r io.ReaderAt, off int64, n int64) *ChunkReader {
|
||||
return &ChunkReader{r: r, base: off, off: off, limit: off + n}
|
||||
}
|
||||
|
||||
// ByteSliceReader just extends byte.Reader to make base slice accessible
|
||||
type ByteSliceReader struct {
|
||||
*bytes.Reader
|
||||
base []byte
|
||||
}
|
||||
|
||||
func NewByteSliceReader(b []byte) *ByteSliceReader {
|
||||
return &ByteSliceReader{
|
||||
base: b,
|
||||
Reader: bytes.NewReader(b),
|
||||
}
|
||||
}
|
||||
|
||||
// ByteSliceReader implements the Sliced interface
|
||||
func (self *ByteSliceReader) Slice(from, to int64) (b []byte, err error) {
|
||||
if from < 0 || to >= int64(self.Len()) {
|
||||
err = io.EOF
|
||||
} else {
|
||||
b = self.base[from:to]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewChunkReaderFromBytes is a convenience shortcut to get a SectionReader over a byte slice
|
||||
func NewChunkReaderFromBytes(b []byte) *ChunkReader {
|
||||
return NewChunkReader(NewByteSliceReader(b), 0, int64(len(b)))
|
||||
}
|
||||
|
||||
/*
|
||||
The following is adapted from io.SectionReader
|
||||
*/
|
||||
|
||||
func (s *ChunkReader) Size() int64 {
|
||||
return s.limit - s.base
|
||||
}
|
||||
|
||||
var errWhence = errors.New("Seek: invalid whence")
|
||||
var errOffset = errors.New("Seek: invalid offset")
|
||||
|
||||
func (s *ChunkReader) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
default:
|
||||
return 0, errWhence
|
||||
case 0:
|
||||
offset += s.base
|
||||
case 1:
|
||||
offset += s.off
|
||||
case 2:
|
||||
offset += s.limit
|
||||
}
|
||||
if offset < s.base {
|
||||
return 0, errOffset
|
||||
}
|
||||
s.off = offset
|
||||
return offset - s.base, nil
|
||||
}
|
||||
|
||||
func (s *ChunkReader) Read(p []byte) (n int, err error) {
|
||||
if s.off >= s.limit {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if max := s.limit - s.off; int64(len(p)) > max {
|
||||
p = p[0:max]
|
||||
}
|
||||
n, err = s.r.ReadAt(p, s.off)
|
||||
s.off += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *ChunkReader) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
if off < 0 || off >= s.limit-s.base {
|
||||
return 0, io.EOF
|
||||
}
|
||||
off += s.base
|
||||
if max := s.limit - off; int64(len(p)) > max {
|
||||
p = p[0:max]
|
||||
n, err = s.r.ReadAt(p, off)
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
n, err = s.r.ReadAt(p, off)
|
||||
return
|
||||
}
|
||||
|
||||
// added methods to that ChunkReader implements the Sliced interface
|
||||
func (s *ChunkReader) Slice(from, to int64) (b []byte, err error) {
|
||||
if from < 0 || to >= s.Size() {
|
||||
err = io.EOF
|
||||
} else {
|
||||
if sl, ok := s.r.(Sliced); ok {
|
||||
b, err = sl.Slice(s.base+from, s.base+to)
|
||||
} else {
|
||||
err = errors.New("not sliceable base")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// added method so that ChunkReader implements the io.WriterTo interface
|
||||
// WriteTo method is used by io.Copy
|
||||
// This is so that we avoid one extra step of allocation (if the underlying initial Reader implements Sliced
|
||||
func (r *ChunkReader) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var b []byte
|
||||
var m int
|
||||
// if b, _ := r.Slice(r.off-r.base, r.limit-r.base); b == nil {
|
||||
// if slices not available we do it with extra allocation
|
||||
b = make([]byte, r.limit-r.off)
|
||||
m, err = r.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// }
|
||||
m, err = w.Write(b)
|
||||
if m > len(b) {
|
||||
panic("bytes.Reader.WriteTo: invalid Write count")
|
||||
}
|
||||
r.off = r.base + int64(m)
|
||||
n = int64(m)
|
||||
if m != len(b) && err == nil {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
// w
|
||||
return
|
||||
}
|
||||
|
||||
func (self *LazyChunkReader) Size() (n int64) {
|
||||
self.ReadAt(nil, 0)
|
||||
return self.size
|
||||
}
|
||||
|
||||
func (self *LazyChunkReader) Read(b []byte) (read int, err error) {
|
||||
read, err = self.ReadAt(b, self.off)
|
||||
self.off += int64(read)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LazyChunkReader) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
default:
|
||||
return 0, errWhence
|
||||
case 0:
|
||||
offset += 0
|
||||
case 1:
|
||||
offset += s.off
|
||||
case 2:
|
||||
offset += s.size
|
||||
}
|
||||
if offset < 0 {
|
||||
return 0, errOffset
|
||||
}
|
||||
s.off = offset
|
||||
return offset, nil
|
||||
}
|
||||
92
bzz/common_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
func testDataReader(l int) (r *ChunkReader, slice []byte) {
|
||||
slice = make([]byte, l)
|
||||
if _, err := rand.Read(slice); err != nil {
|
||||
panic("rand error")
|
||||
}
|
||||
r = NewChunkReaderFromBytes(slice)
|
||||
return
|
||||
}
|
||||
|
||||
func randomChunks(l int64, branches int64, chunkC chan *Chunk) (key Key, errC chan error) {
|
||||
chunker := &TreeChunker{
|
||||
Branches: branches,
|
||||
}
|
||||
chunker.Init()
|
||||
key = make([]byte, 32)
|
||||
b := make([]byte, l)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
panic("no rand")
|
||||
}
|
||||
wg := &sync.WaitGroup{}
|
||||
errC = chunker.Split(key, NewChunkReaderFromBytes(b), chunkC, wg)
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
func testStore(m ChunkStore, l int64, branches int64, t *testing.T) {
|
||||
|
||||
chunkC := make(chan *Chunk)
|
||||
key, errC := randomChunks(l, branches, chunkC)
|
||||
|
||||
SPLIT:
|
||||
for {
|
||||
select {
|
||||
case chunk := <-chunkC:
|
||||
m.Put(chunk)
|
||||
case err, ok := <-errC:
|
||||
if err != nil {
|
||||
t.Errorf("Chunker error: %v", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
break SPLIT
|
||||
}
|
||||
}
|
||||
}
|
||||
chunker := &TreeChunker{
|
||||
Branches: branches,
|
||||
}
|
||||
chunker.Init()
|
||||
chunkC = make(chan *Chunk)
|
||||
var r SectionReader
|
||||
r = chunker.Join(key, chunkC)
|
||||
|
||||
quit := make(chan bool)
|
||||
|
||||
go func() {
|
||||
for ch := range chunkC {
|
||||
go func(chunk *Chunk) {
|
||||
storedChunk, err := m.Get(chunk.Key)
|
||||
if err == notFound {
|
||||
glog.V(logger.Detail).Infof("[BZZ] chunk '%x' not found", chunk.Key)
|
||||
} else if err != nil {
|
||||
glog.V(logger.Detail).Infof("[BZZ] error retrieving chunk %x: %v", chunk.Key, err)
|
||||
} else {
|
||||
chunk.SData = storedChunk.SData
|
||||
}
|
||||
glog.V(logger.Detail).Infof("[BZZ] chunk '%x' not found", chunk.Key[:4])
|
||||
close(chunk.C)
|
||||
}(ch)
|
||||
}
|
||||
}()
|
||||
|
||||
b := make([]byte, l)
|
||||
n, err := r.ReadAt(b, 0)
|
||||
if err != io.EOF {
|
||||
t.Errorf("read error (%v/%v) %v", n, l, err)
|
||||
close(quit)
|
||||
}
|
||||
}
|
||||
96
bzz/database.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package bzz
|
||||
|
||||
// this is a clone of an earlier state of the ethereum ethdb/database
|
||||
// no need for queueing/caching
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/compression/rle"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
const openFileLimit = 128
|
||||
|
||||
type LDBDatabase struct {
|
||||
db *leveldb.DB
|
||||
comp bool
|
||||
}
|
||||
|
||||
func NewLDBDatabase(file string) (*LDBDatabase, error) {
|
||||
// Open the db
|
||||
db, err := leveldb.OpenFile(file, &opt.Options{OpenFilesCacheCapacity: openFileLimit})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
database := &LDBDatabase{db: db, comp: false}
|
||||
|
||||
return database, nil
|
||||
}
|
||||
|
||||
func (self *LDBDatabase) Put(key []byte, value []byte) {
|
||||
if self.comp {
|
||||
value = rle.Compress(value)
|
||||
}
|
||||
|
||||
err := self.db.Put(key, value, nil)
|
||||
if err != nil {
|
||||
fmt.Println("Error put", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LDBDatabase) Get(key []byte) ([]byte, error) {
|
||||
dat, err := self.db.Get(key, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if self.comp {
|
||||
return rle.Decompress(dat)
|
||||
}
|
||||
|
||||
return dat, nil
|
||||
}
|
||||
|
||||
func (self *LDBDatabase) Delete(key []byte) error {
|
||||
return self.db.Delete(key, nil)
|
||||
}
|
||||
|
||||
func (self *LDBDatabase) LastKnownTD() []byte {
|
||||
data, _ := self.Get([]byte("LTD"))
|
||||
|
||||
if len(data) == 0 {
|
||||
data = []byte{0x0}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (self *LDBDatabase) NewIterator() iterator.Iterator {
|
||||
return self.db.NewIterator(nil, nil)
|
||||
}
|
||||
|
||||
func (self *LDBDatabase) Write(batch *leveldb.Batch) error {
|
||||
return self.db.Write(batch, nil)
|
||||
}
|
||||
|
||||
func (self *LDBDatabase) Close() {
|
||||
// Close the leveldb database
|
||||
self.db.Close()
|
||||
}
|
||||
|
||||
func (self *LDBDatabase) Print() {
|
||||
iter := self.db.NewIterator(nil, nil)
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
value := iter.Value()
|
||||
|
||||
fmt.Printf("%x(%d): ", key, len(key))
|
||||
node := common.NewValueFromBytes(value)
|
||||
fmt.Printf("%v\n", node)
|
||||
}
|
||||
}
|
||||
377
bzz/dbstore.go
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
// disk storage layer for the package bzz
|
||||
// dbStore implements the ChunkStore interface and is used by the DPA as
|
||||
// persistent storage of chunks
|
||||
// it implements purging based on access count allowing for external control of
|
||||
// max capacity
|
||||
|
||||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
const gcArraySize = 10000
|
||||
const gcArrayFreeRatio = 0.1
|
||||
|
||||
// key prefixes for leveldb storage
|
||||
const kpIndex = 0
|
||||
const kpData = 1
|
||||
|
||||
var keyAccessCnt = []byte{2}
|
||||
var keyEntryCnt = []byte{3}
|
||||
var keyDataIdx = []byte{4}
|
||||
var keyGCPos = []byte{5}
|
||||
|
||||
type gcItem struct {
|
||||
idx uint64
|
||||
value uint64
|
||||
idxKey []byte
|
||||
}
|
||||
|
||||
type dbStore struct {
|
||||
db *LDBDatabase
|
||||
|
||||
// this should be stored in db, accessed transactionally
|
||||
entryCnt, accessCnt, dataIdx, capacity uint64
|
||||
|
||||
gcPos, gcStartPos []byte
|
||||
gcArray []*gcItem
|
||||
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
type dpaDBIndex struct {
|
||||
Idx uint64
|
||||
Access uint64
|
||||
}
|
||||
|
||||
func bytesToU64(data []byte) uint64 {
|
||||
if len(data) < 8 {
|
||||
return 0
|
||||
}
|
||||
return binary.LittleEndian.Uint64(data)
|
||||
}
|
||||
|
||||
func u64ToBytes(val uint64) []byte {
|
||||
data := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(data, val)
|
||||
return data
|
||||
}
|
||||
|
||||
func getIndexGCValue(index *dpaDBIndex) uint64 {
|
||||
return index.Access
|
||||
}
|
||||
|
||||
func (s *dbStore) updateIndexAccess(index *dpaDBIndex) {
|
||||
index.Access = s.accessCnt
|
||||
}
|
||||
|
||||
func getIndexKey(hash Key) []byte {
|
||||
HashSize := len(hash)
|
||||
key := make([]byte, HashSize+1)
|
||||
key[0] = 0
|
||||
// db keys derived from hash:
|
||||
// two halves swapped for uniformly distributed prefix
|
||||
copy(key[1:HashSize/2+1], hash[HashSize/2:HashSize])
|
||||
copy(key[HashSize/2+1:HashSize+1], hash[0:HashSize/2])
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func getDataKey(idx uint64) []byte {
|
||||
key := make([]byte, 9)
|
||||
key[0] = 1
|
||||
binary.BigEndian.PutUint64(key[1:9], idx)
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func encodeIndex(index *dpaDBIndex) []byte {
|
||||
data, _ := rlp.EncodeToBytes(index)
|
||||
return data
|
||||
}
|
||||
|
||||
func encodeData(chunk *Chunk) []byte {
|
||||
return chunk.SData
|
||||
}
|
||||
|
||||
func decodeIndex(data []byte, index *dpaDBIndex) {
|
||||
dec := rlp.NewStream(bytes.NewReader(data), 0)
|
||||
dec.Decode(index)
|
||||
}
|
||||
|
||||
func decodeData(data []byte, chunk *Chunk) {
|
||||
chunk.SData = data
|
||||
chunk.Size = int64(binary.LittleEndian.Uint64(data[0:8]))
|
||||
}
|
||||
|
||||
func gcListPartition(list []*gcItem, left int, right int, pivotIndex int) int {
|
||||
pivotValue := list[pivotIndex].value
|
||||
dd := list[pivotIndex]
|
||||
list[pivotIndex] = list[right]
|
||||
list[right] = dd
|
||||
storeIndex := left
|
||||
for i := left; i < right; i++ {
|
||||
if list[i].value < pivotValue {
|
||||
dd = list[storeIndex]
|
||||
list[storeIndex] = list[i]
|
||||
list[i] = dd
|
||||
storeIndex++
|
||||
}
|
||||
}
|
||||
dd = list[storeIndex]
|
||||
list[storeIndex] = list[right]
|
||||
list[right] = dd
|
||||
return storeIndex
|
||||
}
|
||||
|
||||
func gcListSelect(list []*gcItem, left int, right int, n int) int {
|
||||
if left == right {
|
||||
return left
|
||||
}
|
||||
pivotIndex := (left + right) / 2
|
||||
pivotIndex = gcListPartition(list, left, right, pivotIndex)
|
||||
if n == pivotIndex {
|
||||
return n
|
||||
} else {
|
||||
if n < pivotIndex {
|
||||
return gcListSelect(list, left, pivotIndex-1, n)
|
||||
} else {
|
||||
return gcListSelect(list, pivotIndex+1, right, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbStore) collectGarbage(ratio float32) {
|
||||
it := s.db.NewIterator()
|
||||
it.Seek(s.gcPos)
|
||||
if it.Valid() {
|
||||
s.gcPos = it.Key()
|
||||
} else {
|
||||
s.gcPos = nil
|
||||
}
|
||||
gcnt := 0
|
||||
|
||||
for (gcnt < gcArraySize) && (uint64(gcnt) < s.entryCnt) {
|
||||
|
||||
if (s.gcPos == nil) || (s.gcPos[0] != kpIndex) {
|
||||
it.Seek(s.gcStartPos)
|
||||
if it.Valid() {
|
||||
s.gcPos = it.Key()
|
||||
} else {
|
||||
s.gcPos = nil
|
||||
}
|
||||
}
|
||||
|
||||
if (s.gcPos == nil) || (s.gcPos[0] != kpIndex) {
|
||||
break
|
||||
}
|
||||
|
||||
gci := new(gcItem)
|
||||
gci.idxKey = s.gcPos
|
||||
var index dpaDBIndex
|
||||
decodeIndex(it.Value(), &index)
|
||||
gci.idx = index.Idx
|
||||
// the smaller, the more likely to be gc'd
|
||||
gci.value = getIndexGCValue(&index)
|
||||
s.gcArray[gcnt] = gci
|
||||
gcnt++
|
||||
it.Next()
|
||||
if it.Valid() {
|
||||
s.gcPos = it.Key()
|
||||
} else {
|
||||
s.gcPos = nil
|
||||
}
|
||||
}
|
||||
|
||||
cutidx := gcListSelect(s.gcArray, 0, gcnt-1, int(float32(gcnt)*ratio))
|
||||
cutval := s.gcArray[cutidx].value
|
||||
|
||||
// fmt.Print(gcnt, " ", s.entryCnt, " ")
|
||||
|
||||
// actual gc
|
||||
for i := 0; i < gcnt; i++ {
|
||||
if s.gcArray[i].value <= cutval {
|
||||
batch := new(leveldb.Batch)
|
||||
batch.Delete(s.gcArray[i].idxKey)
|
||||
batch.Delete(getDataKey(s.gcArray[i].idx))
|
||||
s.entryCnt--
|
||||
batch.Put(keyEntryCnt, u64ToBytes(s.entryCnt))
|
||||
s.db.Write(batch)
|
||||
}
|
||||
}
|
||||
|
||||
// fmt.Println(s.entryCnt)
|
||||
|
||||
s.db.Put(keyGCPos, s.gcPos)
|
||||
}
|
||||
|
||||
func (s *dbStore) Put(chunk *Chunk) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
ikey := getIndexKey(chunk.Key)
|
||||
var index dpaDBIndex
|
||||
|
||||
if s.tryAccessIdx(ikey, &index) {
|
||||
if chunk.dbStored != nil {
|
||||
close(chunk.dbStored)
|
||||
}
|
||||
return // already exists, only update access
|
||||
}
|
||||
|
||||
data := encodeData(chunk)
|
||||
//data := ethutil.Encode([]interface{}{entry})
|
||||
|
||||
if s.entryCnt >= s.capacity {
|
||||
s.collectGarbage(gcArrayFreeRatio)
|
||||
}
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
|
||||
s.entryCnt++
|
||||
batch.Put(keyEntryCnt, u64ToBytes(s.entryCnt))
|
||||
s.dataIdx++
|
||||
batch.Put(keyDataIdx, u64ToBytes(s.dataIdx))
|
||||
s.accessCnt++
|
||||
batch.Put(keyAccessCnt, u64ToBytes(s.accessCnt))
|
||||
|
||||
batch.Put(getDataKey(s.dataIdx), data)
|
||||
|
||||
index.Idx = s.dataIdx
|
||||
s.updateIndexAccess(&index)
|
||||
|
||||
idata := encodeIndex(&index)
|
||||
batch.Put(ikey, idata)
|
||||
|
||||
s.db.Write(batch)
|
||||
if chunk.dbStored != nil {
|
||||
close(chunk.dbStored)
|
||||
}
|
||||
}
|
||||
|
||||
// try to find index; if found, update access cnt and return true
|
||||
func (s *dbStore) tryAccessIdx(ikey []byte, index *dpaDBIndex) bool {
|
||||
idata, err := s.db.Get(ikey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
decodeIndex(idata, index)
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
|
||||
s.accessCnt++
|
||||
batch.Put(keyAccessCnt, u64ToBytes(s.accessCnt))
|
||||
s.updateIndexAccess(index)
|
||||
idata = encodeIndex(index)
|
||||
batch.Put(ikey, idata)
|
||||
|
||||
s.db.Write(batch)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *dbStore) Get(key Key) (chunk *Chunk, err error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
var index dpaDBIndex
|
||||
|
||||
if s.tryAccessIdx(getIndexKey(key), &index) {
|
||||
var data []byte
|
||||
data, err = s.db.Get(getDataKey(index.Idx))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hasher := hasherfunc.New()
|
||||
hasher.Write(data)
|
||||
if bytes.Compare(hasher.Sum(nil), key) != 0 {
|
||||
s.db.Delete(getDataKey(index.Idx))
|
||||
err = notFound
|
||||
return
|
||||
}
|
||||
|
||||
chunk = &Chunk{
|
||||
Key: key,
|
||||
}
|
||||
decodeData(data, chunk)
|
||||
} else {
|
||||
err = notFound
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func (s *dbStore) updateAccessCnt(key Key) {
|
||||
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
var index dpaDBIndex
|
||||
s.tryAccessIdx(getIndexKey(key), &index) // result_chn == nil, only update access cnt
|
||||
|
||||
}
|
||||
|
||||
func (s *dbStore) setCapacity(c uint64) {
|
||||
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.capacity = c
|
||||
|
||||
if s.entryCnt > c {
|
||||
var ratio float32
|
||||
ratio = float32(1.01) - float32(c)/float32(s.entryCnt)
|
||||
if ratio < gcArrayFreeRatio {
|
||||
ratio = gcArrayFreeRatio
|
||||
}
|
||||
if ratio > 1 {
|
||||
ratio = 1
|
||||
}
|
||||
for s.entryCnt > c {
|
||||
s.collectGarbage(ratio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbStore) getEntryCnt() uint64 {
|
||||
return s.entryCnt
|
||||
}
|
||||
|
||||
func newDbStore(path string) (s *dbStore, err error) {
|
||||
s = new(dbStore)
|
||||
|
||||
s.db, err = NewLDBDatabase(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.setCapacity(5000000) // TODO define default capacity as constant
|
||||
|
||||
s.gcStartPos = make([]byte, 1)
|
||||
s.gcStartPos[0] = kpIndex
|
||||
s.gcArray = make([]*gcItem, gcArraySize)
|
||||
|
||||
data, _ := s.db.Get(keyEntryCnt)
|
||||
s.entryCnt = bytesToU64(data)
|
||||
data, _ = s.db.Get(keyAccessCnt)
|
||||
s.accessCnt = bytesToU64(data)
|
||||
data, _ = s.db.Get(keyDataIdx)
|
||||
s.dataIdx = bytesToU64(data)
|
||||
s.gcPos, _ = s.db.Get(keyGCPos)
|
||||
if s.gcPos == nil {
|
||||
s.gcPos = s.gcStartPos
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *dbStore) close() {
|
||||
s.db.Close()
|
||||
}
|
||||
51
bzz/dbstore_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func initDbStore() (m *dbStore) {
|
||||
os.RemoveAll("/tmp/bzz")
|
||||
m, err := newDbStore("/tmp/bzz")
|
||||
if err != nil {
|
||||
panic("no dbStore")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func testDbStore(l int64, branches int64, t *testing.T) {
|
||||
m := initDbStore()
|
||||
defer m.close()
|
||||
testStore(m, l, branches, t)
|
||||
}
|
||||
|
||||
func TestDbStore128_0x1000000(t *testing.T) {
|
||||
testDbStore(0x1000000, 128, t)
|
||||
}
|
||||
|
||||
func TestDbStore128_10000(t *testing.T) {
|
||||
testDbStore(10000, 128, t)
|
||||
}
|
||||
|
||||
func TestDbStore128_1000(t *testing.T) {
|
||||
testDbStore(1000, 128, t)
|
||||
}
|
||||
|
||||
func TestDbStore128_100(t *testing.T) {
|
||||
testDbStore(100, 128, t)
|
||||
}
|
||||
|
||||
func TestDbStore2_100(t *testing.T) {
|
||||
testDbStore(100, 2, t)
|
||||
}
|
||||
|
||||
func TestDbStoreNotFound(t *testing.T) {
|
||||
m := initDbStore()
|
||||
defer m.close()
|
||||
zeroKey := make([]byte, 32)
|
||||
_, err := m.Get(zeroKey)
|
||||
if err != notFound {
|
||||
t.Errorf("Expected notFound, got %v", err)
|
||||
}
|
||||
}
|
||||
181
bzz/dpa.go
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
/*
|
||||
DPA provides the client API entrypoints Store and Retrieve to store and retrieve
|
||||
It can store anything that has a byte slice representation, so files or serialised objects etc.
|
||||
|
||||
Storage: DPA calls the Chunker to segment the input datastream of any size to a merkle hashed tree of chunks. The key of the root block is returned to the client.
|
||||
|
||||
Retrieval: given the key of the root block, the DPA retrieves the block chunks and reconstructs the original data and passes it back as a lazy reader. A lazy reader is a reader with on-demand delayed processing, i.e. the chunks needed to reconstruct a large file are only fetched and processed if that particular part of the document is actually read.
|
||||
|
||||
As the chunker produces chunks, DPA dispatches them to the chunk store for storage or retrieval.
|
||||
|
||||
The ChunkStore interface is implemented by :
|
||||
|
||||
- memStore: a memory cache
|
||||
- dbStore: local disk/db store
|
||||
- localStore: a combination (sequence of) memStore and dbStore
|
||||
- netStore: dht storage
|
||||
*/
|
||||
|
||||
const (
|
||||
storeChanCapacity = 100
|
||||
retrieveChanCapacity = 100
|
||||
)
|
||||
|
||||
var (
|
||||
notFound = errors.New("not found")
|
||||
)
|
||||
|
||||
var dpaLogger = logger.NewLogger("BZZ")
|
||||
|
||||
type DPA struct {
|
||||
Chunker Chunker
|
||||
ChunkStore ChunkStore
|
||||
storeC chan *Chunk
|
||||
retrieveC chan *Chunk
|
||||
|
||||
lock sync.Mutex
|
||||
running bool
|
||||
wg *sync.WaitGroup
|
||||
quitC chan bool
|
||||
}
|
||||
|
||||
// Chunk also serves as a request object passed to ChunkStores
|
||||
// in case it is a retrieval request, Data is nil and Size is 0
|
||||
// Note that Size is not the size of the data chunk, which is Data.Size()
|
||||
// but the size of the subtree encoded in the chunk
|
||||
// 0 if request, to be supplied by the dpa
|
||||
type Chunk struct {
|
||||
SData []byte // nil if request, to be supplied by dpa
|
||||
Size int64 // size of the data covered by the subtree encoded in this chunk
|
||||
Key Key // always
|
||||
C chan bool // to signal data delivery by the dpa
|
||||
req *requestStatus // request Status needed by netStore
|
||||
wg *sync.WaitGroup // wg to synchronize
|
||||
dbStored chan bool // never remove a chunk from memStore before it is written to dbStore
|
||||
source *peer // source peer of the chunk
|
||||
}
|
||||
|
||||
type ChunkStore interface {
|
||||
Put(*Chunk) // effectively there is no error even if there is an error
|
||||
Get(Key) (*Chunk, error)
|
||||
}
|
||||
|
||||
// Public API. Main entry point for document retrieval directly. Used by the
|
||||
// FS-aware API and httpaccess
|
||||
// Chunk retrieval blocks on netStore requests with a timeout so reader will
|
||||
// report error if retrieval of chunks within requested range time out.
|
||||
func (self *DPA) Retrieve(key Key) SectionReader {
|
||||
return self.Chunker.Join(key, self.retrieveC)
|
||||
}
|
||||
|
||||
// Public API. Main entry point for document storage directly. Used by the
|
||||
// FS-aware API and httpaccess
|
||||
func (self *DPA) Store(data SectionReader, wg *sync.WaitGroup) (key Key, err error) {
|
||||
key = make([]byte, self.Chunker.KeySize())
|
||||
errC := self.Chunker.Split(key, data, self.storeC, wg)
|
||||
|
||||
SPLIT:
|
||||
for {
|
||||
select {
|
||||
case err, ok := <-errC:
|
||||
if err != nil {
|
||||
glog.V(logger.Error).Infof("[BZZ] chunker split error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
break SPLIT
|
||||
}
|
||||
|
||||
case <-self.quitC:
|
||||
break SPLIT
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func (self *DPA) Start() {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if self.running {
|
||||
return
|
||||
}
|
||||
self.running = true
|
||||
self.quitC = make(chan bool)
|
||||
self.storeLoop()
|
||||
self.retrieveLoop()
|
||||
glog.V(logger.Info).Infof("[BZZ] Swarm DPA started.")
|
||||
}
|
||||
|
||||
func (self *DPA) Stop() {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if !self.running {
|
||||
return
|
||||
}
|
||||
self.running = false
|
||||
close(self.quitC)
|
||||
}
|
||||
|
||||
// retrieveLoop dispatches the parallel chunk retrieval requests received on the
|
||||
// retrieve channel to its ChunkStore (netStore or localStore)
|
||||
func (self *DPA) retrieveLoop() {
|
||||
self.retrieveC = make(chan *Chunk, retrieveChanCapacity)
|
||||
|
||||
go func() {
|
||||
RETRIEVE:
|
||||
for ch := range self.retrieveC {
|
||||
|
||||
go func(chunk *Chunk) {
|
||||
glog.V(logger.Detail).Infof("[BZZ] dpa: retrieve loop : chunk '%x'", chunk.Key)
|
||||
storedChunk, err := self.ChunkStore.Get(chunk.Key)
|
||||
if err == notFound {
|
||||
glog.V(logger.Detail).Infof("[BZZ] chunk '%x' not found", chunk.Key)
|
||||
} else if err != nil {
|
||||
glog.V(logger.Detail).Infof("[BZZ] error retrieving chunk %x: %v", chunk.Key, err)
|
||||
} else {
|
||||
chunk.SData = storedChunk.SData
|
||||
chunk.Size = storedChunk.Size
|
||||
}
|
||||
close(chunk.C)
|
||||
}(ch)
|
||||
select {
|
||||
case <-self.quitC:
|
||||
break RETRIEVE
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// storeLoop dispatches the parallel chunk store requests received on the
|
||||
// store channel to its ChunkStore (netStore or localStore)
|
||||
func (self *DPA) storeLoop() {
|
||||
self.storeC = make(chan *Chunk)
|
||||
go func() {
|
||||
STORE:
|
||||
for ch := range self.storeC {
|
||||
go func(chunk *Chunk) {
|
||||
self.ChunkStore.Put(chunk)
|
||||
if chunk.wg != nil {
|
||||
glog.V(logger.Detail).Infof("[BZZ] DPA.storeLoop %064x", chunk.Key)
|
||||
chunk.wg.Done()
|
||||
}
|
||||
}(ch)
|
||||
select {
|
||||
case <-self.quitC:
|
||||
break STORE
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
135
bzz/dpa_test.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testDataSize = 0x1000000
|
||||
|
||||
func TestDPArandom(t *testing.T) {
|
||||
os.RemoveAll("/tmp/bzz")
|
||||
dbStore, err := newDbStore("/tmp/bzz")
|
||||
dbStore.setCapacity(50000)
|
||||
if err != nil {
|
||||
t.Errorf("DB error: %v", err)
|
||||
}
|
||||
memStore := newMemStore(dbStore)
|
||||
localStore := &localStore{
|
||||
memStore,
|
||||
dbStore,
|
||||
}
|
||||
chunker := &TreeChunker{}
|
||||
chunker.Init()
|
||||
dpa := &DPA{
|
||||
Chunker: chunker,
|
||||
ChunkStore: localStore,
|
||||
}
|
||||
dpa.Start()
|
||||
reader, slice := testDataReader(testDataSize)
|
||||
wg := &sync.WaitGroup{}
|
||||
key, err := dpa.Store(reader, wg)
|
||||
if err != nil {
|
||||
t.Errorf("Store error: %v", err)
|
||||
}
|
||||
wg.Wait()
|
||||
resultReader := dpa.Retrieve(key)
|
||||
resultSlice := make([]byte, len(slice))
|
||||
n, err := resultReader.ReadAt(resultSlice, 0)
|
||||
if err != io.EOF {
|
||||
t.Errorf("Retrieve error: %v", err)
|
||||
}
|
||||
if n != len(slice) {
|
||||
t.Errorf("Slice size error got %d, expected %d.", n, len(slice))
|
||||
}
|
||||
if !bytes.Equal(slice, resultSlice) {
|
||||
t.Errorf("Comparison error.")
|
||||
}
|
||||
ioutil.WriteFile("/tmp/slice.bzz.16M", slice, 0666)
|
||||
ioutil.WriteFile("/tmp/result.bzz.16M", resultSlice, 0666)
|
||||
localStore.memStore = newMemStore(dbStore)
|
||||
resultReader = dpa.Retrieve(key)
|
||||
for i, _ := range resultSlice {
|
||||
resultSlice[i] = 0
|
||||
}
|
||||
n, err = resultReader.ReadAt(resultSlice, 0)
|
||||
if err != io.EOF {
|
||||
t.Errorf("Retrieve error after removing memStore: %v", err)
|
||||
}
|
||||
if n != len(slice) {
|
||||
t.Errorf("Slice size error after removing memStore got %d, expected %d.", n, len(slice))
|
||||
}
|
||||
if !bytes.Equal(slice, resultSlice) {
|
||||
t.Errorf("Comparison error after removing memStore.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDPA_capacity(t *testing.T) {
|
||||
os.RemoveAll("/tmp/bzz")
|
||||
dbStore, err := newDbStore("/tmp/bzz")
|
||||
if err != nil {
|
||||
t.Errorf("DB error: %v", err)
|
||||
}
|
||||
memStore := newMemStore(dbStore)
|
||||
localStore := &localStore{
|
||||
memStore,
|
||||
dbStore,
|
||||
}
|
||||
localStore.memStore.setCapacity(0)
|
||||
chunker := &TreeChunker{}
|
||||
chunker.Init()
|
||||
dpa := &DPA{
|
||||
Chunker: chunker,
|
||||
ChunkStore: localStore,
|
||||
}
|
||||
dpa.Start()
|
||||
reader, slice := testDataReader(testDataSize)
|
||||
wg := &sync.WaitGroup{}
|
||||
key, err := dpa.Store(reader, wg)
|
||||
if err != nil {
|
||||
t.Errorf("Store error: %v", err)
|
||||
}
|
||||
wg.Wait()
|
||||
resultReader := dpa.Retrieve(key)
|
||||
resultSlice := make([]byte, len(slice))
|
||||
n, err := resultReader.ReadAt(resultSlice, 0)
|
||||
if err != io.EOF {
|
||||
t.Errorf("Retrieve error: %v", err)
|
||||
}
|
||||
if n != len(slice) {
|
||||
t.Errorf("Slice size error got %d, expected %d.", n, len(slice))
|
||||
}
|
||||
if !bytes.Equal(slice, resultSlice) {
|
||||
t.Errorf("Comparison error.")
|
||||
}
|
||||
// Clear memStore
|
||||
localStore.memStore.setCapacity(0)
|
||||
// check whether it is, indeed, empty
|
||||
dpa.ChunkStore = memStore
|
||||
resultReader = dpa.Retrieve(key)
|
||||
n, err = resultReader.ReadAt(resultSlice, 0)
|
||||
if err == nil {
|
||||
t.Errorf("Was able to read %d bytes from an empty memStore.")
|
||||
}
|
||||
// check how it works with localStore
|
||||
dpa.ChunkStore = localStore
|
||||
// localStore.dbStore.setCapacity(0)
|
||||
resultReader = dpa.Retrieve(key)
|
||||
for i, _ := range resultSlice {
|
||||
resultSlice[i] = 0
|
||||
}
|
||||
n, err = resultReader.ReadAt(resultSlice, 0)
|
||||
if err != io.EOF {
|
||||
t.Errorf("Retrieve error after clearing memStore: %v", err)
|
||||
}
|
||||
if n != len(slice) {
|
||||
t.Errorf("Slice size error after clearing memStore got %d, expected %d.", n, len(slice))
|
||||
}
|
||||
if !bytes.Equal(slice, resultSlice) {
|
||||
t.Errorf("Comparison error after clearing memStore.")
|
||||
}
|
||||
}
|
||||
185
bzz/hive.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/kademlia"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
type peer struct {
|
||||
*bzzProtocol
|
||||
}
|
||||
|
||||
// peer not necessary here
|
||||
// bzz protocol could implement kademlia.Node interface with
|
||||
// Addr(), LastActive() and Drop()
|
||||
|
||||
// Hive is the logistic manager of the swarm
|
||||
// it uses a generic kademlia nodetable to find best peer list
|
||||
// for any target
|
||||
// this is used by the netstore to search for content in the swarm
|
||||
// the bzz protocol peersMsgData exchange is relayed to Kademlia
|
||||
// for db storage and filtering
|
||||
// connections and disconnections are reported and relayed
|
||||
// to keep the nodetable uptodate
|
||||
|
||||
type hive struct {
|
||||
addr kademlia.Address
|
||||
kad *kademlia.Kademlia
|
||||
path string
|
||||
ping chan bool
|
||||
more chan bool
|
||||
}
|
||||
|
||||
func newHive() (*hive, error) {
|
||||
kad := kademlia.New()
|
||||
kad.BucketSize = 3
|
||||
kad.MaxProx = 10
|
||||
kad.ProxBinSize = 8
|
||||
return &hive{
|
||||
kad: kad,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (self *hive) start(baseAddr *peerAddr, hivepath string, connectPeer func(string) error) (err error) {
|
||||
self.ping = make(chan bool)
|
||||
self.more = make(chan bool)
|
||||
self.path = hivepath
|
||||
|
||||
self.addr = kademlia.Address(baseAddr.hash)
|
||||
self.kad.Start(self.addr)
|
||||
err = self.kad.Load(self.path)
|
||||
if err != nil {
|
||||
glog.V(logger.Warn).Infof("[BZZ] KΛÐΞMLIΛ Warning: error reading kaddb '%s' (skipping): %v", self.path, err)
|
||||
err = nil
|
||||
}
|
||||
/* this loop is doing the actual table maintenance
|
||||
including bootstrapping and maintaining a healthy table
|
||||
Note: At the moment, this does not have any timer/timeout . That means if your
|
||||
peers do not reply to launch the game into movement , it will stay stuck
|
||||
add or remove a peer to wake up
|
||||
*/
|
||||
go self.pinger()
|
||||
go func() {
|
||||
// whenever pinged ask kademlia about most preferred peer
|
||||
for _ = range self.ping {
|
||||
node, proxLimit := self.kad.GetNodeRecord()
|
||||
if node != nil && len(node.Url) > 0 {
|
||||
glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: call for bee %v", node)
|
||||
// enode or any lower level connection address is unnecessary in future
|
||||
// discovery table is used to look it up.
|
||||
connectPeer(node.Url)
|
||||
} else if proxLimit > -1 {
|
||||
// a random peer is taken from the table
|
||||
peers := self.kad.GetNodes(kademlia.RandomAddressAt(self.addr, rand.Intn(self.kad.MaxProx)), 1)
|
||||
if len(peers) > 0 {
|
||||
// a random address at prox bin 0 is sent for lookup
|
||||
randAddr := kademlia.RandomAddressAt(self.addr, proxLimit)
|
||||
req := &retrieveRequestMsgData{
|
||||
Key: Key(randAddr[:]),
|
||||
}
|
||||
glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: call any bee in area %x messenger bee %v", randAddr[:4], peers[0])
|
||||
peers[0].(peer).retrieve(req)
|
||||
}
|
||||
if self.more == nil {
|
||||
glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: buzz buzz need more bees")
|
||||
self.more = make(chan bool)
|
||||
go self.pinger()
|
||||
}
|
||||
self.more <- true
|
||||
glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: buzz kept alive")
|
||||
} else {
|
||||
if self.more != nil {
|
||||
close(self.more)
|
||||
self.more = nil
|
||||
}
|
||||
}
|
||||
glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: queen's address: %x, population: %d (%d)\n%v", self.addr[:4], self.kad.Count(), self.kad.DBCount(), self.kad)
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (self *hive) pinger() {
|
||||
clock := time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-clock.C:
|
||||
if self.kad.DBCount() > 0 {
|
||||
select {
|
||||
case self.ping <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
case _, more := <-self.more:
|
||||
if !more {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *hive) stop() error {
|
||||
// closing ping channel quits the updateloop
|
||||
close(self.ping)
|
||||
if self.more != nil {
|
||||
close(self.more)
|
||||
self.more = nil
|
||||
}
|
||||
return self.kad.Stop(self.path)
|
||||
}
|
||||
|
||||
func (self *hive) addPeer(p peer) {
|
||||
|
||||
glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: hi new bee %v", p)
|
||||
self.kad.AddNode(p)
|
||||
// self lookup (can be encoded as nil/zero key since peers addr known) + no id ()
|
||||
// the most common way of saying hi in bzz is initiation of gossip
|
||||
// let me know about anyone new from my hood , here is the storageradius
|
||||
// to send the 6 byte self lookup
|
||||
// we do not record as request or forward it, just reply with peers
|
||||
p.retrieve(&retrieveRequestMsgData{})
|
||||
glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: 'whatsup wheresdaparty' sent to %v", p)
|
||||
self.ping <- true
|
||||
}
|
||||
|
||||
func (self *hive) removePeer(p peer) {
|
||||
glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: bee %v gone offline", p)
|
||||
self.kad.RemoveNode(p)
|
||||
self.ping <- false
|
||||
}
|
||||
|
||||
// Retrieve a list of live peers that are closer to target than us
|
||||
func (self *hive) getPeers(target Key, max int) (peers []peer) {
|
||||
var addr kademlia.Address
|
||||
copy(addr[:], target[:])
|
||||
for _, node := range self.kad.GetNodes(addr, max) {
|
||||
peers = append(peers, node.(peer))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newNodeRecord(addr *peerAddr) *kademlia.NodeRecord {
|
||||
addr.new()
|
||||
now := time.Now()
|
||||
return &kademlia.NodeRecord{
|
||||
Addr: kademlia.Address(addr.hash),
|
||||
Active: time.Now().Unix(),
|
||||
Url: addr.enode,
|
||||
After: now.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// called by the protocol when receiving peerset (for target address)
|
||||
// peersMsgData is converted to a slice of NodeRecords for Kademlia
|
||||
// this is to store all thats needed
|
||||
func (self *hive) addPeerEntries(req *peersMsgData) {
|
||||
var nrs []*kademlia.NodeRecord
|
||||
for _, p := range req.Peers {
|
||||
nrs = append(nrs, newNodeRecord(p))
|
||||
}
|
||||
self.kad.AddNodeRecords(nrs)
|
||||
}
|
||||
237
bzz/httpaccess.go
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
A simple http server interface to Swarm
|
||||
*/
|
||||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
// "net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
const (
|
||||
rawType = "application/octet-stream"
|
||||
)
|
||||
|
||||
var (
|
||||
rawUrl = regexp.MustCompile("^/+raw/*")
|
||||
trailingSlashes = regexp.MustCompile("/+$")
|
||||
// forever = time.Unix(0, 0)
|
||||
forever = time.Now()
|
||||
)
|
||||
|
||||
type sequentialReader struct {
|
||||
reader io.Reader
|
||||
pos int64
|
||||
ahead map[int64](chan bool)
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// starts up http server
|
||||
// TODO: started by dpa/api rather than backend
|
||||
func startHttpServer(api *Api, port string) {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r, api)
|
||||
})
|
||||
go http.ListenAndServe(":"+port, nil)
|
||||
glog.V(logger.Info).Infof("[BZZ] Swarm HTTP proxy started on localhost:%s", port)
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request, api *Api) {
|
||||
requestURL := r.URL
|
||||
// This is wrong
|
||||
// if requestURL.Host == "" {
|
||||
// var err error
|
||||
// requestURL, err = url.Parse(r.Referer() + requestURL.String())
|
||||
// if err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: HTTP request URL: '%s', Host: '%s', Path: '%s', Referer: '%s', Accept: '%s'", r.RequestURI, requestURL.Host, requestURL.Path, r.Referer(), r.Header.Get("Accept"))
|
||||
uri := requestURL.Path
|
||||
var raw bool
|
||||
path := rawUrl.ReplaceAllStringFunc(uri, func(string) string {
|
||||
raw = true
|
||||
return ""
|
||||
})
|
||||
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: %s request '%s' received.", r.Method, uri)
|
||||
|
||||
switch {
|
||||
case r.Method == "POST" || r.Method == "PUT":
|
||||
key, err := api.dpa.Store(io.NewSectionReader(&sequentialReader{
|
||||
reader: r.Body,
|
||||
ahead: make(map[int64]chan bool),
|
||||
}, 0, r.ContentLength), nil)
|
||||
if err == nil {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: Content for '%064x' stored", key)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if r.Method == "POST" {
|
||||
if raw {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(common.Bytes2Hex(key))))
|
||||
} else {
|
||||
http.Error(w, "No POST to "+uri+" allowed.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// PUT
|
||||
if raw {
|
||||
http.Error(w, "No PUT to /raw allowed.", http.StatusBadRequest)
|
||||
return
|
||||
} else {
|
||||
path = regularSlashes(path)
|
||||
mime := r.Header.Get("Content-Type")
|
||||
// TODO proper root hash separation
|
||||
glog.V(logger.Debug).Infof("[BZZ] Modify '%s' to store '%064x' as '%s'.", path, key, mime)
|
||||
newKey, err := api.Modify(path[:64], path[65:], common.Bytes2Hex(key), mime)
|
||||
if err == nil {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm replaced manifest by '%s'", newKey)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(newKey)))
|
||||
} else {
|
||||
http.Error(w, "PUT to "+path+"failed.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
case r.Method == "DELETE":
|
||||
if raw {
|
||||
http.Error(w, "No DELETE to /raw allowed.", http.StatusBadRequest)
|
||||
return
|
||||
} else {
|
||||
path = regularSlashes(path)
|
||||
glog.V(logger.Debug).Infof("[BZZ] Delete '%s'.", path)
|
||||
newKey, err := api.Modify(path[:64], path[65:], "", "")
|
||||
if err == nil {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm replaced manifest by '%s'", newKey)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(newKey)))
|
||||
} else {
|
||||
http.Error(w, "DELETE to "+path+"failed.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
case r.Method == "GET" || r.Method == "HEAD":
|
||||
path = trailingSlashes.ReplaceAllString(path, "")
|
||||
if raw {
|
||||
// resolving host
|
||||
key, err := api.Resolve(path)
|
||||
if err != nil {
|
||||
glog.V(logger.Error).Infof("[BZZ] Swarm: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// retrieving content
|
||||
reader := api.dpa.Retrieve(key)
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: Reading %d bytes.", reader.Size())
|
||||
|
||||
// setting mime type
|
||||
qv := requestURL.Query()
|
||||
mimeType := qv.Get("content_type")
|
||||
if mimeType == "" {
|
||||
mimeType = rawType
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
http.ServeContent(w, r, uri, forever, reader)
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: Serve raw content '%s' (%d bytes) as '%s'", uri, reader.Size(), mimeType)
|
||||
|
||||
// retrieve path via manifest
|
||||
} else {
|
||||
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: Structured GET request '%s' received.", uri)
|
||||
|
||||
// call to api.getPath on uri
|
||||
reader, mimeType, status, err := api.getPath(path)
|
||||
if err != nil {
|
||||
if _, ok := err.(errResolve); ok {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: %v", err)
|
||||
status = http.StatusBadRequest
|
||||
} else {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: error retrieving '%s': %v", uri, err)
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
http.Error(w, err.Error(), status)
|
||||
return
|
||||
}
|
||||
|
||||
// set mime type and status headers
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
if status > 0 {
|
||||
w.WriteHeader(status)
|
||||
} else {
|
||||
status = 200
|
||||
}
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: Served '%s' (%d bytes) as '%s' (status code: %v)", uri, reader.Size(), mimeType, status)
|
||||
|
||||
http.ServeContent(w, r, path, forever, reader)
|
||||
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *sequentialReader) ReadAt(target []byte, off int64) (n int, err error) {
|
||||
self.lock.Lock()
|
||||
// assert self.pos <= off
|
||||
if self.pos > off {
|
||||
glog.V(logger.Error).Infof("[BZZ] Swarm: non-sequential read attempted from sequentialReader; %d > %d",
|
||||
self.pos, off)
|
||||
panic("Non-sequential read attempt")
|
||||
}
|
||||
if self.pos != off {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: deferred read in POST at position %d, offset %d.",
|
||||
self.pos, off)
|
||||
wait := make(chan bool)
|
||||
self.ahead[off] = wait
|
||||
self.lock.Unlock()
|
||||
if <-wait {
|
||||
// failed read behind
|
||||
n = 0
|
||||
err = io.ErrUnexpectedEOF
|
||||
return
|
||||
}
|
||||
self.lock.Lock()
|
||||
}
|
||||
localPos := 0
|
||||
for localPos < len(target) {
|
||||
n, err = self.reader.Read(target[localPos:])
|
||||
localPos += n
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: Read %d bytes into buffer size %d from POST, error %v.",
|
||||
n, len(target), err)
|
||||
if err != nil {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: POST stream's reading terminated with %v.", err)
|
||||
for i := range self.ahead {
|
||||
self.ahead[i] <- true
|
||||
delete(self.ahead, i)
|
||||
}
|
||||
self.lock.Unlock()
|
||||
return localPos, err
|
||||
}
|
||||
self.pos += int64(n)
|
||||
}
|
||||
wait := self.ahead[self.pos]
|
||||
if wait != nil {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Swarm: deferred read in POST at position %d triggered.",
|
||||
self.pos)
|
||||
delete(self.ahead, self.pos)
|
||||
close(wait)
|
||||
}
|
||||
self.lock.Unlock()
|
||||
return localPos, err
|
||||
}
|
||||
38
bzz/localstore.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// localstore.go
|
||||
package bzz
|
||||
|
||||
type localStore struct {
|
||||
memStore *memStore
|
||||
dbStore *dbStore
|
||||
}
|
||||
|
||||
// localStore is itself a chunk store , to stores a chunk only
|
||||
// its integrity is checked ?
|
||||
func (self *localStore) Put(chunk *Chunk) {
|
||||
chunk.dbStored = make(chan bool)
|
||||
self.memStore.Put(chunk)
|
||||
if chunk.wg != nil {
|
||||
chunk.wg.Add(1)
|
||||
}
|
||||
go func() {
|
||||
self.dbStore.Put(chunk)
|
||||
if chunk.wg != nil {
|
||||
chunk.wg.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Get(chunk *Chunk) looks up a chunk in the local stores
|
||||
// This method is blocking until the chunk is retrieved so additional timeout is needed to wrap this call
|
||||
func (self *localStore) Get(key Key) (chunk *Chunk, err error) {
|
||||
chunk, err = self.memStore.Get(key)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
chunk, err = self.dbStore.Get(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
self.memStore.Put(chunk)
|
||||
return
|
||||
}
|
||||
309
bzz/manifest.go
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
const (
|
||||
manifestType = "application/bzz-manifest+json"
|
||||
)
|
||||
|
||||
type manifestTrie struct {
|
||||
dpa *DPA
|
||||
entries [257]*manifestTrieEntry // indexed by first character of path, entries[256] is the empty path entry
|
||||
hash Key // if hash != nil, it is stored
|
||||
}
|
||||
|
||||
type manifestJSON struct {
|
||||
Entries []*manifestTrieEntry `json:"entries"`
|
||||
}
|
||||
|
||||
type manifestTrieEntry struct {
|
||||
Path string `json:"path"`
|
||||
Hash string `json:"hash"` // for manifest content type, empty until subtrie is evaluated
|
||||
ContentType string `json:"contentType"`
|
||||
Status int `json:"status"`
|
||||
subtrie *manifestTrie
|
||||
}
|
||||
|
||||
func loadManifest(dpa *DPA, hash Key) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand
|
||||
|
||||
glog.V(logger.Detail).Infof("[BZZ] manifest lookup key: '%064x'.", hash)
|
||||
// retrieve manifest via DPA
|
||||
manifestReader := dpa.Retrieve(hash)
|
||||
return readManifest(manifestReader, hash, dpa)
|
||||
}
|
||||
|
||||
func readManifest(manifestReader SectionReader, hash Key, dpa *DPA) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand
|
||||
|
||||
// TODO check size for oversized manifests
|
||||
manifestData := make([]byte, manifestReader.Size())
|
||||
var size int
|
||||
size, err = manifestReader.Read(manifestData)
|
||||
if int64(size) < manifestReader.Size() {
|
||||
glog.V(logger.Detail).Infof("[BZZ] Manifest %064x not found.", hash)
|
||||
if err == nil {
|
||||
err = fmt.Errorf("Manifest retrieval cut short: %v < %v", size, manifestReader.Size())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(logger.Detail).Infof("[BZZ] Manifest %064x retrieved", hash)
|
||||
man := manifestJSON{}
|
||||
err = json.Unmarshal(manifestData, &man)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Manifest %064x is malformed: %v", hash, err)
|
||||
glog.V(logger.Detail).Infof("[BZZ] %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(logger.Detail).Infof("[BZZ] Manifest %064x has %d entries.", hash, len(man.Entries))
|
||||
|
||||
trie = &manifestTrie{
|
||||
dpa: dpa,
|
||||
}
|
||||
for _, entry := range man.Entries {
|
||||
trie.addEntry(entry)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *manifestTrie) addEntry(entry *manifestTrieEntry) {
|
||||
self.hash = nil // trie modified, hash needs to be re-calculated on demand
|
||||
|
||||
if len(entry.Path) == 0 {
|
||||
self.entries[256] = entry
|
||||
return
|
||||
}
|
||||
|
||||
b := byte(entry.Path[0])
|
||||
if (self.entries[b] == nil) || (self.entries[b].Path == entry.Path) {
|
||||
self.entries[b] = entry
|
||||
return
|
||||
}
|
||||
|
||||
oldentry := self.entries[b]
|
||||
cpl := 0
|
||||
for (len(entry.Path) > cpl) && (len(oldentry.Path) > cpl) && (entry.Path[cpl] == oldentry.Path[cpl]) {
|
||||
cpl++
|
||||
}
|
||||
|
||||
if (oldentry.ContentType == manifestType) && (cpl == len(oldentry.Path)) {
|
||||
if self.loadSubTrie(oldentry) != nil {
|
||||
return
|
||||
}
|
||||
entry.Path = entry.Path[cpl:]
|
||||
oldentry.subtrie.addEntry(entry)
|
||||
oldentry.Hash = ""
|
||||
return
|
||||
}
|
||||
|
||||
commonPrefix := entry.Path[:cpl]
|
||||
|
||||
subtrie := &manifestTrie{
|
||||
dpa: self.dpa,
|
||||
}
|
||||
entry.Path = entry.Path[cpl:]
|
||||
oldentry.Path = oldentry.Path[cpl:]
|
||||
subtrie.addEntry(entry)
|
||||
subtrie.addEntry(oldentry)
|
||||
|
||||
self.entries[b] = &manifestTrieEntry{
|
||||
Path: commonPrefix,
|
||||
Hash: "",
|
||||
ContentType: manifestType,
|
||||
subtrie: subtrie,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *manifestTrie) getCountLast() (cnt int, entry *manifestTrieEntry) {
|
||||
for _, e := range self.entries {
|
||||
if e != nil {
|
||||
cnt++
|
||||
entry = e
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *manifestTrie) deleteEntry(path string) {
|
||||
self.hash = nil // trie modified, hash needs to be re-calculated on demand
|
||||
|
||||
if len(path) == 0 {
|
||||
self.entries[256] = nil
|
||||
return
|
||||
}
|
||||
|
||||
b := byte(path[0])
|
||||
entry := self.entries[b]
|
||||
if (entry != nil) && (entry.Path == path) {
|
||||
self.entries[b] = nil
|
||||
return
|
||||
}
|
||||
|
||||
epl := len(entry.Path)
|
||||
if (entry.ContentType == manifestType) && (len(path) >= epl) && (path[:epl] == entry.Path) {
|
||||
if self.loadSubTrie(entry) != nil {
|
||||
return
|
||||
}
|
||||
entry.subtrie.deleteEntry(path[epl:])
|
||||
entry.Hash = ""
|
||||
// remove subtree if it has less than 2 elements
|
||||
cnt, lastentry := entry.subtrie.getCountLast()
|
||||
if cnt < 2 {
|
||||
if lastentry != nil {
|
||||
lastentry.Path = entry.Path + lastentry.Path
|
||||
}
|
||||
self.entries[b] = lastentry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *manifestTrie) recalcAndStore() error {
|
||||
if self.hash != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(`{"entries":[`)
|
||||
|
||||
list := &manifestJSON{}
|
||||
for _, entry := range self.entries {
|
||||
if entry != nil {
|
||||
if entry.Hash == "" { // TODO: paralellize
|
||||
err := entry.subtrie.recalcAndStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry.Hash = fmt.Sprintf("%064x", entry.subtrie.hash)
|
||||
}
|
||||
list.Entries = append(list.Entries, entry)
|
||||
}
|
||||
}
|
||||
|
||||
manifest, err := json.Marshal(list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sr := io.NewSectionReader(bytes.NewReader(manifest), 0, int64(len(manifest)))
|
||||
wg := &sync.WaitGroup{}
|
||||
key, err2 := self.dpa.Store(sr, wg)
|
||||
wg.Wait()
|
||||
self.hash = key
|
||||
return err2
|
||||
}
|
||||
|
||||
func (self *manifestTrie) loadSubTrie(entry *manifestTrieEntry) (err error) {
|
||||
if entry.subtrie == nil {
|
||||
hash := common.Hex2Bytes(entry.Hash)
|
||||
entry.subtrie, err = loadManifest(self.dpa, hash)
|
||||
entry.Hash = "" // might not match, should be recalculated
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *manifestTrie) listWithPrefixInt(prefix, rp string, cb func(entry *manifestTrieEntry, suffix string)) (err error) {
|
||||
plen := len(prefix)
|
||||
var start, stop int
|
||||
if plen == 0 {
|
||||
start = 0
|
||||
stop = 256
|
||||
} else {
|
||||
start = int(prefix[0])
|
||||
stop = start
|
||||
}
|
||||
|
||||
for i := start; i <= stop; i++ {
|
||||
entry := self.entries[i]
|
||||
if entry != nil {
|
||||
epl := len(entry.Path)
|
||||
if entry.ContentType == manifestType {
|
||||
l := plen
|
||||
if epl < l {
|
||||
l = epl
|
||||
}
|
||||
if prefix[:l] == entry.Path[:l] {
|
||||
sterr := self.loadSubTrie(entry)
|
||||
if sterr == nil {
|
||||
entry.subtrie.listWithPrefixInt(prefix[l:], rp+entry.Path[l:], cb)
|
||||
} else {
|
||||
err = sterr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (epl >= plen) && (prefix == entry.Path[:plen]) {
|
||||
cb(entry, rp+entry.Path[plen:])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *manifestTrie) listWithPrefix(prefix string, cb func(entry *manifestTrieEntry, suffix string)) (err error) {
|
||||
return self.listWithPrefixInt(prefix, "", cb)
|
||||
}
|
||||
|
||||
func (self *manifestTrie) findPrefixOf(path string) (entry *manifestTrieEntry, pos int) {
|
||||
|
||||
glog.V(logger.Detail).Infof("[BZZ] findPrefixOf(%s)", path)
|
||||
|
||||
if len(path) == 0 {
|
||||
return self.entries[256], 0
|
||||
}
|
||||
|
||||
b := byte(path[0])
|
||||
entry = self.entries[b]
|
||||
if entry == nil {
|
||||
return self.entries[256], 0
|
||||
}
|
||||
epl := len(entry.Path)
|
||||
glog.V(logger.Detail).Infof("[BZZ] path = %v entry.Path = %v epl = %v", path, entry.Path, epl)
|
||||
if (len(path) >= epl) && (path[:epl] == entry.Path) {
|
||||
glog.V(logger.Detail).Infof("[BZZ] entry.ContentType = %v", entry.ContentType)
|
||||
if entry.ContentType == manifestType {
|
||||
if self.loadSubTrie(entry) != nil {
|
||||
return nil, 0
|
||||
}
|
||||
entry, pos = entry.subtrie.findPrefixOf(path[epl:])
|
||||
if entry != nil {
|
||||
pos += epl
|
||||
}
|
||||
} else {
|
||||
pos = epl
|
||||
}
|
||||
} else {
|
||||
entry = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// file system manifest always contains regularized paths
|
||||
// no leading or trailing slashes, only single slashes inside
|
||||
func regularSlashes(path string) (res string) {
|
||||
for i := 0; i < len(path); i++ {
|
||||
if (path[i] != '/') || ((i > 0) && (path[i-1] != '/')) {
|
||||
res = res + path[i:i+1]
|
||||
}
|
||||
}
|
||||
if (len(res) > 0) && (res[len(res)-1] == '/') {
|
||||
res = res[:len(res)-1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *manifestTrie) getEntry(spath string) (entry *manifestTrieEntry, fullpath string) {
|
||||
path := regularSlashes(spath)
|
||||
var pos int
|
||||
entry, pos = self.findPrefixOf(path)
|
||||
return entry, path[:pos]
|
||||
}
|
||||
58
bzz/manifest_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
// "encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func manifest(paths ...string) (manifestReader SectionReader) {
|
||||
var entries []string
|
||||
for _, path := range paths {
|
||||
entry := fmt.Sprintf(`{"path":"%s"}`, path)
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
manifest := fmt.Sprintf(`{"entries":[%s]}`, strings.Join(entries, ","))
|
||||
return io.NewSectionReader(strings.NewReader(manifest), 0, int64(len(manifest)))
|
||||
}
|
||||
|
||||
func testGetEntry(t *testing.T, path, match string, paths ...string) *manifestTrie {
|
||||
trie, err := readManifest(manifest(paths...), nil, nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error making manifest: %v", err)
|
||||
}
|
||||
checkEntry(t, path, match, trie)
|
||||
return trie
|
||||
}
|
||||
|
||||
func checkEntry(t *testing.T, path, match string, trie *manifestTrie) {
|
||||
entry, fullpath := trie.getEntry(path)
|
||||
if match == "-" && entry != nil {
|
||||
t.Errorf("expected no match for '%s', got '%s'", path, fullpath)
|
||||
} else if entry == nil {
|
||||
if match != "-" {
|
||||
t.Errorf("expected entry '%s' to match '%s', got no match", match, path)
|
||||
}
|
||||
} else if fullpath != match {
|
||||
t.Errorf("incorrect entry retrieved for '%s'. expected path '%v', got '%s'", path, match, fullpath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEntry(t *testing.T) {
|
||||
// file system manifest always contains regularized paths
|
||||
testGetEntry(t, "a", "a", "a")
|
||||
testGetEntry(t, "b", "-", "a")
|
||||
testGetEntry(t, "/a//", "a", "a")
|
||||
// fallback
|
||||
testGetEntry(t, "/a", "", "")
|
||||
testGetEntry(t, "/a/b", "a/b", "a/b")
|
||||
// longest/deepest math
|
||||
testGetEntry(t, "a/b", "a/b", "a", "a/b", "a/bb", "a/b/c")
|
||||
testGetEntry(t, "//a//b//", "a/b", "a", "a/b", "a/bb", "a/b/c")
|
||||
}
|
||||
|
||||
func TestDeleteEntry(t *testing.T) {
|
||||
|
||||
}
|
||||
335
bzz/memstore.go
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
// memory storage layer for the package blockhash
|
||||
|
||||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
memTreeLW = 2 // log2(subtree count) of the subtrees
|
||||
memTreeFLW = 14 // log2(subtree count) of the root layer
|
||||
dbForceUpdateAccessCnt = 1000
|
||||
)
|
||||
|
||||
type memStore struct {
|
||||
memtree *memTree
|
||||
entryCnt, capacity uint // stored entries
|
||||
accessCnt uint64 // access counter; oldest is thrown away when full
|
||||
dbAccessCnt uint64
|
||||
dbStore *dbStore
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
/*
|
||||
a hash prefix subtree containing subtrees or one storage entry (but never both)
|
||||
|
||||
- access[0] stores the smallest (oldest) access count value in this subtree
|
||||
- if it contains more subtrees and its subtree count is at least 4, access[1:2]
|
||||
stores the smallest access count in the first and second halves of subtrees
|
||||
(so that access[0] = min(access[1], access[2])
|
||||
- likewise, if subtree count is at least 8,
|
||||
access[1] = min(access[3], access[4])
|
||||
access[2] = min(access[5], access[6])
|
||||
(access[] is a binary tree inside the multi-bit leveled hash tree)
|
||||
*/
|
||||
|
||||
func newMemStore(d *dbStore) (m *memStore) {
|
||||
m = &memStore{}
|
||||
m.memtree = newMemTree(memTreeFLW, nil, 0)
|
||||
m.dbStore = d
|
||||
m.setCapacity(500)
|
||||
return
|
||||
}
|
||||
|
||||
func (x Key) Size() uint {
|
||||
return uint(len(x))
|
||||
}
|
||||
|
||||
func (x Key) isEqual(y Key) bool {
|
||||
return bytes.Compare(x, y) == 0
|
||||
}
|
||||
|
||||
func (h Key) bits(i, j uint) uint {
|
||||
ii := i >> 3
|
||||
jj := i & 7
|
||||
if ii >= h.Size() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if jj+j <= 8 {
|
||||
return uint((h[ii] >> jj) & ((1 << j) - 1))
|
||||
}
|
||||
|
||||
res := uint(h[ii] >> jj)
|
||||
jj = 8 - jj
|
||||
j -= jj
|
||||
for j != 0 {
|
||||
ii++
|
||||
if j < 8 {
|
||||
res += uint(h[ii]&((1<<j)-1)) << jj
|
||||
return res
|
||||
}
|
||||
res += uint(h[ii]) << jj
|
||||
jj += 8
|
||||
j -= 8
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type memTree struct {
|
||||
subtree []*memTree
|
||||
parent *memTree
|
||||
parentIdx uint
|
||||
|
||||
bits uint // log2(subtree count)
|
||||
width uint // subtree count
|
||||
|
||||
entry *Chunk // if subtrees are present, entry should be nil
|
||||
lastDBaccess uint64
|
||||
access []uint64
|
||||
}
|
||||
|
||||
func newMemTree(b uint, parent *memTree, pidx uint) (node *memTree) {
|
||||
node = new(memTree)
|
||||
node.bits = b
|
||||
node.width = 1 << uint(b)
|
||||
node.subtree = make([]*memTree, node.width)
|
||||
node.access = make([]uint64, node.width-1)
|
||||
node.parent = parent
|
||||
node.parentIdx = pidx
|
||||
if parent != nil {
|
||||
parent.subtree[pidx] = node
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (node *memTree) updateAccess(a uint64) {
|
||||
aidx := uint(0)
|
||||
var aa uint64
|
||||
oa := node.access[0]
|
||||
for node.access[aidx] == oa {
|
||||
node.access[aidx] = a
|
||||
if aidx > 0 {
|
||||
aa = node.access[((aidx-1)^1)+1]
|
||||
aidx = (aidx - 1) >> 1
|
||||
} else {
|
||||
pidx := node.parentIdx
|
||||
node = node.parent
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
nn := node.subtree[pidx^1]
|
||||
if nn != nil {
|
||||
aa = nn.access[0]
|
||||
} else {
|
||||
aa = 0
|
||||
}
|
||||
aidx = (node.width + pidx - 2) >> 1
|
||||
}
|
||||
|
||||
if (aa != 0) && (aa < a) {
|
||||
a = aa
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *memStore) setCapacity(c uint) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
for c < s.entryCnt {
|
||||
s.removeOldest()
|
||||
}
|
||||
s.capacity = c
|
||||
}
|
||||
|
||||
func (s *memStore) getEntryCnt() uint {
|
||||
return s.entryCnt
|
||||
}
|
||||
|
||||
// entry (not its copy) is going to be in memStore
|
||||
func (s *memStore) Put(entry *Chunk) {
|
||||
if s.capacity == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if s.entryCnt >= s.capacity {
|
||||
s.removeOldest()
|
||||
}
|
||||
|
||||
s.accessCnt++
|
||||
|
||||
node := s.memtree
|
||||
bitpos := uint(0)
|
||||
for node.entry == nil {
|
||||
l := entry.Key.bits(bitpos, node.bits)
|
||||
st := node.subtree[l]
|
||||
if st == nil {
|
||||
st = newMemTree(memTreeLW, node, l)
|
||||
bitpos += node.bits
|
||||
node = st
|
||||
break
|
||||
}
|
||||
bitpos += node.bits
|
||||
node = st
|
||||
}
|
||||
|
||||
if node.entry != nil {
|
||||
|
||||
if node.entry.Key.isEqual(entry.Key) {
|
||||
node.updateAccess(s.accessCnt)
|
||||
if entry.SData == nil {
|
||||
entry.Size = node.entry.Size
|
||||
entry.SData = node.entry.SData
|
||||
}
|
||||
if entry.req == nil {
|
||||
entry.req = node.entry.req
|
||||
}
|
||||
entry.C = node.entry.C
|
||||
node.entry = entry
|
||||
return
|
||||
}
|
||||
|
||||
for node.entry != nil {
|
||||
|
||||
l := node.entry.Key.bits(bitpos, node.bits)
|
||||
st := node.subtree[l]
|
||||
if st == nil {
|
||||
st = newMemTree(memTreeLW, node, l)
|
||||
}
|
||||
st.entry = node.entry
|
||||
node.entry = nil
|
||||
st.updateAccess(node.access[0])
|
||||
|
||||
l = entry.Key.bits(bitpos, node.bits)
|
||||
st = node.subtree[l]
|
||||
if st == nil {
|
||||
st = newMemTree(memTreeLW, node, l)
|
||||
}
|
||||
bitpos += node.bits
|
||||
node = st
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
node.entry = entry
|
||||
node.lastDBaccess = s.dbAccessCnt
|
||||
node.updateAccess(s.accessCnt)
|
||||
s.entryCnt++
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *memStore) Get(hash Key) (chunk *Chunk, err error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
node := s.memtree
|
||||
bitpos := uint(0)
|
||||
for node.entry == nil {
|
||||
l := hash.bits(bitpos, node.bits)
|
||||
st := node.subtree[l]
|
||||
if st == nil {
|
||||
return nil, notFound
|
||||
}
|
||||
bitpos += node.bits
|
||||
node = st
|
||||
}
|
||||
|
||||
if node.entry.Key.isEqual(hash) {
|
||||
s.accessCnt++
|
||||
node.updateAccess(s.accessCnt)
|
||||
chunk = node.entry
|
||||
if s.dbAccessCnt-node.lastDBaccess > dbForceUpdateAccessCnt {
|
||||
s.dbAccessCnt++
|
||||
node.lastDBaccess = s.dbAccessCnt
|
||||
if s.dbStore != nil {
|
||||
s.dbStore.updateAccessCnt(hash)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = notFound
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *memStore) removeOldest() {
|
||||
node := s.memtree
|
||||
|
||||
for node.entry == nil {
|
||||
|
||||
aidx := uint(0)
|
||||
av := node.access[aidx]
|
||||
|
||||
for aidx < node.width/2-1 {
|
||||
if av == node.access[aidx*2+1] {
|
||||
node.access[aidx] = node.access[aidx*2+2]
|
||||
aidx = aidx*2 + 1
|
||||
} else if av == node.access[aidx*2+2] {
|
||||
node.access[aidx] = node.access[aidx*2+1]
|
||||
aidx = aidx*2 + 2
|
||||
} else {
|
||||
panic(nil)
|
||||
}
|
||||
}
|
||||
pidx := aidx*2 + 2 - node.width
|
||||
if (node.subtree[pidx] != nil) && (av == node.subtree[pidx].access[0]) {
|
||||
if node.subtree[pidx+1] != nil {
|
||||
node.access[aidx] = node.subtree[pidx+1].access[0]
|
||||
} else {
|
||||
node.access[aidx] = 0
|
||||
}
|
||||
} else if (node.subtree[pidx+1] != nil) && (av == node.subtree[pidx+1].access[0]) {
|
||||
if node.subtree[pidx] != nil {
|
||||
node.access[aidx] = node.subtree[pidx].access[0]
|
||||
} else {
|
||||
node.access[aidx] = 0
|
||||
}
|
||||
pidx++
|
||||
} else {
|
||||
panic(nil)
|
||||
}
|
||||
|
||||
//fmt.Println(pidx)
|
||||
node = node.subtree[pidx]
|
||||
|
||||
}
|
||||
|
||||
if node.entry.dbStored != nil {
|
||||
<-node.entry.dbStored
|
||||
}
|
||||
|
||||
if node.entry.SData != nil {
|
||||
node.entry = nil
|
||||
s.entryCnt--
|
||||
}
|
||||
|
||||
node.access[0] = 0
|
||||
|
||||
//---
|
||||
|
||||
aidx := uint(0)
|
||||
for {
|
||||
aa := node.access[aidx]
|
||||
if aidx > 0 {
|
||||
aidx = (aidx - 1) >> 1
|
||||
} else {
|
||||
pidx := node.parentIdx
|
||||
node = node.parent
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
aidx = (node.width + pidx - 2) >> 1
|
||||
}
|
||||
if (aa != 0) && ((aa < node.access[aidx]) || (node.access[aidx] == 0)) {
|
||||
node.access[aidx] = aa
|
||||
}
|
||||
}
|
||||
}
|
||||
35
bzz/memstore_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testMemStore(l int64, branches int64, t *testing.T) {
|
||||
m := newMemStore(nil)
|
||||
testStore(m, l, branches, t)
|
||||
}
|
||||
|
||||
func TestMemStore128_10000(t *testing.T) {
|
||||
testMemStore(10000, 128, t)
|
||||
}
|
||||
|
||||
func TestMemStore128_1000(t *testing.T) {
|
||||
testMemStore(1000, 128, t)
|
||||
}
|
||||
|
||||
func TestMemStore128_100(t *testing.T) {
|
||||
testMemStore(100, 128, t)
|
||||
}
|
||||
|
||||
func TestMemStore2_100(t *testing.T) {
|
||||
testMemStore(100, 2, t)
|
||||
}
|
||||
|
||||
func TestMemStoreNotFound(t *testing.T) {
|
||||
m := newMemStore(nil)
|
||||
zeroKey := make([]byte, 32)
|
||||
_, err := m.Get(zeroKey)
|
||||
if err != notFound {
|
||||
t.Errorf("Expected notFound, got %v", err)
|
||||
}
|
||||
}
|
||||
418
bzz/netstore.go
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
/*
|
||||
netStore is a network storage for chunks (a dht = distributed hash table of sorts)
|
||||
it is the entrypoint for chunk store/retrieval requests
|
||||
both local (coming from DPA api) and network (coming from peers via bzz protocol)
|
||||
it implements the ChunkStore interface and embeds local storage
|
||||
|
||||
its called by the bzz protocol instance running on each peer, so this is heavily
|
||||
parallelised.
|
||||
For routing and peer selection it embeds the hive which is a kademlia-driven
|
||||
logistics engine for swarm.
|
||||
It is aware of the node's network address
|
||||
*/
|
||||
type netStore struct {
|
||||
localStore *localStore
|
||||
lock sync.Mutex
|
||||
hive *hive
|
||||
self *peerAddr
|
||||
ready chan bool
|
||||
path string
|
||||
}
|
||||
|
||||
/*
|
||||
request status values:
|
||||
- started searching
|
||||
- found
|
||||
*/
|
||||
|
||||
const (
|
||||
reqSearching = iota // after search for chunk started until found or timed out
|
||||
reqFound // chunk found search terminated
|
||||
)
|
||||
|
||||
const (
|
||||
// maximum number of peers that a retrieved message is delivered to
|
||||
requesterCount = 3
|
||||
)
|
||||
|
||||
var (
|
||||
// timeout interval before retrieval is timed out
|
||||
searchTimeout = 3 * time.Second
|
||||
zeroKey = Key(common.Hash{}.Bytes())
|
||||
)
|
||||
|
||||
// each chunk when first requested opens a record associated with the request
|
||||
// next time a request for the same chunk arrives, this record is updated
|
||||
// this request status keeps track of the request ID-s as well as the requesting
|
||||
// peers and has a channel that is closed when the chunk is retrieved. Multiple
|
||||
// local callers can wait on this channel (or combined with a timeout, block with a
|
||||
// select).
|
||||
type requestStatus struct {
|
||||
key Key
|
||||
status int
|
||||
requesters map[uint64][]*retrieveRequestMsgData
|
||||
C chan bool
|
||||
}
|
||||
|
||||
func newRequestStatus() *requestStatus {
|
||||
return &requestStatus{
|
||||
requesters: make(map[uint64][]*retrieveRequestMsgData),
|
||||
C: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// netstore contructor, takes path argument that is used to initialise dbStore,
|
||||
// the persistent (disk) storage component of localStore
|
||||
// the second argument is the hive, the connection/logistics manager for the node
|
||||
func newNetStore(path string, h *hive) (netstore *netStore, err error) {
|
||||
dbStore, err := newDbStore(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
netstore = &netStore{
|
||||
localStore: &localStore{
|
||||
memStore: newMemStore(dbStore),
|
||||
dbStore: dbStore,
|
||||
},
|
||||
path: path,
|
||||
hive: h,
|
||||
ready: make(chan bool),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// netStore is started only when the network is started and the node has a
|
||||
// network address advertised to peers via the bzz protocol handshake (Status Msg)
|
||||
func (self *netStore) start(addr *peerAddr) (err error) {
|
||||
self.self = addr
|
||||
close(self.ready)
|
||||
return
|
||||
}
|
||||
|
||||
// The bzz protocol instance is started by the network when a bzz capable peer
|
||||
// is connected. Since this is on a different thread, the protocol needs to
|
||||
// wait for the netStore to be started before it sends out the handshake (Status Msg)
|
||||
// by closing the ready channel by start addr will block until network address
|
||||
// is available to the netStore also.
|
||||
func (self *netStore) addr() *peerAddr {
|
||||
<-self.ready
|
||||
return self.self
|
||||
}
|
||||
|
||||
// not relevant as of yet
|
||||
// but will quit the synchronisation loop(s)
|
||||
func (self *netStore) stop() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// called from dpa, entrypoint for *local* chunk store requests
|
||||
func (self *netStore) Put(entry *Chunk) {
|
||||
chunk, err := self.localStore.Get(entry.Key)
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.Put: localStore.Get returned with %v.", err)
|
||||
if err != nil {
|
||||
chunk = entry
|
||||
} else if chunk.SData == nil {
|
||||
chunk.SData = entry.SData
|
||||
chunk.Size = entry.Size
|
||||
} else {
|
||||
return
|
||||
}
|
||||
// from this point on the storage logic is the same with network storage requests
|
||||
self.put(chunk)
|
||||
}
|
||||
|
||||
// store logic common to local and network chunk store requests
|
||||
func (self *netStore) put(entry *Chunk) {
|
||||
self.localStore.Put(entry)
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.put: localStore.Put of %064x completed, %d bytes (%p).", entry.Key, len(entry.SData), entry)
|
||||
if entry.req != nil {
|
||||
// if entry had a requst status, it means it has recently been requested
|
||||
// by at least one peer
|
||||
if entry.req.status == reqSearching {
|
||||
// the status is set to found
|
||||
entry.req.status = reqFound
|
||||
// closing C singals to other routines (local requests)
|
||||
// that the chunk is has been retrieved
|
||||
close(entry.req.C)
|
||||
// deliver the chunk to requesters upstream
|
||||
self.propagateResponse(entry)
|
||||
}
|
||||
} else {
|
||||
go self.store(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// store propagates store requests to specific peers given by the kademlia hive
|
||||
// except for peers that the store request came from (if any)
|
||||
func (self *netStore) store(chunk *Chunk) {
|
||||
for _, peer := range self.hive.getPeers(chunk.Key, 0) {
|
||||
if chunk.source == nil || peer.Addr() != chunk.source.Addr() {
|
||||
peer.storeRequest(chunk.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the entrypoint for network store requests
|
||||
func (self *netStore) addStoreRequest(req *storeRequestMsgData) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.addStoreRequest: req = %v", req)
|
||||
chunk, err := self.localStore.Get(req.Key)
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.addStoreRequest: chunk reference %p", chunk)
|
||||
// we assume that a returned chunk is the one stored in the memory cache
|
||||
if err != nil {
|
||||
chunk = &Chunk{
|
||||
Key: req.Key,
|
||||
SData: req.SData,
|
||||
Size: int64(binary.LittleEndian.Uint64(req.SData[0:8])),
|
||||
}
|
||||
} else if chunk.SData == nil {
|
||||
// need data, validate now
|
||||
hasher := hasherfunc.New()
|
||||
hasher.Write(req.SData)
|
||||
if !bytes.Equal(hasher.Sum(nil), req.Key) {
|
||||
// data does not validate, ignore
|
||||
glog.V(logger.Warn).Infof("[BZZ] netStore.addStoreRequest: chunk invalid. store request ignored: %v", req)
|
||||
return
|
||||
}
|
||||
|
||||
chunk.SData = req.SData
|
||||
chunk.Size = int64(binary.LittleEndian.Uint64(req.SData[0:8]))
|
||||
} else {
|
||||
// data is found, store request ignored
|
||||
return
|
||||
}
|
||||
chunk.source = req.peer
|
||||
self.put(chunk)
|
||||
}
|
||||
|
||||
// Get is the entrypoint for local retrieve requests
|
||||
// waits for response or times out
|
||||
func (self *netStore) Get(key Key) (chunk *Chunk, err error) {
|
||||
chunk = self.get(key)
|
||||
timeout := time.Now().Add(searchTimeout)
|
||||
if chunk.SData == nil {
|
||||
req := &retrieveRequestMsgData{
|
||||
Key: chunk.Key,
|
||||
Id: generateId(),
|
||||
}
|
||||
req.setTimeout(&timeout)
|
||||
self.startSearch(req, chunk)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
// TODO: use self.timer time.Timer and reset with defer disableTimer
|
||||
timer := time.After(searchTimeout)
|
||||
select {
|
||||
case <-timer:
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.Get: %064x request time out ", key)
|
||||
err = notFound
|
||||
case <-chunk.req.C:
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.Get: %064x retrieved, %d bytes (%p)", key, len(chunk.SData), chunk)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve logic common for local and network chunk retrieval
|
||||
func (self *netStore) get(key Key) (chunk *Chunk) {
|
||||
var err error
|
||||
chunk, err = self.localStore.Get(key)
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.get: localStore.Get of %064x returned with %v.", key, err)
|
||||
// we assume that a returned chunk is the one stored in the memory cache
|
||||
if err != nil {
|
||||
// no data and no request status
|
||||
chunk = &Chunk{
|
||||
Key: key,
|
||||
}
|
||||
self.localStore.memStore.Put(chunk)
|
||||
}
|
||||
|
||||
if chunk.req == nil {
|
||||
chunk.req = newRequestStatus()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// entrypoint for network retrieve requests
|
||||
func (self *netStore) addRetrieveRequest(req *retrieveRequestMsgData) {
|
||||
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
// if key is zero or nil or matches requester address, then the request
|
||||
// is a self lookup and not to be forwarded
|
||||
if !req.isLookup() {
|
||||
chunk := self.get(req.Key)
|
||||
if chunk.SData != nil {
|
||||
chunk.req.status = reqFound
|
||||
}
|
||||
|
||||
req = self.strategyUpdateRequest(chunk.req, req) // may change req status
|
||||
|
||||
if chunk.req.status == reqFound {
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.addRetrieveRequest: %064x - content found, delivering...", req.Key)
|
||||
self.deliver(req, chunk)
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.addRetrieveRequest: %064x. Start net search by forwarding retrieve request. For now responding with peers.", req.Key)
|
||||
self.startSearch(req, chunk)
|
||||
|
||||
} else {
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.addRetrieveRequest: self lookup for %v: responding with peers...", req.peer)
|
||||
}
|
||||
|
||||
self.peers(req)
|
||||
}
|
||||
|
||||
// logic propagating retrieve requests to peers given by the kademlia hive
|
||||
// it's assumed that caller holds the lock
|
||||
func (self *netStore) startSearch(req *retrieveRequestMsgData, chunk *Chunk) {
|
||||
chunk.req.status = reqSearching
|
||||
peers := self.hive.getPeers(chunk.Key, 0)
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.startSearch: %064x - received %d peers from KΛÐΞMLIΛ...", chunk.Key, len(peers))
|
||||
for _, peer := range peers {
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.startSearch: sending retrieveRequests to peer [%064x]", req.Key)
|
||||
glog.V(logger.Debug).Infof("[BZZ] req.requesters: %v", chunk.req.requesters)
|
||||
var requester bool
|
||||
OUT:
|
||||
for _, recipients := range chunk.req.requesters {
|
||||
for _, recipient := range recipients {
|
||||
if recipient.peer.Addr() == peer.Addr() {
|
||||
requester = true
|
||||
break OUT
|
||||
}
|
||||
}
|
||||
}
|
||||
if !requester {
|
||||
peer.retrieve(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateId() uint64 {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
return uint64(r.Int63())
|
||||
}
|
||||
|
||||
/*
|
||||
adds a new peer to an existing open request
|
||||
only add if less than requesterCount peers forwarded the same request id so far
|
||||
note this is done irrespective of status (searching or found)
|
||||
*/
|
||||
func (self *netStore) addRequester(rs *requestStatus, req *retrieveRequestMsgData) {
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.addRequester: key %064x - add peer [%v] to req.Id %v", req.Key, req.peer, req.Id)
|
||||
list := rs.requesters[req.Id]
|
||||
rs.requesters[req.Id] = append(list, req)
|
||||
}
|
||||
|
||||
// add peer request the chunk and decides the timeout for the response if still searching
|
||||
func (self *netStore) strategyUpdateRequest(rs *requestStatus, origReq *retrieveRequestMsgData) (req *retrieveRequestMsgData) {
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.strategyUpdateRequest: key %064x", origReq.Key)
|
||||
// we do not create an alternative one
|
||||
req = origReq
|
||||
if rs != nil {
|
||||
self.addRequester(rs, req)
|
||||
if rs.status == reqSearching {
|
||||
req.setTimeout(self.searchTimeout(rs, req))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// once a chunk is found propagate it its requesters unless timed out
|
||||
func (self *netStore) propagateResponse(chunk *Chunk) {
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.propagateResponse: key %064x", chunk.Key)
|
||||
for id, requesters := range chunk.req.requesters {
|
||||
counter := requesterCount
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.propagateResponse id %064x", id)
|
||||
msg := &storeRequestMsgData{
|
||||
Key: chunk.Key,
|
||||
SData: chunk.SData,
|
||||
Id: uint64(id),
|
||||
}
|
||||
for _, req := range requesters {
|
||||
if req.timeout == nil || req.timeout.After(time.Now()) {
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.propagateResponse store -> %064x with %v", req.Id, req.peer)
|
||||
go req.peer.store(msg)
|
||||
counter--
|
||||
if counter <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// called on each request when a chunk is found,
|
||||
// delivery is done by sending a request to the requesting peer
|
||||
func (self *netStore) deliver(req *retrieveRequestMsgData, chunk *Chunk) {
|
||||
// here we should check MaxSize if set to a value lower than chunk.Size
|
||||
// we withhold
|
||||
// also check for timeout and withhold if expired
|
||||
if req.MaxSize == 0 || int64(req.MaxSize) >= chunk.Size {
|
||||
storeReq := &storeRequestMsgData{
|
||||
Key: req.Key,
|
||||
Id: req.Id,
|
||||
SData: chunk.SData,
|
||||
requestTimeout: req.timeout, //
|
||||
// StorageTimeout *time.Time // expiry of content
|
||||
// Metadata metaData
|
||||
}
|
||||
req.peer.store(storeReq)
|
||||
}
|
||||
}
|
||||
|
||||
// the immediate response to a retrieve request,
|
||||
// sends relevant peer data given by the kademlia hive to the requester
|
||||
func (self *netStore) peers(req *retrieveRequestMsgData) {
|
||||
// FIXME: should check req.MaxPeers but then should not default to zero or make sure we set it when sending retrieveRequests
|
||||
// we might need chunk.req to cache relevant peers response, or would it expire?
|
||||
if req != nil && req.MaxPeers >= 0 {
|
||||
var addrs []*peerAddr
|
||||
if req.timeout == nil || time.Now().Before(*(req.timeout)) {
|
||||
key := req.Key
|
||||
if isZeroKey(key) {
|
||||
key = req.peer.addrKey()
|
||||
req.Key = nil
|
||||
}
|
||||
for _, peer := range self.hive.getPeers(key, int(req.MaxPeers)) {
|
||||
addrs = append(addrs, peer.remoteAddr)
|
||||
}
|
||||
glog.V(logger.Debug).Infof("[BZZ] netStore.peers sending %d addresses. req.Id: %v, req.Key: %v", len(addrs), req.Id, req.Key)
|
||||
|
||||
peersData := &peersMsgData{
|
||||
Peers: addrs,
|
||||
Key: req.Key,
|
||||
Id: req.Id,
|
||||
}
|
||||
peersData.setTimeout(req.timeout)
|
||||
req.peer.peers(peersData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decides the timeout promise sent with the immediate peers response to a retrieve request
|
||||
// if timeout is explicitly set and expired
|
||||
func (self *netStore) searchTimeout(rs *requestStatus, req *retrieveRequestMsgData) (timeout *time.Time) {
|
||||
reqt := req.getTimeout()
|
||||
t := time.Now().Add(searchTimeout)
|
||||
if reqt != nil && reqt.Before(t) {
|
||||
return reqt
|
||||
} else {
|
||||
return &t
|
||||
}
|
||||
}
|
||||
641
bzz/protocol.go
Normal file
|
|
@ -0,0 +1,641 @@
|
|||
package bzz
|
||||
|
||||
/*
|
||||
BZZ implements the bzz wire protocol of swarm
|
||||
the protocol instance is launched on each peer by the network layer if the
|
||||
BZZ protocol handler is registered on the p2p server.
|
||||
|
||||
The protocol takes care of actually communicating the bzz protocol
|
||||
encoding and decoding requests for storage and retrieval
|
||||
handling the protocol handshake
|
||||
dispaching to netstore for handling the DHT logic
|
||||
registering peers in the KΛÐΞMLIΛ table via the hive logistic manager
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/kademlia"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/errs"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = 0
|
||||
ProtocolLength = uint64(8)
|
||||
ProtocolMaxMsgSize = 10 * 1024 * 1024
|
||||
NetworkId = 322
|
||||
)
|
||||
|
||||
// bzz protocol message codes
|
||||
const (
|
||||
statusMsg = iota // 0x01
|
||||
storeRequestMsg // 0x02
|
||||
retrieveRequestMsg // 0x03
|
||||
peersMsg // 0x04
|
||||
)
|
||||
|
||||
const (
|
||||
ErrMsgTooLarge = iota
|
||||
ErrDecode
|
||||
ErrInvalidMsgCode
|
||||
ErrVersionMismatch
|
||||
ErrNetworkIdMismatch
|
||||
ErrNoStatusMsg
|
||||
ErrExtraStatusMsg
|
||||
)
|
||||
|
||||
var errorToString = map[int]string{
|
||||
ErrMsgTooLarge: "Message too long",
|
||||
ErrDecode: "Invalid message",
|
||||
ErrInvalidMsgCode: "Invalid message code",
|
||||
ErrVersionMismatch: "Protocol version mismatch",
|
||||
ErrNetworkIdMismatch: "NetworkId mismatch",
|
||||
ErrNoStatusMsg: "No status message",
|
||||
ErrExtraStatusMsg: "Extra status message",
|
||||
}
|
||||
|
||||
// bzzProtocol represents the swarm wire protocol
|
||||
// an instance is running on each peer
|
||||
type bzzProtocol struct {
|
||||
netStore *netStore
|
||||
peer *p2p.Peer
|
||||
remoteAddr *peerAddr
|
||||
key Key
|
||||
rw p2p.MsgReadWriter
|
||||
errors *errs.Errors
|
||||
requestDb *LDBDatabase
|
||||
quitC chan bool
|
||||
}
|
||||
|
||||
/*
|
||||
Handshake
|
||||
|
||||
[0x01, Version: B_8, ID: B, Addr: [NodeID: B_64, IP: B_4 or B_6, Port: P], NetworkID; B_8, Caps: [[cap1: B_3, capVersion1: P], [cap2: B_3, capVersion2: P], ...]]
|
||||
|
||||
* Version: 8 byte integer version of the protocol
|
||||
* ID: arbitrary byte sequence client identifier human readable
|
||||
* Addr: the address advertised by the node, format identical to DEVp2p wire protocol
|
||||
* NetworkID: 8 byte integer network identifier
|
||||
* Caps: swarm-specific capabilities, format identical to devp2p
|
||||
|
||||
*/
|
||||
type statusMsgData struct {
|
||||
Version uint64
|
||||
ID string
|
||||
Addr *peerAddr
|
||||
NetworkId uint64
|
||||
Caps []p2p.Cap
|
||||
// Strategy uint64
|
||||
}
|
||||
|
||||
func (self *statusMsgData) String() string {
|
||||
return fmt.Sprintf("Status: Version: %v, ID: %v, Addr: %v, NetworkId: %v, Caps: %v", self.Version, self.ID, self.Addr, self.NetworkId, self.Caps)
|
||||
}
|
||||
|
||||
/*
|
||||
Given the chunker I see absolutely no reason why not allow storage and delivery
|
||||
of larger data . See my discussion on flexible chunking.
|
||||
store requests are forwarded to the peers in their kademlia proximity bin
|
||||
if they are distant
|
||||
if they are within our storage radius or have any incentive to store it
|
||||
then attach your nodeID to the metadata
|
||||
if the storage request is sufficiently close (within our proxLimit, i. e., the
|
||||
last row of the routing table), then sending it to all peers will not guarantee convergence, so there needs to be an absolute expiry of the request too.
|
||||
Maybe the protocol should specify a forward probability exponentially
|
||||
declining with age.
|
||||
|
||||
Store request
|
||||
|
||||
[+0x02, key: B_32, metadata: [], data: B_4k]: the data chunk to be stored, preceded by its key.
|
||||
|
||||
|
||||
*/
|
||||
type storeRequestMsgData struct {
|
||||
Key Key // hash of datasize | data
|
||||
SData []byte // the actual chunk Data
|
||||
// optional
|
||||
Id uint64 // request ID. if delivery, the ID is retrieve request ID
|
||||
requestTimeout *time.Time // expiry for forwarding - [not serialised][not currently used]
|
||||
storageTimeout *time.Time // expiry of content - [not serialised][not currently used]
|
||||
Metadata metaData // routing and accounting metadata [not currently used]
|
||||
peer *peer // [not serialised] protocol registers the requester
|
||||
}
|
||||
|
||||
func (self storeRequestMsgData) String() string {
|
||||
var from string
|
||||
if self.peer == nil {
|
||||
from = "self"
|
||||
} else {
|
||||
from = self.peer.Addr().String()
|
||||
}
|
||||
return fmt.Sprintf("From: %v, Key: %x; ID: %v, requestTimeout: %v, storageTimeout: %v, SData %x", from, self.Key[:4], self.Id, self.requestTimeout, self.storageTimeout, self.SData[:10])
|
||||
}
|
||||
|
||||
/*
|
||||
Root key retrieve request
|
||||
Timeout in milliseconds. Note that zero timeout retrieval requests do not request forwarding, but prompt for a peers message response. therefore they serve also
|
||||
as messages to retrieve peers.
|
||||
|
||||
MaxSize specifies the maximum size that the peer will accept. This is useful in
|
||||
particular if we allow storage and delivery of multichunk payload representing
|
||||
the entire or partial subtree unfolding from the requested root key.
|
||||
So when only interested in limited part of a stream (infinite trees) or only
|
||||
testing chunk availability etc etc, we can indicate it by limiting the size here.
|
||||
|
||||
Request ID can be newly generated or kept from the request originator.
|
||||
If request ID Is missing or zero, the request is handled as a lookup only
|
||||
prompting a peers response but not launching a search. Lookup requests are meant
|
||||
to be used to bootstrap kademlia tables.
|
||||
|
||||
In the special case that the key is the zero value as well, the remote peer's
|
||||
address is assumed (the message is to be handled as a self lookup request).
|
||||
The response is a PeersMsg with the peers in the kademlia proximity bin
|
||||
corresponding to the address.
|
||||
|
||||
Retrieve request
|
||||
|
||||
[0x03, key: B_32, Id: B_8, MaxSize: B_8, MaxPeers: B_8, Timeout: B_8, metadata: B]: key of the data chunk to be retrieved, timeout in milliseconds. Note that zero timeout retrievals serve also as messages to retrieve peers.
|
||||
|
||||
*/
|
||||
type retrieveRequestMsgData struct {
|
||||
Key Key
|
||||
// optional
|
||||
Id uint64 // request id, request is a lookup if missing or zero
|
||||
MaxSize uint64 // maximum size of delivery accepted
|
||||
MaxPeers uint64 // maximum number of peers returned
|
||||
Timeout uint64 // the longest time we are expecting a response
|
||||
timeout *time.Time // [not serialised]
|
||||
peer *peer // [not serialised] protocol registers the requester
|
||||
}
|
||||
|
||||
func (self retrieveRequestMsgData) String() string {
|
||||
var from string
|
||||
if self.peer == nil {
|
||||
from = "ourselves"
|
||||
} else {
|
||||
from = self.peer.Addr().String()
|
||||
}
|
||||
var target []byte
|
||||
if len(self.Key) > 3 {
|
||||
target = self.Key[:4]
|
||||
}
|
||||
return fmt.Sprintf("From: %v, Key: %x; ID: %v, MaxSize: %v, MaxPeers: %d", from, target, self.Id, self.MaxSize, self.MaxPeers)
|
||||
}
|
||||
|
||||
// lookups are encoded by missing request ID
|
||||
func (self retrieveRequestMsgData) isLookup() bool {
|
||||
return self.Id == 0
|
||||
}
|
||||
|
||||
func isZeroKey(key Key) bool {
|
||||
return len(key) == 0 || bytes.Equal(key, zeroKey)
|
||||
}
|
||||
|
||||
func (self retrieveRequestMsgData) setTimeout(t *time.Time) {
|
||||
self.timeout = t
|
||||
if t != nil {
|
||||
self.Timeout = uint64(t.UnixNano())
|
||||
} else {
|
||||
self.Timeout = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (self retrieveRequestMsgData) getTimeout() (t *time.Time) {
|
||||
if self.Timeout > 0 && self.timeout == nil {
|
||||
timeout := time.Unix(int64(self.Timeout), 0)
|
||||
t = &timeout
|
||||
self.timeout = t
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// peerAddr is sent in StatusMsg as part of the handshake
|
||||
type peerAddr struct {
|
||||
IP net.IP
|
||||
Port uint16
|
||||
ID []byte // the 64 byte NodeID (ECDSA Public Key)
|
||||
hash common.Hash // [not serialised] Sha3 hash of NodeID
|
||||
enode string // [not serialised] the enode URL of the peers Address
|
||||
}
|
||||
|
||||
func (self peerAddr) String() string {
|
||||
return self.new().enode
|
||||
}
|
||||
|
||||
func (self *peerAddr) new() *peerAddr {
|
||||
self.hash = crypto.Sha3Hash(self.ID)
|
||||
self.enode = fmt.Sprintf("enode://%x@%v:%d", self.ID, self.IP, self.Port)
|
||||
return self
|
||||
}
|
||||
|
||||
/*
|
||||
peers Msg is one response to retrieval; it is always encouraged after a retrieval
|
||||
request to respond with a list of peers in the same kademlia proximity bin.
|
||||
The encoding of a peer is identical to that in the devp2p base protocol peers
|
||||
messages: [IP, Port, NodeID]
|
||||
note that a node's DPA address is not the NodeID but the hash of the NodeID.
|
||||
|
||||
Timeout serves to indicate whether the responder is forwarding the query within
|
||||
the timeout or not.
|
||||
|
||||
The Key is the target (if response to a retrieval request) or missing (zero value)
|
||||
peers address (hash of NodeID) if retrieval request was a self lookup.
|
||||
|
||||
Peers message is requested by retrieval requests with a missing or zero value request ID
|
||||
|
||||
[0x04, Key: B_32, peers: [[IP, Port, NodeID], [IP, Port, NodeID], .... ], Timeout: B_8, Id: B_8 ]
|
||||
|
||||
*/
|
||||
type peersMsgData struct {
|
||||
Peers []*peerAddr //
|
||||
Timeout uint64
|
||||
timeout *time.Time // indicate whether responder is expected to deliver content
|
||||
Key Key // present if a response to a retrieval request
|
||||
Id uint64 // present if a response to a retrieval request
|
||||
//
|
||||
peer *peer
|
||||
}
|
||||
|
||||
func (self peersMsgData) String() string {
|
||||
var from string
|
||||
if self.peer == nil {
|
||||
from = "ourselves"
|
||||
} else {
|
||||
from = self.peer.Addr().String()
|
||||
}
|
||||
var target []byte
|
||||
if len(self.Key) > 3 {
|
||||
target = self.Key[:4]
|
||||
}
|
||||
return fmt.Sprintf("From: %v, Key: %x; ID: %v, Peers: %v", from, target, self.Id, self.Peers)
|
||||
}
|
||||
|
||||
func (self peersMsgData) setTimeout(t *time.Time) {
|
||||
self.timeout = t
|
||||
if t != nil {
|
||||
self.Timeout = uint64(t.UnixNano())
|
||||
} else {
|
||||
self.Timeout = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (self peersMsgData) getTimeout() (t *time.Time) {
|
||||
if self.Timeout > 0 && self.timeout == nil {
|
||||
timeout := time.Unix(int64(self.Timeout), 0)
|
||||
t = &timeout
|
||||
self.timeout = t
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
metadata is as yet a placeholder
|
||||
it will likely contain info about hops or the entire forward chain of node IDs
|
||||
this may allow some interesting schemes to evolve optimal routing strategies
|
||||
metadata for storage and retrieval requests could specify format parameters
|
||||
relevant for the (blockhashing) chunking scheme used (for chunks corresponding
|
||||
to a treenode). For instance all runtime params for the chunker (hashing
|
||||
algorithm used, branching etc.)
|
||||
Finally metadata can hold accounting info relevant to incentivisation scheme
|
||||
*/
|
||||
type metaData struct{}
|
||||
|
||||
/*
|
||||
main entrypoint, wrappers starting a server running the bzz protocol
|
||||
use this constructor to attach the protocol ("class") to server caps
|
||||
the Dev p2p layer then runs the protocol instance on each peer
|
||||
*/
|
||||
func BzzProtocol(netstore *netStore) (p2p.Protocol, error) {
|
||||
|
||||
db, err := NewLDBDatabase(path.Join(netstore.path, "requests"))
|
||||
if err != nil {
|
||||
return p2p.Protocol{}, err
|
||||
}
|
||||
return p2p.Protocol{
|
||||
Name: "bzz",
|
||||
Version: Version,
|
||||
Length: ProtocolLength,
|
||||
Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
||||
return runBzzProtocol(db, netstore, p, rw)
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// the main loop that handles incoming messages
|
||||
// note RemovePeer in the post-disconnect hook
|
||||
func runBzzProtocol(db *LDBDatabase, netstore *netStore, p *p2p.Peer, rw p2p.MsgReadWriter) (err error) {
|
||||
self := &bzzProtocol{
|
||||
netStore: netstore,
|
||||
rw: rw,
|
||||
peer: p,
|
||||
errors: &errs.Errors{
|
||||
Package: "BZZ",
|
||||
Errors: errorToString,
|
||||
},
|
||||
requestDb: db,
|
||||
quitC: make(chan bool),
|
||||
}
|
||||
glog.V(logger.Debug).Infof("[BZZ] listening address: %v", self.netStore.addr())
|
||||
|
||||
go self.storeRequestLoop()
|
||||
|
||||
err = self.handleStatus()
|
||||
if err == nil {
|
||||
for {
|
||||
err = self.handle()
|
||||
if err != nil {
|
||||
// if the handler loop exits, the peer is disconnecting
|
||||
// deregister the peer in the hive
|
||||
self.netStore.hive.removePeer(peer{bzzProtocol: self})
|
||||
break
|
||||
}
|
||||
}
|
||||
close(self.quitC)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) handle() error {
|
||||
msg, err := self.rw.ReadMsg()
|
||||
glog.V(logger.Debug).Infof("[BZZ] Incoming MSG: %v", msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if msg.Size > ProtocolMaxMsgSize {
|
||||
return self.protoError(ErrMsgTooLarge, "%v > %v", msg.Size, ProtocolMaxMsgSize)
|
||||
}
|
||||
// make sure that the payload has been fully consumed
|
||||
defer msg.Discard()
|
||||
/*
|
||||
statusMsg = iota // 0x01
|
||||
storeRequestMsg // 0x02
|
||||
retrieveRequestMsg // 0x03
|
||||
peersMsg // 0x04
|
||||
*/
|
||||
|
||||
switch msg.Code {
|
||||
case statusMsg:
|
||||
// no extra status message allowed. The one needed already handled by
|
||||
// handleStatus
|
||||
glog.V(logger.Debug).Infof("[BZZ] Status message: %v", msg)
|
||||
return self.protoError(ErrExtraStatusMsg, "")
|
||||
|
||||
case storeRequestMsg:
|
||||
// store requests are dispatched to netStore
|
||||
var req storeRequestMsgData
|
||||
if err := msg.Decode(&req); err != nil {
|
||||
return self.protoError(ErrDecode, "msg %v: %v", msg, err)
|
||||
}
|
||||
req.peer = &peer{bzzProtocol: self}
|
||||
self.netStore.addStoreRequest(&req)
|
||||
|
||||
case retrieveRequestMsg:
|
||||
// retrieve Requests are dispatched to netStore
|
||||
var req retrieveRequestMsgData
|
||||
if err := msg.Decode(&req); err != nil {
|
||||
return self.protoError(ErrDecode, "->msg %v: %v", msg, err)
|
||||
}
|
||||
if req.Key == nil {
|
||||
return self.protoError(ErrDecode, "protocol handler: req.Key == nil || req.Timeout == nil")
|
||||
}
|
||||
req.peer = &peer{bzzProtocol: self}
|
||||
glog.V(logger.Debug).Infof("[BZZ] Receiving retrieve request: %s", req.String())
|
||||
self.netStore.addRetrieveRequest(&req)
|
||||
|
||||
case peersMsg:
|
||||
// response to lookups and immediate response to retrieve requests
|
||||
// dispatches new peer data to the hive that adds them to KADDB
|
||||
var req peersMsgData
|
||||
if err := msg.Decode(&req); err != nil {
|
||||
return self.protoError(ErrDecode, "->msg %v: %v", msg, err)
|
||||
}
|
||||
req.peer = &peer{bzzProtocol: self}
|
||||
glog.V(logger.Debug).Infof("[BZZ] Receiving peer addresses: %s", req.String())
|
||||
self.netStore.hive.addPeerEntries(&req)
|
||||
|
||||
default:
|
||||
// no other message is allowed
|
||||
return self.protoError(ErrInvalidMsgCode, "%v", msg.Code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) handleStatus() (err error) {
|
||||
// send precanned status message
|
||||
handshake := &statusMsgData{
|
||||
Version: uint64(Version),
|
||||
ID: "honey",
|
||||
Addr: self.netStore.addr(),
|
||||
NetworkId: uint64(NetworkId),
|
||||
Caps: []p2p.Cap{},
|
||||
// Sync: self.syncer.lastSynced(),
|
||||
}
|
||||
|
||||
if err = p2p.Send(self.rw, statusMsg, handshake); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read and handle remote status
|
||||
var msg p2p.Msg
|
||||
msg, err = self.rw.ReadMsg()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if msg.Code != statusMsg {
|
||||
self.protoError(ErrNoStatusMsg, "first msg has code %x (!= %x)", msg.Code, statusMsg)
|
||||
}
|
||||
|
||||
if msg.Size > ProtocolMaxMsgSize {
|
||||
return self.protoError(ErrMsgTooLarge, "%v > %v", msg.Size, ProtocolMaxMsgSize)
|
||||
}
|
||||
|
||||
var status statusMsgData
|
||||
if err := msg.Decode(&status); err != nil {
|
||||
return self.protoError(ErrDecode, "msg %v: %v", msg, err)
|
||||
}
|
||||
|
||||
if status.NetworkId != NetworkId {
|
||||
return self.protoError(ErrNetworkIdMismatch, "%d (!= %d)", status.NetworkId, NetworkId)
|
||||
}
|
||||
|
||||
if Version != status.Version {
|
||||
return self.protoError(ErrVersionMismatch, "%d (!= %d)", status.Version, Version)
|
||||
}
|
||||
|
||||
self.remoteAddr = status.Addr.new()
|
||||
glog.V(logger.Detail).Infof("[BZZ] self: advertised IP: %v, local address: %v\npeer: advertised IP: %v, remote address: %v\n", self.netStore.addr().IP, self.peer.LocalAddr(), status.Addr.IP, self.peer.RemoteAddr())
|
||||
|
||||
glog.V(logger.Info).Infof("[BZZ] Peer %08x is [bzz] capable (%d/%d)\n", self.remoteAddr.hash[:4], status.Version, status.NetworkId)
|
||||
self.netStore.hive.addPeer(peer{bzzProtocol: self})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) addrKey() []byte {
|
||||
id := self.peer.ID()
|
||||
if self.key == nil {
|
||||
self.key = Key(crypto.Sha3(id[:]))
|
||||
}
|
||||
return self.key
|
||||
}
|
||||
|
||||
// protocol instance implements kademlia.Node interface (embedded hive.peer)
|
||||
func (self *bzzProtocol) Addr() kademlia.Address {
|
||||
return kademlia.Address(self.remoteAddr.hash)
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) Url() string {
|
||||
return self.remoteAddr.enode
|
||||
}
|
||||
|
||||
// TODO:
|
||||
func (self *bzzProtocol) LastActive() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// may need to implement protocol drop only? don't want to kick off the peer
|
||||
// if they are useful for other protocols
|
||||
func (self *bzzProtocol) Drop() {
|
||||
self.peer.Disconnect(p2p.DiscSubprotocolError)
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) String() string {
|
||||
return fmt.Sprintf("%08x: %v\n", self.remoteAddr.hash.Bytes()[:4], self.Url())
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) peerAddr() *peerAddr {
|
||||
p := self.peer
|
||||
id := p.ID()
|
||||
host, port, _ := net.SplitHostPort(p.RemoteAddr().String())
|
||||
intport, _ := strconv.Atoi(port)
|
||||
return &peerAddr{
|
||||
ID: id[:],
|
||||
IP: net.ParseIP(host),
|
||||
Port: uint16(intport),
|
||||
}
|
||||
}
|
||||
|
||||
// outgoing messages
|
||||
func (self *bzzProtocol) retrieve(req *retrieveRequestMsgData) {
|
||||
glog.V(logger.Debug).Infof("[BZZ] Sending retrieve request: %v", req)
|
||||
err := p2p.Send(self.rw, retrieveRequestMsg, req)
|
||||
if err != nil {
|
||||
glog.V(logger.Error).Infof("[BZZ] EncodeMsg error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// storeRequestLoop is buffering store requests to be sent over to the peer
|
||||
// this is to prevent crashes due to network output buffer contention (???)
|
||||
// the messages are supposed to be sent in the p2p priority queue.
|
||||
// TODO: as soon as there is API for that feature, adjust.
|
||||
// TODO: when peer drops the iterator position is not persisted
|
||||
// the request DB is shared between peers, keys are prefixed by the peers address
|
||||
// and the iterator
|
||||
func (self *bzzProtocol) storeRequestLoop() {
|
||||
|
||||
start := make([]byte, 64)
|
||||
copy(start, self.addrKey())
|
||||
|
||||
key := make([]byte, 64)
|
||||
copy(key, start)
|
||||
var n int
|
||||
var it iterator.Iterator
|
||||
LOOP:
|
||||
for {
|
||||
if n == 0 {
|
||||
it = self.requestDb.NewIterator()
|
||||
// glog.V(logger.Debug).Infof("[BZZ] seek iterator: %x", key)
|
||||
it.Seek(key)
|
||||
if !it.Valid() {
|
||||
// glog.V(logger.Debug).Infof("[BZZ] not valid, sleep, continue: %x", key)
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
key = it.Key()
|
||||
// glog.V(logger.Debug).Infof("[BZZ] found db key: %x", key)
|
||||
n = 100
|
||||
}
|
||||
// glog.V(logger.Debug).Infof("[BZZ] checking key: %x <> %x ", key, self.key())
|
||||
|
||||
// reached the end of this peers range
|
||||
if !bytes.Equal(key[:32], self.addrKey()) {
|
||||
// glog.V(logger.Debug).Infof("[BZZ] reached the end of this peers range: %x", key)
|
||||
n = 0
|
||||
continue
|
||||
}
|
||||
|
||||
chunk, err := self.netStore.localStore.dbStore.Get(key[32:])
|
||||
if err != nil {
|
||||
self.requestDb.Delete(key)
|
||||
continue
|
||||
}
|
||||
// glog.V(logger.Debug).Infof("[BZZ] sending chunk: %x", chunk.Key)
|
||||
|
||||
id := generateId()
|
||||
req := &storeRequestMsgData{
|
||||
Key: chunk.Key,
|
||||
SData: chunk.SData,
|
||||
Id: uint64(id),
|
||||
}
|
||||
self.store(req)
|
||||
|
||||
n--
|
||||
self.requestDb.Delete(key)
|
||||
it.Next()
|
||||
key = it.Key()
|
||||
if len(key) == 0 {
|
||||
key = start
|
||||
if n == 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
n = 0
|
||||
}
|
||||
|
||||
select {
|
||||
case <-self.quitC:
|
||||
break LOOP
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) store(req *storeRequestMsgData) {
|
||||
p2p.Send(self.rw, storeRequestMsg, req)
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) storeRequest(key Key) {
|
||||
peerKey := make([]byte, 64)
|
||||
copy(peerKey, self.addrKey())
|
||||
copy(peerKey[32:], key[:])
|
||||
glog.V(logger.Debug).Infof("[BZZ] enter store request %x into db", peerKey)
|
||||
self.requestDb.Put(peerKey, []byte{0})
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) peers(req *peersMsgData) {
|
||||
p2p.Send(self.rw, peersMsg, req)
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) protoError(code int, format string, params ...interface{}) (err *errs.Error) {
|
||||
err = self.errors.New(code, format, params...)
|
||||
err.Log(glog.V(logger.Info))
|
||||
return
|
||||
}
|
||||
|
||||
func (self *bzzProtocol) protoErrorDisconnect(err *errs.Error) {
|
||||
err.Log(glog.V(logger.Info))
|
||||
if err.Fatal() {
|
||||
self.peer.Disconnect(p2p.DiscSubprotocolError)
|
||||
}
|
||||
}
|
||||
51
bzz/protocol_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
)
|
||||
|
||||
type peerId struct {
|
||||
pubkey []byte
|
||||
}
|
||||
|
||||
func (self *peerId) String() string {
|
||||
return fmt.Sprintf("test peer %x", self.Pubkey()[:4])
|
||||
}
|
||||
|
||||
func (self *peerId) Pubkey() (pubkey []byte) {
|
||||
pubkey = self.pubkey
|
||||
if len(pubkey) == 0 {
|
||||
pubkey = crypto.GenerateNewKeyPair().PublicKey
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newTestPeer() (peer *p2p.Peer) {
|
||||
// peer = NewPeer(&peerId{}, []p2p.Cap{})
|
||||
// peer.pubkeyHook = func(*peerAddr) error { return nil }
|
||||
// peer.ourID = &peerId{}
|
||||
// peer.listenAddr = &peerAddr{}
|
||||
// peer.otherPeers = func() []*Peer { return nil }
|
||||
return
|
||||
}
|
||||
|
||||
func expectMsg(r p2p.MsgReader, code uint64) error {
|
||||
msg, err := r.ReadMsg()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := msg.Discard(); err != nil {
|
||||
return err
|
||||
}
|
||||
if msg.Code != code {
|
||||
return fmt.Errorf("wrong message code: got %d, expected %d", msg.Code, code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {}
|
||||
22
bzz/roundtripper.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
|
||||
// "github.com/ethereum/go-ethereum/common/docserver"
|
||||
// "github.com/ethereum/go-ethereum/jsre"
|
||||
)
|
||||
|
||||
type RoundTripper struct {
|
||||
Port string
|
||||
}
|
||||
|
||||
func (self *RoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
url := fmt.Sprintf("http://localhost:%s/%s/%s", self.Port, req.URL.Host, req.URL.Path)
|
||||
glog.V(logger.Info).Infof("[BZZ] roundtripper: proxying request '%s' to '%s'", req.RequestURI, url)
|
||||
return http.Get(url)
|
||||
}
|
||||
52
bzz/roundtripper_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package bzz
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/docserver"
|
||||
)
|
||||
|
||||
const port = "8500"
|
||||
|
||||
func TestRoundTripper(t *testing.T) {
|
||||
t.Skip("")
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
http.ServeContent(w, r, "", time.Unix(0, 0), strings.NewReader(r.RequestURI))
|
||||
} else {
|
||||
http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
go http.ListenAndServe(":"+port, nil)
|
||||
|
||||
rt := &RoundTripper{port}
|
||||
ds := docserver.New("/")
|
||||
ds.RegisterProtocol("bzz", rt)
|
||||
|
||||
resp, err := ds.Client().Get("bzz://test.com/path")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
content, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
return
|
||||
}
|
||||
if string(content) != "/test.com/path" {
|
||||
t.Errorf("incorrect response from http server: expected '%v', got '%v'", "/test.com/path", string(content))
|
||||
}
|
||||
|
||||
}
|
||||
801
common/kademlia/kademlia.go
Normal file
|
|
@ -0,0 +1,801 @@
|
|||
package kademlia
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
)
|
||||
|
||||
const (
|
||||
bucketSize = 20
|
||||
maxProx = 255
|
||||
connRetryExp = 2
|
||||
)
|
||||
|
||||
var (
|
||||
purgeInterval = 42 * time.Hour
|
||||
initialRetryInterval = 42 * 100 * time.Millisecond
|
||||
)
|
||||
|
||||
type Kademlia struct {
|
||||
// immutable baseparam
|
||||
addr Address
|
||||
|
||||
// adjustable parameters
|
||||
MaxProx int
|
||||
ProxBinSize int
|
||||
BucketSize int
|
||||
PurgeInterval time.Duration
|
||||
InitialRetryInterval time.Duration
|
||||
ConnRetryExp int
|
||||
|
||||
nodeDB [][]*NodeRecord
|
||||
nodeIndex map[Address]*NodeRecord
|
||||
dbcursors []int
|
||||
// state
|
||||
proxLimit int
|
||||
proxSize int
|
||||
|
||||
//
|
||||
count int
|
||||
buckets []*bucket
|
||||
|
||||
dblock sync.RWMutex
|
||||
lock sync.RWMutex
|
||||
quitC chan bool
|
||||
}
|
||||
|
||||
type Address common.Hash
|
||||
|
||||
func (a Address) String() string {
|
||||
return fmt.Sprintf("%x", a[:])
|
||||
}
|
||||
|
||||
type Node interface {
|
||||
Addr() Address
|
||||
Url() string
|
||||
LastActive() time.Time
|
||||
Drop()
|
||||
}
|
||||
|
||||
// allow inactive peers under
|
||||
type NodeRecord struct {
|
||||
Addr Address `json:address`
|
||||
Url string `json:url`
|
||||
Active int64 `json:active`
|
||||
After int64 `json:after`
|
||||
after time.Time
|
||||
checked time.Time
|
||||
node Node
|
||||
}
|
||||
|
||||
func (self *NodeRecord) setActive() {
|
||||
if self.node != nil {
|
||||
self.Active = self.node.LastActive().Unix()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *NodeRecord) setChecked() {
|
||||
self.checked = time.Now()
|
||||
}
|
||||
|
||||
// persisted node record database ()
|
||||
type kadDB struct {
|
||||
Address Address `json:address`
|
||||
Nodes [][]*NodeRecord `json:nodes`
|
||||
}
|
||||
|
||||
// public constructor
|
||||
// hash is a byte slice of length equal to self.HashBytes
|
||||
func New() *Kademlia {
|
||||
return &Kademlia{}
|
||||
}
|
||||
|
||||
// accessor for KAD self address
|
||||
func (self *Kademlia) Addr() Address {
|
||||
return self.addr
|
||||
}
|
||||
|
||||
// accessor for KAD self count
|
||||
// TODO: either memoize or lock
|
||||
func (self *Kademlia) Count() (sum int) {
|
||||
for _, b := range self.buckets {
|
||||
sum += len(b.nodes)
|
||||
}
|
||||
return
|
||||
// return self.count
|
||||
}
|
||||
|
||||
// accessor for KAD offline db count
|
||||
func (self *Kademlia) DBCount() int {
|
||||
return len(self.nodeIndex)
|
||||
}
|
||||
|
||||
// kademlia table + kaddb table displayed with ascii
|
||||
func (self *Kademlia) String() string {
|
||||
|
||||
var rows []string
|
||||
// rows = append(rows, fmt.Sprintf("KΛÐΞMLIΛ basenode address: %064x\n population: %d (%d)", self.addr[:], self.Count(), self.DBCount()))
|
||||
rows = append(rows, "=========================================================================")
|
||||
rows = append(rows, fmt.Sprintf("%v : MaxProx: %d, ProxBinSize: %d, BucketSize: %d, proxLimit: %d, proxSize: %d", time.Now(), self.MaxProx, self.ProxBinSize, self.BucketSize, self.proxLimit, self.proxSize))
|
||||
|
||||
for i, b := range self.buckets {
|
||||
|
||||
if i == self.proxLimit {
|
||||
rows = append(rows, fmt.Sprintf("===================== PROX LIMIT: %d =================================", i))
|
||||
}
|
||||
row := []string{fmt.Sprintf("%03d", i), fmt.Sprintf("%2d", len(b.nodes))}
|
||||
var k int
|
||||
c := self.dbcursors[i]
|
||||
for ; k < len(b.nodes); k++ {
|
||||
p := b.nodes[(c+k)%len(b.nodes)]
|
||||
row = append(row, fmt.Sprintf("%s", p.Addr().String()[:8]))
|
||||
if k == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
for ; k < 3; k++ {
|
||||
row = append(row, " ")
|
||||
}
|
||||
row = append(row, fmt.Sprintf("| %2d %2d", len(self.nodeDB[i]), self.dbcursors[i]))
|
||||
|
||||
for j, p := range self.nodeDB[i] {
|
||||
row = append(row, fmt.Sprintf("%08x", p.Addr[:4]))
|
||||
if j == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
rows = append(rows, strings.Join(row, " "))
|
||||
if i == self.MaxProx {
|
||||
break
|
||||
}
|
||||
}
|
||||
rows = append(rows, "=========================================================================")
|
||||
return strings.Join(rows, "\n")
|
||||
}
|
||||
|
||||
// Start brings up a pool of entries potentially from an offline persisted source
|
||||
// and sets default values for optional parameters
|
||||
func (self *Kademlia) Start(addr Address) error {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if self.quitC != nil {
|
||||
return nil
|
||||
}
|
||||
self.addr = addr
|
||||
if self.MaxProx == 0 {
|
||||
self.MaxProx = maxProx
|
||||
}
|
||||
if self.BucketSize == 0 {
|
||||
self.BucketSize = bucketSize
|
||||
}
|
||||
if self.InitialRetryInterval == 0 {
|
||||
self.InitialRetryInterval = initialRetryInterval
|
||||
}
|
||||
if self.PurgeInterval == 0 {
|
||||
self.PurgeInterval = purgeInterval
|
||||
}
|
||||
if self.ConnRetryExp == 0 {
|
||||
self.ConnRetryExp = connRetryExp
|
||||
}
|
||||
// runtime parameters
|
||||
if self.ProxBinSize == 0 {
|
||||
self.ProxBinSize = self.BucketSize
|
||||
}
|
||||
|
||||
self.buckets = make([]*bucket, self.MaxProx+1)
|
||||
for i, _ := range self.buckets {
|
||||
self.buckets[i] = &bucket{size: self.BucketSize} // will initialise bucket{int(0),[]Node(nil),sync.Mutex}
|
||||
}
|
||||
|
||||
self.nodeDB = make([][]*NodeRecord, self.MaxProx+1)
|
||||
self.dbcursors = make([]int, self.MaxProx+1)
|
||||
self.nodeIndex = make(map[Address]*NodeRecord)
|
||||
|
||||
self.quitC = make(chan bool)
|
||||
glog.V(logger.Info).Infof("[KΛÐ] started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop saves the routing table into a persistant form
|
||||
func (self *Kademlia) Stop(path string) (err error) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if self.quitC == nil {
|
||||
return
|
||||
}
|
||||
close(self.quitC)
|
||||
self.quitC = nil
|
||||
|
||||
if len(path) > 0 {
|
||||
err = self.Save(path)
|
||||
if err != nil {
|
||||
glog.V(logger.Warn).Infof("[KΛÐ]: unable to save node records: %v", err)
|
||||
} else {
|
||||
glog.V(logger.Info).Infof("[KΛÐ]: node records saved to '%v'", path)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveNode is the entrypoint where nodes are taken offline
|
||||
func (self *Kademlia) RemoveNode(node Node) (err error) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
var found bool
|
||||
index := self.proximityBin(node.Addr())
|
||||
bucket := self.buckets[index]
|
||||
for i := 0; i < len(bucket.nodes); i++ {
|
||||
if node.Addr() == bucket.nodes[i].Addr() {
|
||||
found = true
|
||||
bucket.nodes = append(bucket.nodes[:i], bucket.nodes[(i+1):]...)
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
glog.V(logger.Info).Infof("[KΛÐ]: remove node %v from table", node)
|
||||
|
||||
self.count--
|
||||
if len(bucket.nodes) < bucket.size {
|
||||
err = fmt.Errorf("insufficient nodes (%v) in bucket %v", len(bucket.nodes), index)
|
||||
}
|
||||
self.adjustProxLess(index)
|
||||
|
||||
r := self.nodeIndex[node.Addr()]
|
||||
r.node = nil
|
||||
now := time.Now()
|
||||
r.after = now
|
||||
r.After = now.Unix()
|
||||
r.Active = now.Unix()
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AddNode is the entry point where new nodes are registered
|
||||
func (self *Kademlia) AddNode(node Node) (err error) {
|
||||
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
|
||||
index := self.proximityBin(node.Addr())
|
||||
|
||||
// insert in kademlia table of active nodes
|
||||
bucket := self.buckets[index]
|
||||
// if bucket is full insertion replaces the worst node
|
||||
// TODO probably should give priority to peers with active traffic
|
||||
if worst, pos := bucket.insert(node); worst != nil {
|
||||
glog.V(logger.Info).Infof("[KΛÐ]: replace node %v (%d) with node %v", worst, pos, node)
|
||||
// no prox adjustment needed
|
||||
// do not change count
|
||||
} else {
|
||||
glog.V(logger.Info).Infof("[KΛÐ]: add new node %v to table", node)
|
||||
self.count++
|
||||
self.adjustProxMore(index)
|
||||
}
|
||||
|
||||
// insert in kaddb, kademlia node record database
|
||||
self.dblock.Lock()
|
||||
defer self.dblock.Unlock()
|
||||
record, found := self.nodeIndex[node.Addr()]
|
||||
if !found {
|
||||
glog.V(logger.Info).Infof("[KΛÐ]: add new record %v to node db", node)
|
||||
record = &NodeRecord{
|
||||
Addr: node.Addr(),
|
||||
Url: node.Url(),
|
||||
}
|
||||
self.nodeIndex[node.Addr()] = record
|
||||
self.nodeDB[index] = append(self.nodeDB[index], record)
|
||||
}
|
||||
record.node = node
|
||||
record.setActive()
|
||||
record.setChecked()
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// proxLimit is dynamically adjusted so that 1) there is no
|
||||
// empty buckets in bin < proxLimit and 2) the sum of all items are the maximum
|
||||
// possible but lower than ProxBinSize
|
||||
// adjust Prox (proxLimit and proxSize after an insertion of add nodes into bucket r)
|
||||
func (self *Kademlia) adjustProxMore(r int) {
|
||||
if r >= self.proxLimit {
|
||||
exLimit := self.proxLimit
|
||||
exSize := self.proxSize
|
||||
self.proxSize++
|
||||
|
||||
var i int
|
||||
for i = self.proxLimit; i < self.MaxProx && len(self.buckets[i].nodes) > 0 && self.proxSize-len(self.buckets[i].nodes) > self.ProxBinSize; i++ {
|
||||
self.proxSize -= len(self.buckets[i].nodes)
|
||||
}
|
||||
self.proxLimit = i
|
||||
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: Max Prox Bin: Lower Limit: %v (was %v): Bin Size: %v (was %v)", self.proxLimit, exLimit, self.proxSize, exSize)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Kademlia) adjustProxLess(r int) {
|
||||
exLimit := self.proxLimit
|
||||
exSize := self.proxSize
|
||||
if r >= self.proxLimit {
|
||||
self.proxSize--
|
||||
}
|
||||
|
||||
if r < self.proxLimit && len(self.buckets[r].nodes) == 0 {
|
||||
for i := self.proxLimit - 1; i > r; i-- {
|
||||
self.proxSize += len(self.buckets[i].nodes)
|
||||
}
|
||||
self.proxLimit = r
|
||||
} else if self.proxLimit > 0 && r >= self.proxLimit-1 {
|
||||
var i int
|
||||
for i = self.proxLimit - 1; i > 0 && len(self.buckets[i].nodes)+self.proxSize <= self.ProxBinSize; i-- {
|
||||
self.proxSize += len(self.buckets[i].nodes)
|
||||
}
|
||||
self.proxLimit = i
|
||||
}
|
||||
|
||||
if exLimit != self.proxLimit || exSize != self.proxSize {
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: Max Prox Bin: Lower Limit: %v (was %v): Bin Size: %v (was %v)", self.proxLimit, exLimit, self.proxSize, exSize)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
GetNodes(target) returns the list of nodes belonging to the same proximity bin
|
||||
as the target. The most proximate bin will be the union of the bins between
|
||||
proxLimit and MaxProx.
|
||||
*/
|
||||
func (self *Kademlia) GetNodes(target Address, max int) []Node {
|
||||
return self.getNodes(target, max).nodes
|
||||
}
|
||||
|
||||
func (self *Kademlia) getNodes(target Address, max int) (r nodesByDistance) {
|
||||
self.lock.RLock()
|
||||
defer self.lock.RUnlock()
|
||||
r.target = target
|
||||
index := self.proximityBin(target)
|
||||
start := index
|
||||
var down bool
|
||||
if index >= self.proxLimit {
|
||||
index = self.proxLimit
|
||||
start = self.MaxProx
|
||||
down = true
|
||||
}
|
||||
var n int
|
||||
limit := max
|
||||
if max == 0 {
|
||||
limit = 1000
|
||||
}
|
||||
for {
|
||||
bucket := self.buckets[start].nodes
|
||||
for i := 0; i < len(bucket); i++ {
|
||||
r.push(bucket[i], limit)
|
||||
n++
|
||||
}
|
||||
if max == 0 && start <= index && (n > 0 || start == 0) ||
|
||||
max > 0 && down && start <= index && (n >= limit || n == self.Count() || start == 0) {
|
||||
break
|
||||
}
|
||||
if down {
|
||||
start--
|
||||
} else {
|
||||
if start == self.MaxProx {
|
||||
if index == 0 {
|
||||
break
|
||||
}
|
||||
start = index - 1
|
||||
down = true
|
||||
} else {
|
||||
start++
|
||||
}
|
||||
}
|
||||
}
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: serve %d (=<%d) nodes for target lookup %v (PO%d)", n, self.MaxProx, target, index)
|
||||
return
|
||||
}
|
||||
|
||||
// AddNodeRecords adds node records to kaddb (persisted node record db)
|
||||
func (self *Kademlia) AddNodeRecords(nrs []*NodeRecord) {
|
||||
self.dblock.Lock()
|
||||
defer self.dblock.Unlock()
|
||||
var n int
|
||||
var nodes []*NodeRecord
|
||||
for _, node := range nrs {
|
||||
_, found := self.nodeIndex[node.Addr]
|
||||
if !found && node.Addr != self.addr {
|
||||
node.setChecked()
|
||||
self.nodeIndex[node.Addr] = node
|
||||
index := self.proximityBin(node.Addr)
|
||||
dbcursor := self.dbcursors[index]
|
||||
nodes = self.nodeDB[index]
|
||||
newnodes := make([]*NodeRecord, len(nodes)+1)
|
||||
copy(newnodes[:], nodes[:dbcursor])
|
||||
newnodes[dbcursor] = node
|
||||
copy(newnodes[dbcursor+1:], nodes[dbcursor:])
|
||||
self.nodeDB[index] = newnodes
|
||||
n++
|
||||
}
|
||||
}
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: received %d node records, added %d new", len(nrs), n)
|
||||
}
|
||||
|
||||
/*
|
||||
GetNodeRecord return one node record with the highest priority for desired
|
||||
connection.
|
||||
This is used to pick candidates for live nodes that are most wanted for
|
||||
a higly connected low centrality network structure for Swarm which best suits
|
||||
for a Kademlia-style routing.
|
||||
|
||||
The candidate is chosen using the following strategy.
|
||||
We check for missing online nodes in the buckets for 1 upto Max BucketSize rounds.
|
||||
On each round we proceed from the low to high proximity order buckets.
|
||||
If the number of active nodes (=connected peers) is < rounds, then start looking
|
||||
for a known candidate. To determine if there is a candidate to recommend the
|
||||
node record database row corresponding to the bucket is checked.
|
||||
If the row cursor is on position i, the ith element in the row is chosen.
|
||||
If the record is scheduled not to be retried before NOW, the next element is taken.
|
||||
If the record is scheduled can be retried, it is set as checked, scheduled for
|
||||
checking and is returned. The time of the next check is in X (duration) such that
|
||||
X = ConnRetryExp * delta where delta is the time past since the last check and
|
||||
ConnRetryExp is constant obsoletion factor. (Note that when node records are added
|
||||
from peer messages, they are marked as checked and placed at the cursor, ie.
|
||||
given priority over older entries). Entries which were checked more than
|
||||
purgeInterval ago are deleted from the kaddb row. If no candidate is found after
|
||||
a full round of checking the next bucket up is considered. If no candidate is
|
||||
found when we reach the maximum-proximity bucket, the next round starts.
|
||||
|
||||
node record a is more favoured to b a > b iff a is a passive node (record of
|
||||
offline past peer)
|
||||
|proxBin(a)| < |proxBin(b)|
|
||||
|| (proxBin(a) < proxBin(b) && |proxBin(a)| == |proxBin(b)|)
|
||||
|| (proxBin(a) == proxBin(b) && lastChecked(a) < lastChecked(b))
|
||||
|
||||
This has double role. Starting as naive node with empty db, this implements
|
||||
Kademlia bootstrapping
|
||||
As a mature node, it fills short lines. All on demand.
|
||||
|
||||
The second argument returned names the first missing slot found
|
||||
*/
|
||||
func (self *Kademlia) GetNodeRecord() (node *NodeRecord, proxLimit int) {
|
||||
// return value -1 indicates that buckets are filled in all
|
||||
proxLimit = -1
|
||||
self.dblock.RLock()
|
||||
defer self.dblock.RUnlock()
|
||||
|
||||
for rounds := 1; rounds <= self.BucketSize; rounds++ {
|
||||
ROUND:
|
||||
for po, dbrow := range self.nodeDB {
|
||||
if po > self.MaxProx {
|
||||
break ROUND
|
||||
}
|
||||
bin := self.buckets[po]
|
||||
bin.lock.Lock()
|
||||
if len(bin.nodes) < rounds {
|
||||
if proxLimit < 0 {
|
||||
proxLimit = po
|
||||
}
|
||||
var count int
|
||||
var purge []int
|
||||
n := self.dbcursors[po]
|
||||
|
||||
// try node records in the relavant kaddb row (of identical prox order)
|
||||
// if they are ripe for checking
|
||||
ROW:
|
||||
for count < len(dbrow) {
|
||||
node = dbrow[n]
|
||||
if (node.after == time.Time{}) {
|
||||
node.after = time.Unix(node.After, 0)
|
||||
}
|
||||
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: kaddb record %v (PO%03d:%d) not to be retried before %d %v", node.Addr, po, n, node.After, node.after)
|
||||
|
||||
// time since last known connection attempt
|
||||
delta := node.checked.Unix() - node.After
|
||||
if delta < 4 {
|
||||
node.After = 0
|
||||
}
|
||||
|
||||
if node.node == nil && node.after.Before(time.Now()) {
|
||||
if node.checked.Add(self.PurgeInterval).Before(time.Now()) {
|
||||
// delete node
|
||||
purge = append(purge, n)
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: inactive node record %v (PO%03d:%d) last check: %v, next check: %v", node.Addr, po, n, node.checked, node.after)
|
||||
} else {
|
||||
// scheduling next check
|
||||
if node.After == 0 {
|
||||
node.after = time.Now().Add(self.InitialRetryInterval)
|
||||
node.After = node.after.Unix()
|
||||
} else {
|
||||
node.After = delta*int64(self.ConnRetryExp) + node.After
|
||||
node.after = time.Unix(node.After, 0)
|
||||
}
|
||||
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: serve node record %v (PO%03d:%d), last check: %v, next check: %v", node.Addr, po, n, node.checked, node.after)
|
||||
}
|
||||
break ROW
|
||||
}
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: kaddb record %v (PO%03d:%d) not ready. skipped. not to be retried before: %v", node.Addr, po, n, node.after)
|
||||
n++
|
||||
count++
|
||||
// cycle: n = n % len(dbrow)
|
||||
if n >= len(dbrow) {
|
||||
n = 0
|
||||
}
|
||||
}
|
||||
self.dbcursors[po] = n
|
||||
self.deleteNodeRecords(po, purge...)
|
||||
if node != nil {
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: rounds %d: prox limit: PO%03d\n%v", rounds, proxLimit, node)
|
||||
node.setChecked()
|
||||
bin.lock.Unlock()
|
||||
return
|
||||
}
|
||||
} // if len < rounds
|
||||
bin.lock.Unlock()
|
||||
} // for po-s
|
||||
glog.V(logger.Detail).Infof("[KΛÐ]: rounds %d: proxlimit: PO%03d", rounds, proxLimit)
|
||||
if proxLimit == 0 || proxLimit < 0 && self.BucketSize == rounds {
|
||||
return
|
||||
}
|
||||
} // for round
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// deletes the noderecords of a kaddb row corresponding to the indexes
|
||||
// caller must hold the dblock,
|
||||
// the call is unsafe, no index checks
|
||||
func (self *Kademlia) deleteNodeRecords(row int, indexes ...int) {
|
||||
var prev int
|
||||
var nodes []*NodeRecord
|
||||
dbrow := self.nodeDB[row]
|
||||
for _, next := range indexes {
|
||||
// need to adjust dbcursor
|
||||
if next > 0 {
|
||||
if next <= self.dbcursors[row] {
|
||||
self.dbcursors[row]--
|
||||
}
|
||||
nodes = append(nodes, dbrow[prev:next]...)
|
||||
}
|
||||
prev = next + 1
|
||||
delete(self.nodeIndex, dbrow[next].Addr)
|
||||
}
|
||||
self.nodeDB[row] = append(nodes, dbrow[prev:]...)
|
||||
}
|
||||
|
||||
// in situ mutable bucket
|
||||
type bucket struct {
|
||||
size int
|
||||
nodes []Node
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// nodesByDistance is a list of nodes, ordered by distance to target.
|
||||
type nodesByDistance struct {
|
||||
nodes []Node
|
||||
target Address
|
||||
}
|
||||
|
||||
func sortedByDistanceTo(target Address, slice []Node) bool {
|
||||
var last Address
|
||||
for i, node := range slice {
|
||||
if i > 0 {
|
||||
if target.ProxCmp(node.Addr(), last) < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
last = node.Addr()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// push(node, max) adds the given node to the list, keeping the total size
|
||||
// below max elements.
|
||||
func (h *nodesByDistance) push(node Node, max int) {
|
||||
// returns the firt index ix such that func(i) returns true
|
||||
ix := sort.Search(len(h.nodes), func(i int) bool {
|
||||
return h.target.ProxCmp(h.nodes[i].Addr(), node.Addr()) >= 0
|
||||
})
|
||||
|
||||
if len(h.nodes) < max {
|
||||
h.nodes = append(h.nodes, node)
|
||||
}
|
||||
if ix < len(h.nodes) {
|
||||
copy(h.nodes[ix+1:], h.nodes[ix:])
|
||||
h.nodes[ix] = node
|
||||
}
|
||||
}
|
||||
|
||||
// insert adds a peer to a bucket either by appending to existing items if
|
||||
// bucket length does not exceed bucketSize, or by replacing the worst
|
||||
// Node in the bucket
|
||||
func (self *bucket) insert(node Node) (dropped Node, pos int) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if len(self.nodes) >= self.size { // >= allows us to add peers beyond the bucketsize limitation
|
||||
dropped, pos = self.worstNode()
|
||||
if dropped != nil {
|
||||
self.nodes[pos] = node
|
||||
glog.V(logger.Info).Infof("[KΛÐ] dropping node %v (%d)", dropped, pos)
|
||||
dropped.Drop()
|
||||
return
|
||||
}
|
||||
}
|
||||
self.nodes = append(self.nodes, node)
|
||||
return
|
||||
}
|
||||
|
||||
// worst expunges the single worst node in a row, where worst entry is the node
|
||||
// that has been inactive for the longests time
|
||||
func (self *bucket) worstNode() (node Node, pos int) {
|
||||
var oldest time.Time
|
||||
for p, n := range self.nodes {
|
||||
if (oldest == time.Time{}) || !oldest.Before(n.LastActive()) {
|
||||
oldest = n.LastActive()
|
||||
node = n
|
||||
pos = p
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Taking the proximity order relative to a fix point x classifies the points in
|
||||
the space (n byte long byte sequences) into bins. Items in each are at
|
||||
most half as distant from x as items in the previous bin. Given a sample of
|
||||
uniformly distributed items (a hash function over arbitrary sequence) the
|
||||
proximity scale maps onto series of subsets with cardinalities on a negative
|
||||
exponential scale.
|
||||
|
||||
It also has the property that any two item belonging to the same bin are at
|
||||
most half as distant from each other as they are from x.
|
||||
|
||||
If we think of random sample of items in the bins as connections in a network of interconnected nodes than relative proximity can serve as the basis for local
|
||||
decisions for graph traversal where the task is to find a route between two
|
||||
points. Since in every hop, the finite distance halves, there is
|
||||
a guaranteed constant maximum limit on the number of hops needed to reach one
|
||||
node from the other.
|
||||
*/
|
||||
|
||||
func (self *Kademlia) proximityBin(other Address) (ret int) {
|
||||
ret = proximity(self.addr, other)
|
||||
if ret > self.MaxProx {
|
||||
ret = self.MaxProx
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Proximity(x, y) returns the proximity order of the MSB distance between x and y
|
||||
|
||||
The distance metric MSB(x, y) of two equal length byte sequences x an y is the
|
||||
value of the binary integer cast of the x^y, ie., x and y bitwise xor-ed.
|
||||
the binary cast is big endian: most significant bit first (=MSB).
|
||||
|
||||
Proximity(x, y) is a discrete logarithmic scaling of the MSB distance.
|
||||
It is defined as the reverse rank of the integer part of the base 2
|
||||
logarithm of the distance.
|
||||
It is calculated by counting the number of common leading zeros in the (MSB)
|
||||
binary representation of the x^y.
|
||||
|
||||
(0 farthest, 255 closest, 256 self)
|
||||
*/
|
||||
func proximity(one, other Address) (ret int) {
|
||||
for i := 0; i < len(one); i++ {
|
||||
oxo := one[i] ^ other[i]
|
||||
for j := 0; j < 8; j++ {
|
||||
if (uint8(oxo)>>uint8(7-j))&0x1 != 0 {
|
||||
return i*8 + j
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(one) * 8
|
||||
}
|
||||
|
||||
// the string form of the binary representation of an address
|
||||
func (a Address) Bin() string {
|
||||
var bs []string
|
||||
for _, b := range a[:] {
|
||||
bs = append(bs, fmt.Sprintf("%08b", b))
|
||||
}
|
||||
return strings.Join(bs, "")
|
||||
}
|
||||
|
||||
// Address.ProxCmp compares the distances a->target and b->target.
|
||||
// Returns -1 if a is closer to target, 1 if b is closer to target
|
||||
// and 0 if they are equal.
|
||||
func (target Address) ProxCmp(a, b Address) int {
|
||||
for i := range target {
|
||||
da := a[i] ^ target[i]
|
||||
db := b[i] ^ target[i]
|
||||
if da > db {
|
||||
return 1
|
||||
} else if da < db {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// save persists kaddb on disk (written to file on path in json format.
|
||||
// save is called by Kademlia.Stop()
|
||||
func (self *Kademlia) Save(path string) error {
|
||||
|
||||
kad := kadDB{
|
||||
Address: self.addr,
|
||||
Nodes: self.nodeDB,
|
||||
}
|
||||
|
||||
for _, b := range kad.Nodes {
|
||||
for _, node := range b {
|
||||
node.setActive()
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(&kad, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, data, os.ModePerm)
|
||||
}
|
||||
|
||||
// Load(path) loads the node record database (kaddb) from file on path.
|
||||
// TODO: urls will be supported and handled with bzz-enabled dox clienta
|
||||
func (self *Kademlia) Load(path string) (err error) {
|
||||
var data []byte
|
||||
data, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var kad kadDB
|
||||
err = json.Unmarshal(data, &kad)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if self.addr != kad.Address {
|
||||
return fmt.Errorf("invalid kad db: address mismatch, expected %v, got %v", self.addr, kad.Address)
|
||||
}
|
||||
self.nodeDB = kad.Nodes
|
||||
return
|
||||
}
|
||||
|
||||
// randomAddressAt(address, prox) generates a random address
|
||||
// at proximity order prox relative to address
|
||||
// if prox is negative a random address is generated
|
||||
func RandomAddressAt(self Address, prox int) (addr Address) {
|
||||
addr = self
|
||||
var pos int
|
||||
if prox >= 0 {
|
||||
pos = prox / 8
|
||||
trans := prox % 8
|
||||
transbytea := byte(0)
|
||||
for j := 0; j <= trans; j++ {
|
||||
transbytea |= 1 << uint8(7-j)
|
||||
}
|
||||
flipbyte := byte(1 << uint8(7-trans))
|
||||
transbyteb := transbytea ^ byte(255)
|
||||
randbyte := byte(rand.Intn(255))
|
||||
addr[pos] = ((addr[pos] & transbytea) ^ flipbyte) | randbyte&transbyteb
|
||||
}
|
||||
for i := pos + 1; i < len(addr); i++ {
|
||||
addr[i] = byte(rand.Intn(255))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// randomAddressAt() generates a random address
|
||||
func RandomAddress() Address {
|
||||
return RandomAddressAt(Address{}, -1)
|
||||
}
|
||||
408
common/kademlia/kademlia_test.go
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
package kademlia
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
quickrand = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
quickcfgGetNodes = &quick.Config{MaxCount: 5000, Rand: quickrand}
|
||||
quickcfgBootStrap = &quick.Config{MaxCount: 1000, Rand: quickrand}
|
||||
)
|
||||
|
||||
type testNode struct {
|
||||
addr Address
|
||||
}
|
||||
|
||||
func (n *testNode) String() string {
|
||||
return fmt.Sprintf("%x", n.addr[:])
|
||||
}
|
||||
|
||||
func (n *testNode) Addr() Address {
|
||||
return n.addr
|
||||
}
|
||||
|
||||
func (n *testNode) Drop() {
|
||||
}
|
||||
|
||||
func (n *testNode) Url() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *testNode) LastActive() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (n *testNode) Add(a Address) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAddNode(t *testing.T) {
|
||||
addr, ok := gen(Address{}, quickrand).(Address)
|
||||
other, ok := gen(Address{}, quickrand).(Address)
|
||||
if !ok {
|
||||
t.Errorf("oops")
|
||||
}
|
||||
kad := New()
|
||||
kad.Start(addr)
|
||||
err := kad.AddNode(&testNode{addr: other})
|
||||
_ = err
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
test := func(test *bootstrapTest) bool {
|
||||
// for any node kad.le, Target and N
|
||||
kad := New()
|
||||
kad.MaxProx = test.MaxProx
|
||||
kad.BucketSize = test.BucketSize
|
||||
kad.Start(test.Self)
|
||||
var err error
|
||||
|
||||
// t.Logf("bootstapTest MaxProx: %v BucketSize: %v\n", test.MaxProx, test.BucketSize)
|
||||
|
||||
addr := gen(Address{}, r).(Address)
|
||||
prox := proximity(addr, test.Self)
|
||||
|
||||
for p := 0; p <= prox; p++ {
|
||||
var nrs []*NodeRecord
|
||||
for i := 0; i < test.BucketSize; i++ {
|
||||
nrs = append(nrs, &NodeRecord{
|
||||
Addr: RandomAddressAt(test.Self, p),
|
||||
})
|
||||
}
|
||||
kad.AddNodeRecords(nrs)
|
||||
}
|
||||
|
||||
node := &testNode{addr}
|
||||
|
||||
n := 0
|
||||
for n < 100 {
|
||||
err = kad.AddNode(node)
|
||||
if err != nil {
|
||||
t.Errorf("backend not accepting node")
|
||||
return false
|
||||
}
|
||||
var nrs []*NodeRecord
|
||||
prox := proximity(node.addr, test.Self)
|
||||
for i := 0; i < test.BucketSize; i++ {
|
||||
nrs = append(nrs, &NodeRecord{
|
||||
Addr: RandomAddressAt(test.Self, prox+1),
|
||||
})
|
||||
}
|
||||
kad.AddNodeRecords(nrs)
|
||||
|
||||
var lens []int
|
||||
for i := 0; i <= test.MaxProx; i++ {
|
||||
lens = append(lens, len(kad.buckets[i].nodes))
|
||||
}
|
||||
|
||||
record, _ := kad.GetNodeRecord()
|
||||
if record == nil {
|
||||
// t.Logf("after round %d, no more node records needed", n)
|
||||
break
|
||||
}
|
||||
node = &testNode{record.Addr}
|
||||
n++
|
||||
}
|
||||
exp := test.BucketSize * (test.MaxProx + 1)
|
||||
if kad.Count() != exp {
|
||||
t.Errorf("incorrect number of peers, expected %d, got %d\n%v", exp, kad.Count(), kad)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if err := quick.Check(test, quickcfgBootStrap); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
test := func(test *getNodesTest) bool {
|
||||
// for any node kad.le, Target and N
|
||||
kad := New()
|
||||
kad.MaxProx = 10
|
||||
kad.Start(test.Self)
|
||||
var err error
|
||||
// t.Logf("getNodesTest %v: %v\n", len(test.All), test)
|
||||
for _, node := range test.All {
|
||||
err = kad.AddNode(node)
|
||||
if err != nil {
|
||||
t.Errorf("backend not accepting node")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(test.All) == 0 || test.N == 0 {
|
||||
return true
|
||||
}
|
||||
nodes := kad.GetNodes(test.Target, test.N)
|
||||
|
||||
// check that the number of results is min(N, kad.len)
|
||||
wantN := test.N
|
||||
if tlen := kad.Count(); tlen < test.N {
|
||||
wantN = tlen
|
||||
}
|
||||
|
||||
if len(nodes) != wantN {
|
||||
t.Errorf("wrong number of nodes: got %d, want %d", len(nodes), wantN)
|
||||
return false
|
||||
}
|
||||
|
||||
if hasDuplicates(nodes) {
|
||||
t.Errorf("result contains duplicates")
|
||||
return false
|
||||
}
|
||||
|
||||
if !sortedByDistanceTo(test.Target, nodes) {
|
||||
t.Errorf("result is not sorted by distance to target")
|
||||
return false
|
||||
}
|
||||
|
||||
// check that the result nodes have minimum distance to target.
|
||||
farthestResult := nodes[len(nodes)-1].Addr()
|
||||
for i, b := range kad.buckets {
|
||||
for j, n := range b.nodes {
|
||||
if contains(nodes, n.Addr()) {
|
||||
continue // don't run the check below for nodes in result
|
||||
}
|
||||
if test.Target.ProxCmp(n.Addr(), farthestResult) < 0 {
|
||||
_ = i * j
|
||||
t.Errorf("kad.le contains node that is closer to target but it's not in result")
|
||||
// t.Logf("bucket %v, item %v\n", i, j)
|
||||
// t.Logf(" Target: %x", test.Target)
|
||||
// t.Logf(" Farthest Result: %x", farthestResult)
|
||||
// t.Logf(" ID: %x (%d)", n.Addr(), kad.proximityBin(n.Addr()))
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if err := quick.Check(test, quickcfgGetNodes); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
type proxTest struct {
|
||||
add bool
|
||||
index int
|
||||
addr Address
|
||||
}
|
||||
|
||||
var (
|
||||
addresses []Address
|
||||
)
|
||||
|
||||
func TestProxAdjust(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
self := gen(Address{}, r).(Address)
|
||||
|
||||
kad := New()
|
||||
kad.MaxProx = 10
|
||||
kad.Start(self)
|
||||
var err error
|
||||
for i := 0; i < 100; i++ {
|
||||
a := gen(Address{}, r).(Address)
|
||||
addresses = append(addresses, a)
|
||||
err = kad.AddNode(&testNode{addr: a})
|
||||
if err != nil {
|
||||
t.Errorf("backend not accepting node")
|
||||
return
|
||||
}
|
||||
if !kad.proxCheck(t) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
test := func(test *proxTest) bool {
|
||||
node := &testNode{test.addr}
|
||||
if test.add {
|
||||
kad.AddNode(node)
|
||||
} else {
|
||||
kad.RemoveNode(node)
|
||||
}
|
||||
return kad.proxCheck(t)
|
||||
}
|
||||
if err := quick.Check(test, quickcfgGetNodes); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveLoad(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
addresses := gen([]Address{}, r).([]Address)
|
||||
self := addresses[0]
|
||||
kad := New()
|
||||
kad.MaxProx = 10
|
||||
kad.Start(self)
|
||||
var err error
|
||||
for _, a := range addresses[1:] {
|
||||
err = kad.AddNode(&testNode{addr: a})
|
||||
if err != nil {
|
||||
t.Errorf("backend not accepting node")
|
||||
return
|
||||
}
|
||||
}
|
||||
nodes := kad.GetNodes(self, 100)
|
||||
path := "/tmp/bzz.peers"
|
||||
kad.Stop(path)
|
||||
kad = New()
|
||||
kad.Start(self)
|
||||
kad.Load(path)
|
||||
for _, b := range kad.nodeDB {
|
||||
for _, node := range b {
|
||||
node.node = &testNode{node.Addr}
|
||||
err = kad.AddNode(node.node)
|
||||
if err != nil {
|
||||
t.Errorf("backend not accepting node")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
loadednodes := kad.GetNodes(self, 100)
|
||||
for i, node := range loadednodes {
|
||||
if nodes[i].Addr() != node.Addr() {
|
||||
t.Errorf("node mismatch at %d/%d", i, len(nodes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Kademlia) proxCheck(t *testing.T) bool {
|
||||
var sum, i int
|
||||
var b *bucket
|
||||
for i, b = range self.buckets {
|
||||
l := len(b.nodes)
|
||||
// if we are in the high prox multibucket
|
||||
if i >= self.proxLimit {
|
||||
sum += l
|
||||
} else if l == 0 {
|
||||
t.Errorf("bucket %d empty, yet proxLimit is %d\n%v", len(b.nodes), self.proxLimit, self)
|
||||
return false
|
||||
}
|
||||
}
|
||||
// check if merged high prox bucket does not exceed size
|
||||
if sum > 0 {
|
||||
// if sum > self.ProxBinSize {
|
||||
// t.Errorf("bucket %d is empty, yet proxSize is %d\n%v", i, self.proxSize, self)
|
||||
// return false
|
||||
// }
|
||||
if sum != self.proxSize {
|
||||
t.Errorf("proxSize incorrect, expected %v, got %v", sum, self.proxSize)
|
||||
return false
|
||||
}
|
||||
if self.proxLimit > 0 && sum+len(self.buckets[self.proxLimit-1].nodes) < self.ProxBinSize {
|
||||
t.Errorf("proxBinSize incorrect, expected %v got %v", sum, self.proxSize)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type bootstrapTest struct {
|
||||
MaxProx int
|
||||
BucketSize int
|
||||
Self Address
|
||||
}
|
||||
|
||||
func (*bootstrapTest) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
t := &bootstrapTest{
|
||||
Self: gen(Address{}, rand).(Address),
|
||||
MaxProx: 10 + rand.Intn(3),
|
||||
BucketSize: rand.Intn(3) + 1,
|
||||
}
|
||||
return reflect.ValueOf(t)
|
||||
}
|
||||
|
||||
type getNodesTest struct {
|
||||
Self Address
|
||||
Target Address
|
||||
All []Node
|
||||
N int
|
||||
}
|
||||
|
||||
func (c getNodesTest) String() string {
|
||||
return fmt.Sprintf("A: %064x\nT: %064x\n(%d)\n", c.Self[:], c.Target[:], c.N)
|
||||
}
|
||||
|
||||
func (*getNodesTest) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
t := &getNodesTest{
|
||||
Self: gen(Address{}, rand).(Address),
|
||||
Target: gen(Address{}, rand).(Address),
|
||||
N: rand.Intn(bucketSize),
|
||||
}
|
||||
for _, a := range gen([]Address{}, rand).([]Address) {
|
||||
t.All = append(t.All, &testNode{addr: a})
|
||||
}
|
||||
return reflect.ValueOf(t)
|
||||
}
|
||||
|
||||
func (*proxTest) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
var add bool
|
||||
if rand.Intn(1) == 0 {
|
||||
add = true
|
||||
}
|
||||
var t *proxTest
|
||||
if add {
|
||||
t = &proxTest{
|
||||
addr: gen(Address{}, rand).(Address),
|
||||
add: add,
|
||||
}
|
||||
} else {
|
||||
t = &proxTest{
|
||||
index: rand.Intn(len(addresses)),
|
||||
add: add,
|
||||
}
|
||||
}
|
||||
return reflect.ValueOf(t)
|
||||
}
|
||||
|
||||
func hasDuplicates(slice []Node) bool {
|
||||
seen := make(map[Address]bool)
|
||||
for _, node := range slice {
|
||||
if seen[node.Addr()] {
|
||||
return true
|
||||
}
|
||||
seen[node.Addr()] = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func contains(nodes []Node, addr Address) bool {
|
||||
for _, n := range nodes {
|
||||
if n.Addr() == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// gen wraps quick.Value so it's easier to use.
|
||||
// it generates a random value of the given value's type.
|
||||
func gen(typ interface{}, rand *rand.Rand) interface{} {
|
||||
v, ok := quick.Value(reflect.TypeOf(typ), rand)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("couldn't generate random value of type %T", typ))
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
func (Address) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
var id Address
|
||||
// m := rand.Intn(len(id))
|
||||
for i := 0; i < len(id); i++ {
|
||||
id[i] = byte(rand.Uint32())
|
||||
}
|
||||
return reflect.ValueOf(id)
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import (
|
|||
|
||||
"github.com/ethereum/ethash"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/bzz"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/compiler"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
|
|
@ -106,9 +107,11 @@ type Config struct {
|
|||
// If nil, an ephemeral key is used.
|
||||
NodeKey *ecdsa.PrivateKey
|
||||
|
||||
NAT nat.Interface
|
||||
Shh bool
|
||||
Dial bool
|
||||
NAT nat.Interface
|
||||
Shh bool
|
||||
Dial bool
|
||||
Bzz bool
|
||||
BzzPort string
|
||||
|
||||
Etherbase common.Address
|
||||
GasPrice *big.Int
|
||||
|
|
@ -222,6 +225,8 @@ type Ethereum struct {
|
|||
whisper *whisper.Whisper
|
||||
pow *ethash.Ethash
|
||||
protocolManager *ProtocolManager
|
||||
downloader *downloader.Downloader
|
||||
Swarm *bzz.Api
|
||||
SolcPath string
|
||||
solc *compiler.Solidity
|
||||
|
||||
|
|
@ -388,7 +393,25 @@ func New(config *Config) (*Ethereum, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protocols := append([]p2p.Protocol{}, eth.protocolManager.SubProtocols...)
|
||||
|
||||
if config.Bzz {
|
||||
eth.Swarm, err = bzz.NewApi(config.DataDir, config.BzzPort)
|
||||
if err != nil {
|
||||
glog.V(logger.Warn).Infof("[BZZ] error creating swarm: %v. Protocol skipped", err)
|
||||
} else {
|
||||
var proto p2p.Protocol
|
||||
proto, err = eth.Swarm.Bzz()
|
||||
if err != nil {
|
||||
glog.V(logger.Warn).Infof("[BZZ] error creating swarm: %v. Protocol skipped", err)
|
||||
eth.Swarm = nil
|
||||
} else {
|
||||
protocols = append(protocols, proto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.Shh {
|
||||
protocols = append(protocols, eth.whisper.Protocol())
|
||||
}
|
||||
|
|
@ -410,6 +433,8 @@ func New(config *Config) (*Ethereum, error) {
|
|||
eth.net.ListenAddr = ":" + config.Port
|
||||
}
|
||||
|
||||
eth.net.Protocols = protocols
|
||||
|
||||
vm.Debug = config.VmDebug
|
||||
|
||||
return eth, nil
|
||||
|
|
@ -537,6 +562,7 @@ func (s *Ethereum) Start() error {
|
|||
ClientString: s.net.Name,
|
||||
ProtocolVersion: s.EthVersion(),
|
||||
})
|
||||
|
||||
err := s.net.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -554,6 +580,15 @@ func (s *Ethereum) Start() error {
|
|||
s.whisper.Start()
|
||||
}
|
||||
|
||||
if s.Swarm != nil && s.net.Self() != nil {
|
||||
glog.V(logger.Info).Infof("net.self after net started: %v, listening on: %v", s.net.Self(), s.net.ListenAddr)
|
||||
listenAddr := s.net.ListenAddr
|
||||
if listenAddr == "" {
|
||||
listenAddr = ":0"
|
||||
}
|
||||
s.Swarm.Start(s.net.Self(), listenAddr, s.AddPeer)
|
||||
}
|
||||
|
||||
glog.V(logger.Info).Infoln("Server started")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -614,6 +649,11 @@ func (s *Ethereum) Stop() {
|
|||
}
|
||||
s.StopAutoDAG()
|
||||
|
||||
if s.Swarm != nil {
|
||||
s.Swarm.Stop()
|
||||
}
|
||||
|
||||
glog.V(logger.Info).Infoln("Server stopped")
|
||||
close(s.shutdownChan)
|
||||
}
|
||||
|
||||
|
|
|
|||