Begin work on API, decouple frontend to be more dynamic.

This commit is contained in:
Gabriel Simmer 2019-03-02 17:41:13 -08:00
parent b07c20d43a
commit a2a1e0e573
No known key found for this signature in database
GPG key ID: 0394452F45EF2817
8 changed files with 226 additions and 85 deletions

View file

@ -17,6 +17,10 @@ you can also optionally use the `build-pi.sh` to build it for a raspberry pi (te
then navigate to `localhost:3000`
## api
initially the heavy lifting was done by the server, but the need for a better frontend was clear.
## credits
svg icons via https://iconsvg.xyz

View file

@ -1,5 +1,8 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="utf-8" http-equiv="encoding">
<title>Home | PiNas</title>
<link rel="stylesheet" href="/assets/styles.css">
</head>

View file

@ -1,51 +1,40 @@
{{ $path := .Path }}
{{ $prev := .Previous }}
{{ $prefix := .Prefix }}
{{ $singleprefix := .SinglePrefix }}
<html>
<head>
<title>{{ $path }} | PiNas</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="utf-8" http-equiv="encoding">
<title>| PiNas</title>
<link rel="stylesheet" href="/assets/styles.css">
<script type="text/javascript" src="/assets/fileListing.js"></script>
<script type="text/javascript" src="/assets/app.js"></script>
</head>
<body ondragenter="dragEnter(event)" ondragexit="dragExit(event)">
<body>
<div id="uploadoverlay" class="fileupload hidden">
<p><img src="/assets/svgs/newfile.svg" alt="">Upload</p>
</div>
<header>
<a href="/" class="back-button">
<img src="/assets/svgs/back.svg" alt="">
<img src="/assets/svgs/back.svg" alt="" data-dismiss-context="false">
</a>
<h1 style="display: inline-block">PiNas</h1>
</header>
<main>
<div>
<h1>{{$prefix}}/{{$path}}</h1>
<h1 id="path-header"></h1>
</div>
<div class="content">
{{ if eq $prefix "files" }}
<div class="context-menu hidden" id="context" data-dismiss-context="false">
<ul class="context-actions">
<li>Archive</li>
<li>Get MD5</li>
</ul>
</div>
<div class="content" id="filelist">
<form action="/upload" method="post" enctype="multipart/form-data">
<input id="path" type="value" value="{{$path}}" name="path">
<input id="path" type="value" name="path">
<input type="file" data-multiple-caption="{count} files selected" multiple name="file">
<input type="submit" value="Upload">
</form>
{{end}}
<p class="directory"><a href="/{{$prefix}}/{{$prev}}">..</a></p>
{{range $file := .Files}}
{{ if $file.IsDir }}
<p class="directory"><a href="/{{$prefix}}/{{$path}}/{{$file.Name}}">
<img src="/assets/svgs/directory.svg" alt="">
{{$file.Name}}</a>
</p>
{{ end }}
{{ if not $file.IsDir }}
<p class="file"><a href="/{{$singleprefix}}/{{$path}}/{{$file.Name}}">
<img src="/assets/svgs/file.svg" alt="">
{{$file.Name}}</a>
</p>
{{end}}
{{end}}
<p class="directory"><a href="#" id="previous-dir">..</a></p>
</div>
</main>
</body>

88
assets/web/static/app.js Normal file
View file

@ -0,0 +1,88 @@
const $ = element => document.getElementById(element);
const init = event => {
var path = window.location.pathname;
var pathSplit = path.split("/");
var path = pathSplit.splice(1).join("/");
document.title = path + " | PiNAS";
getFiles(pathSplit[0], path, json => {
console.log(json);
document.getElementById("previous-dir").href = "/" + json.Prefix + "/" + json.Previous;
buildList(json);
});
$("path-header").innerText = path;
document.querySelector("body").addEventListener('contextmenu', function(ev) {
if (ev.target.localName == "a") {
ev.preventDefault();
var d = document.getElementById('context');
d.classList.remove("hidden");
d.style.position = "absolute";
d.style.left = ev.clientX + 'px';
d.style.top = ev.clientY + 'px';
}
return false;
}, false);
document.querySelector("body").addEventListener('click', function(ev) {
let shouldDismiss = ev.target.dataset.dismissContext == undefined && ev.target.parentElement.classList.contains("context-actions") == false && ev.target.localName != 'a';
if (ev.which == 1 && shouldDismiss) {
ev.preventDefault();
var d = document.getElementById('context');
d.classList.add("hidden");
return false;
}
}, false);
}
const getFiles = (prefix, path, callback) => {
fetch('/api/' + prefix + '/' + path)
.then(function (response) {
return response.json();
})
.then(function (jsonResult) {
callback(jsonResult);
});
}
const buildList = data => {
for (var i = 0; i < data.Files.length; i++) {
let fileItem = document.createElement('p');
let fileLink = document.createElement('a');
if (data.Files[i].IsDirectory == true) {
fileItem.classList.add("directory");
fileLink.href = "/" + data.Prefix + "/" + data.Path + "/" + data.Files[i].Name;
}
else {
fileItem.classList.add("file");
fileLink.href = "/" + data.SinglePrefix + "/" + data.Path + "/" + data.Files[i].Name;
}
fileLink.innerText = data.Files[i].Name;
fileItem.appendChild(fileLink);
document.getElementById("filelist").appendChild(fileItem);
}
}
const upload = (file, path) => {
var formData = new FormData();
formData.append("path", path);
formData.append("file", file);
fetch('/upload', { // Your POST endpoint
method: 'POST',
body: formData
}).then(
success => console.log(success) // Handle the success response object
).catch(
error => console.log(error) // Handle the error response object
);
};
window.addEventListener('load', init);

View file

@ -1,40 +0,0 @@
function dragEnter(e) {
document.getElementById("uploadoverlay").classList.remove("hidden");
}
function dragExit(e) {
document.getElementById("uploadoverlay").classList.add("hidden");
}
document.addEventListener("drop", function(e) {
var files = e.target.files || e.dataTransfer.files
console.log(files)
document.getElementById("uploadoverlay").classList.add("hidden");
for (var i = 0; i <= files.length; i++) {
upload(files[i]);
}
})
window.addEventListener("dragover",function(e){
e = e || event;
e.preventDefault();
},false);
window.addEventListener("drop",function(e){
e = e || event;
e.preventDefault();
},false);
var upload = (file) => {
var path = document.getElementById("path").value;
var formData = new FormData();
formData.append("path", path);
formData.append("file", file);
fetch('/upload', { // Your POST endpoint
method: 'POST',
body: formData
}).then(
success => console.log(success) // Handle the success response object
).catch(
error => console.log(error) // Handle the error response object
);
};

View file

@ -81,3 +81,19 @@ h1, h2, h3 {
a.back-button {
display: inline-block;
}
.context-menu {
background-color: #ededed;
padding: 10px;
}
.context-actions {
list-style: none;
padding: 0;
margin: 0;
}
.context-actions li {
border-bottom: 1px solid black;
cursor: pointer;
}

View file

@ -1,11 +1,12 @@
package files
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/gmemstr/nas/common"
"github.com/gorilla/mux"
"html/template"
"io"
"io/ioutil"
"net/http"
@ -20,12 +21,17 @@ type Config struct {
type Directory struct {
Path string
Files []os.FileInfo
Files []FileInfo
Previous string
Prefix string
SinglePrefix string
}
type FileInfo struct {
IsDirectory bool
Name string
}
// Lists out directory using template.
func Listing(tier string) common.Handler {
@ -62,6 +68,15 @@ func Listing(tier string) common.Handler {
}
fileDir, err := ioutil.ReadDir(path)
var fileList []FileInfo;
for _, file := range fileDir {
info := FileInfo{
IsDirectory: file.IsDir(),
Name: file.Name(),
}
fileList = append(fileList, info)
}
path = strings.Replace(path, storage, "", -1)
// Figure out what our previous location was.
@ -71,19 +86,15 @@ func Listing(tier string) common.Handler {
directory := Directory{
Path: path,
Files: fileDir,
Files: fileList,
Previous: previousPath,
Prefix: prefix,
SinglePrefix: singleprefix,
}
t, err := template.ParseFiles("assets/web/listing.html")
if err != nil {
panic(err)
}
t.Execute(w, directory)
return nil
resultJson, err := json.Marshal(directory);
w.Write(resultJson);
return nil;
}
}
@ -155,3 +166,50 @@ func UploadFile() common.Handler {
}
}
func Md5File(tier string) common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
vars := mux.Vars(r)
id := vars["file"]
var returnMD5String string
d, err := ioutil.ReadFile("assets/config/config.json")
var config Config;
err = json.Unmarshal(d, &config)
if err != nil {
panic(err)
}
// Default to hot storage
storage := config.HotStorage
if err != nil {
fmt.Println(err)
return nil
}
file, err := os.Open(storage + "/" + id)
if err != nil {
panic(err)
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
panic(err)
}
//Get the 16 bytes hash
hashInBytes := hash.Sum(nil)[:16]
//Convert the bytes to a string
returnMD5String = hex.EncodeToString(hashInBytes)
w.Write([]byte(returnMD5String))
return nil
}
}

