Rewriting frontend in react, refactoring of routes.
4
.gitignore
vendored
|
@ -19,3 +19,7 @@ config.json
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
nas
|
nas
|
||||||
|
|
||||||
|
*.db
|
||||||
|
.lock
|
||||||
|
assets/web/*
|
|
@ -1,52 +1 @@
|
||||||
<html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>React App</title><link href="/static/css/main.feacb500.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],f=0,i=[];f<n.length;f++)t=n[f],p[t]&&i.push(p[t][0]),p[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);i.length;)i.shift()();return c.push.apply(c,u||[]),a()}function a(){for(var e,r=0;r<c.length;r++){for(var t=c[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==p[u]&&(n=!1)}n&&(c.splice(r--,1),e=f(f.s=t[0]))}return e}var t={},p={1:0},c=[];function f(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,f),r.l=!0,r.exports}f.m=l,f.c=t,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(r,e){if(1&e&&(r=f(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)f.d(t,n,function(e){return r[e]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var r=window.webpackJsonp=window.webpackJsonp||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;a()}([])</script><script src="/static/js/2.585c3ac0.chunk.js"></script><script src="/static/js/main.f266202f.chunk.js"></script></body></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 | GoNAS</title>
|
|
||||||
<link rel="stylesheet" href="/assets/styles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<div>
|
|
||||||
<h1 style="display: inline-block">GoNAS</h1>
|
|
||||||
<p>Hot Storage Usage (<span id="hotUsagePercent"></span>%)</p>
|
|
||||||
<progress max="100" id="hotUsage"></progress>
|
|
||||||
<p>Cold Storage Usage (<span id="coldUsagePercent"></span>%)</p>
|
|
||||||
<progress max="100" id="coldUsage"></progress>
|
|
||||||
</div>
|
|
||||||
<div class="content index">
|
|
||||||
<a href="/files/">
|
|
||||||
<img src="/assets/svgs/hotfiles.svg" alt="">Hot Files</a><br>
|
|
||||||
<a href="/archive/">
|
|
||||||
<img src="/assets/svgs/coldfiles.svg" alt="">Cold Files</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<script>
|
|
||||||
const $ = selector => document.querySelector(selector);
|
|
||||||
|
|
||||||
const init = event => {
|
|
||||||
getUsages();
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUsages = () => {
|
|
||||||
fetch('/api/diskusage')
|
|
||||||
.then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(function (jsonResult) {
|
|
||||||
console.log(jsonResult);
|
|
||||||
let hot = (jsonResult.HotStorage.Free) / (jsonResult.HotStorage.Total) * 100;
|
|
||||||
let cold = (jsonResult.ColdStorage.Free) / (jsonResult.ColdStorage.Total) * 100;
|
|
||||||
// Flip values to reflect % used rather than % free.
|
|
||||||
$("#hotUsage").value = 100 - hot;
|
|
||||||
$("#hotUsagePercent").innerText = Math.floor(100 - hot);
|
|
||||||
$("#coldUsage").value = 100 - cold;
|
|
||||||
$("#coldUsagePercent").innerText = Math.floor(100 - cold);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', init);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,41 +0,0 @@
|
||||||
<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>| GoNAS</title>
|
|
||||||
<link rel="stylesheet" href="/assets/styles.css">
|
|
||||||
<script type="text/javascript" src="/assets/app.js"></script>
|
|
||||||
</head>
|
|
||||||
<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="" data-dismiss-context="false">
|
|
||||||
</a>
|
|
||||||
<h1 style="display: inline-block">GoNAS</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div>
|
|
||||||
<h1 id="path-header"></h1>
|
|
||||||
</div>
|
|
||||||
<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" name="path">
|
|
||||||
<input type="file" multiple name="file">
|
|
||||||
<input type="submit" value="Upload">
|
|
||||||
</form>
|
|
||||||
<p class="directory"><a href="#" id="previous-dir">..</a></p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,87 +0,0 @@
|
||||||
const $ = selector => document.querySelector(selector);
|
|
||||||
|
|
||||||
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;
|
|
||||||
$("#path").value = 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);
|
|
||||||
|
|
||||||
$("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 = $('#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);
|
|
||||||
$("#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);
|
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
body {
|
|
||||||
font-family: Open Sans, Arial, sans-serif;
|
|
||||||
color: #454545;
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: #fefefe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fileupload {
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0,0,0,0.5);
|
|
||||||
z-index: 2;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
font-size: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
margin: 2em auto;
|
|
||||||
max-width: 800px;
|
|
||||||
padding: 1em;
|
|
||||||
line-height: 1.4;
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
.directory, .file {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.directory a, .file a {
|
|
||||||
color: #454545;
|
|
||||||
text-decoration: none;
|
|
||||||
display: block;
|
|
||||||
width:100%;
|
|
||||||
border: 1px solid #454545;
|
|
||||||
margin-top: -1px;
|
|
||||||
padding: 0.5em;
|
|
||||||
-webkit-transition: background-color 0.5s linear;
|
|
||||||
-moz-transition: background-color 0.5s linear;
|
|
||||||
-ms-transition: background-color 0.5s linear;
|
|
||||||
-o-transition: background-color 0.5s linear;
|
|
||||||
transition: background-color 0.5s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.directory:hover a , .file:hover a {
|
|
||||||
background-color: lightgray;
|
|
||||||
-webkit-transition: background-color 0.5s linear;
|
|
||||||
-moz-transition: background-color 0.5s linear;
|
|
||||||
-ms-transition: background-color 0.5s linear;
|
|
||||||
-o-transition: background-color 0.5s linear;
|
|
||||||
transition: background-color 0.5s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index a {
|
|
||||||
color: #454545;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress[value] {
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
|
|
||||||
<circle cx="12" cy="12" r="10"/>
|
|
||||||
<path d="M12 8l-4 4 4 4M16 12H9"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 265 B |
|
@ -1,6 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
|
|
||||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
|
||||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
|
||||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 356 B |
|
@ -1,4 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
|
|
||||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 287 B |
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
|
|
||||||
<path d="M13 2H6a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V9l-7-7z"/>
|
|
||||||
<path d="M13 3v6h6"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 295 B |
|
@ -1,7 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="#000000" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel">
|
|
||||||
<line x1="22" y1="12" x2="2" y2="12"></line>
|
|
||||||
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
|
|
||||||
<line x1="6" y1="16" x2="6" y2="16"></line>
|
|
||||||
<line x1="10" y1="16" x2="10" y2="16"></line>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 465 B |
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="#ffffff"
|
|
||||||
stroke-width="1" stroke-linecap="butt" stroke-linejoin="round">
|
|
||||||
<path d="M20 11.08V8l-6-6H6a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h6"/>
|
|
||||||
<path d="M14 3v5h5M18 21v-6M15 18h6"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 300 B |
|
@ -1,10 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="68" stroke="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" fill="#fff" fill-rule="evenodd">
|
|
||||||
<path d="M14.5834 1.088c-.3937.0232-.7687.1763-1.0663.4352-.8802-.4872-1.9694-.3835-2.742.2612-.7377-.2487-1.5523-.0147-2.0456.5875-1.074-.2264-2.1895.11-2.9595.8922-2.176-.261-2.8072 1.2404-2.0456 2.655a1.828 1.828 0 0 0 .1306 2.6114c-.2986.876-.0105 1.845.718 2.4155-.1703.8653.2332 1.7424 1.001 2.176.0573 1.0676.703 2.015 1.6756 2.46.1713 1.0887 1.076 1.912 2.176 1.9803.2973.9598 1.1718 1.6244 2.176 1.654C7.7036 21.025 5.3738 25.1 5.792 29.3778l-.4135.74a9.814 9.814 0 0 0-4.2222 6.9586c-.334 2.8214.5712 5.65 2.4813 7.752.3287 1.4856.78 2.9413 1.3492 4.3523a10.881 10.881 0 0 0 6.7242 8.7045 22.675 22.675 0 0 0 6.5284 3.6559c2.013 2.2464 4.883 3.5352 7.8994 3.547h.1306c3.0162-.012 5.8864-1.3007 7.8993-3.547 2.3636-.8487 4.5697-2.084 6.5284-3.656a10.881 10.881 0 0 0 6.7242-8.7045 27.512 27.512 0 0 0 1.3492-4.3523 9.814 9.814 0 0 0 2.4814-7.752c-.334-2.8213-1.874-5.3595-4.2223-6.9586l-.6963-.74a10.141 10.141 0 0 0-5.7015-10.0102c1.0043-.0294 1.879-.694 2.1762-1.654 1.1-.0682 2.0047-.8916 2.176-1.9802a2.873 2.873 0 0 0 1.6756-2.4591c.768-.4337 1.1713-1.3108 1-2.176a2.176 2.176 0 0 0 .7181-2.4155 1.828 1.828 0 0 0 .615-1.2782c.0245-.4915-.15-.972-.4844-1.3332.7617-1.3927 0-2.8942-2.0456-2.6548a3.221 3.221 0 0 0-3.0683-1.0446c-.4898-.593-1.2926-.8262-2.0238-.5875-.7725-.6447-1.8617-.7484-2.742-.2612-.854-.5707-1.9785-.5267-2.7855.1088-.8154-.29-1.7225-.1323-2.3937.4135a2.459 2.459 0 0 0-2.6984 1.2186h-.8487c-1.8945 1.3177-3.2534 3.2712-3.83 5.5056a9.64 9.64 0 0 0-3.83-5.6361h-.8487a2.459 2.459 0 0 0-2.6984-1.0881 2.481 2.481 0 0 0-2.4372-.4135 5.29 5.29 0 0 0-1.6757-.544z"
|
|
||||||
fill="#000"/>
|
|
||||||
<path d="M10.0353 7.03a41.061 41.061 0 0 1 11.5335 7.8123c-.9575 3.9605-6.1585 4.1564-8.0517 4.0476.36-.1187.66-.3775.827-.7182a21.764 21.764 0 0 1-3.3294-.6963 1.11 1.11 0 0 0 .8922-.5223 9.553 9.553 0 0 1-3.0031-1.2404c.4176.065.8443-.028 1.197-.26a11.622 11.622 0 0 1-2.8725-1.7845 2.176 2.176 0 0 0 1.2186-.1958A11.076 11.076 0 0 1 6.14 11.5988c.4.11.8246.0722 1.197-.1088a9.23 9.23 0 0 1-1.9803-2.1761c.4285.2282.9425.2282 1.37 0-.6875-.65-1.294-1.3807-1.8062-2.1762a2.892 2.892 0 0 0 1.2839 0 6.072 6.072 0 0 0-1.1533-2.176c.978.0703 1.9597.0703 2.9377 0l-.718-.8704c1.1-.2 2.214-.086 3.2425.3264.3917-.3047 0-.6964-.4788-1.088a11.796 11.796 0 0 1 2.7202.6746c.4134-.3917-.3047-.74-.653-1.175 1.02.1534 1.995.5325 2.8508 1.1098.4787-.457 0-.827-.283-1.2186.8958.3445 1.7108.8706 2.3937 1.545.1832-.1414.3007-.3514.3253-.5815a.849.849 0 0 0-.1947-.6372c.7474.4107 1.4.9742 1.915 1.654.3363-.3.4577-.7582.3046-1.175a19.014 19.014 0 0 1 2.1762 2.1761 1.827 1.827 0 0 0 .348-.9575c2.0456 1.9803 4.94 6.9854.74 8.9657a39.432 39.432 0 0 0-12.6433-6.6808zm32.2067 0a41.064 41.064 0 0 0-11.5335 7.8123c1 3.9605 6.1585 4.1564 8.0517 4.0476a1.415 1.415 0 0 1-.7399-.7399c1.1263-.145 2.2396-.3777 3.3295-.6964a1.11 1.11 0 0 1-.8704-.5223 9.551 9.551 0 0 0 3.003-1.2403c-.4246.0706-.8602-.0228-1.2186-.2612 1.0394-.4516 2.007-1.0527 2.8725-1.7844-.4166.0537-.8398-.0143-1.2186-.196.8494-.5174 1.6253-1.147 2.3067-1.8714a1.697 1.697 0 0 1-1.1969-.1089 9.228 9.228 0 0 0 1.9803-2.1761 1.458 1.458 0 0 1-1.371 0 11.752 11.752 0 0 0 1.8062-2.177 2.895 2.895 0 0 1-1.2839 0 6.068 6.068 0 0 1 1.1533-2.176 20.488 20.488 0 0 1-2.9378 0l.74-.7617a5.876 5.876 0 0 0-3.2424.3264c-.3917-.3046 0-.6963.4787-1.088a11.79 11.79 0 0 0-2.7201.6746c-.5223-.4788.1958-.827.5658-1.2622-1.02.1534-1.995.5325-2.8508 1.1098-.4787-.457 0-.827.283-1.2186-.8958.3445-1.7108.8706-2.3937 1.545a.849.849 0 0 1-.1523-1.2188c-.7474.4107-1.4.9742-1.915 1.654-.3364-.3-.4578-.7582-.3047-1.175-.783.6654-1.5107 1.393-2.176 2.176a1.828 1.828 0 0 1-.3482-.9575c-2.0455 1.9803-4.9398 6.9854-.74 8.9657 3.745-2.9704 8-5.2342 12.556-6.6807z"
|
|
||||||
fill="#75a928"/>
|
|
||||||
<path d="M33.494 47.4397c-.1418 3.8536-3.3706 6.8684-7.2248 6.746-3.8632.1722-7.1355-2.8177-7.3118-6.6807.1418-3.8536 3.3705-6.8684 7.2248-6.746 3.863-.1722 7.1355 2.8177 7.3118 6.6807zm-11.5-19.15c3.072 2.4345 3.6152 6.8867 1.2187 9.9884-1.9194 3.4344-6.234 4.7016-9.7056 2.8508-3.072-2.4346-3.6152-6.8867-1.2186-9.9885 1.9193-3.4343 6.2338-4.7016 9.7055-2.8507zm8.1488-.37c-3.072 2.4345-3.6152 6.8867-1.2186 9.9884 1.9193 3.4344 6.2338 4.7017 9.7055 2.8508 3.072-2.4346 3.6152-6.8867 1.2186-9.9885-1.9193-3.4343-6.2338-4.7016-9.7055-2.8507zM6.9452 31.554c3.2642-.8704 1.088 13.4703-1.545 12.295a7.747 7.747 0 0 1 1.545-12.2951zm37.6035-.2177c-3.264-.8704-1.088 13.4703 1.545 12.295 1.7518-1.6948 2.6038-4.1152 2.2998-6.5336a7.747 7.747 0 0 0-3.8449-5.7615zM33.494 20.6733c5.6144-.9575 10.293 2.3937 10.0972 8.487-.174 2.3938-12.0993-8.1605-10.0972-8.487zm-15.5158-.196C12.3638 19.52 7.685 22.87 7.88 28.9643c.174 2.3285 12.1646-8.1387 10.0973-8.487zm8.0734-1.4144c-3.3512 0-6.5284 2.4808-6.5284 3.9823s2.655 3.6777 6.5284 3.7212 6.5284-1.4798 6.5284-3.3512-3.6777-4.3523-6.6372-4.3523zm.196 37.212c2.916-.1306 6.833.9357 6.8548 2.3502s-3.547 4.4828-7.0507 4.3522-7.1594-2.9595-7.116-4.0476c0-1.5885 4.3522-2.83 7.3118-2.742zM15.454 47.875c2.176 2.5025 3.0248 6.92 1.284 8.204s-5.6362.5875-8.465-3.4818c-1.8933-3.4383-1.654-6.8984-.3047-7.9864 2.002-1.2187 5.114.4352 7.5077 3.2zm21.1737-.8052c-2.1762 2.633-3.5036 7.4423-1.8715 8.9874 1.5668 1.1968 5.7885 1.0445 8.9004-3.286a8.27 8.27 0 0 0 .2176-9.03c-1.915-1.4798-4.6787.4134-7.2465 3.3295z"
|
|
||||||
fill="#bc1142"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 5.4 KiB |
260
auth/auth.go
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
|
"github.com/gmemstr/nas/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
enc = "cookie_session_encryption"
|
||||||
|
|
||||||
|
// This is the key with which each cookie is encrypted, I'll recommend moving it to a env file
|
||||||
|
cookieName = "NAS_SESSION"
|
||||||
|
cookieExpiry = 60 * 60 * 24 * 30 // 30 days in seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
func UserPermissions(username string, permission int) (bool, error) {
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", "assets/config/users.db")
|
||||||
|
defer db.Close()
|
||||||
|
isAllowed := false
|
||||||
|
if err != nil {
|
||||||
|
return isAllowed, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statement, err := db.Prepare("SELECT permissions FROM users WHERE username=?")
|
||||||
|
if err != nil {
|
||||||
|
return isAllowed, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := statement.Query(username)
|
||||||
|
if err != nil {
|
||||||
|
return isAllowed, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var level int
|
||||||
|
for rows.Next() {
|
||||||
|
err = rows.Scan(&level)
|
||||||
|
if err != nil {
|
||||||
|
return isAllowed, err
|
||||||
|
}
|
||||||
|
if level >= permission {
|
||||||
|
isAllowed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isAllowed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequireAuthorization(permission int) common.Handler {
|
||||||
|
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
|
||||||
|
usr, err := DecryptCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
if strings.Contains(r.Header.Get("Accept"), "html") || r.Method == "GET" {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: "Unauthorized! Redirecting to /login",
|
||||||
|
StatusCode: http.StatusTemporaryRedirect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: "Unauthorized!",
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.User = usr
|
||||||
|
|
||||||
|
username := rc.User.Username
|
||||||
|
|
||||||
|
hasPermission, err := UserPermissions(string(username), permission)
|
||||||
|
|
||||||
|
if !hasPermission {
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: "Unauthorized! Redirecting to /admin",
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateSession(u *common.User) (*http.Cookie, error) {
|
||||||
|
secret := os.Getenv("POGO_SECRET")
|
||||||
|
|
||||||
|
iv, err := generateRandomString(16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userJSON, err := json.Marshal(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hexedJSON := hex.EncodeToString(userJSON)
|
||||||
|
|
||||||
|
encKey := deriveKey(enc, secret)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(encKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the block with 0x0e
|
||||||
|
if remBytes := len(hexedJSON) % aes.BlockSize; remBytes != 0 {
|
||||||
|
t := []byte(hexedJSON)
|
||||||
|
|
||||||
|
for i := 0; i < aes.BlockSize-remBytes; i++ {
|
||||||
|
t = append(t, 0x0e)
|
||||||
|
}
|
||||||
|
hexedJSON = string(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
encCipher := make([]byte, len(hexedJSON)+aes.BlockSize)
|
||||||
|
|
||||||
|
mode.CryptBlocks(encCipher, []byte(hexedJSON))
|
||||||
|
|
||||||
|
cipherbase64 := base64urlencode(encCipher)
|
||||||
|
ivbase64 := base64urlencode(iv)
|
||||||
|
|
||||||
|
// Cookie format: iv.cipher.created_on.expire_on.HMAC
|
||||||
|
cookieStr := fmt.Sprintf("%s.%s", ivbase64, cipherbase64)
|
||||||
|
|
||||||
|
c := &http.Cookie{
|
||||||
|
Name: cookieName,
|
||||||
|
Value: cookieStr,
|
||||||
|
MaxAge: cookieExpiry,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptCookie(r *http.Request) (*common.User, error) {
|
||||||
|
secret := os.Getenv("POGO_SECRET")
|
||||||
|
|
||||||
|
c, err := r.Cookie(cookieName)
|
||||||
|
if err != nil {
|
||||||
|
if err != http.ErrNoCookie {
|
||||||
|
log.Printf("error in reading Cookie: %v", err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
csplit := strings.Split(c.Value, ".")
|
||||||
|
if len(csplit) != 2 {
|
||||||
|
return nil, errors.New("Invalid number of values in cookie")
|
||||||
|
}
|
||||||
|
|
||||||
|
ivb, cipherb := csplit[0], csplit[1]
|
||||||
|
|
||||||
|
iv, err := base64urldecode(ivb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dcipher, err := base64urldecode(cipherb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(iv) != 16 {
|
||||||
|
return nil, errors.New("IV length is not 16")
|
||||||
|
}
|
||||||
|
|
||||||
|
encKey := deriveKey(enc, secret)
|
||||||
|
|
||||||
|
if len(dcipher)%aes.BlockSize != 0 {
|
||||||
|
return nil, errors.New("ciphertext not multiple of blocksize")
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(encKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buf := make([]byte, len(dcipher))
|
||||||
|
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
|
||||||
|
mode.CryptBlocks(buf, []byte(dcipher))
|
||||||
|
|
||||||
|
tstr := fmt.Sprintf("%x", buf)
|
||||||
|
|
||||||
|
// Remove aes padding, 0e is used because it was used in encryption to mark padding
|
||||||
|
padIndex := strings.Index(tstr, "0e")
|
||||||
|
if padIndex == -1 {
|
||||||
|
return nil, errors.New("Padding Index is -1")
|
||||||
|
}
|
||||||
|
tstr = tstr[:padIndex]
|
||||||
|
|
||||||
|
data, err := hex.DecodeString(tstr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = hex.DecodeString(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u := &common.User{}
|
||||||
|
err = json.Unmarshal(data, u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveKey(msg, secret string) []byte {
|
||||||
|
key := []byte(secret)
|
||||||
|
sha256hash := hmac.New(sha256.New, key)
|
||||||
|
sha256hash.Write([]byte(msg))
|
||||||
|
|
||||||
|
return sha256hash.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomString(l int) ([]byte, error) {
|
||||||
|
rBytes := make([]byte, l)
|
||||||
|
|
||||||
|
_, err := rand.Read(rBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func base64urldecode(str string) ([]byte, error) {
|
||||||
|
base64str := strings.Replace(string(str), "-", "+", -1)
|
||||||
|
base64str = strings.Replace(base64str, "_", "/", -1)
|
||||||
|
|
||||||
|
s, err := base64.RawStdEncoding.DecodeString(base64str)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func base64urlencode(str []byte) string {
|
||||||
|
base64str := strings.Replace(string(str), "+", "-", -1)
|
||||||
|
base64str = strings.Replace(base64str, "/", "_", -1)
|
||||||
|
|
||||||
|
return base64.RawStdEncoding.EncodeToString([]byte(base64str))
|
||||||
|
}
|
108
files/files.go
|
@ -1,10 +1,9 @@
|
||||||
package files
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gmemstr/nas/auth"
|
||||||
"github.com/gmemstr/nas/common"
|
"github.com/gmemstr/nas/common"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"io"
|
"io"
|
||||||
|
@ -32,12 +31,13 @@ type FileInfo struct {
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lists out directory using template.
|
func GetUserDirectory(r *http.Request, tier string) (string, string, string) {
|
||||||
func Listing(tier string) common.Handler {
|
usr, err := auth.DecryptCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", ""
|
||||||
|
}
|
||||||
|
|
||||||
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
|
username := usr.Username
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["file"]
|
|
||||||
|
|
||||||
d, err := ioutil.ReadFile("assets/config/config.json")
|
d, err := ioutil.ReadFile("assets/config/config.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -49,26 +49,46 @@ func Listing(tier string) common.Handler {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to hot storage
|
// Default to hot storage
|
||||||
storage := config.HotStorage
|
storage := config.HotStorage + username
|
||||||
prefix := "files"
|
prefix := "files"
|
||||||
singleprefix := "file"
|
singleprefix := "file"
|
||||||
if tier == "cold" {
|
if tier == "cold" {
|
||||||
storage = config.ColdStorage
|
storage = config.ColdStorage + username
|
||||||
prefix = "archive"
|
prefix = "archive"
|
||||||
singleprefix = "archived"
|
singleprefix = "archived"
|
||||||
}
|
}
|
||||||
path := storage
|
|
||||||
|
|
||||||
if id != "" {
|
return storage, prefix, singleprefix
|
||||||
path = path + id
|
}
|
||||||
|
|
||||||
|
// Lists out directory using template.
|
||||||
|
func Listing() common.Handler {
|
||||||
|
|
||||||
|
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["file"]
|
||||||
|
tier := vars["tier"]
|
||||||
|
storage, prefix, singleprefix := GetUserDirectory(r, tier)
|
||||||
|
if storage == "" && prefix == "" && singleprefix == "" {
|
||||||
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: "Unauthorized, or unable to find cookie",
|
||||||
|
StatusCode: http.StatusTemporaryRedirect,
|
||||||
}
|
}
|
||||||
if err != nil {
|
}
|
||||||
panic(err)
|
path := storage
|
||||||
|
if id != "" {
|
||||||
|
path = storage + id
|
||||||
}
|
}
|
||||||
|
|
||||||
fileDir, err := ioutil.ReadDir(path)
|
fileDir, err := ioutil.ReadDir(path)
|
||||||
var fileList []FileInfo;
|
if err != nil && path == "" {
|
||||||
|
fmt.Println(path)
|
||||||
|
_ = os.MkdirAll(path, 0644)
|
||||||
|
}
|
||||||
|
var fileList []FileInfo
|
||||||
|
|
||||||
for _, file := range fileDir {
|
for _, file := range fileDir {
|
||||||
info := FileInfo{
|
info := FileInfo{
|
||||||
|
@ -100,11 +120,12 @@ func Listing(tier string) common.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lists out directory using template.
|
// Lists out directory using template.
|
||||||
func ViewFile(tier string) common.Handler {
|
func ViewFile() 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 {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
id := vars["file"]
|
id := vars["file"]
|
||||||
|
tier := vars["tier"]
|
||||||
|
|
||||||
d, err := ioutil.ReadFile("assets/config/config.json")
|
d, err := ioutil.ReadFile("assets/config/config.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -117,14 +138,8 @@ func ViewFile(tier string) common.Handler {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
// Default to hot storage
|
// Default to hot storage
|
||||||
storage := config.HotStorage
|
storage, _, _ := GetUserDirectory(r, tier)
|
||||||
if tier == "cold" {
|
|
||||||
storage = config.ColdStorage
|
|
||||||
}
|
|
||||||
path := storage + id
|
path := storage + id
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
common.ReadAndServeFile(path, w)
|
common.ReadAndServeFile(path, w)
|
||||||
return nil
|
return nil
|
||||||
|
@ -166,50 +181,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
23
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
68
frontend/README.md
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.<br>
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.<br>
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.<br>
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
|
32
frontend/package.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-dom": "^16.8.6",
|
||||||
|
"react-router-dom": "^5.0.0",
|
||||||
|
"react-scripts": "3.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "rm -rf ../assets/web/* && react-scripts build && mv build/* ../assets/web",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
38
frontend/public/index.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
15
frontend/public/manifest.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
72
frontend/src/App.css
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
body {
|
||||||
|
font-family: Open Sans, Arial, sans-serif;
|
||||||
|
color: #454545;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
margin: 2em auto;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 1em;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Usages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Navigation a {
|
||||||
|
color: #454545;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* From https://loading.io/css */
|
||||||
|
.LoadingSpinner {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoadingSpinner div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 51px;
|
||||||
|
height: 51px;
|
||||||
|
margin: 6px;
|
||||||
|
border: 6px solid #cef;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
|
border-color: #cef transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoadingSpinner div:nth-child(1) {
|
||||||
|
animation-delay: -0.45s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoadingSpinner div:nth-child(2) {
|
||||||
|
animation-delay: -0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoadingSpinner div:nth-child(3) {
|
||||||
|
animation-delay: -0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-ring {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
141
frontend/src/App.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { BrowserRouter, Route, Link } from 'react-router-dom'
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<div className="App">
|
||||||
|
<Route exact path="/" component={Homepage} />
|
||||||
|
<Route exact path="/login" component={Login} />
|
||||||
|
<Route exact path="/hot" component={HotFileListing} />
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Login extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="LoginForm">
|
||||||
|
<form>
|
||||||
|
<label>Username <input type="text" name="username"></input></label>
|
||||||
|
<label>Password <input type="password" name="password"></input></label>
|
||||||
|
<input type="submit" value="Login"></input>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Homepage extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
diskusage: {},
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
fetch("/api/diskusage")
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => this.setState({ diskusage: {
|
||||||
|
hot: 100 - Math.floor((data.HotStorage.Free) / (data.HotStorage.Total) * 100),
|
||||||
|
cold: 100 - Math.floor((data.ColdStorage.Free) / (data.ColdStorage.Total) * 100),
|
||||||
|
}, loading: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading } = this.state;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="LoadingSpinner"><div></div><div></div><div></div><div></div></div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="Usages">
|
||||||
|
<span>Hot Storage Usage: {this.state.diskusage.hot}%</span>
|
||||||
|
<span>Cold Storage Usage: {this.state.diskusage.cold}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="Navigation">
|
||||||
|
<Link to="/hot">Hot</Link>
|
||||||
|
<Link to="/cold">Cold</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HotFileListing extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
files: [],
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
fetch("/api/hot/")
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => this.setState({ files: data, loading: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading } = this.state;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="LoadingSpinner"><div></div><div></div><div></div><div></div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FileList files={this.state.files.Files} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileList extends Component {
|
||||||
|
render () {
|
||||||
|
let fileComponents = this.props.files.map((file) => {
|
||||||
|
if (file.IsDirectory) {
|
||||||
|
return <Directory dir={file}/>
|
||||||
|
}
|
||||||
|
return <File file={file}/>
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul>{fileComponents}</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Directory extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>{this.props.dir.Name}/</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class File extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>{this.props.file.Name}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
9
frontend/src/App.test.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
ReactDOM.render(<App />, div);
|
||||||
|
ReactDOM.unmountComponentAtNode(div);
|
||||||
|
});
|
13
frontend/src/index.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||||
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||||
|
monospace;
|
||||||
|
}
|
12
frontend/src/index.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import * as serviceWorker from './serviceWorker';
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
|
serviceWorker.unregister();
|
7
frontend/src/logo.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||||
|
<g fill="#61DAFB">
|
||||||
|
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||||
|
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||||
|
<path d="M520.5 78.1z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
135
frontend/src/serviceWorker.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl, config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl)
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
10112
frontend/yarn.lock
Normal file
157
router/router.go
|
@ -1,7 +1,10 @@
|
||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gmemstr/nas/auth"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -11,15 +14,6 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NewConfig struct {
|
|
||||||
Name string
|
|
||||||
Host string
|
|
||||||
Email string
|
|
||||||
Description string
|
|
||||||
Image string
|
|
||||||
PodcastURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Handle(handlers ...common.Handler) http.Handler {
|
func Handle(handlers ...common.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
@ -43,61 +37,129 @@ func Init() *mux.Router {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
// "Static" paths
|
// "Static" paths
|
||||||
r.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/web/static"))))
|
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("assets/web/static"))))
|
||||||
|
|
||||||
// Paths that require specific handlers
|
// Paths that require specific handlers
|
||||||
r.Handle("/", Handle(
|
r.Handle("/", Handle(
|
||||||
|
auth.RequireAuthorization(1),
|
||||||
rootHandler(),
|
rootHandler(),
|
||||||
)).Methods("GET")
|
)).Methods("GET")
|
||||||
r.Handle(`/files/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
|
|
||||||
fileList(),
|
r.Handle(`/login`, Handle(
|
||||||
)).Methods("GET")
|
loginHandler(),
|
||||||
r.Handle(`/files/`, Handle(
|
)).Methods("POST", "GET")
|
||||||
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("/api/diskusage", Handle(
|
r.Handle("/api/diskusage", Handle(
|
||||||
|
auth.RequireAuthorization(1),
|
||||||
system.DiskUsages(),
|
system.DiskUsages(),
|
||||||
)).Methods("GET")
|
)).Methods("GET")
|
||||||
|
|
||||||
r.Handle("/api/files/", Handle(
|
r.Handle(`/api/file/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
|
||||||
files.Listing("hot"),
|
auth.RequireAuthorization(1),
|
||||||
)).Methods("GET")
|
files.ViewFile(),
|
||||||
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")
|
)).Methods("GET")
|
||||||
r.Handle("/api/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("/api/archive/", Handle(
|
r.Handle("/api/{tier:(?:hot|cold)}/", Handle(
|
||||||
files.Listing("cold"),
|
auth.RequireAuthorization(1),
|
||||||
|
files.Listing(),
|
||||||
)).Methods("GET")
|
)).Methods("GET")
|
||||||
r.Handle(`/api/archive/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
|
r.Handle(`/api/{tier:^(?:hot|cold)$}/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
|
||||||
files.Listing("cold"),
|
auth.RequireAuthorization(1),
|
||||||
)).Methods("GET")
|
files.Listing(),
|
||||||
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")
|
)).Methods("GET")
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func loginHandler() common.Handler {
|
||||||
|
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
|
||||||
|
if r.Method == "GET" {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
file := "assets/web/index.html"
|
||||||
|
|
||||||
|
return common.ReadAndServeFile(file, w)
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite3", "assets/config/users.db")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: fmt.Sprintf("error in reading user database: %v", err),
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statement, err := db.Prepare("SELECT * FROM users WHERE username=?")
|
||||||
|
|
||||||
|
if _, err := auth.DecryptCookie(r); err == nil {
|
||||||
|
http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: fmt.Sprintf("error in parsing form: %v", err),
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
username := r.Form.Get("username")
|
||||||
|
password := r.Form.Get("password")
|
||||||
|
rows, err := statement.Query(username)
|
||||||
|
|
||||||
|
if username == "" || password == "" || err != nil {
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: "username or password is invalid",
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var id int
|
||||||
|
var dbun string
|
||||||
|
var dbhsh string
|
||||||
|
var dbrn string
|
||||||
|
var dbem string
|
||||||
|
var dbperm int
|
||||||
|
for rows.Next() {
|
||||||
|
err := rows.Scan(&id, &dbun, &dbhsh, &dbrn, &dbem, &dbperm)
|
||||||
|
if err != nil {
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: fmt.Sprintf("error in decoding sql data", err),
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// Create a cookie here because the credentials are correct
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(dbhsh), []byte(password)) == nil {
|
||||||
|
c, err := auth.CreateSession(&common.User{
|
||||||
|
Username: username,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: err.Error(),
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// r.AddCookie(c)
|
||||||
|
w.Header().Add("Set-Cookie", c.String())
|
||||||
|
// And now redirect the user to admin page
|
||||||
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||||
|
db.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &common.HTTPError{
|
||||||
|
Message: "Invalid credentials!",
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handles /.
|
// Handles /.
|
||||||
func rootHandler() common.Handler {
|
func rootHandler() 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 {
|
||||||
|
@ -118,12 +180,3 @@ func rootHandler() common.Handler {
|
||||||
return common.ReadAndServeFile(file, w)
|
return common.ReadAndServeFile(file, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileList() common.Handler {
|
|
||||||
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
file := "assets/web/listing.html"
|
|
||||||
|
|
||||||
return common.ReadAndServeFile(file, w)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,8 +3,10 @@ package system
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gmemstr/nas/common"
|
"github.com/gmemstr/nas/common"
|
||||||
|
"github.com/gmemstr/nas/files"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,20 +43,20 @@ func DiskUsages() common.Handler {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to hot storage
|
storage, _, _ := files.GetUserDirectory(r,"hot")
|
||||||
storage := config.HotStorage
|
|
||||||
err = syscall.Statfs(storage, &statHot)
|
err = syscall.Statfs(storage, &statHot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
_ = os.MkdirAll(storage, 0644)
|
||||||
}
|
}
|
||||||
hotStats := UsageStat{
|
hotStats := UsageStat{
|
||||||
Free: statHot.Bsize * int64(statHot.Bfree),
|
Free: statHot.Bsize * int64(statHot.Bfree),
|
||||||
Total: statHot.Bsize * int64(statHot.Blocks),
|
Total: statHot.Bsize * int64(statHot.Blocks),
|
||||||
}
|
}
|
||||||
storage = config.ColdStorage
|
|
||||||
|
storage, _, _ = files.GetUserDirectory(r,"cold")
|
||||||
err = syscall.Statfs(storage, &statCold)
|
err = syscall.Statfs(storage, &statCold)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
_ = os.MkdirAll(storage, 0644)
|
||||||
}
|
}
|
||||||
coldStats := UsageStat{
|
coldStats := UsageStat{
|
||||||
Free: statCold.Bsize * int64(statCold.Bfree),
|
Free: statCold.Bsize * int64(statCold.Bfree),
|
||||||
|
|
72
webserver.go
|
@ -7,18 +7,86 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/gmemstr/nas/router"
|
"github.com/gmemstr/nas/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main function that defines routes
|
// Main function that defines routes
|
||||||
func main() {
|
func main() {
|
||||||
// Define routes
|
if _, err := os.Stat(".lock"); os.IsNotExist(err) {
|
||||||
// We're live
|
createDatabase()
|
||||||
|
createLockFile()
|
||||||
|
}
|
||||||
|
|
||||||
r := router.Init()
|
r := router.Init()
|
||||||
fmt.Println("Your NAS instance is live on port :3000")
|
fmt.Println("Your NAS instance is live on port :3000")
|
||||||
log.Fatal(http.ListenAndServe(":3000", r))
|
log.Fatal(http.ListenAndServe(":3000", r))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createDatabase() {
|
||||||
|
fmt.Println("Initializing the database")
|
||||||
|
os.Create("assets/config/users.db")
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", "assets/config/users.db")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Problem opening database file! %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec("CREATE TABLE IF NOT EXISTS `users` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, `username` TEXT UNIQUE, `hash` TEXT, `realname` TEXT, `email` TEXT, `permissions` INTEGER )")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Problem creating database! %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := GenerateRandomString(12)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error randomly generating password", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Admin password: ", text)
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(text), 4)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error generating hash", err)
|
||||||
|
}
|
||||||
|
if bcrypt.CompareHashAndPassword(hash, []byte(text)) == nil {
|
||||||
|
fmt.Println("Password hashed")
|
||||||
|
}
|
||||||
|
_, err = db.Exec("INSERT INTO users(id,username,hash,realname,email,permissions) VALUES (0,'admin','" + string(hash) + "','Administrator','admin@localhost',2)")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Problem creating database! %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLockFile() {
|
||||||
|
lock, err := os.Create(".lock")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error: %v", err)
|
||||||
|
}
|
||||||
|
lock.Write([]byte("This file left intentionally empty"))
|
||||||
|
defer lock.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateRandomBytes(n int) ([]byte, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GenerateRandomString returns a URL-safe, base64 encoded
|
||||||
|
// securely generated random string.
|
||||||
|
func GenerateRandomString(s int) (string, error) {
|
||||||
|
b, err := GenerateRandomBytes(s)
|
||||||
|
return base64.URLEncoding.EncodeToString(b), err
|
||||||
|
}
|