aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/faucet
diff options
context:
space:
mode:
authorPéter Szilágyi <peterke@gmail.com>2017-04-11 07:25:53 +0800
committerFelix Lange <fjl@users.noreply.github.com>2017-04-11 07:25:53 +0800
commit706a1e552c96bf75c60844c1dc28fc83778795fc (patch)
treebabbc6193bbdbde23f063a26544c630434047793 /cmd/faucet
parent18bbe124259a852b349e8238ffe394639e29d803 (diff)
downloaddexon-706a1e552c96bf75c60844c1dc28fc83778795fc.tar
dexon-706a1e552c96bf75c60844c1dc28fc83778795fc.tar.gz
dexon-706a1e552c96bf75c60844c1dc28fc83778795fc.tar.bz2
dexon-706a1e552c96bf75c60844c1dc28fc83778795fc.tar.lz
dexon-706a1e552c96bf75c60844c1dc28fc83778795fc.tar.xz
dexon-706a1e552c96bf75c60844c1dc28fc83778795fc.tar.zst
dexon-706a1e552c96bf75c60844c1dc28fc83778795fc.zip
cmd/puppeth: your Ethereum private network manager (#13854)
Diffstat (limited to 'cmd/faucet')
-rw-r--r--cmd/faucet/faucet.go456
-rw-r--r--cmd/faucet/faucet.html143
-rw-r--r--cmd/faucet/website.go235
3 files changed, 834 insertions, 0 deletions
diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go
new file mode 100644
index 000000000..232f0ff9e
--- /dev/null
+++ b/cmd/faucet/faucet.go
@@ -0,0 +1,456 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+// faucet is a Ether faucet backed by a light client.
+package main
+
+//go:generate go-bindata -nometadata -o website.go faucet.html
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "math/big"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/eth"
+ "github.com/ethereum/go-ethereum/ethclient"
+ "github.com/ethereum/go-ethereum/ethstats"
+ "github.com/ethereum/go-ethereum/les"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/node"
+ "github.com/ethereum/go-ethereum/p2p/discover"
+ "github.com/ethereum/go-ethereum/p2p/discv5"
+ "github.com/ethereum/go-ethereum/p2p/nat"
+ "github.com/ethereum/go-ethereum/params"
+ "golang.org/x/net/websocket"
+)
+
+var (
+ genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with")
+ apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection")
+ ethPortFlag = flag.Int("ethport", 30303, "Listener port for the devp2p connection")
+ bootFlag = flag.String("bootnodes", "", "Comma separated bootnode enode URLs to seed with")
+ netFlag = flag.Int("network", 0, "Network ID to use for the Ethereum protocol")
+ statsFlag = flag.String("ethstats", "", "Ethstats network monitoring auth string")
+
+ netnameFlag = flag.String("faucet.name", "", "Network name to assign to the faucet")
+ payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to pay out per user request")
+ minutesFlag = flag.Int("faucet.minutes", 1440, "Number of minutes to wait between funding rounds")
+
+ accJSONFlag = flag.String("account.json", "", "Key json file to fund user requests with")
+ accPassFlag = flag.String("account.pass", "", "Decryption password to access faucet funds")
+
+ githubUser = flag.String("github.user", "", "GitHub user to authenticate with for Gist access")
+ githubToken = flag.String("github.token", "", "GitHub personal token to access Gists with")
+
+ logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet")
+)
+
+var (
+ ether = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)
+)
+
+func main() {
+ // Parse the flags and set up the logger to print everything requested
+ flag.Parse()
+ log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*logFlag), log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
+
+ // Load up and render the faucet website
+ tmpl, err := Asset("faucet.html")
+ if err != nil {
+ log.Crit("Failed to load the faucet template", "err", err)
+ }
+ period := fmt.Sprintf("%d minute(s)", *minutesFlag)
+ if *minutesFlag%60 == 0 {
+ period = fmt.Sprintf("%d hour(s)", *minutesFlag/60)
+ }
+ website := new(bytes.Buffer)
+ template.Must(template.New("").Parse(string(tmpl))).Execute(website, map[string]interface{}{
+ "Network": *netnameFlag,
+ "Amount": *payoutFlag,
+ "Period": period,
+ })
+ // Load and parse the genesis block requested by the user
+ blob, err := ioutil.ReadFile(*genesisFlag)
+ if err != nil {
+ log.Crit("Failed to read genesis block contents", "genesis", *genesisFlag, "err", err)
+ }
+ genesis := new(core.Genesis)
+ if err = json.Unmarshal(blob, genesis); err != nil {
+ log.Crit("Failed to parse genesis block json", "err", err)
+ }
+ // Convert the bootnodes to internal enode representations
+ var enodes []*discv5.Node
+ for _, boot := range strings.Split(*bootFlag, ",") {
+ if url, err := discv5.ParseNode(boot); err == nil {
+ enodes = append(enodes, url)
+ } else {
+ log.Error("Failed to parse bootnode URL", "url", boot, "err", err)
+ }
+ }
+ // Load up the account key and decrypt its password
+ if blob, err = ioutil.ReadFile(*accPassFlag); err != nil {
+ log.Crit("Failed to read account password contents", "file", *accPassFlag, "err", err)
+ }
+ pass := string(blob)
+
+ ks := keystore.NewKeyStore(filepath.Join(os.Getenv("HOME"), ".faucet", "keys"), keystore.StandardScryptN, keystore.StandardScryptP)
+ if blob, err = ioutil.ReadFile(*accJSONFlag); err != nil {
+ log.Crit("Failed to read account key contents", "file", *accJSONFlag, "err", err)
+ }
+ acc, err := ks.Import(blob, pass, pass)
+ if err != nil {
+ log.Crit("Failed to import faucet signer account", "err", err)
+ }
+ ks.Unlock(acc, pass)
+
+ // Assemble and start the faucet light service
+ faucet, err := newFaucet(genesis, *ethPortFlag, enodes, *netFlag, *statsFlag, ks, website.Bytes())
+ if err != nil {
+ log.Crit("Failed to start faucet", "err", err)
+ }
+ defer faucet.close()
+
+ if err := faucet.listenAndServe(*apiPortFlag); err != nil {
+ log.Crit("Failed to launch faucet API", "err", err)
+ }
+}
+
+// request represents an accepted funding request.
+type request struct {
+ Username string `json:"username"` // GitHub user for displaying an avatar
+ Account common.Address `json:"account"` // Ethereum address being funded
+ Time time.Time `json:"time"` // Timestamp when te request was accepted
+ Tx *types.Transaction `json:"tx"` // Transaction funding the account
+}
+
+// faucet represents a crypto faucet backed by an Ethereum light client.
+type faucet struct {
+ config *params.ChainConfig // Chain configurations for signing
+ stack *node.Node // Ethereum protocol stack
+ client *ethclient.Client // Client connection to the Ethereum chain
+ index []byte // Index page to serve up on the web
+
+ keystore *keystore.KeyStore // Keystore containing the single signer
+ account accounts.Account // Account funding user faucet requests
+ nonce uint64 // Current pending nonce of the faucet
+ price *big.Int // Current gas price to issue funds with
+
+ conns []*websocket.Conn // Currently live websocket connections
+ history map[string]time.Time // History of users and their funding requests
+ reqs []*request // Currently pending funding requests
+ update chan struct{} // Channel to signal request updates
+
+ lock sync.RWMutex // Lock protecting the faucet's internals
+}
+
+func newFaucet(genesis *core.Genesis, port int, enodes []*discv5.Node, network int, stats string, ks *keystore.KeyStore, index []byte) (*faucet, error) {
+ // Assemble the raw devp2p protocol stack
+ stack, err := node.New(&node.Config{
+ Name: "geth",
+ Version: params.Version,
+ DataDir: filepath.Join(os.Getenv("HOME"), ".faucet"),
+ NAT: nat.Any(),
+ DiscoveryV5: true,
+ ListenAddr: fmt.Sprintf(":%d", port),
+ DiscoveryV5Addr: fmt.Sprintf(":%d", port+1),
+ MaxPeers: 25,
+ BootstrapNodesV5: enodes,
+ })
+ if err != nil {
+ return nil, err
+ }
+ // Assemble the Ethereum light client protocol
+ if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
+ return les.New(ctx, &eth.Config{
+ LightMode: true,
+ NetworkId: network,
+ Genesis: genesis,
+ GasPrice: big.NewInt(20 * params.Shannon),
+ GpoBlocks: 10,
+ GpoPercentile: 50,
+ EthashCacheDir: "ethash",
+ EthashCachesInMem: 2,
+ EthashCachesOnDisk: 3,
+ })
+ }); err != nil {
+ return nil, err
+ }
+ // Assemble the ethstats monitoring and reporting service'
+ if stats != "" {
+ if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
+ var serv *les.LightEthereum
+ ctx.Service(&serv)
+ return ethstats.New(stats, nil, serv)
+ }); err != nil {
+ return nil, err
+ }
+ }
+ // Boot up the client and ensure it connects to bootnodes
+ if err := stack.Start(); err != nil {
+ return nil, err
+ }
+ for _, boot := range enodes {
+ old, _ := discover.ParseNode(boot.String())
+ stack.Server().AddPeer(old)
+ }
+ // Attach to the client and retrieve and interesting metadatas
+ api, err := stack.Attach()
+ if err != nil {
+ stack.Stop()
+ return nil, err
+ }
+ client := ethclient.NewClient(api)
+
+ return &faucet{
+ config: genesis.Config,
+ stack: stack,
+ client: client,
+ index: index,
+ keystore: ks,
+ account: ks.Accounts()[0],
+ history: make(map[string]time.Time),
+ update: make(chan struct{}, 1),
+ }, nil
+}
+
+// close terminates the Ethereum connection and tears down the faucet.
+func (f *faucet) close() error {
+ return f.stack.Stop()
+}
+
+// listenAndServe registers the HTTP handlers for the faucet and boots it up
+// for service user funding requests.
+func (f *faucet) listenAndServe(port int) error {
+ go f.loop()
+
+ http.HandleFunc("/", f.webHandler)
+ http.Handle("/api", websocket.Handler(f.apiHandler))
+
+ return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
+}
+
+// webHandler handles all non-api requests, simply flattening and returning the
+// faucet website.
+func (f *faucet) webHandler(w http.ResponseWriter, r *http.Request) {
+ w.Write(f.index)
+}
+
+// apiHandler handles requests for Ether grants and transaction statuses.
+func (f *faucet) apiHandler(conn *websocket.Conn) {
+ // Start tracking the connection and drop at the end
+ f.lock.Lock()
+ f.conns = append(f.conns, conn)
+ f.lock.Unlock()
+
+ defer func() {
+ f.lock.Lock()
+ for i, c := range f.conns {
+ if c == conn {
+ f.conns = append(f.conns[:i], f.conns[i+1:]...)
+ break
+ }
+ }
+ f.lock.Unlock()
+ }()
+ // Send a few initial stats to the client
+ balance, _ := f.client.BalanceAt(context.Background(), f.account.Address, nil)
+ nonce, _ := f.client.NonceAt(context.Background(), f.account.Address, nil)
+
+ websocket.JSON.Send(conn, map[string]interface{}{
+ "funds": balance.Div(balance, ether),
+ "funded": nonce,
+ "peers": f.stack.Server().PeerCount(),
+ "requests": f.reqs,
+ })
+ header, _ := f.client.HeaderByNumber(context.Background(), nil)
+ websocket.JSON.Send(conn, header)
+
+ // Keep reading requests from the websocket until the connection breaks
+ for {
+ // Fetch the next funding request and validate against github
+ var msg struct {
+ URL string `json:"url"`
+ }
+ if err := websocket.JSON.Receive(conn, &msg); err != nil {
+ return
+ }
+ if !strings.HasPrefix(msg.URL, "https://gist.github.com/") {
+ websocket.JSON.Send(conn, map[string]string{"error": "URL doesn't link to GitHub Gists"})
+ continue
+ }
+ log.Info("Faucet funds requested", "gist", msg.URL)
+
+ // Retrieve the gist from the GitHub Gist APIs
+ parts := strings.Split(msg.URL, "/")
+ req, _ := http.NewRequest("GET", "https://api.github.com/gists/"+parts[len(parts)-1], nil)
+ if *githubUser != "" {
+ req.SetBasicAuth(*githubUser, *githubToken)
+ }
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ websocket.JSON.Send(conn, map[string]string{"error": err.Error()})
+ continue
+ }
+ var gist struct {
+ Owner struct {
+ Login string `json:"login"`
+ } `json:"owner"`
+ Files map[string]struct {
+ Content string `json:"content"`
+ } `json:"files"`
+ }
+ err = json.NewDecoder(res.Body).Decode(&gist)
+ res.Body.Close()
+ if err != nil {
+ websocket.JSON.Send(conn, map[string]string{"error": err.Error()})
+ continue
+ }
+ if gist.Owner.Login == "" {
+ websocket.JSON.Send(conn, map[string]string{"error": "Nice try ;)"})
+ continue
+ }
+ // Iterate over all the files and look for Ethereum addresses
+ var address common.Address
+ for _, file := range gist.Files {
+ if len(file.Content) == 2+common.AddressLength*2 {
+ address = common.HexToAddress(file.Content)
+ }
+ }
+ if address == (common.Address{}) {
+ websocket.JSON.Send(conn, map[string]string{"error": "No Ethereum address found to fund"})
+ continue
+ }
+ // Ensure the user didn't request funds too recently
+ f.lock.Lock()
+ var (
+ fund bool
+ elapsed time.Duration
+ )
+ if elapsed = time.Since(f.history[gist.Owner.Login]); elapsed > time.Duration(*minutesFlag)*time.Minute {
+ // User wasn't funded recently, create the funding transaction
+ tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether), big.NewInt(21000), f.price, nil)
+ signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainId)
+ if err != nil {
+ websocket.JSON.Send(conn, map[string]string{"error": err.Error()})
+ f.lock.Unlock()
+ continue
+ }
+ // Submit the transaction and mark as funded if successful
+ if err := f.client.SendTransaction(context.Background(), signed); err != nil {
+ websocket.JSON.Send(conn, map[string]string{"error": err.Error()})
+ f.lock.Unlock()
+ continue
+ }
+ f.reqs = append(f.reqs, &request{
+ Username: gist.Owner.Login,
+ Account: address,
+ Time: time.Now(),
+ Tx: signed,
+ })
+ f.history[gist.Owner.Login] = time.Now()
+ fund = true
+ }
+ f.lock.Unlock()
+
+ // Send an error if too frequent funding, othewise a success
+ if !fund {
+ websocket.JSON.Send(conn, map[string]string{"error": fmt.Sprintf("User already funded %s ago", common.PrettyDuration(elapsed))})
+ continue
+ }
+ websocket.JSON.Send(conn, map[string]string{"success": fmt.Sprintf("Funding request accepted for %s into %s", gist.Owner.Login, address.Hex())})
+ select {
+ case f.update <- struct{}{}:
+ default:
+ }
+ }
+}
+
+// loop keeps waiting for interesting events and pushes them out to connected
+// websockets.
+func (f *faucet) loop() {
+ // Wait for chain events and push them to clients
+ heads := make(chan *types.Header, 16)
+ sub, err := f.client.SubscribeNewHead(context.Background(), heads)
+ if err != nil {
+ log.Crit("Failed to subscribe to head events", "err", err)
+ }
+ defer sub.Unsubscribe()
+
+ for {
+ select {
+ case head := <-heads:
+ // New chain head arrived, query the current stats and stream to clients
+ balance, _ := f.client.BalanceAt(context.Background(), f.account.Address, nil)
+ balance = new(big.Int).Div(balance, ether)
+
+ price, _ := f.client.SuggestGasPrice(context.Background())
+ nonce, _ := f.client.NonceAt(context.Background(), f.account.Address, nil)
+
+ f.lock.Lock()
+ f.price, f.nonce = price, nonce
+ for len(f.reqs) > 0 && f.reqs[0].Tx.Nonce() < f.nonce {
+ f.reqs = f.reqs[1:]
+ }
+ f.lock.Unlock()
+
+ f.lock.RLock()
+ for _, conn := range f.conns {
+ if err := websocket.JSON.Send(conn, map[string]interface{}{
+ "funds": balance,
+ "funded": f.nonce,
+ "peers": f.stack.Server().PeerCount(),
+ "requests": f.reqs,
+ }); err != nil {
+ log.Warn("Failed to send stats to client", "err", err)
+ conn.Close()
+ continue
+ }
+ if err := websocket.JSON.Send(conn, head); err != nil {
+ log.Warn("Failed to send header to client", "err", err)
+ conn.Close()
+ }
+ }
+ f.lock.RUnlock()
+
+ case <-f.update:
+ // Pending requests updated, stream to clients
+ f.lock.RLock()
+ for _, conn := range f.conns {
+ if err := websocket.JSON.Send(conn, map[string]interface{}{"requests": f.reqs}); err != nil {
+ log.Warn("Failed to send requests to client", "err", err)
+ conn.Close()
+ }
+ }
+ f.lock.RUnlock()
+ }
+ }
+}
diff --git a/cmd/faucet/faucet.html b/cmd/faucet/faucet.html
new file mode 100644
index 000000000..570145ea2
--- /dev/null
+++ b/cmd/faucet/faucet.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <title>{{.Network}}: GitHub Faucet</title>
+
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
+
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-noty/2.4.1/packaged/jquery.noty.packaged.min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.0/moment.min.js"></script>
+
+ <style>
+ .vertical-center {
+ min-height: 100%;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ }
+ .progress {
+ position: relative;
+ }
+ .progress span {
+ position: absolute;
+ display: block;
+ width: 100%;
+ color: white;
+ }
+ pre {
+ padding: 6px;
+ margin: 0;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div class="vertical-center">
+ <div class="container">
+ <div class="row" style="margin-bottom: 16px;">
+ <div class="col-lg-12">
+ <h1 style="text-align: center;"><i class="fa fa-bath" aria-hidden="true"></i> {{.Network}} GitHub Authenticated Faucet <i class="fa fa-github-alt" aria-hidden="true"></i></h1>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-8 col-lg-offset-2">
+ <div class="input-group">
+ <input id="gist" type="text" class="form-control" placeholder="GitHub Gist URL containing your Ethereum address...">
+ <span class="input-group-btn">
+ <button class="btn btn-default" type="button" onclick="submit()">Give me Ether!</button>
+ </span>
+ </div>
+ </div>
+ </div>
+ <div class="row" style="margin-top: 32px;">
+ <div class="col-lg-6 col-lg-offset-3">
+ <div class="panel panel-small panel-default">
+ <div class="panel-body" style="padding: 0; overflow: auto; max-height: 300px;">
+ <table id="requests" class="table table-condensed" style="margin: 0;"></table>
+ </div>
+ <div class="panel-footer">
+ <table style="width: 100%"><tr>
+ <td style="text-align: center;"><i class="fa fa-rss" aria-hidden="true"></i> <span id="peers"></span> peers</td>
+ <td style="text-align: center;"><i class="fa fa-database" aria-hidden="true"></i> <span id="block"></span> blocks</td>
+ <td style="text-align: center;"><i class="fa fa-heartbeat" aria-hidden="true"></i> <span id="funds"></span> Ethers</td>
+ <td style="text-align: center;"><i class="fa fa-university" aria-hidden="true"></i> <span id="funded"></span> funded</td>
+ </tr></table>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row" style="margin-top: 32px;">
+ <div class="col-lg-12">
+ <h3>How does this work?</h3>
+ <p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to GitHub accounts. Anyone having a GitHub account may request funds within the permitted limit of <strong>{{.Amount}} Ether(s) / {{.Period}}</strong>.</p>
+ <p>To request funds, simply create a <a href="https://gist.github.com/" target="_about:blank">GitHub Gist</a> with your Ethereum address pasted into the contents (the file name doesn't matter), copy paste the gists URL into the above input box and fire away! You can track the current pending requests below the input field to see how much you have to wait until your turn comes.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <script>
+ // Global variables to hold the current status of the faucet
+ var attempt = 0;
+ var server;
+
+ // Define the function that submits a gist url to the server
+ var submit = function() {
+ server.send(JSON.stringify({url: $("#gist")[0].value}));
+ };
+ // Define a method to reconnect upon server loss
+ var reconnect = function() {
+ if (attempt % 2 == 0) {
+ server = new WebSocket("wss://" + location.host + "/api");
+ } else {
+ server = new WebSocket("ws://" + location.host + "/api");
+ }
+ attempt++;
+
+ server.onmessage = function(event) {
+ var msg = JSON.parse(event.data);
+ if (msg === null) {
+ return;
+ }
+
+ if (msg.funds !== undefined) {
+ $("#funds").text(msg.funds);
+ }
+ if (msg.funded !== undefined) {
+ $("#funded").text(msg.funded);
+ }
+ if (msg.peers !== undefined) {
+ $("#peers").text(msg.peers);
+ }
+ if (msg.number !== undefined) {
+ $("#block").text(parseInt(msg.number, 16));
+ }
+ if (msg.error !== undefined) {
+ noty({layout: 'topCenter', text: msg.error, type: 'error'});
+ }
+ if (msg.success !== undefined) {
+ noty({layout: 'topCenter', text: msg.success, type: 'success'});
+ }
+ if (msg.requests !== undefined && msg.requests !== null) {
+ var content = "";
+ for (var i=0; i<msg.requests.length; i++) {
+ content += "<tr><td><div style=\"background: url('https://github.com/" + msg.requests[i].username + ".png?size=64'); background-size: cover; width:32px; height: 32px; border-radius: 4px;\"></div></td><td><pre>" + msg.requests[i].account + "</pre></td><td style=\"width: 100%; text-align: center; vertical-align: middle;\">" + moment.duration(moment(msg.requests[i].time).unix()-moment().unix(), 'seconds').humanize(true) + "</td></tr>";
+ }
+ $("#requests").html("<tbody>" + content + "</tbody>");
+ }
+ }
+ server.onclose = function() { setTimeout(reconnect, 3000); };
+ server.onerror = function() { setTimeout(reconnect, 3000); };
+ }
+ // Establish a websocket connection to the API server
+ reconnect();
+ </script>
+ </body>
+</html>
diff --git a/cmd/faucet/website.go b/cmd/faucet/website.go
new file mode 100644
index 000000000..32650fec4
--- /dev/null
+++ b/cmd/faucet/website.go
@@ -0,0 +1,235 @@
+// Code generated by go-bindata.
+// sources:
+// faucet.html
+// DO NOT EDIT!
+
+package main
+
+import (
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func bindataRead(data []byte, name string) ([]byte, error) {
+ gz, err := gzip.NewReader(bytes.NewBuffer(data))
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+
+ var buf bytes.Buffer
+ _, err = io.Copy(&buf, gz)
+ clErr := gz.Close()
+
+ if err != nil {
+ return nil, fmt.Errorf("Read %q: %v", name, err)
+ }
+ if clErr != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+type asset struct {
+ bytes []byte
+ info os.FileInfo
+}
+
+type bindataFileInfo struct {
+ name string
+ size int64
+ mode os.FileMode
+ modTime time.Time
+}
+
+func (fi bindataFileInfo) Name() string {
+ return fi.name
+}
+func (fi bindataFileInfo) Size() int64 {
+ return fi.size
+}
+func (fi bindataFileInfo) Mode() os.FileMode {
+ return fi.mode
+}
+func (fi bindataFileInfo) ModTime() time.Time {
+ return fi.modTime
+}
+func (fi bindataFileInfo) IsDir() bool {
+ return false
+}
+func (fi bindataFileInfo) Sys() interface{} {
+ return nil
+}
+
+var _faucetHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x58\xe1\x72\xdb\x36\x12\xfe\x2d\x3f\xc5\x86\x77\xad\xa5\xb1\x49\xca\x71\x26\xed\xc8\xa4\x3a\x99\x36\x97\xf6\xe6\xa6\xed\x5c\xd3\xb9\xeb\xb4\x9d\x1b\x90\x5c\x8a\xb0\x41\x80\x05\x16\x92\xd5\x8c\xde\xfd\x06\x00\x45\x51\x8a\x9d\xa4\x97\xde\x1f\x5b\x00\x16\xdf\x7e\xd8\x5d\xec\x2e\x98\x3d\xf9\xea\xbb\x2f\x5f\xff\xf4\xfd\x4b\x68\xa8\x15\xcb\xb3\xcc\xfd\x03\xc1\xe4\x2a\x8f\x50\x46\xcb\xb3\x49\xd6\x20\xab\x96\x67\x93\x49\xd6\x22\x31\x28\x1b\xa6\x0d\x52\x1e\x59\xaa\xe3\xcf\xa3\xc3\x42\x43\xd4\xc5\xf8\x9b\xe5\xeb\x3c\xfa\x77\xfc\xe3\x8b\xf8\x4b\xd5\x76\x8c\x78\x21\x30\x82\x52\x49\x42\x49\x79\xf4\xcd\xcb\x1c\xab\x15\x8e\xf6\x49\xd6\x62\x1e\xad\x39\x6e\x3a\xa5\x69\x24\xba\xe1\x15\x35\x79\x85\x6b\x5e\x62\xec\x07\x97\xc0\x25\x27\xce\x44\x6c\x4a\x26\x30\xbf\x8a\x96\x67\x0e\x87\x38\x09\x5c\xbe\x79\x93\x7c\x8b\xb4\x51\xfa\x6e\xb7\x5b\xc0\x2b\x4e\x5f\xdb\x02\xfe\xc6\x6c\x89\x94\xa5\x41\xc4\x4b\x0b\x2e\xef\xa0\xd1\x58\xe7\x91\xe3\x6c\x16\x69\x5a\x56\xf2\xd6\x24\xa5\x50\xb6\xaa\x05\xd3\x98\x94\xaa\x4d\xd9\x2d\xbb\x4f\x05\x2f\x4c\x4a\x1b\x4e\x84\x3a\x2e\x94\x22\x43\x9a\x75\xe9\x75\x72\x9d\x7c\x96\x96\xc6\xa4\xc3\x5c\xd2\x72\x99\x94\xc6\x44\xa0\x51\xe4\x91\xa1\xad\x40\xd3\x20\x52\x04\xe9\xf2\x7f\xd3\x5b\x2b\x49\x31\xdb\xa0\x51\x2d\xa6\xcf\x92\xcf\x92\xb9\x57\x39\x9e\x7e\xb7\x56\xa7\xd6\x94\x9a\x77\x04\x46\x97\x1f\xac\xf7\xf6\x37\x8b\x7a\x9b\x5e\x27\x57\xc9\x55\x3f\xf0\x7a\x6e\x4d\xb4\xcc\xd2\x00\xb8\xfc\x28\xec\x58\x2a\xda\xa6\x4f\x93\x67\xc9\x55\xda\xb1\xf2\x8e\xad\xb0\xda\x6b\x72\x4b\xc9\x7e\xf2\x4f\xd3\xfb\x98\x0f\x6f\x4f\x5d\xf8\x67\x28\x6b\x55\x8b\x92\x92\x5b\x93\x3e\x4d\xae\x3e\x4f\xe6\xfb\x89\xb7\xf1\xbd\x02\xe7\x34\xa7\x6a\x92\xac\x51\x13\x2f\x99\x88\x4b\x94\x84\x1a\xde\xb8\xd9\x49\xcb\x65\xdc\x20\x5f\x35\xb4\x80\xab\xf9\xfc\x93\x9b\x87\x66\xd7\x4d\x98\xae\xb8\xe9\x04\xdb\x2e\xa0\x16\x78\x1f\xa6\x98\xe0\x2b\x19\x73\xc2\xd6\x2c\x20\x20\xfb\x85\x9d\xd7\xd9\x69\xb5\xd2\x68\x4c\xaf\xac\x53\x86\x13\x57\x72\xe1\x22\x8a\x11\x5f\xe3\x43\xb2\xa6\x63\xf2\xad\x0d\xac\x30\x4a\x58\xc2\x13\x22\x85\x50\xe5\x5d\x98\xf3\xd7\x78\x7c\x88\x52\x09\xa5\x17\xb0\x69\x78\xbf\x0d\xbc\x22\xe8\x34\xf6\xf0\xd0\xb1\xaa\xe2\x72\xb5\x80\xe7\x5d\x7f\x1e\x68\x99\x5e\x71\xb9\x80\xf9\x61\x4b\x96\xee\xcd\x98\xa5\x21\x63\x9d\x4d\xb2\x42\x55\x5b\xef\xc3\x8a\xaf\xa1\x14\xcc\x98\x3c\x3a\x31\xb1\xcf\x44\x47\x02\x2e\x01\x31\x2e\xf7\x4b\x47\x6b\x5a\x6d\x22\xf0\x8a\xf2\x28\x90\x88\x0b\x45\xa4\xda\x05\x5c\x39\x7a\xfd\x96\x13\x3c\x11\x8b\x55\x7c\xf5\x74\xbf\x38\xc9\x9a\xab\x3d\x08\xe1\x3d\xc5\xde\x3f\x83\x67\xa2\x65\xc6\xf7\x7b\x6b\x06\x35\x8b\x0b\x46\x4d\x04\x4c\x73\x16\x37\xbc\xaa\x50\xe6\x11\x69\x8b\x2e\x8e\xf8\x12\xc6\x79\x6f\x9f\xf6\x5e\x58\x6a\x50\xba\x73\x12\x56\x7d\x12\x84\x53\xd8\x15\xa7\xc6\x16\x31\x13\xf4\x28\x78\x96\x36\x57\xfb\x23\xa5\x15\x5f\xf7\x16\x19\xfd\x3c\x31\xce\xe3\xe7\xff\x1c\xfa\x1f\xaa\xae\x0d\x52\x3c\x32\xc7\x48\x98\xcb\xce\x52\xbc\xd2\xca\x76\xc3\xfa\x24\xf3\xb3\xc0\xab\x3c\x5a\x71\x43\x11\xd0\xb6\xeb\x6d\x17\x0d\x47\x52\xba\x8d\x9d\xeb\xb4\x12\x11\x74\x82\x95\xd8\x28\x51\xa1\xce\xa3\xde\x26\xaf\xb8\x21\xf8\xf1\x9f\xff\x80\xde\xc1\x5c\xae\x60\xab\xac\x86\x97\xd4\xa0\x46\xdb\x02\xab\x2a\x17\xdc\x49\x92\x8c\x74\xfb\x48\x7f\x9b\x5d\x5c\x90\x3c\x48\x4d\xb2\xc2\x12\xa9\x41\xb0\x20\x09\x05\xc9\xb8\xc2\x9a\x59\x31\x30\x0e\x42\x11\x28\x59\x0a\x5e\xde\xe5\x91\xb1\x45\xcb\x69\x3a\x8b\x96\xaf\xf8\x1a\xa1\xc5\x40\xe6\x49\x96\x06\xd1\x03\x8d\xd4\xf1\x18\x2c\x76\x70\xc0\x07\xfa\xe5\x24\x68\x49\x75\x0b\xb8\x7e\x3a\x8a\xd8\x87\x5c\xf6\xfc\xc4\x65\xd7\x0f\x0a\x77\x4c\xa2\x00\xff\x37\x36\x2d\x13\xfb\xdf\xfb\xb3\x1f\xce\x70\xba\x29\x76\xf7\x73\xa0\x36\xdc\xf3\xf9\x0d\xa8\x35\xea\x5a\xa8\xcd\x02\x98\x25\x75\x03\x2d\xbb\x1f\x72\xdd\xf5\x7c\x3e\xe6\xed\xea\x3f\x2b\x04\xfa\xf0\xd0\xf8\x9b\x45\x43\x66\x08\x8b\xb0\xe4\xff\xba\xe8\xa8\x50\x1a\xac\x4e\xac\xe1\x34\xba\x70\xf7\x52\x23\x8b\x1f\x6c\xfc\x10\xf7\x5a\xa9\x21\x7d\x8c\x69\xf4\xd0\xa3\x4c\x17\x2d\x33\xd2\x07\xb9\x49\x46\xd5\x1f\xba\xfe\xda\x95\xf7\xc7\x6e\x7f\x88\x4f\x77\xf6\x0e\x51\x87\xda\xe2\x22\x05\xfc\x30\x4b\xa9\xfa\x08\xcd\x15\x23\x56\x30\x83\x1f\xa2\xde\x67\xf9\x83\x7a\x3f\xfc\x58\xfd\x0d\x32\x4d\x05\xb2\xc7\x13\xd4\x88\x40\x6d\x65\x35\x3a\xbf\xbf\x48\x1f\x4b\xc0\x4a\xbe\x46\x6d\x38\x6d\x3f\x94\x01\x56\x07\x0a\x61\x7c\x4c\x21\x4b\x49\xbf\x3b\xd6\xfe\x0f\x97\xfb\x7d\xe5\xe8\x7a\xf9\xb5\xda\x40\xa5\xd0\x00\x35\xdc\x80\x2b\x26\x5f\x64\x69\x73\x3d\x88\x74\xcb\xd7\x6e\xc1\x1b\x15\xea\x50\x4f\xb8\x01\x6d\xa5\xcf\xa3\x4a\x02\x35\x78\x5c\x8a\x64\xf8\x95\xc0\x6b\xe5\xca\xf9\x1a\x25\x41\xcb\x04\x2f\xb9\xb2\x06\x58\x49\x4a\x1b\xa8\xb5\x6a\x01\xef\x1b\x66\x0d\x39\x20\x97\x3e\xd8\x9a\x71\xe1\xef\x92\x77\x29\x28\x0d\xac\x2c\x6d\x6b\x5d\x3b\x22\x57\x80\x52\xd9\x55\xd3\x73\x21\x05\xad\xb2\x92\x40\x28\xb9\x1a\xf8\x98\x8e\xb5\xc0\x88\x58\x79\x67\x2e\x61\x9f\x15\x80\x69\x04\xe2\x58\xb9\x5d\x7d\x55\x60\x65\xe9\xb6\x9b\x04\x5e\xc8\xad\x92\x08\x0d\x5b\x7b\x22\x27\x02\xd0\xb2\xed\x1e\xa8\xe7\xb5\xe1\xd4\xf0\x70\xf0\x0e\x75\xeb\xfa\xcb\x0a\x04\x6f\x39\x81\xaa\x21\x33\xa4\x95\x5c\xb9\x67\xc9\x0b\xcf\x70\xb7\x0b\x94\xa7\x66\x06\xa9\x33\xd5\xf7\xa8\xb9\xaa\x76\x3b\xd7\xba\x78\xd1\x24\x4b\xbb\xb1\xc5\xd5\xb1\xc2\x4b\x30\xbc\xed\xc4\x16\x4a\x8d\x8c\x10\x18\x64\xec\xe4\x41\xe1\xca\x63\x12\xea\xba\x6f\x49\x23\x20\xa6\x57\xee\xb9\xf6\x1f\x56\x28\x4b\x8b\x42\x30\x79\xe7\xaa\xcd\x50\x12\xb3\x94\x2d\xfd\x51\x1e\x2e\x86\xd0\x31\xe3\xce\xc5\x25\x29\x7f\xd4\xfe\x7d\x66\x60\xea\x46\x35\x17\xe8\x9f\x70\x3e\x7a\xe4\xb9\xb3\x93\xeb\xb3\x67\x97\x50\xaa\x6e\x1b\x76\xfb\x7d\x8e\x9a\xf1\xf5\x77\x80\x62\x85\x5a\x23\x84\xe2\x5e\xa8\x7b\x60\xb2\x82\x9a\x6b\x04\xb6\x61\xdb\x27\xf0\x93\xb2\x50\x32\x09\xa4\x59\x79\x17\x74\x5b\xad\x5d\x18\x75\x28\x5d\xa9\x38\x38\xb6\x40\xa1\x36\x5e\x24\xa0\xd5\x1c\x85\xf7\xb2\x41\x84\x46\x6d\xa0\xb5\xa5\x3f\xa0\x73\x2f\xba\x85\x0d\xe3\x04\x56\x12\x17\xe1\xdc\x64\xb5\x84\x52\xb5\x68\x46\x5e\x78\xf0\xfa\x0d\xbf\xfa\x1f\x87\x37\x82\x5f\x4e\x53\x78\x25\x54\xc1\x04\xac\x5d\xc6\x28\x84\xbb\x54\x0a\x5c\x33\x72\x74\x06\x43\x8c\xac\x71\x91\xe2\xed\xe8\xaf\x94\xdb\xbf\x66\xda\x45\x2e\xb6\x1d\x41\xde\x77\xb8\x6e\xce\xa0\x5e\xbb\xbe\xbd\xd7\xf1\x15\xd6\x5c\x06\xcb\xd6\x56\x96\xae\x01\x07\x6a\x18\x41\x68\x29\x0c\x30\x6f\x71\xb0\x5a\x40\x6f\xee\x80\x30\xe0\x79\x39\xc8\x87\xed\xd3\x59\xdf\x71\x07\xb9\xc4\xa0\xac\xa6\x7f\xff\xe1\xbb\x6f\x13\x43\x9a\xcb\x15\xaf\xb7\xd3\x37\x56\x8b\x05\xfc\x75\x1a\xfd\xc5\x37\x62\xb3\x9f\xe7\xbf\x26\x6b\x26\x2c\xee\x66\xb3\xf0\x4c\xb8\x39\xe6\xc7\xa0\x45\x6a\x94\xf7\x85\xc6\x52\x49\x89\x25\x81\xed\x94\xec\xe9\x80\x50\xc6\xec\x39\x1d\x24\x1e\xa0\xc5\x6b\x98\xee\x0d\xf3\x09\x3c\x85\x3c\x87\xf9\x7e\xad\xe7\x0c\x39\x48\xdc\xc0\xbf\xb0\xf8\x41\x95\x77\x48\xd3\x68\x63\xdc\xb5\x88\xe0\x02\x84\x2a\x99\xc3\x4b\x1a\x65\x08\x2e\x20\x4a\x59\xc7\xa3\xc0\x7a\xb2\x03\x14\x06\xdf\x0f\xf6\x41\x58\xe1\xcd\x15\x98\x5e\x5c\x04\x8f\xed\x8d\xaa\x64\x8b\xc6\xb0\x15\x8e\x4f\xe8\x73\xe3\x70\x14\x67\x88\xd6\xac\x20\x07\x6f\xfc\x8e\x69\x83\x41\x24\x71\xf5\xb8\xd7\xe2\xcd\xe1\xc5\xf2\x1c\xa4\x15\x62\xd8\x3f\xd1\xe8\x82\xb9\x17\xdb\x9d\x1d\x89\x27\x21\x75\x3d\xc9\x73\x70\xc5\xc9\xf9\xa8\x3a\xec\x74\x8e\x0d\x65\x74\x96\xb8\xfa\x78\xd8\x31\x1b\xe0\xde\x42\xc3\xea\x7d\x70\x58\x9d\xe2\x61\xf5\x08\xa0\xef\x5a\xde\x85\x17\xba\x9c\x11\x9c\x9f\x78\x04\x4d\xda\xb6\x40\xfd\x2e\xb8\xd0\xb5\xf4\x70\xde\xd4\xdf\x48\x1a\xed\xbd\x84\xab\xe7\xb3\x47\xd0\x51\x6b\xf5\x28\xb8\x54\xb4\x9d\xbe\x11\x6c\xeb\xb2\x2e\x9c\x93\xea\xbe\xf4\x4d\xc6\xf9\x25\x38\x5d\x0b\x18\x10\x2e\xfd\xe3\x60\x01\xe7\x7e\x74\xbe\x7b\x44\x9b\xb1\x65\xe9\xf2\xf1\xc7\xe8\xeb\x31\x06\x8d\xfd\xf8\x51\x9d\x43\x7e\x3d\x52\x0a\x9f\x7e\x0a\x6f\xad\x1e\x87\xa0\x8b\xe1\xbe\x50\x40\x0e\x51\xd4\xc3\x4f\x6a\xa5\x61\xea\x16\x79\x3e\xbf\x01\x9e\x8d\x61\x12\x81\x72\x45\xcd\x0d\xf0\x8b\x8b\x03\xd2\x64\x0f\x73\x91\x43\xe4\xfa\xe8\x8c\xaa\xa5\xef\x67\x42\xd3\xf3\x4b\x54\xb0\xf2\xce\x3d\xc9\x64\xb5\x70\xd9\x6e\x7a\x7e\x28\x86\xa3\x3a\x78\x71\x44\xf9\x67\xfe\x6b\x62\x0d\x6a\x5f\xb9\x2e\x20\x4a\x3a\xb9\xfa\xc2\xf0\xdf\x31\x7f\xfe\xec\x7c\x76\x03\x07\xcc\xd8\xcd\x2e\xa0\x74\x2f\x92\x1b\x08\x5d\xbd\xef\xad\x60\x78\x8f\xf8\x51\xa1\x74\x85\x3a\xd6\xac\xe2\xd6\x2c\xe0\x59\x77\x7f\xf3\x8b\xeb\x04\x5d\x89\xf0\x1d\xa0\xe7\xdd\x69\x5c\x3e\xc4\x65\xdf\x64\x5c\x40\x94\xa5\x4e\x68\xbf\x65\x38\xe5\xf8\xcb\x09\x3c\xd0\xbb\xc2\xf0\x5d\xa3\x9f\x6f\x79\x55\x09\x74\x24\xbc\xc2\xf0\x01\xaa\xb2\xda\x27\xae\x69\x18\x4f\x4f\x79\x10\x6f\x71\x96\x58\xc9\xef\xa7\xb3\xb8\x97\xd9\x8f\x2f\xe1\xdc\xb8\xfc\x5c\x99\xf3\x59\xd2\xd8\x96\x49\xfe\x3b\x4e\x5d\x23\x3c\x0b\xbc\x1d\x63\xd7\xdd\x0e\xde\xde\x8d\x2e\xda\xf0\x32\x9b\x25\x0d\xb5\x62\x1a\x65\xe4\xbf\xce\x38\x72\x83\x8b\x3d\x4a\x98\x3e\x8e\xc8\xdd\x71\x0e\x2d\x85\x32\x78\x52\x23\xc0\x20\xbd\xe6\x2d\x2a\x4b\xd3\xa1\x8e\x5c\xba\xd7\xe2\x7c\x76\x03\xa1\x2e\x1d\x10\xc2\xdd\xfd\xe3\x08\xbb\xbe\xbc\xbd\x34\xae\x83\xe7\xa6\x01\x06\x1b\x2c\x8c\xaf\x10\xd0\xef\xf1\xb5\x38\xd4\xdc\x17\xdf\x7f\x33\xaa\xbb\x03\xea\xd4\x1f\x6f\xf4\x99\x31\x4b\xc3\xb7\xaa\x2c\x0d\xdf\xe1\xff\x1b\x00\x00\xff\xff\x97\x3f\x1d\xc4\x98\x17\x00\x00")
+
+func faucetHtmlBytes() ([]byte, error) {
+ return bindataRead(
+ _faucetHtml,
+ "faucet.html",
+ )
+}
+
+func faucetHtml() (*asset, error) {
+ bytes, err := faucetHtmlBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "faucet.html", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+// Asset loads and returns the asset for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func Asset(name string) ([]byte, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
+ }
+ return a.bytes, nil
+ }
+ return nil, fmt.Errorf("Asset %s not found", name)
+}
+
+// MustAsset is like Asset but panics when Asset would return an error.
+// It simplifies safe initialization of global variables.
+func MustAsset(name string) []byte {
+ a, err := Asset(name)
+ if err != nil {
+ panic("asset: Asset(" + name + "): " + err.Error())
+ }
+
+ return a
+}
+
+// AssetInfo loads and returns the asset info for the given name.
+// It returns an error if the asset could not be found or
+// could not be loaded.
+func AssetInfo(name string) (os.FileInfo, error) {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ if f, ok := _bindata[cannonicalName]; ok {
+ a, err := f()
+ if err != nil {
+ return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
+ }
+ return a.info, nil
+ }
+ return nil, fmt.Errorf("AssetInfo %s not found", name)
+}
+
+// AssetNames returns the names of the assets.
+func AssetNames() []string {
+ names := make([]string, 0, len(_bindata))
+ for name := range _bindata {
+ names = append(names, name)
+ }
+ return names
+}
+
+// _bindata is a table, holding each asset generator, mapped to its name.
+var _bindata = map[string]func() (*asset, error){
+ "faucet.html": faucetHtml,
+}
+
+// AssetDir returns the file names below a certain
+// directory embedded in the file by go-bindata.
+// For example if you run go-bindata on data/... and data contains the
+// following hierarchy:
+// data/
+// foo.txt
+// img/
+// a.png
+// b.png
+// then AssetDir("data") would return []string{"foo.txt", "img"}
+// AssetDir("data/img") would return []string{"a.png", "b.png"}
+// AssetDir("foo.txt") and AssetDir("notexist") would return an error
+// AssetDir("") will return []string{"data"}.
+func AssetDir(name string) ([]string, error) {
+ node := _bintree
+ if len(name) != 0 {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ pathList := strings.Split(cannonicalName, "/")
+ for _, p := range pathList {
+ node = node.Children[p]
+ if node == nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ }
+ }
+ if node.Func != nil {
+ return nil, fmt.Errorf("Asset %s not found", name)
+ }
+ rv := make([]string, 0, len(node.Children))
+ for childName := range node.Children {
+ rv = append(rv, childName)
+ }
+ return rv, nil
+}
+
+type bintree struct {
+ Func func() (*asset, error)
+ Children map[string]*bintree
+}
+var _bintree = &bintree{nil, map[string]*bintree{
+ "faucet.html": &bintree{faucetHtml, map[string]*bintree{}},
+}}
+
+// RestoreAsset restores an asset under the given directory
+func RestoreAsset(dir, name string) error {
+ data, err := Asset(name)
+ if err != nil {
+ return err
+ }
+ info, err := AssetInfo(name)
+ if err != nil {
+ return err
+ }
+ err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
+ if err != nil {
+ return err
+ }
+ err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// RestoreAssets restores an asset under the given directory recursively
+func RestoreAssets(dir, name string) error {
+ children, err := AssetDir(name)
+ // File
+ if err != nil {
+ return RestoreAsset(dir, name)
+ }
+ // Dir
+ for _, child := range children {
+ err = RestoreAssets(dir, filepath.Join(name, child))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func _filePath(dir, name string) string {
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
+}
+