├── .gitattributes ├── fs-server_cli.gif ├── fs-server_mobile.gif ├── go.mod ├── go.sum ├── .gitignore ├── fs-server └── main.go ├── LICENSE ├── README.md ├── utils └── utils.go └── fileserver.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-vendored -------------------------------------------------------------------------------- /fs-server_cli.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prdpx7/go-fileserver/HEAD/fs-server_cli.gif -------------------------------------------------------------------------------- /fs-server_mobile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prdpx7/go-fileserver/HEAD/fs-server_mobile.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prdpx7/go-fileserver 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 7 | ) 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 2 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This gitignore is generated using https://github.com/prdpx7/GiG/ 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | # Output of the go coverage tool, specifically when used with LiteIDE 28 | *.out 29 | 30 | # external packages folder 31 | vendor/ 32 | *.gif 33 | MD5SUMS 34 | SHA512SUMS 35 | *.tar.gz 36 | *./fs-server 37 | !fs-server/ -------------------------------------------------------------------------------- /fs-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | fileserver "github.com/prdpx7/go-fileserver" 8 | utils "github.com/prdpx7/go-fileserver/utils" 9 | qrcode "github.com/skip2/go-qrcode" 10 | ) 11 | 12 | // 13 | const ( 14 | PORT = 8000 15 | ) 16 | 17 | func main() { 18 | dirpath := utils.ParseArgs() 19 | localIP := utils.GetLocalIP() 20 | fmt.Printf("Currently Serving `%s` on:\n", dirpath) 21 | fmt.Printf("http://localhost:%d\n", PORT) 22 | 23 | if localIP != nil { 24 | url := fmt.Sprintf("http://%s:%d\n", localIP, PORT) 25 | fmt.Printf(url) 26 | qr, _ := qrcode.New(url, qrcode.High) 27 | // qr.DisableBorder = true 28 | fmt.Println(qr.ToSmallString(false)) 29 | } 30 | 31 | fs := fileserver.CustomFileServer(http.Dir(dirpath)) 32 | portNumber := fmt.Sprintf(":%d", PORT) 33 | err := http.ListenAndServe(portNumber, fileserver.RequestLogger(fs)) 34 | if err != nil { 35 | fmt.Println(err) 36 | } 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pradeep Khileri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-fileserver 2 | > A simple HTTP server to share files over WiFi via QRCode 3 | 4 | # Installation 5 | * You can download compressed version from [releases](https://github.com/prdpx7/go-fileserver/releases) 6 | ``` 7 | wget https://github.com/prdpx7/go-fileserver/releases/download/v0.1/fs-server-2020.07.25.tar.gz 8 | tar -xzf fs-server-2020.07.25.tar.gz 9 | chmod +x fs-server && sudo cp fs-server /usr/local/bin/fs-server 10 | ``` 11 | * Or download the binary directly 12 | ``` 13 | wget https://github.com/prdpx7/go-fileserver/releases/download/v0.1/fs-server 14 | chmod +x fs-server && sudo cp fs-server /usr/local/bin/fs-server 15 | ``` 16 | 17 | * Or you can clone from GitHub and build the binary yourself 18 | ``` 19 | git clone https://github.com/prdpx7/go-fileserver --depth=1 20 | cd go-fileserver/fs-server 21 | # requires go 1.14 22 | go build 23 | # make binary executable 24 | chmod +x ./fs-server 25 | # may require root permission 26 | cp fs-server /usr/local/bin/fs-server 27 | ``` 28 | # Usage 29 | ``` 30 | fs-server - A simple HTTP Server to share files on a network. 31 | Usage: fs-server [OPTIONS] 32 | Options: 33 | -h | --help - show this message 34 | Example: 35 | fs-server - serve files from current directory 36 | fs-server /home/user/documents/ - serve files from given directory 37 | ``` 38 | # Demo 39 | 40 | ### Step 1 - Run in terminal 41 | 42 | 43 | ### Step 2 - Scan QRCode on Phone 44 | 45 | 46 | # Inspiration 47 | * Inspired from [http-server](https://github.com/http-party/http-server) project 48 | 49 | # License 50 | * MIT -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // GetLocalIP i.e. 192.168.X.Y ~ your local private IP 12 | func GetLocalIP() net.IP { 13 | addrs, _ := net.InterfaceAddrs() 14 | for _, a := range addrs { 15 | if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 16 | if ipnet.IP.To4() != nil { 17 | return ipnet.IP 18 | } 19 | } 20 | } 21 | /* 22 | // Not properly working where multiple network interfaces are enabled 23 | 24 | host, _ := os.Hostname() 25 | addrs, _ := net.LookupIP(host) 26 | for _, addr := range addrs { 27 | if ipv4 := addr.To4(); ipv4 != nil { 28 | return ipv4 29 | } 30 | } 31 | */ 32 | return nil 33 | } 34 | 35 | func isDirectoryExists(path string) bool { 36 | if _, err := os.Stat(path); os.IsNotExist(err) { 37 | fmt.Printf("Directory `%s` does not exists\n", path) 38 | return false 39 | } 40 | return true 41 | } 42 | 43 | func showUsage() { 44 | helpMessage := `fs-server - A simple HTTP Server to share files on a network via QRCode. 45 | Usage: fs-server [OPTIONS] 46 | Options: 47 | -h | --help - show this message 48 | Example: 49 | fs-server - serve files from current directory 50 | fs-server /home/user/documents/ - serve files from given directory 51 | ` 52 | fmt.Println(helpMessage) 53 | } 54 | 55 | //ParseArgs ... 56 | func ParseArgs() string { 57 | homeDir, _ := os.UserHomeDir() 58 | currentDir, _ := os.Getwd() 59 | if len(os.Args) > 1 { 60 | opt := os.Args[1] 61 | if strings.HasPrefix(opt, "-h") || strings.HasPrefix(opt, "--h") { 62 | showUsage() 63 | os.Exit(0) 64 | } else { 65 | if opt == "~" { 66 | return homeDir 67 | } else if strings.HasPrefix(opt, "~/") { 68 | dirpath := filepath.Join(homeDir, opt[2:]) 69 | if isDirectoryExists(dirpath) { 70 | return dirpath 71 | } 72 | } else if isDirectoryExists(opt) { 73 | return opt 74 | } 75 | } 76 | } 77 | //fallback to current directory 78 | return currentDir 79 | } 80 | 81 | //HTMLReplacer ... 82 | var HTMLReplacer = strings.NewReplacer( 83 | "&", "&", 84 | "<", "<", 85 | ">", ">", 86 | // """ is shorter than """. 87 | `"`, """, 88 | // "'" is shorter than "'" and apos was not in HTML until HTML5. 89 | "'", "'", 90 | ) 91 | 92 | 93 | //GetHumanReadableSize ... 94 | func GetHumanReadableSize(f os.FileInfo) string{ 95 | if f.IsDir() { 96 | return "--" 97 | } 98 | bytes := f.Size() 99 | mb := float32(bytes)/(1024.0*1024.0) 100 | return fmt.Sprintf("%.2f MB",mb) 101 | } 102 | 103 | //DirListTemplateHTML to be used as index.html for redering DirectoryList 104 | var DirListTemplateHTML = ` 105 | 106 | 107 | 108 | 109 | 110 | Index of / 111 | 112 | 118 | 119 | 120 |

