From 64c69716b986e07878d3243cac48150898d88e08 Mon Sep 17 00:00:00 2001 From: Pierre Dubouilh Date: Fri, 23 Nov 2018 17:59:12 +0100 Subject: [PATCH] add rm/rename commands --- .gitignore | 2 +- readme.md | 35 ++++++---- src/main.go | 4 +- src/main_test.go | 25 +++++-- src/script.js | 165 ++++++++++++++++++++++++----------------------- src/style.css | 1 + 6 files changed, 135 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index 1104fc1..e492ec2 100755 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ debug test gossa.go gossa -gossa-linux +gossa-linux64 gossa-linux-arm gossa-linux-arm64 gossa-mac diff --git a/readme.md b/readme.md index 45584f9..3abd8ab 100644 --- a/readme.md +++ b/readme.md @@ -5,17 +5,19 @@ 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. It's dependency-free and with under 250 lines for the server code, easily code-reviewable. +🎢 A fast and simple webserver for your files, that's dependency-free and with under 240 lines for the server code, easily code-reviewable. ### features * browse through files/directories * upload with drag-and-drop - * create new folders - * move files with drag-and-drop and keyboard + * move/rename/delete files * browse through pictures with a full-screen carousel * simple keyboard navigation/shortcuts * fast ; fills my 80MB/s AC wifi link +### built blobs +built blobs are available on the [release page](https://github.com/pldubouilh/gossa/releases). + ### run ```sh # build @@ -26,15 +28,26 @@ make ``` ### keyboard shortcuts - * Arrows/Enter browse throughout the files/directories and pictures - * Ctrl/Meta + C copy URL to clipboard - * Ctrl/Meta + D create a new directory - * Ctrl/Meta + X cut selected path - * Ctrl/Meta + V paste previously selected paths to directory - * \ search +|shortcut | action| +|-------------|-------------| +|Arrows/Enter | browse through files/directories and pictures| +|Ctrl/Meta + C | copy URL to clipboard| +|Ctrl/Meta + E | rename file/folder| +|Ctrl/Meta + Del | delete file/folder| +|Ctrl/Meta + D | create a new directory| +|Ctrl/Meta + X | cut selected path| +|Ctrl/Meta + V | paste previously selected paths to directory| +|\ | search| -### built blobs -built blobs are available on the [release page](https://github.com/pldubouilh/gossa/releases). +### ui shortcuts +|shortcut | action| +| ------------- |-------------| +|click new folder icon | create new folder| +|click images icon | toggle image carousel| +|click file icon | rename item| +|double click file icon | delete item| +|drag-and-drop item on UI | move item| +|drag-and-drop external item | upload file/folders| ### using with docker a pretty short docker file is provided diff --git a/src/main.go b/src/main.go index 5a68e8f..4d7dd08 100755 --- a/src/main.go +++ b/src/main.go @@ -67,7 +67,7 @@ func row(name string, href string, size float64, ext string) string { } return ` - + ` + sizeToString(size) + ` ` + name + ` @@ -185,6 +185,8 @@ func rpc(w http.ResponseWriter, r *http.Request) { err = os.MkdirAll(payload.Args[0], os.ModePerm) } else if payload.Call == "mv" { err = os.Rename(payload.Args[0], payload.Args[1]) + } else if payload.Call == "rm" { + err = os.Remove(payload.Args[0]) } logVerb("RPC", err, payload) diff --git a/src/main_test.go b/src/main_test.go index 164aece..60bffa2 100644 --- a/src/main_test.go +++ b/src/main_test.go @@ -77,7 +77,7 @@ func testDefaults(t *testing.T, url string) string { t.Fatal("error δΈ­ζ–‡ folder") } - if !strings.Contains(bodyStr, ` 0.2k custom_mime_type.types `) { + if !strings.Contains(bodyStr, ` 0.2k custom_mime_type.types `) { 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/") - if !strings.Contains(bodyStr, ` 0 AAA/ `) { + if !strings.Contains(bodyStr, ` 0 AAA/ `) { t.Fatal("error new folder created") } @@ -140,7 +140,7 @@ func TestGetFolder(t *testing.T) { } bodyStr = testDefaults(t, "http://127.0.0.1:8001/") - if strings.Contains(bodyStr, ` 0 AAA/ `) { + if strings.Contains(bodyStr, ` 0 AAA/ `) { t.Fatal("error folder moved") } @@ -157,7 +157,7 @@ func TestGetFolder(t *testing.T) { } bodyStr = testDefaults(t, "http://127.0.0.1:8001/") - if !strings.Contains(bodyStr, ` 0.0k α„’α…‘ α„’α…‘ `) { + if !strings.Contains(bodyStr, ` 0.0k α„’α…‘ α„’α…‘ `) { t.Fatal("error checking new file row") } @@ -167,4 +167,21 @@ func TestGetFolder(t *testing.T) { if !strings.Contains(bodyStr, `err`) { t.Fatal("error not returned") } + + // ~~~~~~~~~~~~~~~~~ + fmt.Println("\r\n~~~~~~~~~~ test rm rpc & cleanup") + bodyStr = postJSON(t, "http://127.0.0.1:8001/rpc", `{"call":"rm","args":["/hols/AAA"]}`) + if !strings.Contains(bodyStr, `ok`) { + t.Fatal("error returned value") + } + + bodyStr = get(t, "http://127.0.0.1:8001/hols/AAA") + if !strings.Contains(bodyStr, `error`) { + t.Fatal("error not returned") + } + + bodyStr = postJSON(t, "http://127.0.0.1:8001/rpc", `{"call":"rm","args":["/α„’α…‘ α„’α…‘"]}`) + if !strings.Contains(bodyStr, `ok`) { + t.Fatal("error returned value") + } } diff --git a/src/script.js b/src/script.js index 6121b81..0e6febd 100755 --- a/src/script.js +++ b/src/script.js @@ -6,9 +6,8 @@ function cancelDefault (e) { e.stopPropagation() } -function warning (e) { - return 'Leaving will interrupt transfer\n?' -} +const warningMsg = () => 'Leaving will interrupt transfer?\n' +const rmMsg = () => !confirm('Remove file?\n') const barName = document.getElementById('dlBarName') const barPc = document.getElementById('dlBarPc') @@ -17,16 +16,21 @@ const upGrid = document.getElementById('drop-grid') const pics = document.getElementById('pics') const picsHolder = document.getElementById('picsHolder') const picsLabel = document.getElementById('picsLabel') -window.picsToggle = picsToggle +// helpers let allA let imgsIndex let allImgs +const decode = a => decodeURIComponent(a).replace(location.origin, '') +const getArrowSelected = () => document.querySelectorAll('i.arrow-selected')[0] +const getASelected = () => !getArrowSelected() ? false : getArrowSelected().parentElement.parentElement.querySelectorAll('a')[0] +const prependPath = a => a.startsWith('/') ? a : decodeURI(location.pathname) + a +const prevent = e => e.preventDefault() // Soft nav function browseTo (href) { - window.fetch(href).then(r => r.text().then(t => { - const parsed = new window.DOMParser().parseFromString(t, 'text/html') + fetch(href).then(r => r.text().then(t => { + const parsed = new DOMParser().parseFromString(t, 'text/html') const table = parsed.querySelectorAll('table')[0].innerHTML document.body.querySelectorAll('table')[0].innerHTML = table @@ -35,7 +39,7 @@ function browseTo (href) { if (document.head.querySelectorAll('title')[0].innerText !== title) { document.head.querySelectorAll('title')[0].innerText = title document.body.querySelectorAll('h1')[0].innerText = '.' + title - window.history.pushState({}, '', window.encodeURI(title)) + history.pushState({}, '', encodeURI(title)) } init() @@ -55,24 +59,20 @@ window.onClickLink = e => { const refresh = () => browseTo(location.href) const prevPage = () => browseTo(location.href + '../') -const getArrowSelected = () => document.querySelectorAll('i.arrow-selected')[0] -const getASelected = () => !getArrowSelected() ? false : getArrowSelected().parentElement.parentElement.querySelectorAll('a')[0] window.onpopstate = prevPage // RPC function rpcFs (call, args, cb) { console.log('RPC', call, args) - const xhr = new window.XMLHttpRequest() + const xhr = new XMLHttpRequest() xhr.open('POST', location.origin + '/rpc') xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') xhr.send(JSON.stringify({ call, args })) xhr.onload = cb } -const prependPath = (a) => a.startsWith('/') ? a : decodeURI(location.pathname) + a - const mkdirCall = (path, cb) => rpcFs('mkdirp', [prependPath(path)], cb) - +const rmCall = (path1, cb) => rpcFs('rm', [prependPath(path1)], cb) const mvCall = (path1, path2, cb) => rpcFs('mv', [path1, path2], cb) // File upload @@ -81,7 +81,8 @@ let totalUploads = 0 let totalUploadsSize = 0 let totalUploadedSize = [] -const checkDupes = test => allA.find(a => a.innerText.replace('/', '') === test) +const dupe = test => allA.find(a => a.innerText.replace('/', '') === test) +const isDupe = t => dupe(t) ? alert(t + ' already already exists') || true : false function shouldRefresh () { totalDone += 1 @@ -107,17 +108,17 @@ function updatePercent (ev) { function postFile (file, path) { path = decodeURI(location.pathname).slice(0, -1) + path - window.onbeforeunload = warning + window.onbeforeunload = warningMsg barDiv.style.display = 'block' totalUploads += 1 totalUploadsSize += file.size barName.innerText = totalUploads > 1 ? totalUploads + ' files' : file.name - const formData = new window.FormData() + const formData = new FormData() formData.append(file.name, file) - const xhr = new window.XMLHttpRequest() + const xhr = new XMLHttpRequest() xhr.open('POST', location.origin + '/post') xhr.setRequestHeader('gossa-path', encodeURIComponent(path)) xhr.upload.addEventListener('load', shouldRefresh) @@ -126,15 +127,12 @@ function postFile (file, path) { xhr.send(formData) } -const parseDomFolder = f => { - f.createReader().readEntries(e => e.forEach(i => parseDomItem(i))) -} +const parseDomFolder = f => f.createReader().readEntries(e => e.forEach(i => parseDomItem(i))) function parseDomItem (domFile, shoudCheckDupes) { - if (shoudCheckDupes && checkDupes(domFile.name)) { - return window.alert(domFile.name + ' already exists !') + if (shoudCheckDupes && isDupe(domFile.name)) { + return } - if (domFile.isFile) { domFile.file(f => postFile(f, domFile.fullPath)) } else { @@ -146,7 +144,7 @@ function parseDomItem (domFile, shoudCheckDupes) { function pushEntry (entry) { if (!entry.webkitGetAsEntry && !entry.getAsEntry) { - return window.alert('Unsupported browser ! Please update to chrome/firefox.') + return alert('Unsupported browser ! Please update to chrome/firefox.') } else { entry = entry.webkitGetAsEntry() || entry.getAsEntry() } @@ -165,7 +163,7 @@ const setBackgroundLinks = t => { t.style.backgroundColor = 'rgba(123, 123, 123, const getLink = e => e.target.parentElement.querySelectorAll('a.list-links')[0] -document.ondragenter = (e) => { +document.ondragenter = e => { if (isPicMode()) { return } cancelDefault(e) @@ -183,18 +181,18 @@ document.ondragenter = (e) => { } } -upGrid.ondragleave = (e) => { +upGrid.ondragleave = e => { cancelDefault(e) upGrid.style.display = 'none' } -document.ondragover = (e) => { +document.ondragover = e => { cancelDefault(e) return false } // Handle drop - upload or move -document.ondrop = (e) => { +document.ondrop = e => { cancelDefault(e) upGrid.style.display = 'none' resetBackgroundLinks() @@ -216,15 +214,34 @@ document.ondrop = (e) => { // Mkdir icon window.mkdirBtn = function () { - const folder = window.prompt('New folder name', '') + const folder = prompt('new folder name', '') + if (folder && !isDupe(folder)) { + mkdirCall(folder, refresh) + } +} - if (!folder) { +// Icon click handler +const getBtnA = e => e.target.parentElement.parentElement.querySelector('a') + +window.rm = e => { + clearTimeout(window.clickToken) + const path = e.key ? getASelected().href : getBtnA(e).pathname + rmMsg() || rmCall(decode(path), refresh) +} + +window.rename = (e, commit) => { + clearTimeout(window.clickToken) + + if (!commit) { + window.clickToken = setTimeout(window.rename, 300, e, true) return - } else if (checkDupes(folder)) { - return window.alert('Name already already exists') } - mkdirCall(folder, refresh) + const orig = e.key ? getASelected().innerText : getBtnA(e).innerText + const chg = prompt('rename to', orig) + if (chg && !isDupe(chg)) { + mvCall(prependPath(orig), prependPath(chg), refresh) + } } // Keyboard Arrow @@ -232,7 +249,9 @@ const storeLastArrowSrc = src => localStorage.setItem('last-selected' + location function scrollToArrow () { const pos = getArrowSelected().getBoundingClientRect() - window.scrollTo(0, pos.y) + if (pos.top < 0 || pos.bottom > window.innerHeight) { + setTimeout(scrollTo, 50, 0, pos.y) + } } function clearArrowSelected () { @@ -277,13 +296,13 @@ function moveArrow (down) { const itemPos = all[i].getBoundingClientRect() if (i === 0) { - window.scrollTo(0, 0) + scrollTo(0, 0) } else if (i === all.length - 1) { - window.scrollTo(0, document.documentElement.scrollHeight) + scrollTo(0, document.documentElement.scrollHeight) } else if (itemPos.top < 0) { - window.scrollBy(0, -200) + scrollBy(0, -200) } else if (itemPos.bottom > window.innerHeight) { - window.scrollBy(0, 200) + scrollBy(0, 200) } } @@ -293,41 +312,33 @@ const isPic = src => src && picTypes.find(type => src.toLocaleLowerCase().includ const isPicMode = () => pics.style.display === 'flex' window.picsNav = () => picsNav(true) -function setImage (src) { - src = src || allImgs[imgsIndex] - picsLabel.innerText = src.split('/').pop() +function setImage () { + const src = allImgs[imgsIndex] picsHolder.src = src + picsLabel.innerText = src.split('/').pop() storeLastArrowSrc(src) + restoreCursorPos() } function picsOn (ifImgSelected, href) { href = href || getASelected().href - if (isPicMode()) { - return false - } else if (ifImgSelected && !isPic(href)) { + if (isPicMode() || (ifImgSelected && !isPic(href))) { return false } if (isPic(href)) { imgsIndex = allImgs.findIndex(el => el.includes(href)) - setImage() - } else { - setImage(picsHolder.src) } + setImage() pics.style.display = 'flex' return true } -function picsToggle () { - if (!isPicMode()) { - picsOn() - } else { - pics.style.display = 'none' - restoreCursorPos() - } -} +const picsOff = () => { pics.style.display = 'none' } + +window.picsToggle = () => isPicMode() ? picsOff() : picsOn() function picsNav (down) { if (!isPicMode()) { return false } @@ -378,56 +389,50 @@ document.body.addEventListener('keydown', e => { switch (e.code) { case 'Tab': case 'ArrowDown': - e.preventDefault() - return picsNav(true) || moveArrow(true) + return prevent(e) || picsNav(true) || moveArrow(true) case 'ArrowUp': - e.preventDefault() - return picsNav(false) || moveArrow(false) + return prevent(e) || picsNav(false) || moveArrow(false) case 'Enter': case 'ArrowRight': - e.preventDefault() - return picsOn(true) || picsNav(true) || getASelected().click() + return prevent(e) || picsOn(true) || picsNav(true) || getASelected().click() case 'ArrowLeft': - e.preventDefault() - return picsNav(false) || prevPage() + return prevent(e) || picsNav(false) || prevPage() case 'Escape': - if (isPicMode()) { - e.preventDefault() - return picsToggle() - } + return prevent(e) || picsOff() } // Ctrl keys if (e.ctrlKey || e.metaKey) { switch (e.code) { - case 'KeyD': - e.preventDefault() - return isPicMode() || window.mkdirBtn() - case 'KeyC': - e.preventDefault() - return isPicMode() || cpPath() + return prevent(e) || isPicMode() || cpPath() case 'KeyX': - e.preventDefault() - const x = decodeURIComponent(getASelected().href).replace(location.origin, '') - cuts.push(prependPath(x)) - return false + cuts.push(prependPath(decode(getASelected().href))) + return prevent(e) || false case 'KeyV': - e.preventDefault() - return onPaste() + return prevent(e) || onPaste() + + case 'Backspace': + return prevent(e) || isPicMode() || window.rm(e) + + case 'KeyE': + return prevent(e) || isPicMode() || window.rename(e) + + case 'KeyD': + return prevent(e) || isPicMode() || window.mkdirBtn() } } // text search if (e.code.includes('Key')) { typedPath += e.code.replace('Key', '').toLocaleLowerCase() - window.clearTimeout(typedToken) + clearTimeout(typedToken) typedToken = setTimeout(() => { typedPath = '' }, 1000) setCursorToClosestTyped() } diff --git a/src/style.css b/src/style.css index 11af5eb..5dbaa44 100755 --- a/src/style.css +++ b/src/style.css @@ -4,6 +4,7 @@ width: 16px; zoom: 1.2; margin: 1px; + cursor: pointer; } .arrow-icon {