View file

@ -48,29 +48,48 @@ func Init() *mux.Router {
r.Handle("/", Handle(
rootHandler(),
)).Methods("GET")
r.Handle(`/files/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
fileList(),
)).Methods("GET")
r.Handle(`/files/`, Handle(
fileList(),
)).Methods("GET")
r.Handle(`/archive/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
fileList(),
)).Methods("GET")
r.Handle(`/archive/`, Handle(
fileList(),
)).Methods("GET")
r.Handle("/files/", Handle(
r.Handle("/api/files/", Handle(
files.Listing("hot"),
)).Methods("GET")
r.Handle(`/files/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
r.Handle(`/api/files/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Listing("hot"),
)).Methods("GET")
r.Handle(`/file/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.ViewFile("hot"),
)).Methods("GET")
r.Handle("/upload", Handle(
r.Handle("/api/upload", Handle(
files.UploadFile(),
)).Methods("POST")
r.Handle(`/api/filesmd5/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Md5File("hot"),
)).Methods("GET")
r.Handle("/archive/", Handle(
r.Handle("/api/archive/", Handle(
files.Listing("cold"),
)).Methods("GET")
r.Handle(`/archive/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
r.Handle(`/api/archive/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Listing("cold"),
)).Methods("GET")
r.Handle(`/archived/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.ViewFile("cold"),
)).Methods("GET")
r.Handle(`/api/archivemd5/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Md5File("cold"),
)).Methods("GET")
return r
}
@ -84,6 +103,7 @@ func rootHandler() common.Handler {
case "/":
w.Header().Set("Content-Type", "text/html")
file = "assets/web/index.html"
default:
return &common.HTTPError{
Message: fmt.Sprintf("%s: Not Found", r.URL.Path),
@ -95,8 +115,11 @@ func rootHandler() common.Handler {
}
}
func adminHandler() common.Handler {
func fileList() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
return common.ReadAndServeFile("assets/web/admin.html", w)
w.Header().Set("Content-Type", "text/html")
file := "assets/web/listing.html"
return common.ReadAndServeFile(file, w)
}
}