Merge pull request #5 from pldubouilh/template

Template
This commit is contained in:
Pierre Dubouilh 2018-12-23 00:06:38 +00:00 committed by GitHub
commit f427a0fefd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 140 additions and 88 deletions

View file

@ -1,29 +1,28 @@
build: run:
make embed make embed
go vet && go fmt
CGO_ENABLED=0 go build gossa.go CGO_ENABLED=0 go build gossa.go
rm gossa.go rm gossa.go
./gossa fixture
watch:
ls src/* | entr -rc make run
embed: embed:
echo "embedding css and js into binary" echo "embedding css and js into binary"
cp src/main.go gossa.go cp src/main.go gossa.go
perl -pe 's/template_will_be_here/`cat src\/template.go`/ge' -i gossa.go
perl -pe 's/css_will_be_here/`cat src\/style.css`/ge' -i gossa.go perl -pe 's/css_will_be_here/`cat src\/style.css`/ge' -i gossa.go
perl -pe 's/theme_will_be_here/`cat src\/theme.css`/ge' -i gossa.go
perl -pe 's/js_will_be_here/`cat src\/script.js`/ge' -i gossa.go perl -pe 's/js_will_be_here/`cat src\/script.js`/ge' -i gossa.go
perl -pe 's/favicon_will_be_here/`base64 -w0 src\/favicon.png`/ge' -i gossa.go perl -pe 's/favicon_will_be_here/`base64 -w0 src\/favicon.png`/ge' -i gossa.go
run:
make build
./gossa fixture
ci: ci:
cd src && go vet && go fmt
timeout 10 make run & timeout 10 make run &
cd src && sleep 5 && go test cp src/gossa_test.go . && sleep 5 && go test; rm gossa gossa_test.go
ci-watch: ci-watch:
ls src/* | entr -rc make ci ls src/* | entr -rc make ci
watch:
ls src/* | entr -rc make run
build-all: build-all:
make embed make embed

View file

@ -5,13 +5,14 @@ gossa
[![Build Status](https://travis-ci.org/pldubouilh/gossa.svg?branch=master)](https://travis-ci.org/pldubouilh/gossa) [![Build Status](https://travis-ci.org/pldubouilh/gossa.svg?branch=master)](https://travis-ci.org/pldubouilh/gossa)
🎶 A fast and simple webserver for your files, that's dependency-free and with under 240 lines for the server code, easily code-reviewable. 🎶 A fast and simple webserver for your files, that's dependency-free and with under 210 lines for the server code, easily code-reviewable.
### features ### features
* browse through files/directories * browse through files/directories
* upload with drag-and-drop * upload with drag-and-drop
* move/rename/delete files * move/rename/delete files
* browse through pictures with a full-screen carousel * browse through pictures with a full-screen carousel
* stream videos directly from the browser
* simple keyboard navigation/shortcuts * simple keyboard navigation/shortcuts
* fast ; fills my 80MB/s AC wifi link * fast ; fills my 80MB/s AC wifi link
@ -32,12 +33,13 @@ make
|-------------|-------------| |-------------|-------------|
|Arrows/Enter | browse through files/directories and pictures| |Arrows/Enter | browse through files/directories and pictures|
|Ctrl/Meta + C | copy URL to clipboard| |Ctrl/Meta + C | copy URL to clipboard|
|Ctrl/Meta + B | toggle theme (dark/clear)|
|\<any letter\> | search|
|Ctrl/Meta + E | rename file/folder| |Ctrl/Meta + E | rename file/folder|
|Ctrl/Meta + Del | delete file/folder| |Ctrl/Meta + Del | delete file/folder|
|Ctrl/Meta + D | create a new directory| |Ctrl/Meta + D | create a new directory|
|Ctrl/Meta + X | cut selected path| |Ctrl/Meta + X | cut selected path|
|Ctrl/Meta + V | paste previously selected paths to directory| |Ctrl/Meta + V | paste previously selected paths to directory|
|\<any letter\> | search|
### ui shortcuts ### ui shortcuts
|shortcut | action| |shortcut | action|

View file

@ -77,7 +77,7 @@ func testDefaults(t *testing.T, url string) string {
t.Fatal("error 中文 folder") t.Fatal("error 中文 folder")
} }
if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-types icon-blank"></i></td> <td class="file-size"><code>0.2k</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="custom_mime_type.types">custom_mime_type.types</a></td> </tr>`) { if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-types icon-blank"></i></td> <td class="file-size"><code>211.0B</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="custom_mime_type.types">custom_mime_type.types</a></td> </tr>`) {
t.Fatal("error row custom_mime_type") t.Fatal("error row custom_mime_type")
} }
@ -116,7 +116,7 @@ func TestGetFolder(t *testing.T) {
} }
bodyStr = testDefaults(t, "http://127.0.0.1:8001/") bodyStr = testDefaults(t, "http://127.0.0.1:8001/")
if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-folder icon-blank"></i></td> <td class="file-size"><code>0</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="AAA">AAA/</a></td> </tr>`) { if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-folder icon-blank"></i></td> <td class="file-size"><code></code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="AAA">AAA/</a></td> </tr>`) {
t.Fatal("error new folder created") t.Fatal("error new folder created")
} }
@ -140,7 +140,7 @@ func TestGetFolder(t *testing.T) {
} }
bodyStr = testDefaults(t, "http://127.0.0.1:8001/") bodyStr = testDefaults(t, "http://127.0.0.1:8001/")
if strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-folder icon-blank"></i></td> <td class="file-size"><code>0</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="AAA">AAA/</a></td> </tr>`) { if strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-folder icon-blank"></i></td> <td class="file-size"><code></code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="AAA">AAA/</a></td> </tr>`) {
t.Fatal("error folder moved") t.Fatal("error folder moved")
} }
@ -157,7 +157,7 @@ func TestGetFolder(t *testing.T) {
} }
bodyStr = testDefaults(t, "http://127.0.0.1:8001/") bodyStr = testDefaults(t, "http://127.0.0.1:8001/")
if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-하 하 icon-blank"></i></td> <td class="file-size"><code>0.0k</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="%E1%84%92%E1%85%A1%20%E1%84%92%E1%85%A1">하 하</a></td> </tr>`) { if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-하 하 icon-blank"></i></td> <td class="file-size"><code>9.0B</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="%E1%84%92%E1%85%A1%20%E1%84%92%E1%85%A1">하 하</a></td> </tr>`) {
t.Fatal("error checking new file row") t.Fatal("error checking new file row")
} }

View file

@ -6,6 +6,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"html" "html"
"html/template"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@ -21,14 +22,23 @@ var host = flag.String("h", "127.0.0.1", "host to listen to")
var port = flag.String("p", "8001", "port to listen to") var port = flag.String("p", "8001", "port to listen to")
var verb = flag.Bool("verb", true, "verbosity") var verb = flag.Bool("verb", true, "verbosity")
var skipHidden = flag.Bool("k", true, "skip hidden files") var skipHidden = flag.Bool("k", true, "skip hidden files")
var initPath = "" var initPath = ""
var css = `css_will_be_here` // js will be embedded here
var js = `js_will_be_here` // id. css
var favicon = "data:image/png;base64,favicon_will_be_here" // id. b64 favicon
var units = [8]string{"k", "M", "G", "T", "P", "E", "Z", "Y"}
var fs http.Handler var fs http.Handler
var page, _ = template.New("pageTemplate").Parse(`template_will_be_here`)
type rowTemplate struct {
Name string
Href template.HTML
Size string
Ext string
}
type pageTemplate struct {
Title template.HTML
RowsFiles []rowTemplate
RowsFolders []rowTemplate
}
type rpcCall struct { type rpcCall struct {
Call string `json:"call"` Call string `json:"call"`
@ -47,84 +57,52 @@ func logVerb(s ...interface{}) {
} }
} }
func sizeToString(bytes float64) string { func sizeToString(bytes int64) string {
if bytes == 0 { units := [9]string{"B", "k", "M", "G", "T", "P", "E", "Z", "Y"}
return "0" b := float64(bytes)
} u := 0
var u = -1
for { for {
bytes = bytes / 1024 if b < 1024 {
u++ return strconv.FormatFloat(b, 'f', 1, 64) + units[u]
if bytes < 1024 {
return strconv.FormatFloat(bytes, 'f', 1, 64) + units[u]
} }
b = b / 1024
u++
} }
} }
func row(name string, href string, size float64, ext string) string {
if strings.HasPrefix(href, "/") {
href = strings.Replace(href, "/", "", 1)
}
return `<tr>
<td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-` + strings.ToLower(ext) + ` icon-blank"></i></td>
<td class="file-size"><code>` + sizeToString(size) + `</code></td>
<td class="arrow"><i class="arrow-icon"></i></td>
<td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="` + url.PathEscape(href) + `">` + name + `</a></td>
</tr>`
}
func replyList(w http.ResponseWriter, path string) { func replyList(w http.ResponseWriter, path string) {
if !strings.HasSuffix(path, "/") { if !strings.HasSuffix(path, "/") {
path += "/" path += "/"
} }
var head = `<!doctype html><html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>` + html.EscapeString(path) + `</title>
<link href="` + favicon + `" rel="icon" type="image/png"/>
<script>window.onload = function(){` + js + `}</script>
<style type="text/css">` + css + `</style>
</head>
<body>
<div id="drop-grid"> Drop here to upload </div>
<h1>.` + html.EscapeString(path) + `</h1>
<div class="icHolder"><div style="display:none;" class="ic icon-large-images" onclick="window.picsToggle()"></div>
<div class="ic icon-large-folder" onclick="window.mkdirBtn()"></div></div>
<div id="pics" style="display:none;"> <div onclick="window.picsToggle()" id="picsToggleCinema"></div> <img onclick="window.picsNav()" id="picsHolder"/> <span id="picsLabel"></span> </div>
<table>`
_files, err := ioutil.ReadDir(initPath + path) _files, err := ioutil.ReadDir(initPath + path)
check(err) check(err)
p := pageTemplate{}
if path != "/" { if path != "/" {
head += row("../", "../", 0, "folder") p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "../", "", "folder"})
} }
var dirs = ""
var files = ""
for _, el := range _files { for _, el := range _files {
var name = el.Name() name := el.Name()
href := url.PathEscape(name)
if *skipHidden && strings.HasPrefix(name, ".") { if *skipHidden && strings.HasPrefix(name, ".") {
continue continue
} }
if el.IsDir() && strings.HasPrefix(href, "/") {
href = strings.Replace(href, "/", "", 1)
}
if el.IsDir() { if el.IsDir() {
dirs += row(name+"/", name, 0, "folder") p.RowsFolders = append(p.RowsFolders, rowTemplate{name + "/", template.HTML(href), "", "folder"})
} else { } else {
var sl = strings.Split(name, ".") sl := strings.Split(name, ".")
var ext = sl[len(sl)-1] ext := strings.ToLower(sl[len(sl)-1])
files += row(name, name, float64(el.Size()), ext) p.RowsFiles = append(p.RowsFiles, rowTemplate{name, template.HTML(href), sizeToString(el.Size()), ext})
} }
} }
w.Write([]byte(head + dirs + files + `</table> p.Title = template.HTML(html.EscapeString(path))
<br><address><a href="https://github.com/pldubouilh/gossa">Gossa 🎶</a></address> page.Execute(w, p)
<div id="progress" style="display:none;"><span id="dlBarName"></span><div id="dlBarPc">1%</div></div>
</body></html>`))
} }
func doContent(w http.ResponseWriter, r *http.Request) { func doContent(w http.ResponseWriter, r *http.Request) {
@ -206,7 +184,6 @@ func checkPath(p string) (string, error) {
func main() { func main() {
flag.Parse() flag.Parse()
if len(flag.Args()) == 0 { if len(flag.Args()) == 0 {
initPath = "." initPath = "."
} else { } else {
@ -217,11 +194,11 @@ func main() {
initPath, err = filepath.Abs(initPath) initPath, err = filepath.Abs(initPath)
check(err) check(err)
var hostString = *host + ":" + *port hostString := *host + ":" + *port
fmt.Println("Gossa startig on directory " + initPath) fmt.Println("Gossa startig on directory " + initPath)
fmt.Println("Listening on http://" + hostString) fmt.Println("Listening on http://" + hostString)
var root = http.Dir(initPath) root := http.Dir(initPath)
fs = http.StripPrefix("/", http.FileServer(root)) fs = http.StripPrefix("/", http.FileServer(root))
http.HandleFunc("/rpc", rpc) http.HandleFunc("/rpc", rpc)

View file

@ -163,6 +163,11 @@ const setBackgroundLinks = t => { t.style.backgroundColor = 'rgba(123, 123, 123,
const getLink = e => e.target.parentElement.querySelectorAll('a.list-links')[0] const getLink = e => e.target.parentElement.querySelectorAll('a.list-links')[0]
upGrid.ondragleave = e => {
cancelDefault(e)
upGrid.style.display = 'none'
}
document.ondragenter = e => { document.ondragenter = e => {
if (isPicMode()) { return } if (isPicMode()) { return }
cancelDefault(e) cancelDefault(e)
@ -181,10 +186,7 @@ document.ondragenter = e => {
} }
} }
upGrid.ondragleave = e => { document.ondragend = e => resetBackgroundLinks()
cancelDefault(e)
upGrid.style.display = 'none'
}
document.ondragover = e => { document.ondragover = e => {
cancelDefault(e) cancelDefault(e)
@ -402,7 +404,7 @@ document.body.addEventListener('keydown', e => {
return prevent(e) || picsNav(false) || prevPage() return prevent(e) || picsNav(false) || prevPage()
case 'Escape': case 'Escape':
return prevent(e) || picsOff() return prevent(e) || resetBackgroundLinks() || picsOff()
} }
// Ctrl keys // Ctrl keys
@ -426,11 +428,14 @@ document.body.addEventListener('keydown', e => {
case 'KeyD': case 'KeyD':
return prevent(e) || isPicMode() || window.mkdirBtn() return prevent(e) || isPicMode() || window.mkdirBtn()
case 'KeyB':
return prevent(e) || toggleTheme()
} }
} }
// text search // text search
if (e.code.includes('Key')) { if (e.code.includes('Key') && !e.ctrlKey && !e.metaKey) {
typedPath += e.code.replace('Key', '').toLocaleLowerCase() typedPath += e.code.replace('Key', '').toLocaleLowerCase()
clearTimeout(typedToken) clearTimeout(typedToken)
typedToken = setTimeout(() => { typedPath = '' }, 1000) typedToken = setTimeout(() => { typedPath = '' }, 1000)

View file

@ -1,3 +1,9 @@
a {
text-decoration: none;
background-color: transparent !important;
/* border-bottom: .01em solid #dfe6e9; */
}
.icon { .icon {
display: block; display: block;
height: 16px; height: 16px;
@ -75,7 +81,6 @@ h1 {
} }
#progress { #progress {
background-color: white;
width: 99%; width: 99%;
left: 0.5%; left: 0.5%;
right: 0.5%; right: 0.5%;
@ -83,7 +88,6 @@ h1 {
bottom: 0px; bottom: 0px;
padding-bottom: 10px; padding-bottom: 10px;
max-height: 50%; max-height: 50%;
overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
} }
@ -118,10 +122,9 @@ h1 {
text-align: center; text-align: center;
font-size: 4em; font-size: 4em;
font-family: Helvetica; font-family: Helvetica;
background-color: white;
color: green; color: green;
opacity: 0.8; opacity: 0.8;
backdrop-filter: grayscale(100%); background-color: rgba(123, 123, 123, 0.2)
} }
#pics { #pics {

59
src/template.go Normal file
View file

@ -0,0 +1,59 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>{{.Title}}</title>
<link href="data:image/png;base64,favicon_will_be_here" rel="icon" type="image/png" />
<style type="text/css">css_will_be_here</style>
<style type="text/css" id="theme">theme_will_be_here</style>
<script>
const theme = document.getElementById('theme')
const themeNow = () => localStorage.getItem('theme')
const setTheme = () => { theme.disabled = themeNow() === 'regular' }
const toggleTheme = () => localStorage.setItem('theme', (themeNow() === 'regular' ? 'alt' : 'regular')) || setTheme()
setTheme()
</script>
<script>window.onload = function () { js_will_be_here }</script>
</head>
<body>
<div id="drop-grid"> Drop here to upload </div>
<h1>.{{.Title}}</h1>
<div class="icHolder">
<div style="display:none;" class="ic icon-large-images" onclick="window.picsToggle()"></div>
<div class="ic icon-large-folder" onclick="window.mkdirBtn()"></div>
</div>
<div id="pics" style="display:none;">
<div onclick="window.picsToggle()" id="picsToggleCinema"></div> <img onclick="window.picsNav()" id="picsHolder" />
<span id="picsLabel"></span>
</div>
<table>
{{range .RowsFolders}}
<tr>
<td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-{{.Ext}} icon-blank"></i></td>
<td class="file-size"><code>{{.Size}}</code></td>
<td class="arrow"><i class="arrow-icon"></i></td>
<td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="{{.Href}}">{{.Name}}</a></td>
</tr>
{{end}}
{{range .RowsFiles}}
<tr>
<td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-{{.Ext}} icon-blank"></i></td>
<td class="file-size"><code>{{.Size}}</code></td>
<td class="arrow"><i class="arrow-icon"></i></td>
<td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="{{.Href}}">{{.Name}}</a></td>
</tr>
{{end}}
</table>
<div id="progress" style="display:none;">
<span id="dlBarName"></span>
<div id="dlBarPc">1%</div>
</div>
</body>
</html>

7
src/theme.css Normal file
View file

@ -0,0 +1,7 @@
html, a {
background-color: #2d3436; color: #dfe6e9;
}
.arrow, .icon-large-images {
filter: invert(100%) !important;
}