├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go ├── routes.go ├── screenshots ├── edit.png └── home.png ├── static ├── back.png ├── bulma.min.css ├── download.png ├── file.png ├── folder.png ├── github.png ├── new-file.png ├── new-folder.png ├── save.png └── upload.png ├── utils.go └── views ├── edit.html ├── error.html ├── header ├── download.html ├── error-header.html ├── file.html ├── folder.html ├── header.html ├── save.html └── upload.html └── home.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.txt 3 | build/* 4 | build.bat 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Simon Eason 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 | # Network File Browser 2 | 3 | View and interact with a file system over a network with Network File Browser. A single binary file is all you need to host an entire directory through a beautiful web interface; all dependencies are included in the binary. Run it on a storage server to view and upload/download files without needing to install anything on a client device. 4 | 5 | **PLEASE NOTE**: When uploading a file with the same name as an existing one, the existing file will be overwritten. 6 | 7 | **INSPIRATION**: Inspired by the amazing [Filebrowser](https://github.com/filebrowser/filebrowser) program. 8 | 9 | ## Features 10 | 11 | - View file and folder structure 12 | - Download files 13 | - Upload files 14 | - Create new files and folders 15 | - Edit files as text 16 | - **(Coming soon)** Rename files and folders 17 | - **(Coming soon)** Delete files and folders 18 | 19 | ## Usage 20 | 21 | To host a directory with Network File Browser, run the command: ``file-browser -v "[PATH]"`` 22 | 23 | Use the ``-p`` flag to specify a port; 8080 by default. 24 | 25 | **(Optional)** Set this command to run on device start-up to ensure that the desired path's web interface is always available. 26 | 27 | ## Building 28 | 29 | Pre-built binaries are available on the GitHub page. 30 | 31 | ``` 32 | git clone https://github.com/odddollar/File-browser.git 33 | cd File-browser 34 | go build 35 | ``` 36 | 37 | Use ``go build -ldflags="-s -w"`` in place of ``go build`` to produce a significantly smaller binary. 38 | 39 | ## Technologies 40 | 41 | Developed on Windows, but should work on Linux and Mac. I've tested with WSL2 and it seems to work, but further testing on a native Linux machine is probably necessary. 42 | 43 | - Backend: [Gin](https://gin-gonic.com/) web framework for Go 44 | - Frontend: 45 | - Server-side rendered with Go's [http/template](https://pkg.go.dev/html/template) standard library 46 | - [Bulma](https://bulma.io) providing styling 47 | - Vanilla JavaScript for the odd bit of frontend logic 48 | - Go's Embed library to package all static files to a single binary 49 | 50 | ## Screenshots 51 | 52 |  53 | 54 |  55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module File-browser 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/akamensky/argparse v1.4.0 7 | github.com/gin-gonic/gin v1.8.1 8 | ) 9 | 10 | require ( 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/go-playground/locales v0.14.0 // indirect 13 | github.com/go-playground/universal-translator v0.18.0 // indirect 14 | github.com/go-playground/validator/v10 v10.10.0 // indirect 15 | github.com/goccy/go-json v0.9.7 // indirect 16 | github.com/json-iterator/go v1.1.12 // indirect 17 | github.com/leodido/go-urn v1.2.1 // indirect 18 | github.com/mattn/go-isatty v0.0.14 // indirect 19 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 20 | github.com/modern-go/reflect2 v1.0.2 // indirect 21 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 22 | github.com/ugorji/go/codec v1.2.7 // indirect 23 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 24 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect 25 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 26 | golang.org/x/text v0.3.6 // indirect 27 | google.golang.org/protobuf v1.28.0 // indirect 28 | gopkg.in/yaml.v2 v2.4.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= 2 | github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 8 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 9 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 10 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 11 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 12 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 13 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 14 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 15 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 16 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 17 | github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= 18 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 19 | github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= 20 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 21 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 22 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 23 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 25 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 26 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 27 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 28 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 29 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 30 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 31 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 32 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 33 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 34 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 35 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 36 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 37 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 38 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 39 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 40 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 41 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 42 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 43 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 44 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 45 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 46 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 47 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 48 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 49 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 50 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 51 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 52 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 53 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 56 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 57 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 58 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 59 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 60 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 61 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 62 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 63 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 64 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 68 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 70 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 71 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 72 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 73 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 74 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 75 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 76 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 77 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 78 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 82 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 83 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 84 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 85 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 86 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 88 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "html/template" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/akamensky/argparse" 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | //go:embed views 17 | var fViews embed.FS 18 | 19 | //go:embed static 20 | var fStatic embed.FS 21 | 22 | // Global variable to keep track of root path 23 | var rootPath string 24 | 25 | func main() { 26 | // Setup command line arguments 27 | parser := argparse.NewParser("Network File Browser", "View file system contents over a network") 28 | ginMode := parser.Flag("d", "dev", &argparse.Options{Default: false, Help: "Run Gin framework in debug/dev mode"}) 29 | port := parser.Int("p", "port", &argparse.Options{Default: 8080, Help: "Port to host webserver on"}) 30 | rP := parser.String("v", "path", &argparse.Options{Required: true, Help: "Root path to host. Must be absolute and exist"}) 31 | 32 | // Run command line parser 33 | err := parser.Parse(os.Args) 34 | if err != nil { 35 | fmt.Println(parser.Usage(err)) 36 | return 37 | } 38 | 39 | // Set root path to CLI value 40 | rootPath = *rP 41 | rootPath = strings.ReplaceAll(rootPath, "\\", "/") 42 | 43 | // Check if root path is absolute to prevent weird path errors 44 | if !filepath.IsAbs(rootPath) { 45 | fmt.Println(parser.Usage("Path specified isn't absolute")) 46 | return 47 | } 48 | 49 | // Check that root path actually exists 50 | if !pathExists(rootPath) { 51 | fmt.Println(parser.Usage("Path specified doesn't exist")) 52 | return 53 | } 54 | 55 | // Create template 56 | tmpl := template.Must(template.New("").Funcs(template.FuncMap{ 57 | "join": strings.Join, 58 | "append": templateAppend, 59 | "stripLastIndex": templateStripLastIndex, 60 | "isFile": templateIsFile, 61 | }).ParseFS(fViews, "views/*.html", "views/*/*.html")) 62 | 63 | // Set release or debug mode 64 | if !(*ginMode) { 65 | gin.SetMode(gin.ReleaseMode) 66 | } 67 | 68 | // Create router and load HTML/static files 69 | router := gin.Default() 70 | router.SetHTMLTemplate(tmpl) 71 | router.StaticFS("/static", http.FS(subStatic(fStatic))) 72 | 73 | // Handle request to home page 74 | router.GET("/", appRedirect) 75 | 76 | // Handle path for directories and files 77 | router.GET("/app/*path", dirOrFile) 78 | 79 | // Router group for handling files 80 | file := router.Group("/file") 81 | { 82 | // Handle downloading files 83 | file.GET("/*path", downloadFile) 84 | 85 | // Handle postback for uploading/saving files 86 | file.POST("/*path", uploadFile) 87 | } 88 | 89 | // Router path for creating new items 90 | router.POST("/new/:type/*path", createNew) 91 | 92 | // Add route for 404 93 | router.NoRoute(notFound) 94 | 95 | // Run server 96 | if !(*ginMode) { 97 | router.Run(fmt.Sprint(":", *port)) 98 | } else { 99 | router.Run(fmt.Sprint("localhost:", *port)) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // Redirect from / home page to /app 12 | func appRedirect(ctx *gin.Context) { 13 | ctx.Redirect(303, "/app") 14 | } 15 | 16 | // Return error 404 with appropriate template 17 | func notFound(ctx *gin.Context) { 18 | ctx.HTML(404, "error.html", gin.H{ 19 | "Error": 404, 20 | "Message": "\"" + ctx.Request.Host + ctx.Request.URL.Path + "\" not found", 21 | }) 22 | } 23 | 24 | // Return error 403 and template stating not permitted to access that file/directory 25 | func notPermitted(ctx *gin.Context) { 26 | // Get split URL of file 27 | URL := deleteEmpty(strings.Split(ctx.Param("path"), "/")) 28 | 29 | ctx.HTML(403, "error.html", gin.H{ 30 | "Error": 403, 31 | "Message": "\"" + ctx.Request.Host + ctx.Request.URL.Path + "\" is not accessible. Permission is likely denied", 32 | "URL": URL, 33 | }) 34 | } 35 | 36 | // Check if request if for directory or file, handing context to relevant function 37 | func dirOrFile(ctx *gin.Context) { 38 | // Get path from url and add to root path 39 | path := rootPath + ctx.Param("path") 40 | path = strings.ReplaceAll(path, "//", "/") 41 | 42 | // Get path information 43 | info, err := os.Stat(path) 44 | if err != nil { 45 | notFound(ctx) 46 | return 47 | } 48 | 49 | // Run handler function for directory or path 50 | if info.IsDir() { 51 | viewDirectory(ctx, path) 52 | } else { 53 | viewFile(ctx, path) 54 | } 55 | } 56 | 57 | // View file with text editor 58 | func viewFile(ctx *gin.Context, path string) { 59 | // Read file to string 60 | file, err := os.ReadFile(path) 61 | 62 | // Display 403 page if not able to read 63 | if err != nil { 64 | notPermitted(ctx) 65 | return 66 | } 67 | 68 | content := string(file) 69 | 70 | // Get split URL of file 71 | // Used by header buttons to determine where to send form data 72 | URL := deleteEmpty(strings.Split(ctx.Param("path"), "/")) 73 | 74 | // Send data to template 75 | ctx.HTML(200, "edit.html", gin.H{"Content": content, "URL": URL, "Path": path}) 76 | } 77 | 78 | // Return HTML template containing contents of given path 79 | func viewDirectory(ctx *gin.Context, path string) { 80 | // Read file path on server 81 | files, err := os.ReadDir(path) 82 | 83 | // Display 403 page if not able to read 84 | if err != nil { 85 | notPermitted(ctx) 86 | return 87 | } 88 | 89 | // Create variable for storing directory information 90 | var response struct { 91 | URL []string 92 | Path string 93 | Folders []string 94 | Files []string 95 | } 96 | 97 | // Add path and URL data to struct 98 | response.Path = path 99 | response.URL = deleteEmpty(strings.Split(ctx.Param("path"), "/")) 100 | 101 | // Add file and folder information to struct 102 | for _, file := range files { 103 | if file.IsDir() { 104 | response.Folders = append(response.Folders, file.Name()) 105 | } else { 106 | response.Files = append(response.Files, file.Name()) 107 | } 108 | } 109 | 110 | // Send data to template 111 | ctx.HTML(200, "home.html", response) 112 | } 113 | 114 | // Download file from given path 115 | func downloadFile(ctx *gin.Context) { 116 | // Create path to file 117 | path := rootPath + ctx.Param("path") 118 | path = strings.ReplaceAll(path, "//", "/") 119 | 120 | // Send file as attachment 121 | ctx.FileAttachment(path, filepath.Base(path)) 122 | } 123 | 124 | // Upload file to server 125 | func uploadFile(ctx *gin.Context) { 126 | // Run based on content type header 127 | // Will be json if saving file, multipart form if uploading file 128 | if ctx.ContentType() == "application/json" { 129 | // Bind json data to variable 130 | var jsonData struct { 131 | Content string `json:"fileContent"` 132 | } 133 | ctx.BindJSON(&jsonData) 134 | 135 | // Get contents and path of file 136 | path := rootPath + ctx.Param("path") 137 | 138 | // Write new contents to file 139 | err := os.WriteFile(path, []byte(jsonData.Content), 0755) 140 | if err != nil { 141 | panic(err) 142 | } 143 | } else { 144 | // Get list of files in form data 145 | form, _ := ctx.MultipartForm() 146 | files := form.File["file"] 147 | 148 | // Process each file 149 | for _, file := range files { 150 | // Create save path location 151 | path := rootPath + ctx.Param("path") + "/" + file.Filename 152 | path = strings.ReplaceAll(path, "//", "/") 153 | 154 | // Save current file 155 | ctx.SaveUploadedFile(file, path) 156 | } 157 | } 158 | 159 | // Return successful status 160 | ctx.Status(200) 161 | } 162 | 163 | // Create new file or folder on server 164 | func createNew(ctx *gin.Context) { 165 | // Bind json data to variable 166 | var jsonData struct { 167 | Name string `json:"name"` 168 | } 169 | ctx.BindJSON(&jsonData) 170 | 171 | // Create full new path 172 | path := rootPath + ctx.Param("path") + "/" + jsonData.Name 173 | path = strings.ReplaceAll(path, "//", "/") 174 | 175 | // Check that path is valid and doesn't escape root path 176 | if isValidPath(path) { 177 | var err error 178 | 179 | // Check whether file or folder needs to be created 180 | if ctx.Param("type") == "folder" { 181 | // Create folder 182 | err = os.Mkdir(path, 0755) 183 | } else if ctx.Param("type") == "file" { 184 | // Create file 185 | err = createFile(path) 186 | } 187 | 188 | if err != nil { 189 | // Server was not able create this file or folder. 190 | // This is mainly used if a path is valid, but malformed 191 | // (i.e. "C:/Windows/C:/users/hello.txt" where "C:/Windows" is the root path), 192 | // which helps prevent creating things outside the root path. 193 | // Also used to prevent creating things with names that already exist 194 | ctx.Status(403) 195 | return 196 | } 197 | } else { 198 | // Path isn't valid and user was likely trying to escape the root path 199 | ctx.Status(403) 200 | return 201 | } 202 | 203 | // Return successful status 204 | ctx.Status(200) 205 | } 206 | -------------------------------------------------------------------------------- /screenshots/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/screenshots/edit.png -------------------------------------------------------------------------------- /screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/screenshots/home.png -------------------------------------------------------------------------------- /static/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/back.png -------------------------------------------------------------------------------- /static/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/download.png -------------------------------------------------------------------------------- /static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/file.png -------------------------------------------------------------------------------- /static/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/folder.png -------------------------------------------------------------------------------- /static/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/github.png -------------------------------------------------------------------------------- /static/new-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/new-file.png -------------------------------------------------------------------------------- /static/new-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/new-folder.png -------------------------------------------------------------------------------- /static/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/save.png -------------------------------------------------------------------------------- /static/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odddollar/File-browser/52def0ebf4e755b3a4762df2ade552cd69e9bec8/static/upload.png -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // Template utility for removing final index in array of strings 13 | func templateStripLastIndex(s []string) []string { 14 | return s[:len(s)-1] 15 | } 16 | 17 | // Template utility for appending string to array of strings 18 | // (for some reason it doesn't like the regular "append" function) 19 | func templateAppend(s []string, n string) []string { 20 | return append(s, n) 21 | } 22 | 23 | // Template utility for checking if path is a file or directory. 24 | // Used for determining what items/buttons to render in header 25 | func templateIsFile(s []string) bool { 26 | // Join given path with root path 27 | path := rootPath + "/" + strings.Join(s, "/") 28 | 29 | // Get filesystem information 30 | info, err := os.Stat(path) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | // Return if a file 36 | if info.IsDir() { 37 | return false 38 | } 39 | return true 40 | } 41 | 42 | // Remove indexes in array of strings that contain an empty string 43 | func deleteEmpty(s []string) []string { 44 | var r []string 45 | for _, str := range s { 46 | if str != "" { 47 | r = append(r, str) 48 | } 49 | } 50 | return r 51 | } 52 | 53 | // Convert /static/static/* path for embedded files to /static/* 54 | func subStatic(f embed.FS) fs.FS { 55 | t, _ := fs.Sub(f, "static") 56 | return t 57 | } 58 | 59 | // Check if the given path is a subdirectory of the root path. 60 | // Cleans path first 61 | func isValidPath(path string) bool { 62 | // Normalise \ and / 63 | r := filepath.Clean(rootPath) 64 | 65 | // Clean given path 66 | p := filepath.Clean(path) 67 | 68 | // If the given path is in the cleaned root path, then the directory is valid 69 | return strings.Contains(p, r) 70 | } 71 | 72 | // Checks if a given path exists 73 | func pathExists(path string) bool { 74 | _, err := os.Stat(path) 75 | return err == nil 76 | } 77 | 78 | // Only create file if it doesn't already exist. 79 | // If is does exist, returns an error 80 | func createFile(path string) error { 81 | // If file's path doesn't exist, create the file and return nil 82 | if !pathExists(path) { 83 | // If error occurs while writing to file, the path is likely malformed 84 | if err := os.WriteFile(path, []byte(""), 0755); err != nil { 85 | return err 86 | } 87 | 88 | // If the file path didn't exist and was created successfully, return nothing 89 | return nil 90 | } 91 | 92 | // Return error if file does exist 93 | return errors.New("Path already exists: " + path) 94 | } 95 | -------------------------------------------------------------------------------- /views/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |