Repository: donkeysharp/gocho Branch: master Commit: 90af18ba15da Files: 87 Total size: 547.4 KB Directory structure: gitextract_kbx_5a4t/ ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets/ │ ├── .gitignore │ └── assets.go ├── cmd/ │ └── gocho/ │ └── gocho.go ├── docs/ │ └── building.md ├── pkg/ │ ├── cmds/ │ │ └── cmds.go │ ├── config/ │ │ ├── config.go │ │ ├── utils.go │ │ └── wizard.go │ ├── info/ │ │ └── info.go │ └── node/ │ ├── dashboard.go │ ├── index.go │ ├── net.go │ ├── node.go │ ├── packet.go │ ├── serve.go │ └── utils.go ├── ui/ │ ├── .gitignore │ ├── package.json │ ├── public/ │ │ ├── index.html │ │ └── lang/ │ │ ├── en.json │ │ └── es.json │ └── src/ │ ├── App.css │ ├── App.js │ ├── components/ │ │ ├── FormField.js │ │ ├── NodeDetails.js │ │ ├── NodeList.js │ │ ├── Panel.js │ │ └── SideBar.js │ ├── containers/ │ │ ├── Discover.js │ │ └── NodeInfo.js │ ├── i18n.js │ ├── index.css │ └── index.js └── vendor/ ├── github.com/ │ ├── Pallinder/ │ │ └── go-randomdata/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── fullprofile.go │ │ ├── jsondata.go │ │ ├── postalcodes.go │ │ └── random_data.go │ ├── elazarl/ │ │ └── go-bindata-assetfs/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── assetfs.go │ │ └── doc.go │ ├── mitchellh/ │ │ └── go-homedir/ │ │ ├── LICENSE │ │ ├── README.md │ │ └── homedir.go │ └── urfave/ │ └── cli/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── app.go │ ├── appveyor.yml │ ├── category.go │ ├── cli.go │ ├── command.go │ ├── context.go │ ├── errors.go │ ├── flag-types.json │ ├── flag.go │ ├── flag_generated.go │ ├── funcs.go │ ├── generate-flag-types │ ├── help.go │ ├── runtests │ └── sort.go ├── gopkg.in/ │ └── yaml.v2/ │ ├── LICENSE │ ├── LICENSE.libyaml │ ├── README.md │ ├── apic.go │ ├── decode.go │ ├── emitterc.go │ ├── encode.go │ ├── parserc.go │ ├── readerc.go │ ├── resolve.go │ ├── scannerc.go │ ├── sorter.go │ ├── writerc.go │ ├── yaml.go │ ├── yamlh.go │ └── yamlprivateh.go └── vendor.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ dist/ build/ ================================================ FILE: .travis.yml ================================================ language: go install: true sudo: required before_install: - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - sudo apt-get update - sudo apt-get install apt-transport-https -y - curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash - - sudo apt-get install yarn -y -qq cache: yarn: true go: "1.10" script: - go get -u -v github.com/jteeuwen/go-bindata/... - cd ui - yarn install - cd .. - make dist ================================================ FILE: Dockerfile ================================================ FROM debian:latest COPY ./dist/gocho /usr/local/bin/gocho RUN chmod +x /usr/local/bin/gocho \ && mkdir -p /root/public \ && echo 'file1' > /root/public/file1 \ && echo 'file2' > /root/public/file2 \ && echo 'file3' > /root/public/file3 \ && echo 'file4' > /root/public/file4 \ && echo 'file5' > /root/public/file5 \ && echo 'NodeId: root' > /root/.gocho.conf \ && echo 'WebPort: "5555"' >> /root/.gocho.conf \ && echo 'LocalPort: "1337"' >> /root/.gocho.conf \ && echo 'ShareDirectory: "/root/public"' >> /root/.gocho.conf CMD ["/usr/local/bin/gocho", "start"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Sergio Guillen Mantilla Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ VERSION = 0.2.0 GOPATH := $(PWD)/build:$(GOPATH) build-dev: @echo "Building gocho" rm -rf build && mkdir -p build/src/github.com/donkeysharp ln -s $(PWD) $(PWD)/build/src/github.com/donkeysharp/gocho go install -i github.com/donkeysharp/gocho/cmd/gocho clean: rm -rf dist/* rm -rf build dist: clean ui generate @echo "Building gocho for Linux x86_64..." mkdir -p dist/linux64 go build -o dist/linux64/gocho cmd/gocho/gocho.go @zip -j dist/gocho_${VERSION}_linux64.zip dist/linux64/gocho generate: go generate cmd/gocho/gocho.go dist-linux32: @echo "Building gocho for Linux 32bits..." mkdir -p dist/linux386 GOOS=linux GOARCH=386 go build -o dist/linux386/gocho cmd/gocho/gocho.go @zip -j dist/gocho_${VERSION}_linux386.zip dist/linux386/gocho dist-win32: @echo "Building gocho for Windows 32bits..." mkdir -p dist/win32 GOOS=windows GOARCH=386 go build -o dist/win32/gocho.exe cmd/gocho/gocho.go @zip -j dist/gocho_${VERSION}_win32.zip dist/win32/gocho.exe dist-win64: @echo "Building gocho for Windows 64bits..." mkdir -p dist/win64 GOOS=windows GOARCH=amd64 go build -o dist/win64/gocho.exe cmd/gocho/gocho.go @zip -j dist/gocho_${VERSION}_win64.zip dist/win64/gocho.exe dist-darwin: @echo "Building gocho for Darwin 64bits..." mkdir -p dist/darwin GOOS=darwin GOARCH=amd64 go build -o dist/darwin/gocho cmd/gocho/gocho.go @zip -j dist/gocho_${VERSION}_darwin.zip dist/darwin/gocho docker: dist docker build . -t donkeysharp/gocho start: docker run -it -p "1337:1337" --rm donkeysharp/gocho gocho start --debug || true test: docker run -it --rm donkeysharp/gocho || true clean-dashboard: rm -rf assets/assets_gen.go ui: clean-dashboard cd ui \ && yarn build ================================================ FILE: README.md ================================================ Gocho - Local Network File Sharing [![Build Status](https://travis-ci.org/donkeysharp/gocho.svg?branch=master)](https://travis-ci.org/donkeysharp/gocho) ================================== Gocho allows you to share a chosen directory with others on the same local network, without the need to setup Samba or OS-oriented settings. It provides a local dashboard which you can access through your browser, to discover what others are sharing without knowing other's IP addresses. Run Gocho, browse to [localhost:1337](http://localhost:1337) and see what others are sharing! ![alt Gocho dashboard](docs/gocho-dashboard.gif) > **Building The Project:** > > If you want to help and contribute don't forget to check the [Building document](docs/building.md) in order to have your environment ready. ## Install [Download the latest release](https://github.com/donkeysharp/gocho/releases) for your operating system. Currently the following operating systems are being supported: * GNU/Linux 32 bits * GNU/Linux 64 bits * OSX * Windows 32 bits * Windows 64 bits Download, unzip the file and add it to your path or a directory that is already in your system's path. **Example unix-like** $ unzip gocho_0.1.0_darwin.zip $ mv gocho /usr/bin $ gocho --help ## Instructions Gocho needs to be executed using the command line in order to initiate the sharing. There are two ways to start sharing: 1. Specify the settings on a config file 2. Specify the settings using command line flags. ### Specify a settings file Gocho reads a settings file that is located at `$USER_HOME/.gocho.conf`. The format of the file is as follows: ``` NodeId: my-computer WebPort: "5555" LocalPort: "1337" ShareDirectory: /home/user/some/directory ``` If you want Gocho to create this file for you, it's possible to run the configuration wizard by running: $ gocho configure Which will ask for the different settings and create a `.gocho.conf` file. Once settings file is created, run the next to start sharing: $ gocho start ![alt Gocho wizard](docs/gocho-configure.gif) ### Use command line flags If you don't want to specify a configuration file, or want to share a directory which is not specified on the `.gocho.conf` file, run: $ gocho start --dir /some/directory --id my-computer-tmp This is the list of available flags Flag | Description --- | --- --id {value} | Node ID that will be shared to other peers (**Required**) --dir {value} | Directory to share (**Required**) --share-port {value} | Port that will be exposed for file sharing (default: "5555") --local-port {value} | Port for local dashboard (default: "1337") ![alt Gocho flags](docs/gocho-start.gif) ## License Licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. ================================================ FILE: assets/.gitignore ================================================ assets_gen.go ================================================ FILE: assets/assets.go ================================================ package assets import ( "github.com/elazarl/go-bindata-assetfs" ) func AssetFS() *assetfs.AssetFS { return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "../../ui/build"} } ================================================ FILE: cmd/gocho/gocho.go ================================================ package main //go:generate go-bindata -o ../../assets/assets_gen.go -pkg assets ../../ui/build/... import ( "github.com/donkeysharp/gocho/pkg/cmds" "os" ) func main() { cmds.New().Run(os.Args) } ================================================ FILE: docs/building.md ================================================ Building Instructions ===================== ## Requirements You need these tools installed in order to develop Gocho: * Yarn (ui) * NodeJS (ui) * GNU Make (for general process building) * Go (for main code compilation) * go-bindata (for embedding UI inside final binary). > *Installing go-bindata creates a binary, don't forget to add $GOPATH/bin to your path* It's important that go-bindata is in the path becuase `make generate` —the command that embeds ui code into binary— needs it. ## Service Development In order to run Gocho service and start development on it there are some things to consider: the next steps need to be run only the first time if you don't want to modify the UI. This steps need to be executed in the root directory of the project. #### Step 1 Build the UI files e.g. html, javascript and css. $ make ui It will create a `ui/build` directory with the resulting files for the UI #### Step 2 As UI files are embedded inside the final binary, we use [go-binddata](https://github.com/jteeuwen/go-bindata) to achieve this. The `generate` command in the Makefile creates an `assets/assets_gen.go` file with the embedded UI code. $ make generate So far the previous steps need to be run only the first time unless you are modifying UI, in that case check the `UI Development` section. To build `gocho` binary and test it while you do changes run: $ make build-dev Which will create the `gocho` binary at `$GOPATH/bin/gocho` as that command runs `go install github.com/donkeysharp/gocho/cmd/gocho` ## UI Development Gocho UI uses React and was intialized using [Create React App](https://github.com/facebook/create-react-app). All React code for the dashboard is located in the `ui` directory. This directory has the package.json and the yarn.lock file, in order to develop and test the UI you need to run the next $ cd ui # Install UI dependencies $ yarn install # Start development server $ yarn start That will bring up a development server at http://localhost:3000 with the UI so it can be developed. The development server for UI is configured to use a proxy to `http://localhost:1337` (see `package.json`) so it's important to have a backend running, check `Service Development` section. ================================================ FILE: pkg/cmds/cmds.go ================================================ package cmds import ( "fmt" "github.com/donkeysharp/gocho/pkg/config" "github.com/donkeysharp/gocho/pkg/info" "github.com/donkeysharp/gocho/pkg/node" "github.com/urfave/cli" ) func ConfigureAction(c *cli.Context) error { err := config.ConfigureWizard() if err != nil { return cli.NewExitError(err, 1) } return nil } func StartAction(c *cli.Context) error { fmt.Println("Starting Gocho Node...") conf := &config.Config{} conf.Debug = c.Bool("debug") conf.LocalPort = c.String("local-port") conf.WebPort = c.String("share-port") conf.ShareDirectory = c.String("dir") conf.NodeId = c.String("id") if conf.NodeId == "" || conf.ShareDirectory == "" { fmt.Println("Both --dir and --id should be set.") fmt.Println("Checking config file.") var err error conf, err = config.LoadConfig() if err != nil { return cli.NewExitError(err, 1) } } fmt.Println("Configuration loaded") fmt.Println("---") fmt.Println(conf) fmt.Println("---") node.Serve(conf) return nil } func New() *cli.App { app := cli.NewApp() app.Name = info.APP_NAME app.Usage = "Auto-discovery local area network file sharing" app.Version = info.VERSION app.Authors = []cli.Author{ cli.Author{ Name: "Sergio Guillen Mantilla", Email: "serguimant@gmail.com", }, } app.Commands = []cli.Command{ { Name: "start", Usage: "Start Gocho node", Flags: []cli.Flag{ cli.BoolFlag{ Name: "debug", Usage: "Start gocho in debug mode", }, cli.StringFlag{ Name: "id", Usage: "Node ID that will be shared to other peers", EnvVar: "GOCHO_ID", }, cli.StringFlag{ Name: "dir", Usage: "Directory to share", EnvVar: "GOCHO_DIR", }, cli.StringFlag{ Name: "share-port", Usage: "Port that will be exposed for file sharing", EnvVar: "GOCHO_SHARE_PORT", Value: "5555", }, cli.StringFlag{ Name: "local-port", Usage: "Port for local dashboard", EnvVar: "GOCHO_LOCAL_PORT", Value: "1337", }, }, Action: StartAction, }, { Name: "configure", Usage: "Create a configuration file for Gocho node", Action: ConfigureAction, }, } return app } ================================================ FILE: pkg/config/config.go ================================================ package config import ( "fmt" yaml "gopkg.in/yaml.v2" "io/ioutil" ) type Config struct { NodeId string `yaml:"NodeId" json:"nodeId"` WebPort string `yaml:"WebPort" json:"webPort"` LocalPort string `yaml:"LocalPort" json:"localPort"` ShareDirectory string `yaml:"ShareDirectory" json:"sharedDirectory"` ConfigFile string `yaml:"-" json:"-"` Debug bool `yaml:"-" json:"-"` } func (c *Config) String() string { data, err := yaml.Marshal(c) if err != nil { return "" } return string(data) } func ConfigureWizard() error { return configureWizard() } func LoadConfig() (*Config, error) { configFile, err := getConfigFileName() if err != nil { return nil, err } if !fileExists(configFile) { return nil, fmt.Errorf("Error: Config file does not exist\nUse:\n\t$ gocho configure") } data, err := ioutil.ReadFile(configFile) if err != nil { return nil, err } config := &Config{} err = yaml.Unmarshal(data, config) if err != nil { return nil, err } config.ShareDirectory = CleanPath(config.ShareDirectory) return config, nil } ================================================ FILE: pkg/config/utils.go ================================================ package config import ( "fmt" "github.com/Pallinder/go-randomdata" homedir "github.com/mitchellh/go-homedir" "io/ioutil" "os" "os/user" "strings" ) func fileExists(file string) bool { _, err := os.Stat(file) return err == nil } func CleanPath(str string) string { return strings.TrimRight(str, string(os.PathSeparator)) } func writeConfigToFile(c *Config, fileName string) error { data := []byte(c.String()) return ioutil.WriteFile(fileName, data, 0644) } func getConfigFileName() (string, error) { userHome, err := homedir.Dir() if err != nil { return "", err } configFile := fmt.Sprintf("%s%c%s", userHome, os.PathSeparator, ".gocho.conf") return configFile, nil } func getDefaultConfig() (*Config, error) { configFile, err := getConfigFileName() if err != nil { return nil, err } defaultWebPort := "5555" defaultLocalPort := "1337" defaultNodeId := randomdata.SillyName() currentUser, err := user.Current() if err == nil { defaultNodeId = currentUser.Username } config := &Config{ ShareDirectory: "", WebPort: defaultWebPort, LocalPort: defaultLocalPort, NodeId: defaultNodeId, ConfigFile: configFile, } return config, nil } ================================================ FILE: pkg/config/wizard.go ================================================ package config import ( "bufio" "fmt" "os" "strings" ) func configureWizard() error { reader := bufio.NewReader(os.Stdin) config, err := getDefaultConfig() if err != nil { return err } fmt.Println("Gocho Configure Wizard") fmt.Println("It will reset previous \"Gocho\" configure file") var ( shareDirectory string webPort string localPort string nodeId string ) fmt.Printf("Node Id: (%s) ", config.NodeId) fmt.Scanf("%s", &nodeId) fmt.Printf("Share Directory: ") // In windows it fails using fmt.Scanf lineRaw, _, err := reader.ReadLine() fmt.Println(string(lineRaw)) if err != nil || strings.Trim(string(lineRaw), " \t") == "" { fmt.Println("Invalid value for \"Share Directory\"") os.Exit(1) } shareDirectory = string(lineRaw) fmt.Printf("Share Port: (%s) ", config.WebPort) fmt.Scanf("%s", &webPort) fmt.Printf("Dashboard Port: (%s) ", config.LocalPort) fmt.Scanf("%s", &localPort) if nodeId != "" { config.NodeId = nodeId } if shareDirectory != "" { config.ShareDirectory = CleanPath(shareDirectory) } if webPort != "" { config.WebPort = webPort } if localPort != "" { config.LocalPort = localPort } if fileExists(config.ConfigFile) { err := os.Remove(config.ConfigFile) if err != nil { return err } } return writeConfigToFile(config, config.ConfigFile) } ================================================ FILE: pkg/info/info.go ================================================ package info const ( APP_NAME = "gocho" VERSION = "0.2.0-alfa" ) ================================================ FILE: pkg/node/dashboard.go ================================================ package node import ( "container/list" "encoding/json" "fmt" "github.com/donkeysharp/gocho/assets" "github.com/donkeysharp/gocho/pkg/config" "net/http" "log" ) func configHandler(conf *config.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { data, err := json.Marshal(conf) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Add("Content-Type", "application/json") w.Write(data) } } func nodesHandler(nodeList *list.List) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { nodes := make([]*NodeInfo, 0) for el := nodeList.Front(); el != nil; el = el.Next() { tmp := el.Value.(*NodeInfo) nodes = append(nodes, tmp) } data, err := json.Marshal(nodes) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Add("Content-Type", "application/json") w.Write(data) } } func dashboardServe(conf *config.Config, nodeList *list.List) { dashboardMux := http.NewServeMux() dashboardMux.Handle("/", http.FileServer(assets.AssetFS())) dashboardMux.HandleFunc("/api/config", configHandler(conf)) dashboardMux.HandleFunc("/api/nodes", nodesHandler(nodeList)) // We don't want the dashboard to be public address := "localhost" if conf.Debug { address = "0.0.0.0" } fmt.Printf("Starting dashboard at %s:%s\n", address, conf.LocalPort) err := http.ListenAndServe(fmt.Sprintf("%s:%s", address, conf.LocalPort), dashboardMux) if err != nil { log.Fatal(err) } } ================================================ FILE: pkg/node/index.go ================================================ package node import ( "bytes" "fmt" "github.com/donkeysharp/gocho/pkg/config" "net/http" "regexp" ) const ( HTML_BODY = ` ..` HTML_END = ` ` ) type FileServerResponseInterceptor struct { OriginalWriter http.ResponseWriter IndexBuffer *bytes.Buffer } func (f *FileServerResponseInterceptor) WriteHeader(status int) { f.OriginalWriter.WriteHeader(status) } func (f *FileServerResponseInterceptor) Header() http.Header { return f.OriginalWriter.Header() } func (f *FileServerResponseInterceptor) Write(content []byte) (int, error) { // if it's not an html tag why bother evaluating with regex? if content[0] != byte('<') { return f.OriginalWriter.Write(content) } re := regexp.MustCompile("^(.+)$|^$") if !re.Match(bytes.Trim(content, "\n\r")) { return f.OriginalWriter.Write(content) } content = bytes.Trim(content, "\n\r") directoryRegex := regexp.MustCompile("^(.+)$") if directoryRegex.Match(content) { directoryLink := "$2\n" content = directoryRegex.ReplaceAll(content, []byte(directoryLink)) return f.IndexBuffer.Write(content) } fileRegex := regexp.MustCompile("^(.+)$") if fileRegex.Match(content) { fileLink := "$2\n" content = fileRegex.ReplaceAll(content, []byte(fileLink)) return f.IndexBuffer.Write(content) } return 0, nil } func interceptorHandler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { interceptor := &FileServerResponseInterceptor{ OriginalWriter: w, IndexBuffer: bytes.NewBuffer(nil), } r.Header.Del("If-Modified-Since") next.ServeHTTP(interceptor, r) if interceptor.IndexBuffer.Len() > 0 { w.Write([]byte(HTML_BODY)) w.Write(interceptor.IndexBuffer.Bytes()) w.Write([]byte(HTML_END)) } } return http.HandlerFunc(fn) } func fileServe(conf *config.Config) { fileMux := http.NewServeMux() fileMux.Handle("/", interceptorHandler(http.FileServer(http.Dir(conf.ShareDirectory)))) http.ListenAndServe(fmt.Sprintf("0.0.0.0:%s", conf.WebPort), fileMux) } ================================================ FILE: pkg/node/net.go ================================================ package node import ( "container/list" "fmt" "net" "sync" "time" ) var ( nodeMutex sync.Mutex ) func announceNode(nodeInfo *NodeInfo) { address, err := net.ResolveUDPAddr("udp", MULTICAST_ADDRESS) if err != nil { return } conn, err := net.DialUDP("udp", nil, address) if err != nil { return } for { fmt.Println("sending multicast info") message, err := NewAnnouncePacket(nodeInfo) if err != nil { fmt.Println("Could not get announce package") fmt.Println(err) continue } conn.Write([]byte(message)) time.Sleep(ANNOUNCE_INTERVAL_SEC * time.Second) } } func listenForNodes(nodeList *list.List) { address, err := net.ResolveUDPAddr("udp", MULTICAST_ADDRESS) if err != nil { return } conn, err := net.ListenMulticastUDP("udp", nil, address) if err != nil { return } conn.SetReadBuffer(MULTICAST_BUFFER_SIZE) for { packet := make([]byte, MULTICAST_BUFFER_SIZE) size, udpAddr, err := conn.ReadFromUDP(packet) if err != nil { fmt.Println(err) continue } nodeInfo, err := ParseAnnouncePacket(size, udpAddr, packet) if err != nil { fmt.Println(err) continue } fmt.Printf("Received multicast packet from %s Id: %s\n", udpAddr.String(), nodeInfo.Id) go announcedNodeHandler(nodeInfo, nodeList) } } func announcedNodeHandler(nodeInfo *NodeInfo, nodeList *list.List) { nodeMutex.Lock() updateNodeList(nodeInfo, nodeList) nodeMutex.Unlock() fmt.Println("Printing nodes") fmt.Print("[") for el := nodeList.Front(); el != nil; el = el.Next() { fmt.Print(el.Value.(*NodeInfo).Id, " ") } fmt.Print("]\n\n") } func updateNodeList(nodeInfo *NodeInfo, nodeList *list.List) { nodeExists := false for el := nodeList.Front(); el != nil; el = el.Next() { tmp := el.Value.(*NodeInfo) // Already in list if tmp.Id == nodeInfo.Id { tmp.LastMulticast = time.Now().Unix() fmt.Printf("Updating node %s multicast\n", nodeInfo.Id) nodeExists = true break } } for el := nodeList.Front(); el != nil; el = el.Next() { tmp := el.Value.(*NodeInfo) if isNodeExpired(tmp, EXPIRE_TIMEOUT_SEC) { fmt.Println("Node expired, removing: ", tmp.Id) nodeList.Remove(el) } } if !nodeExists { fmt.Printf("Adding new node! %p %s\n", nodeInfo, nodeInfo.Id) nodeInfo.LastMulticast = time.Now().Unix() nodeList.PushBack(nodeInfo) } } func isNodeExpired(nodeInfo *NodeInfo, timeout int) bool { diff := time.Now().Unix() - nodeInfo.LastMulticast return diff > int64(timeout) } ================================================ FILE: pkg/node/node.go ================================================ package node import ( "container/list" "github.com/donkeysharp/gocho/pkg/config" ) const ( MULTICAST_ADDRESS = "239.6.6.6:1337" MULTICAST_BUFFER_SIZE = 4096 NODE_ANNOUNCE_COMMAND = "\x01" HEADER = "\x60\x0D\xF0\x0D" MIN_PACKET_SIZE = 6 EXPIRE_TIMEOUT_SEC = 50 ANNOUNCE_INTERVAL_SEC = 10 ) type NodeInfo struct { Id string `json:"nodeId"` Address string `json:"ipAddress"` WebPort string `json:"webPort"` LastMulticast int64 `json:"-"` } type Announcer struct { config *config.Config } func (a *Announcer) Start(nodeList *list.List) { nodeInfo := &NodeInfo{ Id: a.config.NodeId, Address: "", WebPort: a.config.WebPort, LastMulticast: 0, } go announceNode(nodeInfo) go listenForNodes(nodeList) } ================================================ FILE: pkg/node/packet.go ================================================ package node import ( "encoding/json" "fmt" "net" "strings" ) func NewAnnouncePacket(n *NodeInfo) (string, error) { jsonMessage, err := json.Marshal(n) if err != nil { return "", err } message := fmt.Sprintf("%s%s%s", HEADER, NODE_ANNOUNCE_COMMAND, jsonMessage) return message, nil } func ParseAnnouncePacket(size int, addr *net.UDPAddr, packet []byte) (*NodeInfo, error) { if size <= MIN_PACKET_SIZE { return nil, fmt.Errorf("Invalid packet size") } if strings.Compare(string(packet[0:len(HEADER)]), HEADER) != 0 { return nil, fmt.Errorf("Invalid packet header") } if string(packet[len(HEADER):len(HEADER)+1]) != NODE_ANNOUNCE_COMMAND[0:] { return nil, fmt.Errorf("Command different than NODE_ANNOUNCE_COMMAND") } fmt.Println("Packet command is NODE_ANNOUNCE_COMMAND") payload := string(packet[len(HEADER)+1:]) payload = strings.Trim(payload, "\x00") nodeInfo := &NodeInfo{} err := json.Unmarshal([]byte(payload), nodeInfo) nodeInfo.Address = addr.IP.String() nodeInfo.Id = fmt.Sprintf("%s-%s", nodeInfo.Id, nodeInfo.Address) if err != nil { return nil, err } return nodeInfo, nil } ================================================ FILE: pkg/node/serve.go ================================================ package node import ( "container/list" "github.com/donkeysharp/gocho/pkg/config" "time" ) func startAnnouncer(conf *config.Config, nodeList *list.List) { announcer := &Announcer{ config: conf, } announcer.Start(nodeList) } func Serve(conf *config.Config) { nodeList := list.New() go startAnnouncer(conf, nodeList) go fileServe(conf) go dashboardServe(conf, nodeList) // Enhancement. Open the UI app in a browser openUrl("http://localhost:" + conf.LocalPort) for { time.Sleep(time.Minute * 15) } } ================================================ FILE: pkg/node/utils.go ================================================ package node import ( "os/exec" "runtime" ) // https://stackoverflow.com/a/39324149/916063 // open opens the specified URL in the default browser of the user. func openUrl(url string) error { var cmd string var args []string switch runtime.GOOS { case "windows": cmd = "cmd" args = []string{"/c", "start"} case "darwin": cmd = "open" default: // "linux", "freebsd", "openbsd", "netbsd" cmd = "xdg-open" } args = append(args, url) return exec.Command(cmd, args...).Start() } ================================================ FILE: ui/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: ui/package.json ================================================ { "name": "ui", "version": "0.1.0", "private": false, "proxy": "http://localhost:1337", "dependencies": { "react": "^16.10.0", "react-dom": "^16.10.0", "react-scripts": "3.2.0", "react-i18next": "^10.13.1", "i18next": "^17.2.0", "i18next-browser-languagedetector": "^4.0.0", "i18next-xhr-backend": "^3.2.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build && rm build/static/js/*.map && rm build/static/css/*.map && rm build/service-worker.js && rm build/asset-manifest.json", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "browserslist": {} } ================================================ FILE: ui/public/index.html ================================================ Gocho - Local Network File Sharing
================================================ FILE: ui/public/lang/en.json ================================================ { "menus": [ { "node_information": "Node Information" }, { "discover": "Discover" } ], "sections": { "node_information": { "title": "Node Information", "node_settings": "Node Settings", "node_id": "Node ID", "web_port": "Web Port", "dashboard_port": "Dashboard Port", "shared_directory": "Shared Directory" }, "discover": { "title": "Discover", "auto_discovery": "Auto-Discovery", "node_details": "Node Details", "no_node_selected": "No node selected", "no_nodes_available": "No nodes available", "node_id": "Node ID", "web_port": "Web Port", "URL": "URL", "view_files": "View Files", "hide_files": "Hide Files", "open_in_tab": "Open in tab" } } } ================================================ FILE: ui/public/lang/es.json ================================================ { "menus": [ { "node_information": "Información del Nodo" }, { "discover": "Descubrir" } ], "sections": { "node_information": { "title": "Información del Nodo", "node_settings": "Ajustes del Nodo", "node_id": "ID Nodo", "web_port": "Puerto Web", "dashboard_port": "Puerto Panel de Control", "shared_directory": "Directorio Compartido" }, "discover": { "title": "Descubrir", "auto_discovery": "Auto-Descubrir", "node_details": "Detalles del nodo", "no_node_selected": "Ningún nodo seleccionado", "no_nodes_available": "No hay nodos disponibles", "node_id": "ID Nodo", "web_port": "Puerto Web", "URL": "URL", "view_files": "Ver Archivos", "hide_files": "Ocultar Archivos", "open_in_tab": "Abrir en otra pestaña" } } } ================================================ FILE: ui/src/App.css ================================================ @import "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"; body { font-family: 'Poppins', sans-serif; background-color: #f4f3ef; } .wrapper { display: flex; } .content-wrapper { position: relative; margin-left: 250px; width: 100%; } .content { padding: 10px 80px; } .content-wrapper .nav { border-bottom: 1px #d5d5d5 solid; color: #747474; padding: 23px 50px; } .content-wrapper .nav .navbar-btn { display: none; top: 15px; right: 8px; } .panel { background-color: #fff; border-radius: 5px; border: 1px #d5d5d5 solid; box-shadow: 0 2px 2px rgba(204, 197, 185, 0.5); } .panel header { color: #53BD82; font-weight: bold; font-size: 20px; padding: 10px 15px; border-bottom: 1px #d5d5d5 solid; } .panel .panel-body { padding: 20px; } .sidebar { position: fixed; min-width: 250px; max-width: 250px; min-height: 100vh; background: #fff; transition: all 0.3s; z-index: 99999; border-right: 1px #d5d5d5 solid; } .sidebar a, a:hover, a:focus { color: inherit; text-decoration: none; transition: all 0.3s; } .navbar-btn { position: absolute; right: 0; } .sidebar .sidebar-header { padding: 20px; margin-left: 15px; margin-right: 20px; border-bottom: 1px solid #d5d5d5; font-size: 20px; text-align: center; } .sidebar ul.components { padding: 20px 0; } .sidebar ul li a { padding: 10px 10px 10px 20px; font-size: 1.1em; display: block; color: #959595; width: 100%; } .sidebar ul li a:hover { color: #313131; } .sidebar ul li.active > a { color: #53BD82; } @media (max-width: 768px) { .sidebar { margin-left: -250px; } .sidebar.active { margin-left: 0; } .content-wrapper { margin-left: 0; } .content-wrapper .nav .navbar-btn { display: inline-block; } .content { padding: 10px 10px; } } ul.node-list { padding: 0; list-style-type: none; width: 100%; max-height: 350px; min-height: 0px; overflow: auto; } .node-list li { list-style-type: none; display: block; background-color: #53BD82; color: #fff; margin-bottom: 5px; } .node-list li a { color: #fff; display: block; width: 100%; padding: 8px; border: 1px transparent solid; border-left: 8px #149D51 solid; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .node-list li a.active { background-color: #fff; color: #000; border: 1px #149D51 solid; border-left: 8px #149D51 solid; } .node-list li a.active:hover { background-color: #fff; } .node-list li a:hover { background-color: #73CD9A; } .node-details { border-left: 1px #d5d5d5 solid; padding-left: 20px; } .node-details a { color: #149D51; } .node-details a:hover { color: #000; } .files { width: 100%; height: 500px; border: 1px #d5d5d5 solid; } ================================================ FILE: ui/src/App.js ================================================ import React, { Component } from 'react'; import { withTranslation } from 'react-i18next'; import SideBar from './components/SideBar'; import Discover from './containers/Discover'; import NodeInfo from './containers/NodeInfo'; import './App.css'; class App extends Component { constructor(props) { super(props); this.menu = [ { name: 'menus.0.node_information', component: }, { name: 'menus.1.discover', component: } ]; this.state = { title: this.menu[0].name, selectedItem: 0, toggle: false, } } collapse(e) { this.setState({ toggle: !this.state.toggle, }) } menuSelectedHandler(index) { this.setState({ title: this.menu[index].name, selectedItem: index, }) } render() { const { t } = this.props; return (
{t("node_information")}
{this.menu[this.state.selectedItem].component}
); } } export default withTranslation()(App); ================================================ FILE: ui/src/components/FormField.js ================================================ import React, {Component} from 'react'; class FormField extends Component { render() { let disabled = 'false'; if (this.props.isDisabled) { disabled = 'true'; } let leftCol = this.props.leftCol ? this.props.leftCol : 'col-md-3'; let rightCol = this.props.rightCol ? this.props.rightCol : 'col-md-9'; return
; } } export default FormField; ================================================ FILE: ui/src/components/NodeDetails.js ================================================ import React, {Component} from 'react'; import { withTranslation } from 'react-i18next'; import FormField from './FormField'; const IframeViewer = ((props) => { return