Index of {{.DirName}}

121 | 122 | 123 | 124 | {{range .Files}} 125 | 126 | 129 | 130 | 131 | 132 | {{end}} 133 |
127 | 128 | {{.Size}}{{.Name}}
134 | 135 |
fs-server running @ {{.IPAddr}}
136 | 137 | ` 138 | -------------------------------------------------------------------------------- /fileserver.go: -------------------------------------------------------------------------------- 1 | package fileserver 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "sort" 12 | "strings" 13 | "text/template" 14 | 15 | utils "github.com/prdpx7/go-fileserver/utils" 16 | ) 17 | 18 | type customFileHandler struct { 19 | root http.FileSystem 20 | } 21 | 22 | //CustomFileServer ... 23 | func CustomFileServer(root http.FileSystem) http.Handler { 24 | return &customFileHandler{root} 25 | } 26 | 27 | func (cf *customFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 28 | upath := r.URL.Path 29 | if !strings.HasPrefix(upath, "/") { 30 | upath = "/" + upath 31 | r.URL.Path = upath 32 | } 33 | 34 | ServeFile(w, r, cf.root, path.Clean(upath), true, "") 35 | } 36 | 37 | // ServeFile ... 38 | func ServeFile(w http.ResponseWriter, r *http.Request, fs http.FileSystem, name string, redirect bool, templateName string) { 39 | f, err := fs.Open(name) 40 | if err != nil { 41 | msg, code := toHTTPError(err) 42 | http.Error(w, msg, code) 43 | return 44 | } 45 | defer f.Close() 46 | 47 | d, err := f.Stat() 48 | if err != nil { 49 | msg, code := toHTTPError(err) 50 | http.Error(w, msg, code) 51 | return 52 | } 53 | 54 | if d.IsDir() { 55 | ListDirectory(w, r, f, templateName) 56 | return 57 | } 58 | http.ServeContent(w, r, d.Name(), d.ModTime(), f) 59 | } 60 | 61 | func toHTTPError(err error) (msg string, httpStatus int) { 62 | if os.IsNotExist(err) { 63 | return "404 page not found", http.StatusNotFound 64 | } 65 | if os.IsPermission(err) { 66 | return "403 Forbidden", http.StatusForbidden 67 | } 68 | // Default: 69 | return "500 Internal Server Error", http.StatusInternalServerError 70 | } 71 | 72 | //ListDirectory render directory content in templateName.html 73 | func ListDirectory(w http.ResponseWriter, r *http.Request, f http.File, templateName string) { 74 | RootDir, err := f.Stat() 75 | if err != nil { 76 | panic(err) 77 | } 78 | var dirContents DirectoryContent 79 | dirContents.DirName = RootDir.Name() 80 | dirContents.Files = make([]FileContent, 0) 81 | dirs, err := f.Readdir(-1) 82 | if err != nil { 83 | log.Printf("http: error reading directory: %v", err) 84 | http.Error(w, "Error reading directory", http.StatusInternalServerError) 85 | return 86 | } 87 | sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) 88 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 89 | for _, d := range dirs { 90 | name := d.Name() 91 | fileExtension := "page" 92 | if d.IsDir() { 93 | name += "/" 94 | fileExtension = "folder" 95 | } else if len(filepath.Ext(name)) > 1 { 96 | fileExtension = filepath.Ext(name)[1:] 97 | } 98 | 99 | url := url.URL{Path: name} 100 | fileContent := FileContent{Name: name, Size: utils.GetHumanReadableSize(d), URL: url, Extension: fileExtension} 101 | dirContents.Files = append(dirContents.Files, fileContent) 102 | } 103 | dirContents.IPAddr = r.Host 104 | renderTemplate(w, templateName, dirContents) 105 | } 106 | 107 | //DirectoryContent to be used in rendering Index Page 108 | type DirectoryContent struct { 109 | DirName string 110 | Files []FileContent 111 | IPAddr string 112 | } 113 | 114 | //FileContent ... 115 | type FileContent struct { 116 | Name string 117 | Size string 118 | URL url.URL 119 | Extension string 120 | } 121 | 122 | func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) { 123 | var t *template.Template 124 | var err error 125 | // use default rendering html 126 | if len(tmpl) == 0 { 127 | t = template.New("index") 128 | t, err = t.Parse(utils.DirListTemplateHTML) 129 | } else { 130 | templatePath, _ := filepath.Abs(tmpl + ".html") 131 | fmt.Println("template path", templatePath) 132 | t, err = template.ParseFiles(templatePath) 133 | } 134 | 135 | if err != nil { 136 | fmt.Println("Error in parsing template ", err) 137 | panic(err) 138 | } 139 | t.Execute(w, data) 140 | } 141 | 142 | //RequestLogger ... 143 | func RequestLogger(handler http.Handler) http.Handler { 144 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 | log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) 146 | handler.ServeHTTP(w, r) 147 | }) 148 | } 149 | --------------------------------------------------------------------------------