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` 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 ## credits
svg icons via https://iconsvg.xyz svg icons via https://iconsvg.xyz

View file

@ -1,5 +1,8 @@
<html> <html>
<head> <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> <title>Home | PiNas</title>
<link rel="stylesheet" href="/assets/styles.css"> <link rel="stylesheet" href="/assets/styles.css">
</head> </head>

View file

@ -1,51 +1,40 @@
{{ $path := .Path }}
{{ $prev := .Previous }}
{{ $prefix := .Prefix }}
{{ $singleprefix := .SinglePrefix }}
<html> <html>
<head> <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"> <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> </head>
<body ondragenter="dragEnter(event)" ondragexit="dragExit(event)"> <body>
<div id="uploadoverlay" class="fileupload hidden"> <div id="uploadoverlay" class="fileupload hidden">
<p><img src="/assets/svgs/newfile.svg" alt="">Upload</p> <p><img src="/assets/svgs/newfile.svg" alt="">Upload</p>
</div> </div>
<header> <header>
<a href="/" class="back-button"> <a href="/" class="back-button">
<img src="/assets/svgs/back.svg" alt=""> <img src="/assets/svgs/back.svg" alt="" data-dismiss-context="false">
</a> </a>
<h1 style="display: inline-block">PiNas</h1> <h1 style="display: inline-block">PiNas</h1>
</header> </header>
<main> <main>
<div> <div>
<h1>{{$prefix}}/{{$path}}</h1> <h1 id="path-header"></h1>
</div> </div>
<div class="content"> <div class="context-menu hidden" id="context" data-dismiss-context="false">
{{ if eq $prefix "files" }} <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"> <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="file" data-multiple-caption="{count} files selected" multiple name="file">
<input type="submit" value="Upload"> <input type="submit" value="Upload">
</form> </form>
{{end}} <p class="directory"><a href="#" id="previous-dir">..</a></p>
<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}}
</div> </div>
</main> </main>
</body> </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 { a.back-button {
display: inline-block; 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 package files
import ( import (
"crypto/md5"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gmemstr/nas/common" "github.com/gmemstr/nas/common"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"html/template"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -20,12 +21,17 @@ type Config struct {
type Directory struct { type Directory struct {
Path string Path string
Files []os.FileInfo Files []FileInfo
Previous string Previous string
Prefix string Prefix string
SinglePrefix string SinglePrefix string
} }
type FileInfo struct {
IsDirectory bool
Name string
}
// Lists out directory using template. // Lists out directory using template.
func Listing(tier string) common.Handler { func Listing(tier string) common.Handler {
@ -62,6 +68,15 @@ func Listing(tier string) common.Handler {
} }
fileDir, err := ioutil.ReadDir(path) 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) path = strings.Replace(path, storage, "", -1)
// Figure out what our previous location was. // Figure out what our previous location was.
@ -71,19 +86,15 @@ func Listing(tier string) common.Handler {
directory := Directory{ directory := Directory{
Path: path, Path: path,
Files: fileDir, Files: fileList,
Previous: previousPath, Previous: previousPath,
Prefix: prefix, Prefix: prefix,
SinglePrefix: singleprefix, SinglePrefix: singleprefix,
} }
t, err := template.ParseFiles("assets/web/listing.html") resultJson, err := json.Marshal(directory);
if err != nil { w.Write(resultJson);
panic(err) return nil;
}
t.Execute(w, directory)
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( r.Handle("/", Handle(
rootHandler(), rootHandler(),
)).Methods("GET") )).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"), files.Listing("hot"),
)).Methods("GET") )).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"), files.Listing("hot"),
)).Methods("GET") )).Methods("GET")
r.Handle(`/file/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle( r.Handle(`/file/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.ViewFile("hot"), files.ViewFile("hot"),
)).Methods("GET") )).Methods("GET")
r.Handle("/upload", Handle( r.Handle("/api/upload", Handle(
files.UploadFile(), files.UploadFile(),
)).Methods("POST") )).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"), files.Listing("cold"),
)).Methods("GET") )).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"), files.Listing("cold"),
)).Methods("GET") )).Methods("GET")
r.Handle(`/archived/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle( r.Handle(`/archived/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.ViewFile("cold"), files.ViewFile("cold"),
)).Methods("GET") )).Methods("GET")
r.Handle(`/api/archivemd5/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
files.Md5File("cold"),
)).Methods("GET")
return r return r
} }
@ -84,6 +103,7 @@ func rootHandler() common.Handler {
case "/": case "/":
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
file = "assets/web/index.html" file = "assets/web/index.html"
default: default:
return &common.HTTPError{ return &common.HTTPError{
Message: fmt.Sprintf("%s: Not Found", r.URL.Path), 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 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)
} }
} }